# Agent Note: Settlement, framing, and lifecycle fixes in the CPython backend Status: implemented Archived: 2026-09-04 English | [中文](2026-07-31-code-runtime-python-settlement-fixes.zh.md) ## Problem The CPython subprocess backend for PTC mode, built on the [fd-3 frame protocol](../architecture/2026-07-31-code-runtime-python-fd3-protocol.md), 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](../architecture/2026-07-31-code-runtime-python-fd3-protocol.md). ### Boot-write failure no longer rejects run() In [`src/index.ts`](../../../../packages/experimental/code-runtime-python/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`](../../../../packages/experimental/code-runtime-python/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//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`](../../../../packages/experimental/code-runtime-python/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.write`s 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`](../../../../packages/experimental/code-runtime-python/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`](../../../../packages/experimental/code-runtime-python/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`](../../../../packages/experimental/code-runtime-python/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`](../../../../packages/experimental/code-runtime-python/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`](../../../../packages/experimental/code-runtime-python/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 `A`s 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 `A`s 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 ""` (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.