Every gate wrote its server output into a per-run mktemp dir that its own
cleanup trap deletes on exit — nothing to follow during the run, nothing
to read after it.
- chat -> /tmp/chat.log, web-app -> /tmp/web-app.log,
site -> /tmp/site.log, log-watcher -> /tmp/log-watcher.log
- truncated once at gate start, appended for the rest of the run, so one
file holds the whole run in order
- each gate PRINTS the path as its first line, with the tail -F command
- legs are banner-separated and name their port and env
(===== leg 2 - port 18902 - env WO_IO=epoll =====)
Appending breaks readiness detection unless it is leg-scoped:
- serve() used to grep the whole file for `listening`, which after the
switch to append would match an EARLIER leg and return before the new
server was up. It now records the line count first and searches only
tail -n "+$LEGFROM"; the ASan scan is scoped the same way
- log-watcher's checks grep per-invocation files, so those are kept and
the output is teed into both — process substitution adds no pipeline
stage, so $! is still the command's pid the gate kills and waits on
- its one SYNCHRONOUS invocation appends after it finishes rather than
teeing: the grep on the next line would race tee's flush
Verified, all green: chat 11/0, web-app 46/0, site 21/0, log-watcher 7/0.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
- Task 5: net.close on every path out of a serve iteration (400
included) and the listener on stop; measured 4 -> 54 fds over 50
requests before, 4 -> 4 over 200 after. The loop's comment claimed
the iteration-end drop IS the close -- wrong twice (net.Conn is a
scalar, and a drop would not close an fd); it now says what is true
- Task 6: LW_SOAK=<seconds> in the acceptance script -- each mode under
load, resident+descriptor deltas against a WARMED baseline (warm-up
includes load: cold-to-high-water is not growth), 256 KiB / zero
tolerance; LW_ACCEPT_WOVM soaks another build
- the soak caught ~1.6 MiB/min of in-arena leaks ASan cannot see (the
arena is one allocation to LeakSanitizer); an arena size-class
census + pointer trace attributed five bugs:
- jparse_string sized every decoded string at "rest of the input"
and relabeled len after -- blocks filed on free lists their next
allocation never reads (fs.read_all's mis-size, again); copy out
exact, free at the taken size
- `!=` never dropped fresh operands (headers["authorization"] !=
"Bearer ${key}" leaked both sides per request); Ne now reaps as Eq
- an Int interpolation segment is a fresh int_to_text, not a borrow;
is_borrowed_value_t asks the segment's type
- json.encode(Ctor{...}) had no owner -- record + both field copies
leaked per tool call; its bespoke lowering now drops the argument
- a discarded expression statement owns its result: `pop(lines);`
leaked the popped element; reader builtins excluded
- after: arena live bytes flat per request on every handler; release
soak 30 s per mode watch 0 / run 0 / mcp +20 KiB, descriptors flat;
ASan build flat at 14600 KiB across 601686 requests in 90 s past its
~1200-request quarantine warm-up
- gates: oop-accept ALL CRITERIA MET, oop-e2e 71/0, woc-test 565/0,
wovm-test green, log-watcher 7/0 (soak opt-in, fast path <1 min)
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
- blocking stdlib calls that PARK (net.accept, socket read/write,
time.sleep, a child wait) no longer restart the syscall when the
stop flag is set on an interruption: a server sitting in accept
ignored SIGTERM and only `kill -9` ended it
- a stop is NOT a trap -- builtin.h's WO_SYS_STOPPED carries no error
record and no catch handler sees it (`try` must not swallow
SIGTERM); the VM unwinds the whole stack through the same drop
machinery an uncaught trap uses, so nothing leaks on the way out
- wo_vm_call gained a third outcome (1 = stopped); the CLI maps it to
the status the program's own `return 0` would have given, and a
regular-file read keeps its plain EINTR retry -- it does not park
- an ASSIGNMENT was not an ownership boundary: `api_key =
j.mcp.apiKey` moved the field pointer into the local, so the local
aliased the record and the first unwind freed the same string twice
(SIGSEGV in class_free). `let` copied a Text place, assignment now
does too -- the same double free was latent on the normal exit path,
hidden by the order the compiler happens to emit drops in
- log-watcher-accept is 7 checks: the seventh is the stop itself, with
the hard kill demoted to a fallback whose use is the failure
- measured under ASan: mcp parked, mcp after traffic, watch and run
all exit rc 0 with zero leaks; SIGINT behaves as SIGTERM
- gates: oop-accept ALL CRITERIA MET, oop-e2e 71/0, woc-test 565/0,
wovm-test green, log-watcher 7/0
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The unsoundness is closed. All four MCP tools now answer correctly over HTTP
(get_running_crons, list_logs, tail_log -> ["info two","error three"],
search_log -> its match) where `tail_log` used to return
{"isError":true,"text":"tool failed: not a text value"}. corpus 71/0,
woc 565/0, wovm gates green, ASan clean on the container fixtures.
- builtin.c: multi_push, map_set (key AND value) and multi_set COPY a TEXT
element into the container. The container's declared kinds already make it
the owner of what it holds, so storing a caller-owned pointer gave one
string two owners — `push(res, e.log_path)` freed a record's field out from
under it. OWNED/GCREF elements still move (not copyable; the @gc escape
keeps their counting), so `set`'s @gc gap is untouched and still recorded
- emit.ml: `drop_fresh_text` — after push/set and the `m[k] = v` / `m[i] = v`
sugar, a value that was freshly BUILT (call result, `..` chain,
interpolation) is dropped here, while a value read out of a place is left to
its owner. That asymmetry is the point: before the copy the borrowed case
double freed and the fresh case leaked
- obj.c: the runtime's output stream is line-buffered. A long-running program
writing progress with `print` was invisible when stdout was a file or a pipe
(full buffering), and a killed one lost its log entirely; byte-exact
fixtures are unaffected
- scripts/log-watcher-accept.sh + `just log-watcher`: the acceptance test for
the sample — compile, watch (alert), run (schedule), and three MCP checks.
Hardened after it lied to me: a per-run port (a stale server on a fixed port
answered for it), a connect-probe that fails loudly when OUR server did not
come up, replies read by Content-Length rather than to EOF (the sample never
closes), and kill -9 on teardown
- docs: the copy rule is in the builtin surface; the status board records the
gap as closed and adds the new one — a blocking accept/read swallows SIGTERM,
which belongs to the shard-actor runtime's event loop, not to a patch here
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>