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:
Chinesezjc
2026-08-31 14:28:26 +08:00
committed by Tianyi Cui
parent e6b547bef4
commit 2e3cf144d5
15 changed files with 57 additions and 32 deletions
@@ -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 })