diff --git a/.agents/notes/implemented/bug-fix/2026-07-31-code-runtime-python-settlement-fixes.i18n.yaml b/.agents/notes/implemented/bug-fix/2026-07-31-code-runtime-python-settlement-fixes.i18n.yaml index 71e4b36e6b..58f16d7d70 100644 --- a/.agents/notes/implemented/bug-fix/2026-07-31-code-runtime-python-settlement-fixes.i18n.yaml +++ b/.agents/notes/implemented/bug-fix/2026-07-31-code-runtime-python-settlement-fixes.i18n.yaml @@ -2,5 +2,5 @@ # side as of the last confirmed-consistent state. Both languages carry equal authority; # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write .agents/notes/implemented/bug-fix/2026-07-31-code-runtime-python-settlement-fixes.md -2026-07-31-code-runtime-python-settlement-fixes.md: 10c7af447f420370bc6cbd34c6cc9e3f9e11c2b0 -2026-07-31-code-runtime-python-settlement-fixes.zh.md: f3aef31505c91ebc0e703d50b8a37b9c138fd9a7 +2026-07-31-code-runtime-python-settlement-fixes.md: ce5670c3623e1670eaab07f7c68d2d56fff3d0c7 +2026-07-31-code-runtime-python-settlement-fixes.zh.md: 50d75498805f79b8d5ee0b370e0fa7a3ecc3da0f diff --git a/.agents/notes/implemented/bug-fix/2026-07-31-code-runtime-python-settlement-fixes.md b/.agents/notes/implemented/bug-fix/2026-07-31-code-runtime-python-settlement-fixes.md index 10c7af447f..ce5670c362 100644 --- a/.agents/notes/implemented/bug-fix/2026-07-31-code-runtime-python-settlement-fixes.md +++ b/.agents/notes/implemented/bug-fix/2026-07-31-code-runtime-python-settlement-fixes.md @@ -90,7 +90,7 @@ Also in `src/index.ts`, the binding-rejection catch branch now checks `settled` - `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 `ceiling - envelope` bound (268435392) 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 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 nine no-fail-before fixes (reasons in the Problem section), not pinned by a fail-before test. +- `tests/runtime.spec.ts` — the output-cap case asserts the `ceiling - envelope` bound (268435392) 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 nine no-fail-before fixes (reasons in the Problem section), not pinned by a fail-before test. ## Alternatives considered diff --git a/.agents/notes/implemented/bug-fix/2026-07-31-code-runtime-python-settlement-fixes.zh.md b/.agents/notes/implemented/bug-fix/2026-07-31-code-runtime-python-settlement-fixes.zh.md index f3aef31505..50d7549880 100644 --- a/.agents/notes/implemented/bug-fix/2026-07-31-code-runtime-python-settlement-fixes.zh.md +++ b/.agents/notes/implemented/bug-fix/2026-07-31-code-runtime-python-settlement-fixes.zh.md @@ -90,7 +90,7 @@ Status: implemented - `tests/boot-write-failure.spec.ts` 对 `spawn` 做 mock,使 fd-3 管道在引导写入时抛出异常(这是真实子进程无法被迫进入的唯一路径),并断言 `run()` resolve 出一个 `worker-exit` 而非 reject。一个同级用例让被 mock 的 `spawn` 同步抛出,并断言 `run()` 仍然 resolve 出一个 `worker-exit`,且会移除它的暂存目录——以被 mock 的 `spawn` 在其 argv 中收到的确切引导路径为准,因此一个同级 worker 的并发暂存不会让它变得不稳定。两者都被隔离在这个 spec 中,因此真实子进程测试套件不受影响。 - `tests/residual-detach.spec.ts` 对 `detachResidual` 做单元测试:向前传递的副本与残余数据相等、拥有一个大小与其自身长度一致的底层存储(fixture 保持在 Node 的 Buffer 池阈值之上),并且不与源帧的 `ArrayBuffer` 共享。 -- `tests/runtime.spec.ts`:output-cap 用例断言 `ceiling - envelope` 上界(268435392)及其消息。一个 daemon 线程用例驱动四个线程穿过结算的 flush 发出未结束的写入。一个 native-write 用例在抬高后的 `maxLogBytes` 之下,通过 `os.write` 写入 200 KiB 且不含换行符,断言它回读时恰好是一条日志条目(证明散逸输出是按行聚合的,而不是在管道分片边界处被切开);一个配套用例写入 `b"one\ntwo\nthree"`,断言得到三条条目(证明真正的换行符仍然起分隔作用)。一个 newline-free-flood 用例在一个 4 KiB 的 `maxLogBytes` 之下写入 2 MiB,断言捕获终止于截断标记且保持在预算之内(证明残余数据受账本约束,而不是被整体缓冲);一个 NUL-flood 配套用例在同一预算之下写入 4000 个不含换行符的 NUL,断言发生截断(证明残余数据是按序列化开销计费的,约为原始的 6 倍,且在度量时不分配转义后的副本);一个 illegal-UTF-8 用例在一个 3072 字节的预算之下控速发出单字节 `\xff` 写入,并对 `Buffer.concat` 做包装以度量峰值合并缓冲区,断言它保持在 2048 之下(按 U+FFFD 宽度 3 计费时残余数据在约 1024 原始字节处冲刷;一次原始字节的少计会让它达到约 3072,因此该界限具有区分力);一个 CESU-8/overlong 用例把结构良构但非法的 `ED A0 80` 一次一个字节地控速发出,断言同样的峰值界限(按每序列真实的 9 计费时它提前冲刷;按结构宽度 3 计费会使峰值增至三倍,因此把逐前导字节范围检查回退会使它变红);一个 broken-multibyte 用例在分开的分片里先写入一个 3 字节的前导字节、再写入一个新的 ASCII 字节,断言同时捕获到一个 `A` 和一个 U+FFFD(覆盖 `accrueStrayCost` 的跨分片断裂序列分支);一个 post-truncation 用例写入一个 108 字节的载荷(小于最小的 PIPE_BUF,因此是一次原子写入),其首行耗尽一个 64 字节的预算,断言第二行被丢弃(覆盖单次 `data` 回调中的截断后准入空操作,无需 v8-ignore);一个 short-escape 用例写入一行混合了制表符、引号、反斜杠、一个 `\uXXXX` 控制字符、一个多字节字符和 ASCII 的内容,断言它原样完成往返(覆盖 `jsonStringCostUpTo` 的每一条分支);一个 reassembly 用例写入一个跨越每个合法多字节前导字节类别(E0 范围、普通 3 字节、F0 和 F4)、越过管道缓冲区的载荷,断言它原样完成往返且不含 U+FFFD(覆盖 `accrueStrayCost` 的逐前导字节范围与跨分片重组);一个 lone-surrogate 用例在一个 4 KiB 预算之下伪造一个以 1000 个 `\ud800` 转义洪泛的 fd-3 `log` 帧,断言发生截断(该计数正落在计 3 字节会放行、计 6 字节则截断的窗口内,证明该代理项是按其完整转义宽度计费的);一个 stray-sealing 用例在抬高后的预算之下控速发出 60000 次单字节、不含换行符的 `os.write(1, …)` 调用,并对 `Buffer.concat` 做包装以度量复制量,断言这股细流合并为一条条目、且累积复制量保持在一个实测的 256 KiB 阈值之下(封存后的形态复制约 120 KB,重新合并的形态复制约 538 KB,因此把封存回退成重新合并会使该断言变红——证明分片列表在越过 `MAX_PENDING_CHUNKS` 后封存为块)。一个 closeDeadline-flush 用例让 leader 写入一段不含换行符的诊断,随后 spawn 一个持有管道不放的 `setsid` 孤儿进程,断言该诊断在 `logs` 中存留下来(证明残余数据在截止时间销毁流之前被冲刷)。same-group 回收用例 spawn 一个忽略 SIGTERM 的同进程组后代,它释放管道并递增一个心跳文件;该测试断言在宽限窗口的 SIGKILL 之后心跳停止:无论被杀死的后代是被回收还是作为僵尸进程滞留,这个断言都成立,因此它在 PID 1 不 wait() 孤儿进程的环境下同样成立。一个 dispose-after-resolve 用例断言,对一个已完成、且存在同进程组存活者的运行调用 `dispose()`,只有在该存活者停止执行之后才返回(证明该运行会一直留在 `live` 中,直到它的进程组被回收),并带有一个 `expect(afterDispose).toBeGreaterThan(0)` 守卫,使得当心跳文件从未被写入时,冻结心跳的断言不会被空洞地通过。一个 deadline 用例忙阻塞事件循环越过两个定时器,断言该存活者的心跳冻结(证明轮询的截止时间分支自身发送 SIGKILL,而不是取消尚未触发的升级)。cross-loop 用例在主协程通过 `await asyncio.sleep` 让出时,从一个工作线程自己的 `asyncio.run` 事件循环运行一个绑定,断言该回复完成往返而不是超时;一个配套用例放弃某个线程的调用,使其事件循环关闭,随后在一个后续绑定之前回答它——断言 pump 在关闭事件循环上的 `call_soon_threadsafe` 之后仍然存活(由宿主门控的顺序使其具有确定性,未修复时会把后续绑定拖到墙钟上挂起)。inherited-soft-limit 用例通过一个 `ulimit -S -t` 包装脚本运行解释器,将 CPU 软限制设为低于 `cpuSeconds`,并断言实际应用的 `RLIMIT_CPU` 软限制是继承来的值,而不是配置的值(用 CPU 而非地址空间,因为 macOS 忽略 `ulimit -v`)。一个配套用例继承 1 秒的 CPU 软限制,让程序捕获 SIGXCPU 并忙循环越过它,断言结算复查报告 timeout——证明复查用的是实际生效的软限制,而不是配置的 `cpuSeconds`。一个 control-heavy-diagnostic 用例在一个较小的 `maxValueBytes` 之下抛出一个 NUL 洪泛异常,断言序列化后的帧能放得下(证明该诊断是按序列化开销计量的)。一个 tail-copy 用例(`maxLogBytes: 256`、`addressSpaceMb: 384`)让程序在一个变量里构建一个尾部并写入 `"\n" + tail`,其中 `tail` 为 150 MiB——构建峰值约 2 倍(约 300 MiB,落在地址空间之内,因此模型自身的分配会成功,任何 OOM 都属于缺陷路径),而修复前的整尾重新缓冲会加上第三份约 150 MiB 的副本、越过 384 MiB;切片后的前缀让该次运行得以截断并完成(仅 Linux 的 RLIMIT_AS 复现,macOS 走顺利路径——fixture 自身的构建必须放进地址空间,这是这些 RLIMIT_AS 用例的一条通用规则)。一个 output-budget/address-space 用例断言一个 50 MB 的 `maxLogBytes` 和一个 50 MB 的 `maxValueBytes` 各自对照一个 256 MiB 的 `addressSpaceMb` 在加载期被拒绝(乘以最坏情况的 12 之后超过解释器基线之后剩下的余量),而默认的各项上限对照 512 MiB 则加载成功,对两项预算对称地门控;一个具区分力的用例断言一个 48 MiB 的 `maxLogBytes` 对照一个 512 MiB 的 `addressSpaceMb` 被拒绝——48×8 = 384 MiB 放得进 448 MiB 的可预算余量(旧的 8× 倍数会错误放行),但 48×12 = 576 MiB 放不进。该倍数覆盖的约 12× 峰值来自换行路径上一次接近预算的写入——调用方自己的字符串、行切片与 encode 副本同时存活;结算期 flush 已不再是承重者,因为 `flush_line` 在 push 之前就丢弃了 pending 分块,因此只持有两份副本而非三份。一个 inherited-RLIMIT_AS 用例通过一个 `ulimit -v 131072` 包装层运行解释器,配以一个配置的 512 MiB `addressSpaceMb` 所允许的 32 MiB `maxLogBytes`,断言引导期的重新检查把它作为 `exception` 拒绝、且其消息点名了继承的 RLIMIT_AS(128 MiB 的继承限制在基线之后剩下的太少;仅 Linux,macOS 忽略 `ulimit -v`,该次运行会继续)。一个 non-integer-budget 用例断言一个小数的 `maxLogBytes`/`maxValueBytes` 在加载期被拒绝。一个 combined-peak 用例(`maxLogBytes: 32 MiB`、`maxValueBytes: 32 MiB`、`addressSpaceMb: 512`——每项预算单独都被 12× 门放行)写入约 33M 个不含换行符的星芒面字符(缓冲、未冲刷)后返回约 33M 个星芒面字符,断言该次运行以 `output-limit` 结算(该值本身就超过它 32 MiB 的预算);修复前未冲刷的日志 pending 加上值的构建加编码峰值会一起越过 512 MiB 地址空间而 OOM,因此在分帧值之前先冲刷日志正是让值检查得以完成的原因(仅 Linux 的 RLIMIT_AS 复现;在 macOS 上超预算的值在两种顺序下都报 output-limit)。一个宽完成值用例(`maxValueBytes: 20 MiB`、`addressSpaceMb: 384`)返回 `[0] * 6_000_000`——JSON 约 12 MB、低于预算,因此必须成功往返;修复前 O(width) 的遍历为每个元素分配遍历元组与编码器栈项(约为序列化尺寸的 28×,超出门保留的 12×),在一个计量器已放行的值上 OOM,而 O(depth) 游标使唯一按宽度分配的只剩输出字符串本身。一个宽 binding 实参用例(`addressSpaceMb: 384`)以 `[0] * 6_000_000` 调用 binding 并断言长度回传:`_lossless_json_violation` 运行在模型构造的实参上,子进程侧没有任何字节预算先行约束,其逐元素元组实测 459.1 MiB,而游标为 0.0 MiB。一个回溯用例从 binding 返回一个 4 MiB 字符串并断言其成功往返:旧的标量正则保留的引擎状态与字符串宽度成正比(1 MiB 时 146 MiB,4 MiB 时 557.8 MiB,超过默认的 512 MiB),会在 `_pump_replies` 内抛出 MemoryError 并把该次调用搁置到墙钟。一个 control-heavy 计费用例在 16 MiB 的 `maxValueBytes` 之下返回 8M 个 NUL,断言得到 `output-limit` 而非 `exception`:以计数替代物化转义形式来计费,在字节数完全相同的前提下实测 19.1 MiB 对 228.9 MiB。一个 addressSpaceMb 下界用例断言 64 MiB 与 32 MiB 在加载期被拒绝,且消息点名 `addressSpaceMb`,而不是预算循环给出的负数上限。一个进程身份用例断言 leader 的启动时刻在 Linux 上可稳定读取、在 Darwin 上报告 undefined,这正是使被复用的 pgid 不会收到本次运行 SIGTERM 的那道守卫。一个 paced-replies 用例在一轮 `asyncio.gather` 中 resolve 八条 4 MiB 的值,断言这些帧能够往返;本修复移除的峰值(32.0 MiB 缓冲 → 0.0 MiB)位于宿主 fd-3 可写缓冲内部、透过 seam 不可见,因此该用例钉住的是往返与无回归匹配,其峰值只能在树外度量。一个 late-drop 用例让该次运行在 `maxWallMs` 上结算,随后才 resolve 那个 pending 的 binding,断言得到一个 `timeout` 结果、一个 undefined 值、且迟到路径确实被执行过——这三条断言在修复前也全部成立,因为 `sendReply` 本就丢弃结算之后的值、只是更晚,因此该用例钉住的是顺序,而非一个透过 seam 可观测的行为。一个"绑定全部用名"用例(`rebinds every name the failure path uses`)断言一个真实的 `ValueError` 在 send-done 绑定之后仍然存活——这是一个有测修复,它同时绑定 `__main__.ProtocolChannel.send_sync`、`__main__.ProtocolChannel.write_encoded` 与 `__main__._encode_json_plain` 三个名字——正是 shipped 的 `send_done` 若做调用时刻查找时会迟解析的那三个——并用它钉住 `done` 帧不被一次调用时刻的查找跳过;而完成值的 TOCTOU 预编码、stray UTF-8 预算冲刷的扣留与结算后到达的迟到拒绝的 settled 先查,都被计入那九处无 fail-before 修复(理由见 Problem 段),不由 fail-before 测试钉住。 +- `tests/runtime.spec.ts`:output-cap 用例断言 `ceiling - envelope` 上界(268435392)及其消息。一个 daemon 线程用例驱动四个线程穿过结算的 flush 发出未结束的写入。一个 native-write 用例在抬高后的 `maxLogBytes` 之下,通过 `os.write` 写入 200 KiB 且不含换行符,断言它回读时恰好是一条日志条目(证明散逸输出是按行聚合的,而不是在管道分片边界处被切开);一个配套用例写入 `b"one\ntwo\nthree"`,断言得到三条条目(证明真正的换行符仍然起分隔作用)。一个 newline-free-flood 用例在一个 4 KiB 的 `maxLogBytes` 之下写入 2 MiB,断言捕获终止于截断标记且保持在预算之内(证明残余数据受账本约束,而不是被整体缓冲);一个 NUL-flood 配套用例在同一预算之下写入 4000 个不含换行符的 NUL,断言发生截断(证明残余数据是按序列化开销计费的,约为原始的 6 倍,且在度量时不分配转义后的副本);一个 illegal-UTF-8 用例在一个 3072 字节的预算之下控速发出单字节 `\xff` 写入,并对 `Buffer.concat` 做包装以度量峰值合并缓冲区,断言它保持在 2048 之下(按 U+FFFD 宽度 3 计费时残余数据在约 1024 原始字节处冲刷;一次原始字节的少计会让它达到约 3072,因此该界限具有区分力);一个 CESU-8/overlong 用例把结构良构但非法的 `ED A0 80` 一次一个字节地控速发出,断言同样的峰值界限(按每序列真实的 9 计费时它提前冲刷;按结构宽度 3 计费会使峰值增至三倍,因此把逐前导字节范围检查回退会使它变红);一个 broken-multibyte 用例在分开的分片里先写入一个 3 字节的前导字节、再写入一个新的 ASCII 字节,断言同时捕获到一个 `A` 和一个 U+FFFD(覆盖 `accrueStrayCost` 的跨分片断裂序列分支);一个 post-truncation 用例写入一个 108 字节的载荷(小于最小的 PIPE_BUF,因此是一次原子写入),其首行耗尽一个 64 字节的预算,断言第二行被丢弃(覆盖单次 `data` 回调中的截断后准入空操作,无需 v8-ignore);一个 short-escape 用例写入一行混合了制表符、引号、反斜杠、一个 `\uXXXX` 控制字符、一个多字节字符和 ASCII 的内容,断言它原样完成往返(覆盖 `jsonStringCostUpTo` 的每一条分支);一个 reassembly 用例写入一个跨越每个合法多字节前导字节类别(E0 范围、普通 3 字节、F0 和 F4)、越过管道缓冲区的载荷,断言它原样完成往返且不含 U+FFFD(覆盖 `accrueStrayCost` 的逐前导字节范围与跨分片重组);一个 lone-surrogate 用例在一个 4 KiB 预算之下伪造一个以 1000 个 `\ud800` 转义洪泛的 fd-3 `log` 帧,断言发生截断(该计数正落在计 3 字节会放行、计 6 字节则截断的窗口内,证明该代理项是按其完整转义宽度计费的);一个 stray-sealing 用例在抬高后的预算之下控速发出 60000 次单字节、不含换行符的 `os.write(1, …)` 调用,并对 `Buffer.concat` 做包装以度量复制量,断言这股细流合并为一条条目、且累积复制量保持在一个实测的 256 KiB 阈值之下(封存后的形态复制约 120 KB,重新合并的形态复制约 538 KB,因此把封存回退成重新合并会使该断言变红——证明分片列表在越过 `MAX_PENDING_CHUNKS` 后封存为块)。一个 closeDeadline-flush 用例让 leader 写入一段不含换行符的诊断,随后 spawn 一个持有管道不放的 `setsid` 孤儿进程,断言该诊断在 `logs` 中存留下来(证明残余数据在截止时间销毁流之前被冲刷)。same-group 回收用例 spawn 一个忽略 SIGTERM 的同进程组后代,它释放管道并递增一个心跳文件;该测试断言在宽限窗口的 SIGKILL 之后心跳停止:无论被杀死的后代是被回收还是作为僵尸进程滞留,这个断言都成立,因此它在 PID 1 不 wait() 孤儿进程的环境下同样成立。一个 dispose-after-resolve 用例断言,对一个已完成、且存在同进程组存活者的运行调用 `dispose()`,只有在该存活者停止执行之后才返回(证明该运行会一直留在 `live` 中,直到它的进程组被回收),并带有一个 `expect(afterDispose).toBeGreaterThan(0)` 守卫,使得当心跳文件从未被写入时,冻结心跳的断言不会被空洞地通过。一个 deadline 用例忙阻塞事件循环越过两个定时器,断言该存活者的心跳冻结(证明轮询的截止时间分支自身发送 SIGKILL,而不是取消尚未触发的升级)。cross-loop 用例在主协程通过 `await asyncio.sleep` 让出时,从一个工作线程自己的 `asyncio.run` 事件循环运行一个绑定,断言该回复完成往返而不是超时;一个配套用例放弃某个线程的调用,使其事件循环关闭,随后在一个后续绑定之前回答它——断言 pump 在关闭事件循环上的 `call_soon_threadsafe` 之后仍然存活(由宿主门控的顺序使其具有确定性,未修复时会把后续绑定拖到墙钟上挂起)。inherited-soft-limit 用例通过一个 `ulimit -S -t` 包装脚本运行解释器,将 CPU 软限制设为低于 `cpuSeconds`,并断言实际应用的 `RLIMIT_CPU` 软限制是继承来的值,而不是配置的值(用 CPU 而非地址空间,因为 macOS 忽略 `ulimit -v`)。一个配套用例继承 1 秒的 CPU 软限制,让程序捕获 SIGXCPU 并忙循环越过它,断言结算复查报告 timeout——证明复查用的是实际生效的软限制,而不是配置的 `cpuSeconds`。一个 control-heavy-diagnostic 用例在一个较小的 `maxValueBytes` 之下抛出一个 NUL 洪泛异常,断言序列化后的帧能放得下(证明该诊断是按序列化开销计量的)。一个 tail-copy 用例(`maxLogBytes: 256`、`addressSpaceMb: 384`)让程序在一个变量里构建一个尾部并写入 `"\n" + tail`,其中 `tail` 为 150 MiB——构建峰值约 2 倍(约 300 MiB,落在地址空间之内,因此模型自身的分配会成功,任何 OOM 都属于缺陷路径),而修复前的整尾重新缓冲会加上第三份约 150 MiB 的副本、越过 384 MiB;切片后的前缀让该次运行得以截断并完成(仅 Linux 的 RLIMIT_AS 复现,macOS 走顺利路径——fixture 自身的构建必须放进地址空间,这是这些 RLIMIT_AS 用例的一条通用规则)。一个 output-budget/address-space 用例断言一个 50 MB 的 `maxLogBytes` 和一个 50 MB 的 `maxValueBytes` 各自对照一个 256 MiB 的 `addressSpaceMb` 在加载期被拒绝(乘以最坏情况的 12 之后超过解释器基线之后剩下的余量),而默认的各项上限对照 512 MiB 则加载成功,对两项预算对称地门控;一个具区分力的用例断言一个 48 MiB 的 `maxLogBytes` 对照一个 512 MiB 的 `addressSpaceMb` 被拒绝——48×8 = 384 MiB 放得进 448 MiB 的可预算余量(旧的 8× 倍数会错误放行),但 48×12 = 576 MiB 放不进。该倍数覆盖的约 12× 峰值来自换行路径上一次接近预算的写入——调用方自己的字符串、行切片与 encode 副本同时存活;结算期 flush 已不再是承重者,因为 `flush_line` 在 push 之前就丢弃了 pending 分块,因此只持有两份副本而非三份。一个 inherited-RLIMIT_AS 用例通过一个 `ulimit -v 131072` 包装层运行解释器,配以一个配置的 512 MiB `addressSpaceMb` 所允许的 32 MiB `maxLogBytes`,断言引导期的重新检查把它作为 `exception` 拒绝、且其消息点名了继承的 RLIMIT_AS(128 MiB 的继承限制在基线之后剩下的太少;仅 Linux,macOS 忽略 `ulimit -v`,该次运行会继续)。一个 non-integer-budget 用例断言一个小数的 `maxLogBytes`/`maxValueBytes` 在加载期被拒绝。一个 combined-peak 用例(`maxLogBytes: 32 MiB`、`maxValueBytes: 32 MiB`、`addressSpaceMb: 512`——每项预算单独都被 12× 门放行)写入约 33M 个不含换行符的星芒面字符(缓冲、未冲刷)后返回约 33M 个星芒面字符,断言该次运行以 `output-limit` 结算(该值本身就超过它 32 MiB 的预算);修复前未冲刷的日志 pending 加上值的构建加编码峰值会一起越过 512 MiB 地址空间而 OOM,因此在分帧值之前先冲刷日志正是让值检查得以完成的原因(仅 Linux 的 RLIMIT_AS 复现;在 macOS 上超预算的值在两种顺序下都报 output-limit)。一个宽完成值用例(`maxValueBytes: 20 MiB`、`addressSpaceMb: 384`)返回 `[0] * 6_000_000`——JSON 约 12 MB、低于预算,因此必须成功往返;修复前 O(width) 的遍历为每个元素分配遍历元组与编码器栈项(约为序列化尺寸的 28×,超出门保留的 12×),在一个计量器已放行的值上 OOM,而 O(depth) 游标使唯一按宽度分配的只剩输出字符串本身。一个宽 binding 实参用例(`addressSpaceMb: 384`)以 `[0] * 6_000_000` 调用 binding 并断言长度回传:`_lossless_json_violation` 运行在模型构造的实参上,子进程侧没有任何字节预算先行约束,其逐元素元组实测 459.1 MiB,而游标为 0.0 MiB。一个回溯用例从 binding 返回一个 4 MiB 字符串并断言其成功往返:旧的标量正则保留的引擎状态与字符串宽度成正比(1 MiB 时 146 MiB,4 MiB 时 557.8 MiB,超过默认的 512 MiB),会在 `_pump_replies` 内抛出 MemoryError 并把该次调用搁置到墙钟。一个 control-heavy 计费用例在 16 MiB 的 `maxValueBytes` 之下返回 8M 个 NUL,断言得到 `output-limit` 而非 `exception`:以计数替代物化转义形式来计费,在字节数完全相同的前提下实测 19.1 MiB 对 228.9 MiB。一个 addressSpaceMb 下界用例断言 64 MiB 与 32 MiB 在加载期被拒绝,且消息点名 `addressSpaceMb`,而不是预算循环给出的负数上限。一个进程身份用例断言 leader 的启动时刻在 Linux 上可稳定读取、在 Darwin 上报告 undefined,这正是使被复用的 pgid 不会收到本次运行 SIGTERM 的那道守卫。一个 paced-replies 用例在一轮 `asyncio.gather` 中 resolve 八条 4 MiB 的值,断言这些帧能够往返;本修复移除的峰值(32.0 MiB 缓冲 → 0.0 MiB)位于宿主 fd-3 可写缓冲内部、透过 seam 不可见,因此该用例钉住的是往返与无回归匹配,其峰值只能在树外度量。一个 late-drop 用例让该次运行在 `maxWallMs` 上结算,随后才 resolve 那个 pending 的 binding,断言得到一个 `timeout` 结果、一个 undefined 值、且迟到路径确实被执行过——这三条断言在修复前也全部成立,因为 `sendReply` 本就丢弃结算之后的值、只是更晚,因此该用例钉住的是顺序,而非一个透过 seam 可观测的行为。一个"绑定全部用名"用例(`rebinds every name the failure path uses`)断言一个真实的 `ValueError` 在 send-done 绑定之后仍然存活——这是一个有测修复,由一个逐名重绑 `__main__.ProtocolChannel.send_sync`、`__main__.ProtocolChannel.write_encoded` 与 `__main__._encode_json_plain` 的用例钉住——这三个名字正是 shipped 的 `send_done` 若做调用时刻查找时会迟解析的那三个——并钉住 `done` 帧不被一次调用时刻的查找跳过;而完成值的 TOCTOU 预编码、stray UTF-8 预算冲刷的扣留与结算后到达的迟到拒绝的 settled 先查,都被计入那九处无 fail-before 修复(理由见 Problem 段),不由 fail-before 测试钉住。 ## Alternatives considered