Agent Note: Settlement, framing, and lifecycle fixes in the CPython backend

September 4, 2026 · View on GitHub

Status: implemented

English | 中文

Problem

The CPython subprocess backend for PTC mode, built on the fd-3 frame protocol, resolves every program outcome as a CodeRunResult, rejects run() only for seam misuse, and disposes to quiescence so no subprocess that stays in the child's process group outlives the fiber (a descendant that escapes the group with setsid() is the documented exception — see the package README's Known Limitations). A sequence of review passes surfaced defects that broke those contracts in ways unit coverage did not catch — each hid behind a /* v8 ignore */, a captured-callable that read as a fix but was not, a memory effect invisible through the seam, a load-time bound that double-counted, a process-group escalation that a survivor could outlast, a cross-event-loop completion that silently deadlocked, a synchronous throw outside the settlement path, or a transport boundary rendered as a log boundary. Most behavioral fixes ship with a test that fails without them; eleven do not, and are called out as such — the chunked frame read (a syscall-count improvement with no cross-platform-deterministic failure), the confirmed-empty finalize (its only seam-observable effect, a frozen heartbeat, freezes the instant SIGKILL is delivered, which the pre-fix finalize-on-delivery code also produced, and the discriminating probe is the signal-0 check the Alternatives reject as cross-environment-unreliable), the shared stdout/stderr budget (its only seam-observable difference is which entry boundary a mid-stream flush lands on, and that depends on the relative arrival timing of two independent OS pipes, which os.sched_yield does not make deterministic; the per-pipe memory bounds it strengthens ARE covered by the single-pipe flood tests), the flush_line join-clear-push reorder (it lowers the settlement-flush peak from three copies to two, but the 12x load gate already covers the three-copy newline path, so every gate-admitted config stays within the address space under both orders and no seam-observable difference exists — the memory effect is inside the Python child, unmeasurable through the seam like the shared-budget case), pacing binding replies (its in-tree case only asserts the framed replies still round-trip; the peak it removes lives inside the host's fd-3 writable buffer, unseen through the seam, so the 32.0 MiB → 0.0 MiB reduction is measurable only out-of-tree), dropping a late binding resolution before snapshot (its three assertions all also hold pre-fix, because sendReply already dropped after-settlement values — just later than the snapshot), the done-value TOCTOU pre-encoding (a concurrent mutation racing the encode cannot be deterministically constructed through the seam — its daemon-mutation regression only asserts the result is never a worker-exit, which is probabilistic and non-discriminating, so under the existing no-fail-before-with-a-reason precedent it is registered as no-fail-before), the stray-UTF-8 budget-flush retention (a budget flush landing exactly on a multibyte boundary is not schedulable through the seam; it is cross-referenced as v8-ignored), and the late-rejection settled guard (a rejection arriving after the run has already settled cannot be deterministically constructed from the seam), and the log-fragment seal (a 25 M single-character drip that would OOM is not deterministically constructible in CI; the in-tree case only asserts it completes and truncates), and the unknown-binding preview cap (the whole-target JSON.stringify peak is a transient allocation inside the reply path — its only seam-observable trace is peak memory under a forged near-ceiling global/name, not measurable through the seam; the in-tree case only asserts the run completes).

Decision

Independent corrections, each in the package that owns the defect.

The unknown-binding preview is escaped from a 1 KiB prefix

The unknown-binding reply built its message with JSON.stringify over the WHOLE capped target (global + . + name, each up to maxValueBytes code units) — the escaped form could reach ~6x the input under control-heavy fields, a multi-hundred-MB spike near the maxValueBytes ceiling that no hostile-peer bound would have admitted. The preview is now escaped from a 1 KiB prefix of the target (enough to identify the binding); capMessage still enforces the reply budget.

A merged open-log entry is billed once, split across its fragments

An explicit flush() of an unterminated line emits a log frame with open: true, and the host appends the next frame to the SAME entry (print('a', end='', flush=True); print('b') reads back as one 'ab' entry, not a fake newline). The split-billing arithmetic — first fragment pays quotes+content+separator, continuations and the closing frame pay content only, host caps logBudget - 1/logBudget + 2, the sub-2-byte walk guard, the child's _open_started keying — is stated once, in the fd-3 protocol note's wire-contract section.

Boot-write failure no longer rejects run()

In src/index.ts the fd-3 boot-frame write is the last statement of run()'s synchronous setup. Its catch calls finish(), and finish() reads wallTimer and onAbort and — through settle()live. Those bindings are const and were declared AFTER the boot-write, so on a synchronous write failure finish() touched them in their temporal dead zone and threw a ReferenceError. That escaped the Promise executor and REJECTED run(), violating the seam's "outcomes resolve" contract: the caller saw a thrown error instead of the worker-exit the catch constructs. The boot-write block is now emitted after wallTimer, onAbort, and live are initialized, and the /* v8 ignore */ that had hidden the branch from coverage is removed so the catch is measured.

Log capture is serialized against settlement

In py/bootstrap.py the settlement flush_out()/flush_err() on the main coroutine read and clear each stream's _pending list and mutate the shared LogBuffer ledger. Model code may start daemon threads whose print/write mutate the same state concurrently. Capturing the bound method (out_stream.flush_line) fixed only WHICH callable settlement invokes, not what it reads mid-flight: an interleaved flush could join a _pending list being mutated under it, corrupting the ledger and costing the done frame — stranding the run to the wall clock. LogBuffer now owns one re-entrant lock shared by both streams; _LogStream.write and flush_line, and LogBuffer.push, take it, so the whole read-modify-write is atomic across threads.

Fd-3 residual is copied, not viewed

Also in src/index.ts, after the newline loop over a Buffer.concat of the pending fd-3 chunks, the leftover partial line was carried forward as the subarray VIEW it was sliced to. A view keeps the entire concat backing allocation alive, so a large frame followed by a tiny trailing fragment pinned a whole frame's worth of memory while pendingBytes — set to the fragment's length — reported far less than was retained. The residual is now detached into a fresh right-sized Buffer via the exported detachResidual helper, letting the concat allocation be collected and keeping pendingBytes an honest measure.

Output-cap load bound is parse-cap minus envelope, not divided by six

The load-time check that rejects a maxLogBytes/maxValueBytes larger than one fd-3 frame can carry divided the frame ceiling by six for worst-case escape expansion. But both budgets are metered in ALREADY-ESCAPED serialized bytes — the host log ledger charges the serialized cost via jsonStringCostUpTo (which walks to the cap without allocating the escaped copy), checkDoneValue measures the escaped form, and the producing-side _cap_message also caps by serialized cost — so a payload admitted under the cap occupies at most cap + envelope on the wire; escaping is inside the charge and must not be multiplied in again. The bound is now FRAME_PARSE_CAP_BYTES - FRAME_ENVELOPE_BYTES (the receive path rejects raw frames past the 64 MiB parse cap before decoding — the run settles as a worker-exit — so a budget must not exceed what an honest child's frame can carry through that parser), and the unused MAX_JSON_ESCAPE_EXPANSION constant is gone. The old bound was not unsafe — it under-admitted — but it silently forbade legitimate large caps. The same load check also rejects a NON-INTEGER maxLogBytes/maxValueBytes: the child reads each budget through int(...), which floors a float, so maxLogBytes: 3.5 would truncate at 3 bytes child-side while the host meters the fraction — the two sides enforcing different public config. Rejecting the float at load keeps them in step, matching the worker backend.

Same-group survivors are reaped before the fiber goes quiescent

A model program can leave a descendant in the child's OWN process group (no setsid, so kill(-pid) reaches it) that ignores SIGTERM but releases the inherited stdout/stderr/fd-3 pipes. The leader then exits, its close fires because the pipes drained, and settlement runs while that descendant is still alive. kill() arms an unref'd SIGKILL timer after SIGTERM; the fix is that settle() no longer resolves the run's finished promise — nor drops the run from live — immediately when an escalation is in flight. Instead, when killing is set and the process group is not yet empty (process.kill(-pid, 0) does not throw ESRCH), it polls the group on a REF'd timer, bounded by graceMs + CLOSE_REAP_MARGIN_MS, and both drops the run from live and resolves finished only once the group has emptied. The ref'd poll is the load-bearing part: it keeps the host event loop alive until the SIGKILL has actually reaped the group, so even a short-lived host — a one-shot headless run, a config subprocess — cannot exit and reparent the survivor to init. Deferring the live removal is what makes a dispose() racing a just-resolved run() still await the survivor: dropping the run from live at settlement (before the reap) would let teardown snapshot an empty set and return while the descendant lived. In the normal case (the leader was the only member) the first probe returns ESRCH and settlement finalizes with zero added latency. teardown() awaits each run's finished, so disposal is genuinely quiescent, matching its JSDoc — including for a run that already resolved.

Settlement also CANCELS the SIGKILL timer the moment the group is confirmed empty (the normal path, and when the poll sees the survivor gone). Leaving it armed would expose a PID-reuse hazard: a kill(-pid) left pending for up to graceMs after the leader was reaped could hit a RECYCLED pgid once the kernel reused the leader's pid, SIGKILLing an unrelated group (killGroup swallowing ESRCH does not help — the danger is precisely the kill that SUCCEEDS against a reused group). Clearing it on the empty probe bounds the reuse window to only the genuine-survivor case, where the group cannot be empty to reuse.

The window the cleared timer cannot cover is closed by an IDENTITY check inside killGroup. Every signal it sends is a raw process.kill(-child.pid, sig), which — unlike child.kill() — has no handle guard, so it would reach a recycled pgid during the interval between the leader being reaped and close firing (measured at 3039 ms with a pipe-holding descendant). The leader's start time is therefore read once at spawn (/proc/<pid>/stat field 22) and re-read before each signal, with two rulings: a reading that is PRESENT AND DIFFERENT means the number now belongs to another process, so the signal is withheld; an ABSENT reading means the leader was already reaped, which is the ordinary case for every escalation — its /proc entry is gone while the group it led can still hold the survivor this teardown exists to reap — so the signal proceeds. Absent is also the constant reading on a platform with no /proc, where the guard is inert and the pre-existing behavior stands. Reading absent as a mismatch is not hypothetical: the first version did, which withheld the grace SIGKILL and the poll deadline's SIGKILL, and the three same-group heartbeat cases went red on the Linux coverage lane while passing on Darwin, where the reader always returns undefined.

The reap poll also handles a host event loop BLOCKED past both timers. If a synchronous computation holds the loop from before the poll was scheduled until after its deadline, both the poll timer and the grace-window SIGKILL timer are overdue when the loop resumes, and Node runs the earlier-scheduled poll first — so the grace SIGKILL may never have fired. The deadline branch therefore sends SIGKILL ITSELF (idempotent if the timer already ran) rather than cancelling the unfired escalation, then grants ONE more CLOSE_REAP_MARGIN_MS and keeps polling until the group is confirmed empty, because finalizing on mere signal delivery would declare quiescence while the group is still dying. The outer bound on the wait is therefore graceMs + 2 * CLOSE_REAP_MARGIN_MS. A final hard bound finalizes if that extra margin elapses with the group still non-empty; that branch carries a /* v8 ignore */ because it is reachable only where a SIGKILL'd survivor lingers as a zombie and is never wait()'d — a container whose PID 1 does not reap orphans — which cannot be built deterministically across CI platforms. The ignore's reason states that environment dependence rather than claiming the branch cannot run, cross-referencing the Alternatives entry that rejected the signal-0 reap assertion for the same reason.

RLIMIT clamps against the inherited soft limit, not only the hard

In py/bootstrap.py _clamped bounded a requested (soft, hard) rlimit pair by the inherited HARD limit alone. A deployment that inherited a soft limit below the requested one — say inherited (100, 200), requested (150, 160) — got back (150, 160), RAISING the effective soft from 100 to 150: for RLIMIT_AS that loosens the memory ceiling, for RLIMIT_CPU it defers SIGXCPU, both violating "strictest of configured and inherited". _clamped now clamps each side against its own inherited counterpart (RLIM_INFINITY imposing no ceiling), then pins soft under hard so setrlimit never sees an inverted pair. The settlement-time CPU recheck (die_if_cpu_exhausted) follows the same rule: it compares spent CPU against the EFFECTIVE clamped cpu_soft, not the configured cpuSeconds, so a program that traps SIGXCPU and burns past a stricter inherited soft before returning is reported as a timeout rather than a false success. The recheck restores SIG_DFL BEFORE unblocking a program-masked SIGXCPU (pthread_sigmask(SIG_UNBLOCK, ...), captured at import): a program that installed a custom handler AND masked the signal would otherwise have that pending handler run at the unblock — in model code, able to re-mask or raise — so the disposition must already be SIG_DFL when the signal is released; with SIG_DFL first the pending signal kills inside the kernel with no bytecode window, and the kill re-raise is the fallback for the never-pending case. The SIGXCPU diagnostic no longer names the configured cpuSeconds as the effective budget — under a stricter inherited soft that number is wrong — and instead reports that CPU time was exhausted at "at most the configured N seconds", which holds whichever limit fired.

Concurrent binding replies are paced against fd 3

sendReply ignored proto.write's false return, so a program resolving several large values in one asyncio.gather round encoded every reply in the same turn and queued all of them in fd 3's writable buffer. Binding resolution carries no seam-level byte cap to bound that, and the failure kills the HOST process rather than failing the run: measured on a 64 KiB-highWaterMark pipe, eight 4 MiB replies buffered 32.0 MiB at once. Replies now go through a queue that encodes and writes one frame at a time, awaiting drain when the pipe is full, which measured a 0.0 MiB peak for the same shape. The encode happens inside the loop so a queued reply the run no longer needs is dropped by the settled check without ever being serialized. The same settled predicate also guards the reply callback AFTER await fn(...) but BEFORE snapshotJsonValue, so a wide value that resolves after settlement is dropped before its width is walked — the host does not expand a late value for a run whose outcome is already fixed.

Pacing changes nothing the model can observe. The child matches each reply to its call by id from a pump that reads fd 3 continuously, so arrival order was never observable, and the bindings themselves still run concurrently -- only the host's peak memory and the flush timing change. That is also why serializing is not a narrowing of the seam's concurrency contract, which was the reason this was first deferred; that reasoning was wrong.

Binding replies complete on the calling loop's thread

Also in py/bootstrap.py, a binding reply Future is created on the loop that ran dispatch. When the model calls a binding from a worker THREAD via asyncio.run(tools.x(...)), that Future belongs to the thread's loop, not the main loop where _pump_replies reads the reply. asyncio.Future is not thread-safe: completing it from another thread does not wake its own loop, so the direct set_result/set_exception left the awaiting thread stranded and the run degraded to a wall-clock timeout. Each pending entry now records its Future's loop alongside the Future, and _pump_replies completes it via that loop's call_soon_threadsafe. The shared pending/next_id state is guarded by a threading.Lock held across the id claim, the fd-3 write, and the counter advance, so concurrent callers cannot interleave frames out of the id order the host requires. call_soon_threadsafe onto a loop that has already CLOSED (the worker thread finished and abandoned its call before the reply arrived) raises RuntimeError; that schedule is wrapped so the moot reply is dropped rather than letting the exception end the pump task and strand every later reply.

The blocking frame reader reads in chunks, not byte by byte

ProtocolChannel.read_frame — used for the boot and run handshake frames — read through FileIO.readline() on the unbuffered (buffering=0) fd, which issues one os.read(1) per byte. The run frame arrives AFTER RLIMIT_CPU is in force, so a legitimate multi-megabyte program burned seconds of CPU in millions of single-byte syscalls before ast.parse ran — potentially exhausting the budget on the read alone. It now reads in _READ_CHUNK_BYTES chunks into the same _pending residual buffer the async reader already uses (the wrapping os.fdopen object is gone; both readers call os.read(self._fd, ...) directly), so the read cost is trivial and read-ahead past a newline is preserved for the next frame. Both readers track a running scan offset (find(b"\n", scanned)) so a large frame accumulated across many chunks is scanned once, not re-scanned from index 0 per chunk — a chunked rescan would have replaced the byte-at-a-time cost with an O(N²) memchr cost on the same large-frame path.

Synchronous spawn failure resolves worker-exit, not reject

Also in src/index.ts, spawn is called before the settlement Promise executor exists. Node defers only a fixed set of spawn errnos (EACCES, EAGAIN, EMFILE, ENFILE, ENOENT) to an asynchronous error event, which the settlement path already turns into a worker-exit; every other errno throws SYNCHRONOUSLY from spawn. A pythonBin longer than the platform PATH_MAX passes the load-time validation (non-empty, no NUL) but makes spawn throw ENAMETOOLONG here — outside the executor — so run() REJECTED instead of resolving, violating resolve-don't-reject, and left this run's just-materialized staging directory on disk since only settle() removes it. The spawn call and the fd-3 narrowing are now wrapped: a synchronous throw removes the staging directory and resolves the same worker-exit class (python spawn error: …) the async error event produces.

Interpreter selection and the child environment settle at load

pythonBin resolves once at plugin load to an executable absolute path and is version-probed under the same scrubbed environment used for runs. The provider requires CPython 3.10 or newer and retains that exact path, so a later PATH or working-directory change cannot switch interpreters; an explicit path that is not an executable regular file, an unresolved basename, or an unsupported interpreter fails before ctx.codeRuntime registers. The synchronous probe has a fixed five-second deadline and sends SIGKILL at that deadline, so a wrapper that ignores SIGTERM cannot block plugin load. Each probe and run receives only TMPDIR: macOS system Python needs it to avoid emitting a startup warning into captured stderr, while credentials, PATH, HOME, and every other ambient host value remain unavailable to model code. If the validated executable disappears after activation, the ordinary spawn settlement still resolves worker-exit.

Stray pipe output is aggregated by line, not by transport chunk

Also in src/index.ts, native stdout/stderr bytes (C-extension writes, os.write past the pipe buffer) were pushed to logs one entry per Node data chunk. logs entries are joined with \n downstream (PTC mode), so a single newline-free write larger than one pipe read — arriving as several data chunks — read back with model-visible newlines inserted at arbitrary transport boundaries. Capture now accumulates raw Buffer chunks (the same shape as the fd-3 reader, and for the same reasons: a string += accumulator re-copies the whole residual per chunk and scanning it from index 0 each chunk is a second quadratic — both O(N²) on a large newline-free write), splits on the raw 0x0a byte, and admits one entry per complete line. A newline never appears inside a UTF-8 multibyte sequence, so decoding each split line is safe without a streaming decoder. Three separate bounds keep the residual from exhausting host memory, each mirroring the fd-3 reader: the fragment list SEALS into finished blocks past MAX_PENDING_CHUNKS so a program pacing single-byte os.writes cannot accumulate millions of live Buffer objects (whose per-object overhead no byte count sees); the residual is flushed when the COMBINED running SERIALIZED cost of both pipes — tracked through accrueStrayCost, which decodes UTF-8 structurally across chunks so a byte that renders as U+FFFD is charged the three bytes that replacement character serializes to — would cross the budget, so a control-char or illegal-UTF-8 flood flushes at a fraction of the raw bytes rather than accumulating a full budget's worth of raw bytes first, and stdout and stderr are metered together rather than each against the full budget (which would let both retain nearly a budget's worth at once, doubling the peak); and once the ledger has truncated, buffering stops so nothing accumulates for output that can never be admitted. accrueStrayCost charging illegal bytes their U+FFFD width is the fix for a byte that never begins a valid sequence (0x80–0xC1, 0xF5–0xFF), a multibyte sequence that breaks before completing, or a structurally-complete but ILLEGAL sequence: toString('utf8') renders each of those bytes as its own U+FFFD (3 bytes), so it validates each lead's first-continuation range (WHATWG: E0→A0-BF, ED→80-9F, F0→90-BF, F4→80-8F, others 80-BF) and charges 3 per byte of any sequence outside it. Charging the raw 1 undercounted a b"\xff" flood threefold, and charging only the structural width undercounted a CESU-8 surrogate (ED A0 80) or overlong (E0 80 80) threefold just as cheaply, letting the residual grow to a full budget's worth of raw bytes before flushing and, near a large maxLogBytes, expand toward a ~1 GiB peak in the flush's concat plus toString. The per-entry charge on the admitted string is metered by SERIALIZED cost through jsonStringCostUpTo, which walks the string to the cap and stops — the previous Buffer.byteLength(JSON.stringify(text)) allocated the whole escaped form first, so a near-budget control-char-dense line under a large maxLogBytes could momentarily allocate over a gigabyte just to measure it. jsonStringCostUpTo (the string-walking function, reached by a forged log frame whose text JSON.parse produced) charges a LONE surrogate the full six escaped bytes (\uXXXX under ES2019 well-formed JSON.stringify), not the three bytes Buffer.byteLength reports for its U+FFFD rendering, so a \ud800 flood is not undercharged by half; accrueStrayCost walks raw bytes and never sees a surrogate as such — a CESU-8-encoded surrogate reaches it as three bytes its per-lead range check rejects, each charged 3 (total 9), matching what toString('utf8') renders. The residual is flushed on the pipe's end and also explicitly in the closeDeadline handler before it destroys the streams: a setsid escapee holding the pipes open forces settlement through that path without an end, so a final newline-free os.write(1, …) the leader emitted before exiting would otherwise be dropped from logs.

An incompatible output-budget/addressSpaceMb pair is rejected at load

The child (py/bootstrap.py) builds, charges, and frames a maxLogBytes log entry or a maxValueBytes completion value under RLIMIT_AS, and both ledgers trigger on CHARACTER count against a serialized-BYTE budget. An astral character is one character but four bytes of CPython str storage and four UTF-8 bytes, and the heaviest path holds THREE such copies at once: a single sys.stdout.write(line + "\n") keeps the caller's text argument (alive for the whole write call, ~4×), the line slice handed to LogBuffer.push (~4×), and the text.encode("utf-8") copy _push_locked takes to charge and ship it (~4×) — a peak of ~12× the budget. The settlement flush_line path holds only two (its "".join(...) and that encode copy — it drops the pending chunks before pushing), so the newline path is the binding worst case. When a budget approaches addressSpaceMb, a LEGITIMATE near-budget output breaches the address space during that build-and-encode and dies as worker-exit instead of truncating (log) or failing as output-limit (value). Metering every child write against the address space at runtime is the wrong fix: an exact serialized-cost check on the hot path is either a full encode (the allocation being avoided) or a per-character Python loop (which burns the CPU budget — a 10 MB legitimate write hits SIGXCPU under cpuSeconds: 1). Both trade one resource bound for another. Instead src/index.ts rejects the incompatible pair at LOAD: each budget times OUTPUT_BUDGET_WORST_CASE_ADDRESS_SPACE_MULTIPLE (twelve — the three simultaneous ~4× copies of the newline path) must fit the address space LEFT after a fixed INTERPRETER_BASELINE_BYTES reservation for the interpreter's own footprint, with a >= so a budget whose worst-case peak exactly equals that room is rejected (that peak plus the reserved baseline is the whole address space, the RLIMIT_AS edge). flush_line was also made to drop the pending chunks BEFORE its push, matching the newline path's join-clear-push order, so it holds at most the join and its encode rather than three copies. The baseline is reserved SEPARATELY from the multiple because it is a fixed cost, not one that scales with the budget: folding it into the multiple would leave a budget sized right at addressSpaceMb / 12 admitted while its peak plus the interpreter still overran. Both maxLogBytes and maxValueBytes are gated symmetrically; the value path's build-and-encode is the same shape. The check runs on every platform, not just where RLIMIT_AS is enforced: the incompatibility is a property of the config values, so a Linux deployment OOMs regardless of the host that assembled the config, and a uniform load-time rejection is the fail-loud contract (Darwin skips only the runtime setrlimit). This eliminates the class at the config seam rather than patching the write path, so _LogStream keeps its original character-count buffering (a valid lower bound on serialized cost, memory-safe once the budget fits the address space). The value path enforces the same discipline in a second place: _check_done_value (the byte meter) and _encode_json_plain (the frame encoder) walk in O(DEPTH), not O(width). Each container pushes ONE cursor frame that pulls its children one at a time rather than one traversal tuple or stack entry per child — a flat [0] * 6_000_000 serializes to ~12 MB but a per-element walk allocates ~400 MB of bookkeeping (~28× the serialized size, far past the 12× the gate reserves), so a value the meter admits could OOM on the walk's own frames. With the cursor, the only width-proportional allocation is the output string the meter already bounded.

The host gate validates against the CONFIGURED addressSpaceMb, but a launch environment can inherit a STRICTER RLIMIT_AS (a ulimit -v wrapper below addressSpaceMb), which the bootstrap's _clamped correctly lowers the EFFECTIVE limit to — leaving the budgets sized for a ceiling the child never gets. So bootstrap.py re-checks both budgets against the effective clamped soft limit after applying it, mirroring the host gate's multiple and baseline, and raises at boot (caught by the setrlimit-phase handler and reported as exception, the same class as any other resource-limit-application failure) rather than letting a near-budget output OOM mid-run. The two child constants are kept in step with the host's by the shared reasoning, not a wire field.

One residual write-path copy is fixed alongside, independent of the config gate: _LogStream.write's newline branch buffered the whole unterminated tail after the last newline (text[pos:]) into _pending before the flush trigger could bound it, so an early newline followed by a huge tail ("\n" + "A" * 30 MiB) made a second full copy of the model's own string — the RLIMIT_AS death the path exists to avoid, and one the config gate does not cover because the tail can far exceed maxLogBytes. The tail is now sliced to a remaining + 4-character prefix (anything past remaining characters cannot be admitted, the char count being a lower bound on the serialized cost), which the flush trigger then rejects with the marker.

The completion value and error are pre-encoded at their validation point

In py/bootstrap.py, _done_with_value now returns the whole terminal frame as a PRE-ENCODED JSON string on the success path: the admitted value is serialized once here, at the validation point inside _run's try, as '{"type": "done", "value": ' + _encode_json_plain(value) + "}". The program can keep mutating a returned list/dict from a daemon thread or signal handler after it returns, so a second traversal held at a later point would be a TOCTOU — a concurrent mutation into a non-JSON type would let that later encode throw outside the settlement handler and downgrade a settled run host-side to worker-exit. Serializing once, inside the try that wraps this call, closes the window: if a concurrent mutation makes the encode throw, the exception handler classifies it as an exception, and once the string is produced the frame is written verbatim with no further touching of the live value. _run binds the _done_with_value ENTRY NAME into a local (done_with_value_bound) before the program runs, and _done_with_value itself binds _check_done_value and _encode_json_plain as DEF-TIME default arguments — so a __main__ rebind of the entry name or those two names after model execution cannot rewrite a legitimate success into an exception. The log ledgers (host logBudget and child _remaining) start ONE byte below the budget, reserving the serialized outer-array envelope (two brackets and n-1 commas over n entries' separators), so a result that exactly exhausts the ledger still serializes within the configured cap; the truncation marker remains envelope, not payload. Once a ledger has truncated, the host clears both stray pipes' buffered output wholesale (every later byte would be no-op'd by admit, so retaining it would spend host memory on output that can never be admitted); the child runs -u so sys.__stdout__/sys.__stderr__ writes are visible to stray capture immediately, and the settlement flush still drains the original std streams before the done frame (a guard against a buffered wrapper surviving a sys.__stdout__ = boom rebind). The constructor rejects a maxLogBytes below 64 (the smallest budget with one byte of room for the truncation marker's own serialized form); maxValueBytes keeps only the positive-integer requirement, since a completion can be a single byte and the done-frame envelope is seam protocol cost. The marker remains envelope, so a truncated run with admitted entries serializes to at most maxLogBytes + marker + envelope (recorded in the package README). A rebind of a transitive dep the encoder reaches (e.g. _dump_scalar/_dump_string/json/io — a non-exhaustive set) can still make the encode throw and downgrade a success to an exception, which is registered as an accepted residual in the package README.

send_done (a local function inside _run) writes the pre-encoded string through a BOUND channel.write_encoded, and encodes a dict error frame through a bound _encode_json_plain before writing it — it never calls channel.send_sync, whose body re-resolves self.write_encoded and the module-level _encode_json_plain at call time. _encode_json_plain and channel.write_encoded are bound into locals before the program runs, for the same reason flush_out/flush_err/safe_model_traceback are: the program runs as __main__, so import __main__; __main__.ProtocolChannel.send_sync = boom or __main__._encode_json_plain = boom would otherwise re-resolve the send/encode to a rebranded callable at call time and, when that replacement raises, skip the done frame and downgrade a settled verdict to a host-side worker-exit.

A budget flush retains an unfinished trailing multibyte sequence

Also in src/index.ts, flushStray(stray, retainPartialTail) withholds an unfinished multibyte tail from the decode on the BUDGET-triggered flush (the combined-cost threshold in captureStray): when the residual ends on a partial UTF-8 lead sequence (stray.utf8.expected > 0), the leading byte plus the continuations consumed so far (≤3 bytes) are detached from the frame as the new residual, and only the complete prefix is admitted and decoded. Nothing is admitted when the whole residual is a single unfinished sequence, so a legal, un-finished character is never rendered as U+FFFD in a released, un-truncated entry, and no bogus empty entry is pushed. The withheld tail is re-accrued from a FRESH stray.utf8 state — metering it against the post-flush expected > 0 state would charge the carried lead byte as an illegal continuation — so the next chunk continues the walk correctly and the pipe's cost/UTF-8 state is rebuilt over the retained tail. The end/closeDeadline paths pass false and decode the FULL residual unchanged, because there a trailing incomplete sequence is real truncated input and the U+FFFD is the honest render.

A late binding rejection returns before formatting the error

Also in src/index.ts, the binding-rejection catch branch now checks settled and returns BEFORE formatting messageOf(error). A rejection that arrives after maxWallMs, an abort, or dispose has already settled the run would otherwise have messageOf(error) run hostile toString/message getters — spending host heap and time on a run whose outcome is already fixed — before sendReply peeks at settled. Dropping the framed reply early spares that waste. The running loop's otherwise-mostly-linear reply drain also reads by a head cursor into the queue array instead of shift()ing each entry, so a large asyncio.gather of wide bindings awaiting fd 3's drain drains in linear time rather than O(n²) from repeated re-slicing.

A newline-free drip seals its fragments; the CPU soft limit is kept below the hard; the done frame falls back to a fixed literal; the reply queue clears consumed slots

In py/bootstrap.py, _LogStream now seals the pending-fragment list past a cap: a newline-free drip of one character per write would otherwise accumulate one list slot (and one str object) per call, and under a large maxLogBytes a 25 M single-character flood OOMs on its own accounting (plus the same-size list _push_bounded_prefix then builds) before the byte budget is reached. Past _PENDING_MAX_CHUNKS the current fragments are joined into ONE block moved to a _pending_blocks list (the character count is unchanged), bounding the live fragment count exactly as the host-side captureStray seal does; the join is only the ≤cap current fragments, never the whole accumulated buffer, so a large drip stays O(B) rather than re-copying the growing block O(B²/cap) times. The HOST-side open hold mirrors the same seal: a budget-sized single-character open flood (print('x', end='', flush=True) in a loop is an honest-child path) would otherwise accumulate one fragment array slot plus string object header per frame — ~30× overhead the byte cap cannot see, up to ~2 GB of host auxiliary heap near the maxLogBytes load ceiling. Past MAX_PENDING_CHUNKS the held fragments coalesce into openSealed; the closing-frame merge, truncateLogs, and the finish residual all read sealed + current fragments and clear the seal.

_clamped also lowers a clamped RLIMIT_CPU soft limit that EQUALS the hard by one unit (when the hard is at least 2). A ulimit -t N sets both, and with soft == hard the kernel checks the hard limit in the same tick and SIGKILLs a busy loop directly, so SIGXCPU is never delivered — and the host classifies a CPU overrun ONLY on signal === 'SIGXCPU', so a definite budget exhaustion would be misreported as a worker-exit. Lowering the soft one unit gives SIGXCPU a window to fire, so the overrun is reported as a timeout. This is scoped to RLIMIT_CPU (a one-byte soft differential on RLIMIT_AS would only misalign the child's applied limit with the host budget gate, with no signal to preserve). The hard >= 2 guard leaves a hard == 1 blind spot — a 1-second dual limit cannot lower the soft to 0, so a definite overrun there is still reported as worker-exit.

send_done wraps its encode+write in a try and, on any throw from a rebound transitive name (_dump_scalar/os), writes a fixed pre-encoded done frame via the _run-local bound _os_write/_memoryview/_FALLBACK_DONE_FRAME — so a settled exception verdict is never downgraded to a worker-exit, and the host still gets a verdict. The reply queue's head-cursor drain clears each consumed slot so a wide written payload is released immediately, bounding host memory to the current backlog under sustained fd-3 backpressure. The exception classes the settlement-path except clauses catch are likewise bound before any model code runs: _BaseException is a _run LOCAL and a closure cell in _make_failure_reporter; _RuntimeError, _BindingRejection, str, and bool are DEF-TIME default arguments of _pump_replies (a body-local X = X binding is too late — the model's top-level statements run before the pump's first step). A rebind of __main__.BaseException cannot make a program exception escape the handler and lose the done frame; a rebind of __main__.RuntimeError (or _BindingRejection/str/bool) cannot make a closed-loop scheduling failure escape the pump catch and strand every later reply to the wall clock.

Testing

  • tests/runtime.spec.ts rejects absent, non-executable, non-CPython, pre-3.10, and unresponsive interpreter configurations at load; changes PATH after activation to prove the resolved executable is frozen; removes that executable after activation to preserve the late worker-exit path; and asserts a running program sees TMPDIR but not PATH, HOME, or DEEPSEEK_API_KEY. The native-output case pins each source stream's order without requiring a total order across independent channels, and the Darwin resource-limit cases state or skip the platform-specific RLIMIT_AS behavior.
  • snapshots/session/ptc-python-turn replaces the headless PTC worker provider with the private Python provider through the real Loader, replays a Python run_code program over real bash bindings, and pins the Python SDK prompt, tool schema, dispatch events, captured log, and completion value.
  • tests/boot-write-failure.spec.ts mocks spawn so the fd-3 pipe throws on the boot write — the one path a real subprocess cannot be coerced into — and asserts run() resolves a worker-exit rather than rejecting. A sibling case makes the mocked spawn throw SYNCHRONOUSLY and asserts run() still resolves a worker-exit and removes its staging directory, keyed off the exact bootstrap path the mocked spawn received in its argv so a sibling worker's concurrent staging cannot flake it. Both are isolated in this spec so the real-subprocess suite is untouched.
  • tests/residual-detach.spec.ts unit-tests detachResidual: the carried copy equals the residual, owns a backing store sized to its own length (fixture kept above Node's Buffer pool threshold), and does not share the source frame's ArrayBuffer.
  • tests/runtime.spec.ts — the output-cap case asserts the parse-cap - envelope bound (67108800) and its message. A daemon-thread case drives four threads emitting unterminated writes through settlement's flush. A native-write case writes 200 KiB with no newline via os.write under a raised maxLogBytes and asserts it reads back as EXACTLY one log entry (proving stray output is aggregated by line, not split at pipe-chunk boundaries); a companion writes b"one\ntwo\nthree" and asserts three entries (proving real newlines still delimit). A newline-free-flood case writes 2 MiB under a 4 KiB maxLogBytes and asserts the capture ends at the truncation marker and stays under budget (proving the residual is bounded by the ledger, not buffered whole); a NUL-flood companion writes 4000 newline-free NULs under the same budget and asserts truncation (proving the residual is charged by SERIALIZED cost, ~6× raw, measured without allocating the escaped copy); an illegal-UTF-8 case paces single-byte \xff writes under a 3072-byte budget with Buffer.concat wrapped to measure the peak merged buffer, asserting it stays under 2048 (charged at the U+FFFD width 3 the residual flushes near 1024 raw bytes; a raw-byte undercount would let it reach ~3072, so the bound discriminates); a CESU-8/overlong case paces the structurally-well-formed but illegal ED A0 80 one byte at a time and asserts the same peak bound (charged at the true 9 per sequence it flushes early; charging the structural width 3 triples the peak, so reverting the per-lead range check turns it red); a broken-multibyte case writes a 3-byte lead then a fresh ASCII byte in separate chunks and asserts both a captured A and a U+FFFD (exercising accrueStrayCost's cross-chunk broken-sequence branch); a post-truncation case writes a 108-byte payload (under the smallest PIPE_BUF, so one atomic write) whose first line exhausts a 64-byte budget and asserts the second line is dropped (exercising the post-truncation admit no-op in one data callback, no v8-ignore); a short-escape case writes a line mixing a tab, quote, backslash, a \uXXXX control, a multibyte character, and ASCII, asserting it round-trips verbatim (exercising every branch of jsonStringCostUpTo); a reassembly case writes a payload spanning every valid multibyte lead class (E0-range, plain 3-byte, F0, and F4) past the pipe buffer and asserts it round-trips with no U+FFFD (exercising accrueStrayCost's per-lead ranges and cross-chunk reassembly); a lone-surrogate case forges an fd-3 log frame flooding 1000 \ud800 escapes under a 4 KiB budget and asserts truncation (the count sits in the window where charging 3 bytes would admit and 6 bytes truncates, proving the surrogate is charged its full escaped width); a stray-sealing case paces 60000 single-byte newline-free os.write(1, …) calls under a raised budget with Buffer.concat wrapped to measure copy volume, asserting the trickle coalesces to one entry and the cumulative copy stays under a measured 256 KiB threshold (the sealed shape copies ~120 KB, the re-merge shape ~538 KB, so reverting the seal to a re-merge turns the assertion red — proving the fragment list seals into blocks past MAX_PENDING_CHUNKS). A closeDeadline-flush case has the leader write a newline-free diagnostic then spawn a setsid orphan holding the pipes open, and asserts the diagnostic survives in logs (proving the residual is flushed before the deadline destroys the streams). The same-group reap case spawns a SIGTERM-ignoring same-group descendant that releases the pipes and bumps a heartbeat file; the test asserts the heartbeat STOPS after the grace-window SIGKILL — an assertion robust whether the killed descendant is reaped or lingers as a zombie, so it holds where PID 1 does not wait() orphans. A dispose-after-resolve case asserts dispose() of a completed run with a same-group survivor returns only after the survivor stops executing (proving the run stays in live until its group is reaped), with an expect(afterDispose).toBeGreaterThan(0) guard so the frozen-heartbeat assertion cannot pass vacuously when the file was never written. A deadline case busy-blocks the event loop past both timers and asserts the survivor's heartbeat freezes (proving the poll's deadline arm sends SIGKILL itself rather than cancelling the unfired escalation). The cross-loop case runs a binding from a worker thread's own asyncio.run loop while the main coroutine yields with await asyncio.sleep, asserting the reply round-trips instead of timing out; a companion case abandons a thread's call so its loop closes, then answers it before a later binding — asserting the pump survives the closed-loop call_soon_threadsafe (host-gated ordering makes it deterministic, fail-before hangs the later binding to the wall clock). The inherited-soft-limit case runs the interpreter through a ulimit -S -t wrapper that sets a CPU soft limit below cpuSeconds and asserts the applied RLIMIT_CPU soft is the inherited value, not the configured one (CPU rather than address space, since macOS ignores ulimit -v); a companion inherits a 1 s CPU soft, has the program trap SIGXCPU and busy-loop past it, and asserts the settlement recheck reports a timeout — proving the recheck uses the effective soft, not the configured cpuSeconds. A control-heavy-diagnostic case raises a NUL-flood exception under a small maxValueBytes and asserts the serialized frame fits (proving the diagnostic is metered by serialized cost). A tail-copy case (maxLogBytes: 256, addressSpaceMb: 384) has the program build a tail in a variable and write "\n" + tail where tail is 150 MiB — construction peaks at ~2× (~300 MiB, within the address space, so the model's own allocation succeeds and any OOM belongs to the defect path), and the pre-fix whole-tail re-buffer added a third ~150 MiB copy past 384 MiB; the sliced prefix lets the run truncate and complete (Linux-only RLIMIT_AS repro, macOS happy path — the fixture's own construction must fit the address space, a general rule for these RLIMIT_AS cases). An output-budget/address-space case asserts a maxLogBytes of 50 MB AND a maxValueBytes of 50 MB each reject at load against a 256 MiB addressSpaceMb (past the room left after the interpreter baseline when multiplied by the worst-case 12) while the default caps against 512 MiB load, gating both budgets symmetrically; a discriminating case asserts a 48 MiB maxLogBytes against a 512 MiB addressSpaceMb rejects — 48×8 = 384 MiB fits the 448 MiB budgetable (the old 8× multiple wrongly admitted it) but 48×12 = 576 MiB does not. The ~12× peak the multiple covers is the NEWLINE path's single near-budget write — the caller's own string, the line slice, and the encode copy live at once; the settlement flush is no longer the binding case, because flush_line drops the pending chunks before its push and so holds two copies rather than three. An inherited-RLIMIT_AS case runs the interpreter through a ulimit -v 131072 wrapper with a 32 MiB maxLogBytes the configured 512 MiB addressSpaceMb admits, and asserts the boot re-check rejects it as an exception whose message names the inherited RLIMIT_AS (the 128 MiB inherited limit leaves too little after the baseline; Linux-only, macOS ignores ulimit -v and the run proceeds). A non-integer-budget case asserts a fractional maxLogBytes/maxValueBytes rejects at load. A combined-peak case (maxLogBytes: 32 MiB, maxValueBytes: 32 MiB, addressSpaceMb: 512 — each budget admitted alone at 12×) writes ~33M newline-free astral characters (buffered, unflushed) then returns ~33M astral characters, and asserts the run settles as output-limit (the value is itself over its 32 MiB budget); pre-fix the unflushed log pending plus the value's build-and-encode peak added past the 512 MiB address space and OOM'd, so flushing the logs before framing the value is what lets the value check complete (Linux-only RLIMIT_AS repro; on macOS the over-budget value reports output-limit under both orders). A wide-value case (maxValueBytes: 20 MiB, addressSpaceMb: 384) returns [0] * 6_000_000 — ~12 MB of JSON, under the 20 MiB budget, so it must round-trip; pre-fix the O(width) walk allocated ~400 MB of per-element traversal tuples and encoder stack entries (~28× the serialized size, past the 12× the gate reserves) and OOM'd on a value the meter admitted, while the O(depth) cursor keeps the only width-proportional allocation the output string itself (Linux-only RLIMIT_AS repro; the fixture stays within the address space so it is honest on macOS too). A wide-BINDING-ARGUMENT case (addressSpaceMb: 384) calls a binding with [0] * 6_000_000 and asserts the length echoes back: _lossless_json_violation runs on model-built arguments that no child-side budget bounds first, and its per-member tuples measured 459.1 MiB against 0.0 MiB for the cursor. A backtracking case returns a 4 MiB string from a binding and asserts it round-trips: the old scalar regex retained engine state proportional to the string's width (146 MiB at 1 MiB, 557.8 MiB at 4 MiB, past the default 512 MiB), which raised MemoryError inside _pump_replies and stranded the call to the wall clock. A control-heavy metering case returns 8M NULs under a 16 MiB maxValueBytes and asserts output-limit, not exception: charging by counting instead of materializing the escaped form measured 19.1 MiB against 228.9 MiB for an identical byte count. An addressSpaceMb-baseline case asserts 64 MiB and 32 MiB reject at load with a message naming addressSpaceMb, rather than the budget loop's negative admissible limit. A process-identity case asserts the leader's start time reads stably on Linux and reports undefined on Darwin, the guard that keeps a recycled pgid from receiving this run's SIGTERM. A paced-replies case resolves eight 4 MiB values in one asyncio.gather round and asserts the frames round-trip; the peak the fix removes (32.0 MiB buffered → 0.0 MiB) lives in the host's fd-3 writable buffer, invisible through the seam, so this case pins the round-trip and the no-regression match but is measured for its peak only out-of-tree. A late-drop case settles the run on maxWallMs and resolves the pending binding afterwards, asserting a timeout result, an undefined value, and that the late path actually ran — the three assertions all hold pre-fix because sendReply already dropped after-settlement values, just later, so the case pins the ordering, not a seam-observable behavior. A binding-all-names case (rebinds every name the failure path uses) asserts a real ValueError survives send-done binding — a tested fix pinned by a case that rebinds __main__.ProtocolChannel.send_sync, __main__.ProtocolChannel.write_encoded, and __main__._encode_json_plain — the three names the shipped send_done would resolve late if it looked them up at call time — and pins the done frame against that call-time look-up skipping it; the done-value TOCTOU pre-encoding, the stray-UTF-8 budget-flush retention, and the late-rejection settled guard are counted among the ten no-fail-before fixes (reasons in the Problem section), not pinned by a fail-before test. A fragment-cap drip case writes 200 000 single-character newline-free sys.stdout.write calls and asserts the run completes with a truncation marker rather than a MemoryError (no-fail-before: the 25 M-scale OOM is not deterministically constructible in CI). A dual-limit CPU case runs the interpreter through a ulimit -t 2 wrapper and busy-loops past it, asserting a timeout (the soft limit is lowered to 1 so SIGXCPU fires, not a worker-exit). A transitive-name rebind case rebinds __main__._dump_scalar, __main__.os, __main__._os_write, __main__._memoryview, and __main__._FALLBACK_DONE_FRAME and asserts a done frame still lands as an exception, not a worker-exit (the real message is replaced by the fixed fallback literal). A BaseException-rebind case rebinds __main__.BaseException to RuntimeError and raises ValueError, asserting the run still reports an exception, not a worker-exit (the catch uses a pre-program local exception class). A RuntimeError-rebind closed-loop case rebinds __main__.RuntimeError to ValueError as the first program statement and drives the closed-loop worker pattern, asserting the pump survives the dead-loop reply and delivers the later binding (the pump's _RuntimeError is a def-time default argument, so it captures the original before the rebind). A _done_with_value-rebind case rebinds __main__._done_with_value to a raising function and returns a legitimate value, asserting the run still reports the success (the entry name is a _run local bound before the program runs). A sys.__stdout__-flush case writes through sys.__stdout__/sys.__stderr__ without an explicit flush and asserts both bytes appear in logs (the -u unbuffered child plus the settlement drain of the original std streams). The host closes the child's stdin write handle immediately after spawn (the program is an async body that reads nothing from fd 0; a live pipe would hold a host-side handle open past the run, letting a setsid-escaped descendant inheriting fd 0 keep the host process alive). The channel's frame readers bind their decode primitives (_decode_json_plain, os.read, _READ_CHUNK_BYTES, bytes, and len; asyncio.get_event_loop on the async reader) as def-time default arguments, so a __main__ rebind cannot kill the reply pump; _decode_json_plain itself captures json.loads/the two regexes/len/isinstance/str/list the same way. The reply pump's frame reader is a BOUND METHOD captured by _run before the program runs and passed into _pump_replies as an explicit argument (a body-local channel.read_frame_async lookup would resolve a rebound class attribute, since the pump starts after the program's top-level statements). send_done's frame-shape check uses _run's bound _str/_isinstance (a program rebinding __main__.isinstance cannot make a legitimate success fall into the fixed-literal fallback). _make_error_class captures Exception and setattr as def-time defaults, and dispatch binds _lossless_json_violation/asyncio.get_event_loop/the channel's send and write primitives into _run locals (the frame WRITE goes through def-time bound write_encoded+_encode_json_plain rather than send_sync's call-time body, and the log sink directly through the bound encode+write primitives, not send_sync) — a rebind of those names before the first binding call cannot break a legitimate call. compile(wrapped, ..., dont_inherit=True) stops the module's from __future__ import annotations from stringifying the program's type annotations. A basename pythonBin that does not resolve on the CURRENT process PATH now fails at LOAD ('does not resolve on PATH', like the empty/NUL checks): the child spawns with env: {}, so falling back to the bare name would let execvp silently start a system interpreter from the platform default PATH — a product-visible change from the old run-time ENOENT worker-exit to an early, loud configuration error. The bootstrap resets SIGXCPU to SIG_DFL and unblocks it before any model code runs: the child inherits the host's disposition and mask, and a host that ignores or blocks SIGXCPU would let a program run past the soft RLIMIT_CPU until the hard limit's SIGKILL — classifying a definite overrun as worker-exit instead of timeout. (The settle-time enforcer already restores SIG_DFL for a program that traps or masks the signal mid-run; this closes the inherited-state gap.) The float encoder's Decimal(repr(value)).normalize() runs on a fixed module-level _FLOAT_CONTEXT = Context(prec=28) (constructed before any model code): the process-global decimal context would otherwise let a legitimate program's getcontext().prec = 2 silently round the completion value's digits or traps[Inexact] = True make the encode raise, misclassifying a successful run as an exception. A regression case mutates both knobs and asserts a float completion round-trips exactly. The host caps an fd-3 frame's RAW length at 64 MiB (FRAME_PARSE_CAP_BYTES) before toString/JSON.parse: the 256 MiB wire ceiling bounds the bytes, not the decoded structure, and a compact wide frame near it could decode to far more host memory. maxLogBytes/maxValueBytes are load-bounded to that parser cap so an honest child's frames always fit; a model-constructed binding argument above it is dropped (registered in the README). A frame-cap case writes 65 MiB of As plus a newline and asserts a worker-exit with the protocol-frame-exceeded message (the pre-join counter rejects a newline-free single frame; the first-frame check rejects a newline-bearing one BEFORE the join, keeping the peak at one copy of the wire bytes — verified fail-before by reverting to the unconditional counter). A _decode_json_plain-rebind case asserts a binding reply still round-trips; a stdin-EOF case has the program read fd 0 and asserts it sees EOF immediately (the destroyed write handle), verified fail-before by disabling the destroy. A frame-cap case writes 65 MiB of As plus a newline and asserts a worker-exit with the protocol-frame-exceeded message (the pre-join counter rejects a newline-free single frame; the first-frame check rejects a newline-bearing one BEFORE the join, keeping the peak at one copy of the wire bytes — verified fail-before by reverting to the unconditional counter). A multi-frame case lets two within-cap frames whose combined buffer crosses the cap both survive (the first-frame check, not the byte counter, decides), and a sealing-threshold case writes 64 MiB of 4 KiB atomic newline-free writes plus 12289 more bytes before the first newline, asserting the run is a worker-exit (sealing is the ELSE half of the newline branch, so the newline-bearing chunk always reaches the first-frame check). A pythonBin case resolves a basename against a PATH whose first entry is relative ('.') and asserts the absolute entry is used. An exact-limit case (maxLogBytes: 64) writes a 60-character line (62-byte JSON + 1 separator = 63 = the reserved ledger) and a 61-character line (64 > 63), asserting the first is admitted and the second truncates to the marker — the outer-array envelope reservation, pinned at the exact boundary; a companion case asserts maxLogBytes: 61 rejects at construction. A syntax-label case asserts a parse-time syntax error carries File "<model>" (ast.parse passes the same source label as compile and the runtime traceback filter). A SIGXCPU-mask case masks SIGXCPU (pthread_sigmask), burns past the soft limit, and returns, asserting a timeout (the recheck unblocks before re-raising); a trap+mask companion installs a custom handler that re-masks and asserts the same timeout (SIG_DFL is restored before the unblock, so the pending signal kills inside the kernel).

Alternatives considered

Leave the boot-write /* v8 ignore */ and fix only the ordering. Rejected: the ignore is what let the TDZ regression ship uncaught. Removing it makes the catch a measured branch, so per-file 100% coverage now proves the failure path is exercised.

Fix the flush race by capturing more bound methods. Rejected: this is the approach that already failed. Binding a callable fixes reference resolution, not concurrent access to the mutable state the callable reads. Only mutual exclusion over the shared ledger closes the race.

Guard the residual with a size threshold (copy only large frames). Rejected: the branch runs once per newline-bearing read, the copy is bounded by the residual's own length (always a partial line), and a threshold adds a tunable and a second code path for no measurable saving. An unconditional right-sized copy is simpler and always correct.

Assert the residual memory effect through the seam. Rejected: the retained allocation is not observable through CodeRunResult, so a black-box test could not distinguish fixed from unfixed. Extracting detachResidual makes the backing-store invariant a deterministic unit test instead.

Reap the same-group survivor with a fire-and-forget unref'd SIGKILL timer alone. Rejected: an unref'd timer does not keep the host alive, so a host that exits within the grace window (a one-shot run, a config subprocess) never fires the SIGKILL and the survivor is reparented to init — the same "no subprocess outlives the fiber" violation in a different shape, and teardown's "await each child's exit" JSDoc would be false. Awaiting the group's death on a ref'd poll keeps the host alive exactly long enough to reap, at zero cost in the common empty-group case.

Assert the reap with process.kill(pid, 0) throwing ESRCH. Rejected: a SIGKILL'd process lingers as a zombie until its parent wait()s it, and in a container whose PID 1 does not reap orphans the signal-0 probe keeps succeeding, so the assertion would false-fail cross-environment. A heartbeat file that stops advancing detects "no longer executing," which a reaped process and a zombie both satisfy.

Complete the cross-loop Future with a plain set_result and rely on the GIL. Rejected: the GIL serializes bytecode but does not make asyncio.Future cross-loop-safe — completing a Future from a thread other than its loop's does not schedule its callbacks or wake the loop. call_soon_threadsafe on the owning loop is the documented mechanism.

Leave the SIGKILL timer armed after settlement (the earlier same-group fix). Rejected: an unref'd timer left to fire up to graceMs after the leader was reaped can kill(-pid) a RECYCLED pgid, striking an unrelated group; the danger is the kill that succeeds, which killGroup's ESRCH swallow cannot prevent. Clearing the timer once the group is confirmed empty bounds the reuse window to the genuine-survivor case, where the group is not empty to reuse.

Clamp rlimits by the inherited hard limit only. Rejected: that silently RAISES an inherited soft limit stricter than the request, loosening the very containment the clamp exists to preserve. Clamping each side against its own inherited bound (then pinning soft under hard) keeps the strictest of configured and inherited on both.

Bill the host-side capMessage backstop by serialized cost, matching the child's _cap_message. Rejected: the two caps guard different things. _cap_message's output re-crosses fd 3 as a JSON string, so its escaped width is what the frame ceiling bounds — serialized billing is required there. capMessage's output goes straight into CodeRunResult.error.message and never re-crosses a frame-bounded channel, so the honest measure of what it retains is the raw byte length of the model-visible string. An honest child has already capped by serialized cost and raw length ≤ serialized cost, so a well-formed message passes unchanged; a forged control-heavy message could serialize to ~6× its raw length, but since it travels no capped channel, billing it by that inflated wire width would truncate a legitimately-sized diagnostic for no containment gain. Each side's JSDoc documents the split and points at the other.

Push stray pipe output one entry per data chunk. Rejected: logs entries are joined with \n downstream, so a transport chunk boundary would become a model-visible newline — a single native write split across pipe reads would read back with spurious line breaks. Aggregating by real newline (raw-chunk buffer + split on 0x0a) matches the child's line-granular log frames; the ledger still bounds a newline-free flood by admitting-and-truncating the residual when it would cross the budget.

Enforce the fd-3 frame ceiling per-frame (split before the counter check) to avoid a batch-edge false reject. Rejected: the ceiling check reads the byte counter BEFORE any Buffer.concat, precisely so a hostile program cannot force ~2× the 64 MiB frame cap of host memory (the counter and the join are a second copy of everything held). Splitting first to bill a single frame would Buffer.concat an over-ceiling frame before rejecting it, reintroducing that doubling — two regression tests assert the pre-concat order for exactly this reason. The batch-edge false reject the per-frame order would fix (a legitimate near-cap frame whose newline-bearing chunk also carries the next frame's leading bytes nudging the counter over the ceiling for one pipe read) is reachable only when maxLogBytes/maxValueBytes is configured within one pipe read of the 64 MiB cap — orders of magnitude past the 32/64 KiB defaults. The memory-safety bound against hostile input at any config takes precedence over a false reject reachable only at a pathological near-ceiling config; the counter's over-count and this trade-off are documented at the check.

Flush the two stray pipes in residual-arrival order when the combined budget crosses. Rejected: stdout and stderr are independent OS streams whose data events already interleave nondeterministically with each other and with the child's own fd-3 log frames. The seam's CodeRunResult.logs JSDoc reads "in order", which the surrounding text scopes to program-emission order WITHIN a stream — ordering ACROSS concurrent streams is inherently best-effort here, since no host-side flush order can reconstruct the true interleaving the kernel already lost, so preserving a residual's arrival order at the flush buys nothing. A fixed drain order is as valid as any. Tracking a per-residual arrival tick to drain the earlier pipe first would add a branch whose two sides fire only on the relative timing of two OS pipes, which os.sched_yield does not make deterministic, so the branch could not be covered without a flaky test — cost with no observable contract benefit.

Meter the child log ledger against the address space at runtime instead of rejecting the config at load. Rejected: an exact serialized-cost check on every child write is either a full encode — the very allocation an oversized write cannot afford, which the ledger's cheap pre-check exists to avoid — or a per-character Python loop, which burns the CPU budget (a 10 MB legitimate write hits SIGXCPU under cpuSeconds: 1). Each runtime approach trades the memory bound for another resource bound on the hot path. The address-space breach is a property of the maxLogBytes/addressSpaceMb pair, not of any particular write, so rejecting the incompatible pair once at load eliminates the whole class without any per-write cost and keeps _LogStream's original character-count buffering, which is memory-safe once the budget fits the address space.

Consequences

Interpreter misconfiguration fails before the service is published, every run uses the executable selected at load, and the child receives only TMPDIR, removing macOS startup noise without exposing host credentials. The private provider remains absent from shipped profiles while a keyless Loader snapshot pins its source-checkout PTC composition.

The seam's resolve-don't-reject contract holds on the boot-write path and the synchronous-spawn-failure path, both with measured coverage, and neither strands a staging directory. Log capture is thread-safe at the cost of one re-entrant lock acquisition per write and flush, and stray native output is delimited by its own newlines rather than by transport chunks. Fd-3 residual memory is bounded by the actual retained bytes, and both frame readers scan an accumulating frame once rather than quadratically. The output caps admit every value a frame can carry and reject a non-integer budget at load. Disposal is genuinely quiescent against a same-group survivor — bounded by graceMs + 2 * CLOSE_REAP_MARGIN_MS, zero-cost when the group is already empty, with the SIGKILL timer cleared once the group empties so a stale kill cannot strike a recycled pgid — RLIMIT enforcement keeps the strictest of configured and inherited on both soft and hard (and the SIGXCPU diagnostic no longer names a budget the host cannot guarantee), bindings called from model-created threads complete instead of timing out, and the handshake frame reader no longer burns the CPU budget on a large program. Every behavioral fix carries a test that fails without it, except the eleven called out in the Problem section — the chunked frame read (a syscall-count improvement), the confirmed-empty finalize (whose only seam-observable effect freezes at signal delivery, which the pre-fix code also produced), the shared stdout/stderr budget (whose only seam-observable difference turns on nondeterministic cross-pipe arrival timing), the flush_line reorder (whose lowered peak stays within what the 12x gate already admits, so no config behaves differently), pacing binding replies (the 32.0 MiB → 0.0 MiB peak reduction lives inside the host's fd-3 writable buffer, unmeasurable through the seam), dropping a late binding resolution before snapshot (its three assertions all hold pre-fix, so it is not a fail-before case), the done-value TOCTOU pre-encoding (its concurrent-mutation race is not deterministically constructible through the seam, and the daemon-mutation regression's only assertion is probabilistic), the stray-UTF-8 budget-flush retention (a budget flush landing on a multibyte boundary is not schedulable through the seam — v8-ignored), and the late-rejection settled guard (a rejection arriving after settlement is not deterministically constructible from the seam), and the log-fragment seal (its 25 M-scale OOM is not deterministically constructible in CI), and the unknown-binding preview cap (its whole-target JSON.stringify peak is a transient allocation inside the reply path, unmeasurable through the seam) — so a future regression on the rest goes red.