docs(code-runtime-python): correct the claims the new backend invalidated
Adding a published Python backend and reordering `flush_line` left several owning documents stating things that are no longer true. `src/invariant.ts` justified its empty installer with "ships only the fd-3 wire-protocol codec", which the subprocess execution path contradicts. The reason now states the actual one: every relation this backend maintains lives in the CPython child or on the fd-3 wire, so no same-process event sequence is observable from a listener -- the same shape the sibling worker-thread backend uses. The seam's `PORTABLE_RESERVED_WORDS` and `language` JSDoc, the code-runtime README pair, and docs/subsystems/code-runtime both said only TypeScript has a published backend. Corrected in all four, with the generated cordis catalog regenerated for the `language` change. The note attributed the 12x multiple to the settlement flush holding three copies. That stopped being true when `flush_line` was reordered to drop the pending chunks before its push: the binding worst case is the newline path's single near-budget write. Corrected in the note (both sides) and in the test comment that repeated it. The note's Testing section now registers the cases this stack added, and the Chinese side receives the O(depth) entry it never got plus the new ones -- it had drifted from the English. `INTERPRETER_BASELINE_BYTES` argued 64 MiB from a RESIDENT set while RLIMIT_AS bounds address space. It now cites the bootstrap's own measurement (30.23 MiB of mappings for `python3 -I`), making 64 MiB roughly twice the measured baseline. Also: a hardcoded `(:232-235)` comment reference becomes a reference by name, a "which now walks in O(depth) too" change narrative becomes a current-state statement, and a stray double blank line is removed.
This commit is contained in:
1 parent
e6b547bef4
commit
2e3cf144d5
15 files changed
+57
-32
No files matched your search
+2
-2
@@ -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: 0614d4f40c03c01b63c0ef3ae5584e667d94dddc
|
||||
2026-07-31-code-runtime-python-settlement-fixes.zh.md: c1320696c6a8c2316befd79396618157299a9484
|
||||
2026-07-31-code-runtime-python-settlement-fixes.md: b3a5242c661fc162dc95cde41497940d2e36b447
|
||||
2026-07-31-code-runtime-python-settlement-fixes.zh.md: a78aa5a5682b76b6b2d02c1519f29128e59b6111
|
||||
+1
-1
@@ -68,7 +68,7 @@ One residual write-path copy is fixed alongside, independent of the config gate:
|
||||
|
||||
- `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 applieLine truncated
|
||||
- `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 applieLine truncated
|
||||
|
||||
## Alternatives considered
|
||||
|
||||
|
||||
+1
-1
@@ -68,7 +68,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` 事件循环运行一个绑定,断言该回复完成往返而不是超时;一个配套用例放弃某个线程的调用,使其事件循环关闭,Line truncated
|
||||
- `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` 事件循环运行一个绑定,断言该回复完成往返而不是超时;一个配套用例放弃某个线程的调用,使其事件循环关闭,Line truncated
|
||||
|
||||
## Alternatives considered
|
||||
|
||||
|
||||
@@ -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 docs/subsystems/code-runtime.md
|
||||
code-runtime.md: 0f633df9fc657d9d80fc04df3bc8ad6fafdddcb2
|
||||
code-runtime.zh.md: 43b78ce49575741f7ae6c4e2751b63b7562fc99a
|
||||
code-runtime.md: eaffa17b552f4d91440c7f9f4ca089549d1e8966
|
||||
code-runtime.zh.md: 762c21d9366f33129f1e4e386b5d6ae2d3a44258
|
||||
@@ -158,7 +158,7 @@ interface CodeRunFailure {
|
||||
|
||||
## The service
|
||||
|
||||
`CodeRuntime` (`ctx.codeRuntime`, abstract — defined in [`packages/code-runtime/code-runtime/src/index.ts`](../../packages/code-runtime/code-runtime/src/index.ts)) is `run(request)` plus two readonly descriptors: `language` (what the program must be written in — `'typescript'` and `'python'` are the well-known values, those `dsh-tools` presents, and only `'typescript'` has a published backend; a consumer generating language-specific presentation switches on it and fails loud on one it cannot present) and `isolation` (the execution substrate — `'worker-thread'`, `'process'`, `'container'`; a diagnostic label, **not a security claim**). Implementations must keep runs isolated from each other (no cross-run state) and dispose to quiescence: in-flight runs are terminated and awaited before teardown completes.
|
||||
`CodeRuntime` (`ctx.codeRuntime`, abstract — defined in [`packages/code-runtime/code-runtime/src/index.ts`](../../packages/code-runtime/code-runtime/src/index.ts)) is `run(request)` plus two readonly descriptors: `language` (what the program must be written in — `'typescript'` and `'python'` are the well-known values, those `dsh-tools` presents, and each has a published backend; a consumer generating language-specific presentation switches on it and fails loud on one it cannot present) and `isolation` (the execution substrate — `'worker-thread'`, `'process'`, `'container'`; a diagnostic label, **not a security claim**). Implementations must keep runs isolated from each other (no cross-run state) and dispose to quiescence: in-flight runs are terminated and awaited before teardown completes.
|
||||
|
||||
<!-- BEGIN GENERATED cordis-surface (gen-cordis-catalog.ts) — do not edit between markers -->
|
||||
|
||||
|
||||
@@ -158,7 +158,7 @@ interface CodeRunFailure {
|
||||
|
||||
## 服务
|
||||
|
||||
`CodeRuntime`(`ctx.codeRuntime`,抽象服务,定义于 [`packages/code-runtime/code-runtime/src/index.ts`](../../packages/code-runtime/code-runtime/src/index.ts))由 `run(request)` 加两个只读描述符组成:`language`(程序必须使用的语言,已知值为 `'typescript'` 与 `'python'`,即 `dsh-tools` 能呈现的那些,其中只有 `'typescript'` 有已发布的后端;生成语言相关展示的 Consumer 据此切换,遇到无法展示的语言时应显式报错)和 `isolation`(执行基底,`'worker-thread'`、`'process'`、`'container'`;仅为诊断标签,**不构成安全承诺**)。实现必须保证各次运行彼此隔离(无跨运行状态),并在 dispose(资源释放)时等待系统完全停稳:teardown 要等到所有进行中的运行均已终止并结算后才完成。
|
||||
`CodeRuntime`(`ctx.codeRuntime`,抽象服务,定义于 [`packages/code-runtime/code-runtime/src/index.ts`](../../packages/code-runtime/code-runtime/src/index.ts))由 `run(request)` 加两个只读描述符组成:`language`(程序必须使用的语言,已知值为 `'typescript'` 与 `'python'`,即 `dsh-tools` 能呈现的那些,两者都有已发布的后端;生成语言相关展示的 Consumer 据此切换,遇到无法展示的语言时应显式报错)和 `isolation`(执行基底,`'worker-thread'`、`'process'`、`'container'`;仅为诊断标签,**不构成安全承诺**)。实现必须保证各次运行彼此隔离(无跨运行状态),并在 dispose(资源释放)时等待系统完全停稳:teardown 要等到所有进行中的运行均已终止并结算后才完成。
|
||||
|
||||
<!-- BEGIN GENERATED cordis-surface (gen-cordis-catalog.ts) — do not edit between markers -->
|
||||
|
||||
|
||||
@@ -363,8 +363,8 @@ class _LogStream(io.TextIOBase):
|
||||
# against them.
|
||||
with self._logs.lock:
|
||||
if self._pending:
|
||||
# Join, drop the chunks, THEN push — the same order the newline
|
||||
# path uses (:232-235). Pushing before the clear would keep the
|
||||
# Join, drop the chunks, THEN push — the same join-clear-push order
|
||||
# as `_write_locked`'s newline branch. Pushing before the clear would keep the
|
||||
# pending chunks alive through `_push_locked`'s `text.encode`, so
|
||||
# the chunks, their join, and the encode copy would all be live at
|
||||
# once; dropping the chunks first leaves only the join and its
|
||||
@@ -1184,8 +1184,8 @@ def _encode_json_plain(value: Any) -> str:
|
||||
# that pulls its children one at a time and writes each into the shared buffer,
|
||||
# rather than one stack entry (plus a separator marker) per child: a flat
|
||||
# `[0] * 6_000_000` encodes to ~12 MB but per-element frames are ~400 MB — an
|
||||
# RLIMIT_AS death on a value `_check_done_value` already admitted (which now
|
||||
# walks in O(depth) too). The output string is the only width-proportional
|
||||
# RLIMIT_AS death on a value `_check_done_value` already admitted (it walks by
|
||||
# depth as well). The output string is the only width-proportional
|
||||
# allocation, and its size the caller metered within budget. `io.StringIO`
|
||||
# accumulates without the intermediate `"".join(chunks)` second copy. A cursor
|
||||
# frame is [kind, iterator, wrote_any]; a visit frame is (VISIT, value).
|
||||
|
||||
@@ -266,13 +266,16 @@ const OUTPUT_BUDGET_WORST_CASE_ADDRESS_SPACE_MULTIPLE = 12
|
||||
* multiple claims the rest. The budget check subtracts this from `addressSpaceMb`
|
||||
* so a budget sized right at `addressSpaceMb / MULTIPLE` — which the multiple
|
||||
* alone would admit — cannot leave the peak output allocation plus the
|
||||
* interpreter over the limit. 64 MiB is generous for a `python3 -I` process
|
||||
* whose own resident set is tens of MiB; the value is a fixed safety margin, not
|
||||
* a deployment knob.
|
||||
* interpreter over the limit. Sized against ADDRESS SPACE, which is what
|
||||
* `RLIMIT_AS` bounds, not resident set: the bootstrap's own measurement is
|
||||
* 30.23 MiB of mappings for a `python3 -I` child (see `_make_cpu_enforcer`,
|
||||
* which also records the 64 MiB glibc per-thread arena reservation that pushes
|
||||
* it to 102.37 MiB when threads are used). 64 MiB is roughly twice the measured
|
||||
* baseline, leaving room for allocator arenas and import jitter. The value is a
|
||||
* fixed safety margin, not a deployment knob.
|
||||
*/
|
||||
const INTERPRETER_BASELINE_BYTES = 64 * 1024 * 1024
|
||||
|
||||
|
||||
/**
|
||||
* Interval between process-group liveness probes while settlement waits for an
|
||||
* escalated SIGKILL to empty the group (see the `killing` branch in
|
||||
@@ -794,6 +797,14 @@ export class PythonCodeRuntime extends CodeRuntime {
|
||||
// peak plus the reserved baseline is the whole address space, the RLIMIT_AS
|
||||
// edge. `ceil(budgetableBytes / MULTIPLE) - 1` is the last integer strictly
|
||||
// under `budgetableBytes / MULTIPLE`.
|
||||
// Reject a too-small address space on its own terms FIRST. Once
|
||||
// `budgetableBytes` is zero or negative no budget can pass, and the loop
|
||||
// below would report "a limit of -1" (or -2796203 at addressSpaceMb 32) while
|
||||
// naming `maxLogBytes` -- pointing the operator at the knob that is not the
|
||||
// problem. The baseline is what `addressSpaceMb` must clear here.
|
||||
if (budgetableBytes <= 0) {
|
||||
throw new Error(`dsh-code-runtime-python: config.addressSpaceMb must exceed the ${INTERPRETER_BASELINE_BYTES}-byte interpreter baseline with room for the output budgets, so the child has address space left to build and encode them; got ${String(this.config.addressSpaceMb)} MiB (${addressSpaceBytes} bytes)`)
|
||||
}
|
||||
const admissibleBudget = Math.ceil(budgetableBytes / OUTPUT_BUDGET_WORST_CASE_ADDRESS_SPACE_MULTIPLE) - 1
|
||||
for (const key of ['maxLogBytes', 'maxValueBytes'] as const) {
|
||||
if (this.config[key] * OUTPUT_BUDGET_WORST_CASE_ADDRESS_SPACE_MULTIPLE >= budgetableBytes) {
|
||||
|
||||
@@ -15,9 +15,11 @@ export const name = 'code-runtime-python-invariant'
|
||||
export const inject = ['invariants']
|
||||
|
||||
/**
|
||||
* No runtime invariant: this package ships only the fd-3 wire-protocol codec and its Python mirror,
|
||||
* exposing no runtime event sequence or mutable data relation; `protocol.spec.ts` and
|
||||
* `protocol-mirror.e2e.ts` cover the protocol's behavior.
|
||||
* No runtime invariant: every relation this backend maintains — frame ordering, budget accounting,
|
||||
* and process teardown — lives in the CPython subprocess or on the fd-3 wire, so no same-process
|
||||
* event sequence or mutable data relation is observable from a Cordis listener. `protocol.spec.ts`,
|
||||
* `protocol-mirror.e2e.ts`, and the real-subprocess `runtime.spec.ts` cover that behavior, matching
|
||||
* the sibling process-boundary backend `@deepseek-ai/dsh-code-runtime-worker-thread`.
|
||||
*/
|
||||
const install: InvariantInstaller = () => {}
|
||||
|
||||
|
||||
@@ -827,12 +827,24 @@ describe('PythonCodeRuntime — programs and bindings', () => {
|
||||
.rejects.toThrow(/maxValueBytes times the 12x worst-case Unicode expansion must fit/)
|
||||
// Discriminates 12 from 8: a 48 MiB maxLogBytes against a 512 MiB address
|
||||
// space leaves 448 MiB budgetable. 48*8 = 384 MiB fits (the old 8x multiple
|
||||
// wrongly ADMITTED this), but 48*12 = 576 MiB does not — and this is exactly
|
||||
// the config that OOMs, since a settlement flush holds the pending chunks,
|
||||
// their join, and the encode copy at once (~12x). The 12x gate rejects it.
|
||||
// wrongly ADMITTED this), but 48*12 = 576 MiB does not. The ~12x peak this
|
||||
// guards 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: `flush_line` drops the pending chunks
|
||||
// before its push, so it holds two copies, not three.
|
||||
const ctxTwelve = new Context()
|
||||
await expect(ctxTwelve.plugin(PythonCodeRuntime, { maxLogBytes: 48 * 1024 * 1024, addressSpaceMb: 512 }))
|
||||
.rejects.toThrow(/maxLogBytes times the 12x worst-case Unicode expansion must fit/)
|
||||
// An addressSpaceMb at or below the interpreter baseline leaves nothing
|
||||
// budgetable, so no budget value can pass. It is rejected on its own terms:
|
||||
// the budget loop would otherwise report "a limit of -1" (or -2796203 at
|
||||
// 32 MiB) while naming maxLogBytes, sending the operator to the wrong knob.
|
||||
const ctxBaseline = new Context()
|
||||
await expect(ctxBaseline.plugin(PythonCodeRuntime, { addressSpaceMb: 64 }))
|
||||
.rejects.toThrow(/addressSpaceMb must exceed the 67108864-byte interpreter baseline/)
|
||||
const ctxBelow = new Context()
|
||||
await expect(ctxBelow.plugin(PythonCodeRuntime, { addressSpaceMb: 32 }))
|
||||
.rejects.toThrow(/addressSpaceMb must exceed the 67108864-byte interpreter baseline/)
|
||||
// The default caps against the default 512 MiB address space load.
|
||||
const ok = new Context()
|
||||
const fiber = await ok.plugin(PythonCodeRuntime, { maxLogBytes: 65536, maxValueBytes: 32768, addressSpaceMb: 512 })
|
||||
|
||||
@@ -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 packages/code-runtime/code-runtime/README.md
|
||||
README.md: e3d43e7add4992c44fef966651f91addb81aa1eb
|
||||
README.zh.md: bcbeaa8bbdfee33baa1b29e232038e2bd6b77728
|
||||
README.md: b16a8c81e80e07665b7ac868e4cb643529938055
|
||||
README.zh.md: ad5ec96fc8df00c0f3c1b1771bc5efbf148de054
|
||||
@@ -41,7 +41,7 @@ const result = await ctx.codeRuntime.run({
|
||||
|
||||
### Choose a backend
|
||||
|
||||
Backends declare two descriptors you can rely on: `language` — what the program must be written in, with `'typescript'` and `'python'` as the well-known values and only TypeScript shipped — and `isolation` — the execution substrate (`'worker-thread'`, `'process'`, `'container'`), a label for deployments and diagnostics, not a security claim. The shipped backend is [`dsh-code-runtime-worker-thread`](../code-runtime-worker-thread/README.md), which executes TypeScript in a fresh Node worker thread; [`dsh-code-runtime-python`](../code-runtime-python/README.md) owns the wire protocol for the CPython backend.
|
||||
Backends declare two descriptors you can rely on: `language` — what the program must be written in, with `'typescript'` and `'python'` as the well-known values and both backed by published providers — and `isolation` — the execution substrate (`'worker-thread'`, `'process'`, `'container'`), a label for deployments and diagnostics, not a security claim. [`dsh-code-runtime-worker-thread`](../code-runtime-worker-thread/README.md) executes TypeScript in a fresh Node worker thread; [`dsh-code-runtime-python`](../code-runtime-python/README.md) executes Python in a fresh CPython subprocess.
|
||||
|
||||
### Name your bindings portably
|
||||
|
||||
@@ -98,7 +98,7 @@ Read these when the package-level contract is not enough. They move from the PTC
|
||||
|
||||
- [PTC mode Agent Note](../../../.agents/notes/implemented/feature/2026-06-15-ptc.md) — how the tool registry consumes `ctx.codeRuntime` and presents `run_code` to the model.
|
||||
- [Worker-thread backend](../code-runtime-worker-thread/README.md) — the shipped TypeScript execution backend.
|
||||
- [Python protocol package](../code-runtime-python/README.md) — the wire protocol for the CPython backend.
|
||||
- [Python backend](../code-runtime-python/README.md) — the CPython subprocess execution provider and its fd-3 protocol.
|
||||
- [Code runtime subsystem reference](../../../docs/subsystems/code-runtime.md) — request/result vocabulary, bindings, and the `ctx.codeRuntime` cordis surface.
|
||||
- [Capability seams](../../../.agents/notes/implemented/architecture/2026-06-13-capability-seams.md) — the Service Definition / Service Provider / Consumer split.
|
||||
|
||||
|
||||
@@ -41,7 +41,7 @@ const result = await ctx.codeRuntime.run({
|
||||
|
||||
### 选择后端
|
||||
|
||||
后端声明两个你可以依赖的描述符:`language`——程序必须使用的源语言,已知值为 `'typescript'` 与 `'python'`,目前只有 TypeScript 已发布——以及 `isolation`——执行基底(`'worker-thread'`、`'process'`、`'container'`),仅供部署与诊断使用,不构成安全声明。已发布的后端是 [`dsh-code-runtime-worker-thread`](../code-runtime-worker-thread/README.zh.md),在全新的 Node Worker 线程中执行 TypeScript;[`dsh-code-runtime-python`](../code-runtime-python/README.zh.md) 持有 CPython 后端的协议格式(wire protocol)。
|
||||
后端声明两个你可以依赖的描述符:`language`——程序必须使用的源语言,已知值为 `'typescript'` 与 `'python'`,两者都有已发布的提供方——以及 `isolation`——执行基底(`'worker-thread'`、`'process'`、`'container'`),仅供部署与诊断使用,不构成安全声明。[`dsh-code-runtime-worker-thread`](../code-runtime-worker-thread/README.zh.md) 在全新的 Node Worker 线程中执行 TypeScript;[`dsh-code-runtime-python`](../code-runtime-python/README.zh.md) 在全新的 CPython 子进程中执行 Python。
|
||||
|
||||
### 可移植地命名绑定
|
||||
|
||||
@@ -98,7 +98,7 @@ binding-global 与 error-class 名称是语言可移植的:必须匹配标识
|
||||
|
||||
- [PTC mode Agent Note](../../../.agents/notes/implemented/feature/2026-06-15-ptc.zh.md)——工具注册表如何消费 `ctx.codeRuntime` 并把 `run_code` 呈现给模型。
|
||||
- [Worker 线程后端](../code-runtime-worker-thread/README.zh.md)——已发布的 TypeScript 执行后端。
|
||||
- [Python 协议包](../code-runtime-python/README.zh.md)——CPython 后端的协议格式。
|
||||
- [Python 后端](../code-runtime-python/README.zh.md)——CPython 子进程执行提供方及其 fd-3 协议。
|
||||
- [代码运行时子系统参考](../../../docs/subsystems/code-runtime.zh.md)——请求/结果词汇、绑定与 `ctx.codeRuntime` 的 cordis 接口面。
|
||||
- [能力 seam](../../../.agents/notes/implemented/architecture/2026-06-13-capability-seams.zh.md)——Service Definition / Service Provider / Consumer 拆分。
|
||||
|
||||
|
||||
@@ -66,8 +66,8 @@ export const DUNDER_MEMBER = /^__.+__$/
|
||||
/**
|
||||
* Reserved words of every portable target language (ECMAScript ∪ Python),
|
||||
* refused as {@link CodeBindingNamespace.global} / error-class names by all
|
||||
* backends. Python is a portability target here even though only the
|
||||
* TypeScript worker has a published backend. The portable-identifier contract
|
||||
* backends, which ship for both languages: the TypeScript worker thread and
|
||||
* the CPython subprocess. The portable-identifier contract
|
||||
* promises a namespace list valid on one backend is valid on every backend; a
|
||||
* per-language check would let `lambda` pass the TypeScript backend and fail
|
||||
* the Python one. Extending the seam with a new language means widening this
|
||||
@@ -106,7 +106,7 @@ export abstract class CodeRuntime extends Service {
|
||||
* generates language-specific presentation (typed SDK stubs, usage
|
||||
* instructions) switches on it and fails loud on a language it cannot
|
||||
* present. Well-known values: `'typescript'` and `'python'`, those
|
||||
* `dsh-tools` presents; only `'typescript'` has a published backend.
|
||||
* `dsh-tools` presents; each has a published backend.
|
||||
*/
|
||||
abstract readonly language: string
|
||||
|
||||
|
||||
@@ -598,7 +598,7 @@ export const SERVICE_API: readonly ServiceApiEntry[] = [
|
||||
methods: [
|
||||
{
|
||||
signature: 'abstract readonly language: string',
|
||||
description: 'The source language run expects `program` to be written in, as a lowercase identifier. Informational, not gating — a consumer that generates language-specific presentation (typed SDK stubs, usage instructions) switches on it and fails loud on a language it cannot present. Well-known values: `\'typescript\'` and `\'python\'`, those `dsh-tools` presents; only `\'typescript\'` has a published backend.',
|
||||
description: 'The source language run expects `program` to be written in, as a lowercase identifier. Informational, not gating — a consumer that generates language-specific presentation (typed SDK stubs, usage instructions) switches on it and fails loud on a language it cannot present. Well-known values: `\'typescript\'` and `\'python\'`, those `dsh-tools` presents; each has a published backend.',
|
||||
parameters: [],
|
||||
},
|
||||
{
|
||||
|
||||
Reference in new issue
Block a user