Commit Graph
165 Commits
Author SHA1 Message Date
Chinesezjc 9af1e5e9f0 fix(code-runtime-python): correct the log-budget floor to 64 and record the marker envelope bound
The review found the 62 floor off by two (the marker's fixed prefix is 51
characters counting both square brackets, so marker(62) serializes to 63) and
the constructor error over-claiming a bound the marker-as-envelope design does
not deliver. Fixes:
- MIN_LOG_BYTES is 64 (marker-only serialization fits with one byte of room);
  the JSDoc arithmetic counts the brackets; the rejection test pins 63; the
  forged-frame test uses 11 NULs (69 escaped) at 64.
- The constructor error now states the marker-only guarantee, and the README
  Known Limitations (en + zh) records the real bound: a truncated run with
  admitted entries serializes its logs to maxLogBytes + marker + envelope.
- The SIGXCPU-mask tests burn with time.process_time() instead of wall-clock
  perf_counter, so a contended CI runner cannot under-burn the budget.
- The settlement note (en + zh) records the 64 floor and the marker envelope
  bound, including the zh pre-encode section that the earlier pass missed.
- The README constructor-rejection list names the maxLogBytes floor.
Pairings re-recorded; corpus-wide verify-translation-pairing passes 1004.
2026-08-31 14:44:19 +08:00
Chinesezjc 4a8c49f78c fix(code-runtime-python): restore SIGXCPU disposition before unblocking and floor the budgets
Addresses the review's two code warnings and one suggestion:
- die_if_cpu_exhausted now restores SIG_DFL BEFORE unblocking SIGXCPU: a program
  that installed a custom handler AND masked the signal would otherwise have
  that pending handler run at the unblock (in model code, re-masking or raising)
  and escape the re-raise; with SIG_DFL first the pending signal kills inside
  the kernel with no bytecode window. A trap+mask combined regression test pins
  it (the mask-only case was already covered).
- The constructor rejects budgets too small to honor: maxLogBytes must fit the
  truncation marker plus the serialized outer-array envelope (floor 64), and
  maxValueBytes must at least represent the smallest JSON completion (floor 4,
  matching the worker backend). The exact-limit test moves to the 64 floor and
  a rejection test pins the floors.
- The pthread_sigmask None-guard comment cites the real rationale (defensive
  against stripped CPython builds; win32 is refused at construction), not the
  unreachable Windows path.
2026-08-31 14:41:49 +08:00
Chinesezjc a2eda792e3 docs(code-runtime-python): align the accepted-residual dep list across README and note
The residual bullets listed the encoder's transitive deps as an exhaustive set
but disagreed with each other and omitted io. Mark the list as a non-exhaustive
example (e.g. _dump_scalar/_dump_string/json/io) in the README (en + zh) and the
settlement note (en + zh); pairings re-recorded and consistent.
2026-08-31 14:41:49 +08:00
Chinesezjc 4e0d77c1d6 fix(code-runtime-python): reserve the log array envelope byte, unblock SIGXCPU before re-raise
Addresses the review's two remaining code warnings and the three suggestions:
- Log ledgers (host and child) start one byte below the budget, reserving the
  serialized outer-array envelope (two brackets and n-1 commas over n entries'
  separators); the exact-zero test moves to maxLogBytes 104 and a new exact-limit
  case pins that maxLogBytes 5 admits ['a'] (5 bytes) while 4 truncates to the
  marker alone.
- die_if_cpu_exhausted unblocks SIGXCPU (pthread_sigmask SIG_UNBLOCK, captured at
  import, None-guarded for Windows) before re-delivering it, so a program that
  masks SIGXCPU, burns past the soft limit, and returns is still classified as a
  timeout; a regression test pins the masked path.
- ast.parse passes filename="<model>" so parse-time syntax diagnostics carry the
  same source label as compile and runtime tracebacks; the syntax-error test
  asserts the label.
- The NUL-escape test comments use the true six-byte JSON escape \u0000 instead
  of the caret notation; the README Known Limitations (en + zh) records that
  PID-reuse protection is inert on macOS; a combined-rebind regression test pins
  BaseException plus the traceback reporter rebinding together.
2026-08-31 14:41:08 +08:00
Chinesezjc 96597c5ed8 fix(code-runtime-python): bind the _done_with_value entry name and correct the residual documentation
Addresses the review's registration-text accuracy findings:
- _run binds _done_with_value into a local (done_with_value_bound) before the
  program runs, closing the __main__._done_with_value = boom success-rewrite
  vector; a regression test rebinds it and returns a legitimate value, asserting
  the success survives.
- README (en + zh): the CPU-recheck bullet now states the recheck runs
  unconditionally after the program returns (a pre-return overrun dies there as
  a timeout) and the false-success window is only a trap-SIGXCPU program that
  passes the recheck and overruns during the settlement flush/encode; the
  encoder-deps residual rationale is replaced with the actual one (bash-equivalent
  trust, verdict still delivered via the send_done fallback frame) and names the
  now-bound entry; the t.join() deadlock bullet fixes the subject/object (the
  main coroutine joins the worker, blocking the pump's main event loop).
- The portable-identifier-seam architecture note no longer claims the Python
  backend does not exist.
- Settlement note (en + zh) registers the entry-name binding and the new test.
- All pairings re-recorded; corpus-wide verify-translation-pairing passes 1004.
2026-08-31 14:40:35 +08:00
Chinesezjc e6b23e829b docs(code-runtime-python): split the deadlock into its own bullet and qualify the done_value claim
Addresses the review's two registration-text accuracy findings:
- The cross-thread t.join() deadlock is a process-isolation-backend property (the
  pump runs on the child's main event loop), so it is split out of the wide-binding
  REPLY bullet into its own Known Limitations entry with the correct attribution
  (fix belongs in this backend, not packages/core/session); the zh half-width
  space is removed.
- The settlement note's _done_with_value def-time default-arg sentence is
  qualified: it guards a rebind of _check_done_value/_encode_json_plain, while a
  transitive encoder dep (_dump_scalar/io) rebind can still downgrade, which is
  registered as an accepted residual in the package README.
Pairing re-recorded; corpus-wide verify-translation-pairing passes 1004.
2026-08-31 14:39:07 +08:00
Chinesezjc c8bc96007b docs(code-runtime-python): register the CPU-recheck and encoder-deps accepted residuals
Document the two remaining keep-current residuals in the python package README
Known Limitations (en + zh), per the review's accepted-resolution path:
- A trap-SIGXCPU program can exceed the soft CPU limit during settlement encoding
  and still report success (containment holds via hard +1s and wall clock; only
  the classification is degraded, because the recheck cannot meter mid-encode).
- The encoder's direct deps (_dump_scalar/_dump_string/json) resolve at call
  time, so a __main__ rebind after a legit return can downgrade success to
  exception; the value path's top-level deps are def-time bound, the transitive
  ones are an accepted residual.
Pairing re-recorded and consistent.
2026-08-31 14:37:52 +08:00
Chinesezjc f79e53e74c docs(code-runtime-python): align the note consequences, register the deadlock and default-arg mechanisms
Addresses the bot's keep-current findings:
- The settlement note distinguishes the BaseException (lost done frame) and
  RuntimeError (pump killed -> replies stranded to the wall clock) consequences;
  registers the _done_with_value def-time default-arg capture and the new
  RuntimeError-rebind closed-loop test; zh:95 half-width space fixed.
- The python package README Known Limitations records the cross-thread binding +
  sync t.join() deadlock (en + zh).
- The code-runtime Service Definition README no longer claims only the
  worker-thread backend ships: the Python (process) backend is acknowledged,
  with 'container' as future work (en + zh).
- All pairings re-recorded; corpus-wide verify-translation-pairing passes 1002.
2026-08-31 14:36:31 +08:00
Chinesezjc 923fb56128 fix(code-runtime-python): bind the reply-pump exception names as def-time default arguments
A body-local X = X binding in _pump_replies is too late: _run reaches the
model's top-level statements (which run first, since there is no suspension
point between create_task and await __dsh_main__) before the pump's first step,
so a __main__.RuntimeError rebind there would be captured by the body local and
a closed-loop failure would escape the except, killing the pump. Bind
_RuntimeError, _BindingRejection, str, and bool as DEF-TIME default arguments of
_pump_replies (evaluated at import, before any model code runs). Add a regression
test that rebinds __main__.RuntimeError as the first program statement and drives
the closed-loop worker pattern, asserting the pump survives and delivers the
later binding. Update the settlement note (en + zh) to describe the default-arg
capture; pairing re-recorded and consistent.
2026-08-31 14:34:09 +08:00
Chinesezjc b018abf405 fix(code-runtime-python): bind the pump RuntimeError after its docstring and _done_with_value deps as defaults
- The reply pump's _RuntimeError binding is placed after the function docstring
  (so the docstring remains the __doc__) and the dead _run-side binding is
  removed. _done_with_value binds _check_done_value/_encode_json_plain as
  default arguments so a __main__ rebind after model execution cannot rewrite a
  success into an exception.

The _str/_bool/_BindingRejection pump bindings were attempted but break the
closed-loop pump test (the self-referential _BindingRejection local interferes
with the closure), so they are left unbound; rebinding those names (builtins and
one internal class) is outside the practical threat model.
2026-08-31 14:34:09 +08:00
Chinesezjc 2d82b658ba fix(code-runtime-python): bind RuntimeError inside the module-level _pump_replies
The previous commit bound _RuntimeError in _run, but _pump_replies is a separate
module-level function, so its except _RuntimeError referenced an out-of-scope
local and raised NameError instead of catching the closed-loop failure — killing
the pump and timing out the run. Bind _RuntimeError at the top of _pump_replies
too. The closed-loop pump test now passes.
2026-08-31 14:34:09 +08:00
Chinesezjc bcc11f1235 fix(code-runtime-python): bind RuntimeError for the reply pump catch and note the exception-class locals
The reply pump's except RuntimeError resolved the module global at runtime, so a
__main__.RuntimeError rebind could make a closed-loop scheduling failure escape
the catch, killing the pump and stranding every later reply. Bind RuntimeError
into a _run local alongside BaseException and catch the local. The settlement
note Decision now records that the exception classes the settlement-path except
clauses catch are bound into locals / a closure cell before model code runs
(en + zh); pairing re-recorded and consistent.
2026-08-31 14:34:09 +08:00
Chinesezjc 69dc17c906 fix(code-runtime-python): bind BaseException into every settlement-path except clause
The rebindable-BaseException vector the bot flagged existed in every except
clause of the settlement path, not just the _run outer catch: safe_model_traceback
(three guards) and the post-done flush swallow resolved the module-global
BaseException at runtime, so a __main__.BaseException rebind plus a throwing
__str__ could let a render-time exception escape and lose the done frame. Bind
BaseException into a _run local (at the top) and a closure cell in
_make_failure_reporter, and change every such except clause to catch the local
— immune to a one-line rebind.
2026-08-31 14:34:09 +08:00
Chinesezjc 0102cd95bf fix(code-runtime-python): catch the model exception with a pre-program local exception class
The _run outer try/except used the module-global BaseException, which the
program (running as __main__) can rebind: __main__.BaseException = RuntimeError
made the except resolve to RuntimeError, so a subsequent ValueError escaped _run
with no done frame and misreported the run as worker-exit. Bind BaseException
into a _run local before the program runs so the catch is immune; a regression
test rebinds BaseException and raises, asserting an exception, not a worker-exit.

Also correct the NUL-escape comment text: the JSON escape-result side is \^@ (6
bytes, the valid JSON NUL escape), not \x00, so the 6x-budget arithmetic in the
comments is self-consistent. Register the BaseException-rebind case in the
settlement note Testing (en + zh) and re-record the pairing.
2026-08-31 14:34:09 +08:00
Chinesezjc 202c428137 docs(code-runtime-python): correct the fallback-mechanism wording and the no-fail-before count
Addresses the bot's keep-current review findings:
- The module-level fallback comment now states the mechanism truthfully: the
  module globals are RAW primitives bound into _run LOCALS before the program
  runs (the immunity lives in the frame-local binding, not the module global);
  and the fallback literal <unrenderable> is distinguished from the failure
  reporter's _UNRENDERABLE_DIAGNOSTIC text.
- The settlement note's fallback mechanism wording, the transitive-name rebind
  case (now listing the three fallback primitives), and the no-fail-before count
  are aligned en/zh; the zh Problem paste damage is fixed and the Consequences
  count is ten with the 10th item.
- Pairing re-recorded and consistent.
2026-08-31 14:34:09 +08:00
Chinesezjc d3e34d5612 test(code-runtime-python): replace literal NUL bytes in comments with the escape text
The comments describing NUL serialization contained literal NUL bytes, which
interfere with source tooling. Use the \x00 escape text instead.
2026-08-31 14:34:09 +08:00
Chinesezjc fe3ba24057 docs(code-runtime-python): update the no-fail-before count to ten and document the hard==1 CPU blind spot
Addresses the keep-current review findings:
- The settlement note's Problem/Consequences count is nine -> ten, adding the
  log-fragment seal to the no-fail-before enumeration (its 25 M-scale OOM is not
  deterministically constructible in CI); the new Decision section title now
  names all four mechanisms and the double blank line is removed.
- README Known Limitations (en + zh) documents the 1-second dual-limit
  ulimit -t 1 CPU overrun being reported as worker-exit (the hard >= 2 guard
  cannot lower a 1-second soft to 0); pairings re-recorded and consistent.
2026-08-31 14:34:09 +08:00
Chinesezjc 4ff050de71 fix(code-runtime-python): bind the send_done fallback primitives into locals and use a bare except
The done-frame fallback read _os_write/_memoryview/_FALLBACK_DONE_FRAME as module
globals at call time, so a single-line rebind of any of them reopened the
rebind hole the fallback exists to close. Bind them into _run locals before the
program runs, and use a bare except (which catches everything without naming
BaseException, so a rebind of that name cannot defeat the handler). The
transitive-name rebind test now also rebinds _os_write/_memoryview/
_FALLBACK_DONE_FRAME to pin the fallback's immunity.
2026-08-31 14:33:31 +08:00
Chinesezjc 9b29d0226e fix(code-runtime-python): make the log seal incremental, scope the soft-lowering to RLIMIT_CPU, and capture memoryview
Addresses the bot's follow-up review findings on the settlement-path fixes:
- The _LogStream seal joined the WHOLE accumulated buffer past the fragment cap,
  re-copying the growing block O(B^2/cap) times for a large drip. It now seals
  only the current fragments into a _pending_blocks entry (character count
  unchanged), so a 25 M single-character drip stays O(B); the newline/flush/
  _push_bounded_prefix consumers join blocks + fragments once.
- The _clamped soft==hard lowering is scoped to RLIMIT_CPU: for RLIMIT_AS a
  one-byte soft differential would only misalign the child's applied limit with
  the host-side budget gate, with no signal to preserve. The hard == 1 blind
  spot is documented.
- send_done's fallback captures memoryview at import (_memoryview) alongside
  os.write, so a one-line rebind of the name cannot change the fallback write;
  the comment now states the module-level-captured mechanism.
2026-08-31 14:33:31 +08:00
Chinesezjc 72241b9f06 test(code-runtime-python): correct the dual-limit CPU overrun assertion and use a hard limit >= 2
The dual-limit CPU test used ulimit -t 1 (hard == 1), which the _clamped
soft-lowering guard (hard >= 2) intentionally does not lower, and trapped
SIGXCPU (which defeats the fix). Use ulimit -t 2 (hard == 2, so the soft is
lowered to 1) and leave SIGXCPU unhandled; the run then classifies as a timeout.
The message is the CPU-time-exhausted diagnostic, not the literal 'SIGXCPU'.
2026-08-31 14:33:31 +08:00
Chinesezjc dcbce50ec2 fix(code-runtime-python): close the log-fragment OOM, CPU classification, and done-send transitive-dependency findings
Addresses the bot's v16 review on the settlement-path code:
- critical: _LogStream._pending now seals the fragment list past a chunk cap
  (like the host captureStray seal), so a newline-free single-character drip no
  longer accumulates one list slot per write and OOMs on its own accounting.
- _clamped lowers a soft==hard result by one unit (when hard >= 2) so a
  dual-limit ulimit -t leaves SIGXCPU a window to fire and a definite CPU
  overrun is reported as a timeout, not a 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 import-time captured os.write, so a settled exception verdict is never
  downgraded to worker-exit.
- drainReplies clears the consumed replyQueue slot so a wide written payload is
  released immediately, bounding host memory to the current backlog under
  sustained fd-3 backpressure.
Tests added for each (fragment cap drip, dual-limit CPU overrun, transitive-name
rebind done frame).
2026-08-31 14:33:31 +08:00
Chinesezjc 7198234a82 test(code-runtime-python): pin send_done against rebinding write_encoded and _encode_json_plain
The rebinds-every-name fixture previously only rebound ProtocolChannel.send_sync,
which a bound method object ignores and the shipped send_done no longer calls —
so it did not actually guard the call-time-lookup shape. Rebind write_encoded
and _encode_json_plain too (the names send_done would resolve late if it looked
them up at call time) and state that in the settlement note's Testing section
(en + zh), re-recording the pairing.
2026-08-31 14:33:31 +08:00
Chinesezjc da38c16912 fix(code-runtime-python): drain the reply queue by head cursor, not shift()
Each shift() re-slices the remaining array, so draining a large gather of
wide bindings awaiting fd 3's drain was O(n^2). Reading by a head index into
the array keeps the drain linear; the finally still discards everything.
2026-08-31 14:33:31 +08:00
Chinesezjc e0e1aa307d fix(code-runtime-python): bind encode/write for send_done and correct stray-flush retention
Addresses the follow-up review findings on the settlement-path fixes:
- send_done now routes both the pre-encoded VALUE frame and the dict ERROR
  frame through a bound _encode_json_plain + bound write_encoded, never through
  channel.send_sync (whose body re-resolves self.write_encoded and the module
  _encode_json_plain at call time) — a program rebinding ProtocolChannel.
  write_encoded or __main__._encode_json_plain no longer skips the done frame.
- flushStray retention re-accrues the withheld multibyte tail from a FRESH
  utf8 state (previously metering the carried lead against the post-flush
  expected>0 state charged it as an illegal continuation), and skips admitting
  when the whole residual drained into the retained tail so no bogus empty
  entry is pushed.
2026-08-31 14:33:31 +08:00
Chinesezjc be0551f52f fix(code-runtime-python): suppress no-unnecessary-condition on the late-rejection settled guard 2026-08-31 14:33:31 +08:00
Chinesezjc 9e6f279040 fix(code-runtime-python): drop the sealed-blocks ternary in the stray flush to hold 100% branch coverage 2026-08-31 14:33:31 +08:00
Chinesezjc 6634d4800c fix(code-runtime-python): bind done-send callables and cover the stray-flush retention
Corrections to the settlement-path review fixes:
- send_done was invoking channel.send_sync / channel.write_encoded via a late
  method look-up, which a program running as __main__ could rebind through
  __main__.ProtocolChannel.send_sync before the failure path ran — a rebound
  send that raises then skipped the done frame and downgraded a settled
  exception to worker-exit. Bind both channel methods into locals before the
  program runs, mirroring the pre-existing binding of flush_out/flush_err/
  safe_model_traceback.
- Restructure flushStray so the mid-sequence budget-flush retention arm is a
  self-contained v8-ignored branch and the covered default path decodes the
  full residual (not schedulable-through-the-seam boundary).
2026-08-31 14:33:31 +08:00
Chinesezjc f71914ceea fix(code-runtime-python): close four settlement-path review findings
Pace-free completion framing, stray UTF-8 flush, and late-rejection guards:
- Pre-encode the completion value at its validation point so send_done never
  re-walks a live value a mutating daemon thread could change (TOCTOU); a
  mutation-induced encode throw is then classified as 'exception', not a
  host-side worker-exit.
- Budget-triggered stray flush retains an incomplete multibyte UTF-8 tail
  (<=3 bytes) as residual instead of decoding a legal, split character to
  U+FFFD in an admitted entry; the end/closeDeadline paths still full-decode.
- Check 'settled' before formatting a late binding rejection's message, so a
  hostile message getter cannot stall or exhaust a run that already settled.
- Document _check_done_value's first-to-trip ruling in its docstring.
- Rewrite ProtocolChannel.send_sync around a shared write_encoded that the
  done frame's pre-encoded string path uses.
2026-08-31 14:33:31 +08:00
Chinesezjc 441ebd0433 test(code-runtime-python): exempt the mid-drain settle branch from coverage
The drain loop's `if (settled) break` needs the run to settle in the window
between two queued frames. A file probe on the concurrent-replies case shows the
queue does reach depth 11, but the wall clock never lands inside that window, so
the branch is not schedulable from a test; a case written to force it passed
without ever executing the line, so it is removed rather than left as coverage it
does not provide. The branch carries a v8 ignore naming what is unreachable.
2026-08-31 14:33:31 +08:00
Chinesezjc 6f58f9c336 fix(code-runtime-python): pace concurrent binding replies 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 against 0.0 MiB once paced.

Replies now go through a queue that encodes and writes one frame at a time,
awaiting `drain` when the pipe is full. 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.

This was previously deferred on the grounds that serializing would narrow the
seam's concurrency contract. That reasoning was wrong: 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 still run concurrently. Only the
host's peak memory and the flush timing change. The README entry recording the
deferral is removed and the Agent Note records the mechanism instead.
2026-08-31 14:33:31 +08:00
Chinesezjc 2a9a917853 fix(code-runtime-python): drop a late binding resolution before snapshotting it
`sendReply` already refuses to write after the run settled, but only after
`snapshotJsonValue` walked and copied the resolution. Binding resolution carries
no seam-level byte cap, so a binding resolving a wide value after `maxWallMs`,
an abort, or dispose settled the run spent host heap building a frame that was
then discarded. The check moves ahead of the snapshot.

Also in this change:

- `readProcessStart` moved after `messageOf`. Inserting it between `messageOf`'s
  JSDoc and its body left that function undocumented and the orphaned block
  reading as a second doc for the reader; `verify-export-jsdoc` does not catch it
  because `messageOf` is not exported.
- The README pair adds the disposed-runtime rejection to `run()`'s public
  contract, which `src/index.ts` has enforced all along.
- Known Limitations records three deferred constraints that until now existed
  only in review discussion: the combined log-and-value peak the load gate does
  not model, the host-side per-member expansion of a wide binding reply (owned by
  `packages/core/session`, and shared with the worker-thread backend), and the
  absence of fd-3 backpressure for concurrent replies.
- The Agent Note's same-group section records the teardown identity guard and its
  two rulings, including why an ABSENT start-time reading proceeds rather than
  withholding the signal, and that reading it as a mismatch is what turned the
  three same-group heartbeat cases red on Linux.
2026-08-31 14:31:48 +08:00
Chinesezjc 0a46bb3414 style(code-runtime-python): keep the teardown v8-ignore under the line limit
The directive carried its whole justification inline at 203 characters, past the
140 the @stylistic/max-len rule allows (imports and template-literal messages
are exempt; a line comment is not). The reasoning moves to the lines above and
the directive keeps a short pointer, since a v8 ignore must stay on one line.
2026-08-31 14:30:02 +08:00
Chinesezjc 68f61b2e2f test(code-runtime-python): exempt the two single-platform teardown arms from coverage
The PID-reuse guard has two arms no single OS can execute: the non-Linux early
return in readProcessStart (the Linux coverage lane always takes the read path)
and the refusal arm, which needs a real pid recycled into a new group leader
between spawn and teardown -- no test can schedule that. The coverage lane
reported 99.53% statements / 99.14% branches on src/index.ts for exactly these
two.

Both carry a v8 ignore naming what cannot be reached and why, the convention
this file and subprocess-local already use for platform defenses. The reader
itself stays covered by the process-identity test rather than being exempted
wholesale.
2026-08-31 14:30:02 +08:00
Chinesezjc 2ad93da755 fix(code-runtime-python): treat an absent start-time reading as reaped, not recycled
The PID-reuse guard refused to signal whenever the current reading differed
from the one taken at spawn, including when it was ABSENT. On Linux a reaped
leader has no /proc/<pid>/stat, so every teardown after the leader exited
skipped SIGTERM/SIGKILL while the group it led still held survivors -- the
exact case the process-group teardown exists to reap. Three same-group survivor
tests went red on the coverage lane; they pass on Darwin because the reader
always returns undefined there, leaving the guard inert.

Only a present-and-different reading now blocks the signal. Verified on the
self-hosted Linux box: a reaped leader with live survivors allows the signal, a
pid whose start time differs still blocks it, and a live matching process is
signalled.
2026-08-31 14:30:02 +08:00
Chinesezjc 33318a5767 docs(code-runtime-python): state the real load-time rejections and finish the zh README
The README pair described `run()` as rejecting "a malformed binding namespace or
non-positive config", which understated and misplaced the configuration
failures: a non-Unix platform, a non-integer budget, a timer value setTimeout
would clamp, a budget larger than one fd-3 frame, and an incompatible
addressSpaceMb/output-budget pair all throw from the CONSTRUCTOR, so they fail
when the plugin loads rather than on a later run. Both sides now separate the
load-time platform/configuration errors from the run-result contract.

The Chinese README's Model Experience and KV Cache effect sections were still
untranslated English; the pairing record only tracks hashes, so it could not
show that. Both are now translated.
2026-08-31 14:30:02 +08:00
Chinesezjc 2e3cf144d5 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.
2026-08-31 14:28:26 +08:00
Chinesezjc e6b547bef4 fix(code-runtime-python): guard teardown, log prefix, and settlement flush
Four independent corrections in the run lifecycle.

`killGroup` signalled `-child.pid` with a raw `process.kill`. Node keeps the
numeric `child.pid` after the leader is reaped and only clears its internal
handle, so `child.kill()` refuses while the raw call does not; `close` can
trail `exit` by seconds when a pipe-holding descendant keeps the streams open.
A recycled pgid could therefore receive this run's SIGTERM and armed SIGKILL.
`groupEmpty()` does not cover it: it reports whether the group has members, not
whether they are ours, and it first runs after the signal. The leader's start
time is now read at spawn and re-checked before each signal, matching the
position packages/subprocess/subprocess-local already states
("ProcessIdentity ... preventing teardown escalation after PID reuse"). Kept
local rather than depending on that package, which would add an architectural
edge. Linux reads /proc; Darwin has no /proc, so the reader reports undefined
and the guard degrades to the previous behavior instead of forking `ps` on a
teardown path.

`_push_bounded_prefix` built `(*self._pending, extra)`, copying every pending
reference into a same-size tuple before the bounded loop. For a
single-character drip that is a second pointer array as large as the list:
measured +80 MiB of tuple over a 40 MiB list for 5.2M chunks, the allocation
the bounded prefix exists to avoid. It now iterates the list in place and
handles `extra` in the loop's `else`; 4000 randomized inputs produce byte-identical
prefixes.

The settlement `flush_out()`/`flush_err()` ran outside any guard while `done`
was already decided, so a flush raising under memory pressure skipped
`send_done` and downgraded a child-classified `exception` into a host-side
`worker-exit`. Both are now wrapped, swallowing only the log tail.

The boot re-check's `if effective_soft != RLIM_INFINITY` was dead: `_clamped`
is asked for a finite `addr_bytes` on both sides and each branch returns that
value or a `min` with an inherited bound, so RLIM_INFINITY is unreachable. The
guard could only ever have skipped the re-check it claimed to protect.
2026-08-31 14:26:23 +08:00
Chinesezjc 8f7d9121d1 fix(code-runtime-python): bound three child-side walks by depth, not width
Three separate paths in the CPython child allocated state proportional to a
value's width or a string's length, so a legitimate input the byte budgets
admit could die as the program's own MemoryError.

`_lossless_json_violation` enqueued one traversal tuple per member while
running, in `dispatch`, over MODEL-CONSTRUCTED binding arguments that no
child-side byte budget bounds first. It now uses the same (kind, container,
iterator) cursor the other two walks already had, checking dict keys as the
cursor pulls each entry. Measured over `[0] * 6_000_000` (~17 MB of JSON):
459.1 MiB of traversal tuples before, 0.0 MiB after.

`_decode_json_plain` matched JSON strings with a `(?:[^"\\]|\\.)*` repetition,
which makes CPython's engine retain backtracking state proportional to the
string's width: 146 MiB for a 1 MiB string, 557.8 MiB for 4 MiB. A legitimate
multi-megabyte binding reply raised MemoryError inside `_pump_replies`, and
because that pump is the only settler of the call's future, the run stranded
until the wall clock reported `timeout`. Strings now scan chunk-to-chunk over a
character class, which the engine matches without backtracking state; the same
4 MiB decode peaks at the 4.0 MiB result.

`_check_done_value` charged strings and dict keys what
`_dump_string(...).encode()` returned, building the escaped copy plus its
encode to MEASURE it -- ~6x the original each for control-heavy text, so
metering a value the budget then rejects could itself breach RLIMIT_AS and
report `exception` where the seam promises `output-limit`. The new
`_json_str_cost` counts instead, reusing `_json_string_cost`'s C-level passes
and reproducing `_dump_string`'s exact surrogate rules (fold spelled-out pairs,
charge six ASCII bytes per lone surrogate). Identical values, 228.9 MiB -> 19.1
MiB of peak on a 20M-NUL string.

Each fix ships a regression test. The two RLIMIT_AS repros are Linux-only:
Darwin does not apply the limit, so the peaks above are measured directly and
recorded in the test comments.
2026-08-31 14:24:59 +08:00
Chinesezjc 86674ed21e test(code-runtime-python): budget the wide-value walk for an instrumented lane
The O(depth) wide-value regression test ran under `maxWallMs: 20_000`, but the
cursor pulls 6M elements one at a time through Python-level frames: ~11s on an
idle machine, and more under the coverage lane's V8 instrumentation with several
workers sharing a runner. CI reported `timeout` instead of the round-trip.

Raise the run's ceiling to 60s inside a 90s vitest timeout, so the runtime's own
wall clock still fires first on a genuine hang. The assertion is unchanged and
still discriminates: restoring the O(width) `stack.extend` enqueue fails the test
with a child-side MemoryError in ~2.6s.
2026-08-31 14:24:59 +08:00
Chinesezjc bca73068f6 fix(code-runtime-python): walk the completion value in O(depth), not O(width)
`_check_done_value` and `_encode_json_plain` pushed one stack entry per child
(plus a separator marker, and `dict.items()` materialized as a list), so the
bookkeeping scaled with the value's WIDTH rather than its depth. A value the
byte meter admits could then die on the walk's own frames: a flat
`[0] * 2_000_000` serializes to 4.0 MB, but measured peaks were 145.2 MB in the
meter and 114.7 MB in the encoder — 28.7x the serialized size, far past the 12x
the load-time address-space gate reserves.

Each container now pushes ONE cursor frame that pulls its children one at a
time and writes into a shared `io.StringIO`, so the output string is the only
width-proportional allocation and the caller already metered its size. Measured
on the same value: 0.0 MB in the meter and 9.0 MB in the encoder (2.3x), with
identical verdicts.
2026-08-31 14:24:59 +08:00
Chinesezjc 9a8663cc4c fix(code-runtime-python): flush logs before framing the completion value
The load gate bounds maxLogBytes and maxValueBytes independently against the
address space, but the child framed the completion value (materializing its
escaped form to meter it, then encoding the frame) while a newline-free log tail
still sat unflushed in _pending. Those two peaks added, so two budgets each
admitted alone could together breach RLIMIT_AS and die as worker-exit instead of
settling. The success path now flushes both log streams before _done_with_value
runs; the trailing flush stays for the exception path and is an idempotent no-op
after a successful settle. A combined-peak regression test (32 MiB each against
512 MiB) asserts the over-budget value reports output-limit rather than OOMing.

Also corrects the worst-case-multiple JSDoc and Agent Note: after 1088d6f03d
made flush_line drop pending before its push, the settlement-flush path holds
two copies, not three, so the newline path is the sole 12x worst case. The
reorder is recorded as a called-out untested fix (the 12x gate already admits
only configs safe under both flush orders).
2026-08-31 14:24:59 +08:00
Chinesezjc 9d9525549d fix(code-runtime-python): raise the output-budget worst-case multiple to 12 and reject the boundary
The load-time output-budget/addressSpaceMb gate used a worst-case multiple of 8,
assuming two simultaneous ~4x astral copies (the built string and its encode).
Three are live at the peak: on the newline path a single write holds the caller's
text argument, the line slice handed to push, and push's encode copy; the
settlement flush_line path held the pending chunks, their join, and that encode
copy. A budget admitted at 8x (e.g. maxLogBytes 48 MiB against addressSpaceMb 512)
could still OOM the child. The multiple is now 12, the strict `>` is `>=` so a
budget whose peak exactly equals the room left after the interpreter baseline is
rejected (that peak plus the baseline is the whole address space), and flush_line
drops the pending chunks before its push to match the newline path's
join-clear-push order. The child re-check mirror and both note sides move in step;
config-catalog is regenerated from the updated field JSDoc.
2026-08-31 14:22:37 +08:00
Chinesezjc ce91c70f9a test(code-runtime-python): assert the inherited-RLIMIT_AS boot re-check reports exception
The boot re-check raises inside bootstrap's setrlimit-phase handler, which
classifies every resource-limit-application failure as kind 'exception'. The
test asserted 'worker-exit'; align it to the actual class and keep the message
assertion so the case still discriminates a config rejection from a generic
setrlimit error. The Agent Note's two references to the reported kind are
corrected on both language sides and the pair re-recorded.
2026-08-31 14:22:37 +08:00
Chinesezjc 436a97a12d fix(code-runtime-python): reserve the interpreter baseline in the budget gate and re-check against the clamped RLIMIT_AS
The output-budget/address-space gate's 8x multiple had no room for the
interpreter's own footprint, so a budget sized right at addressSpaceMb/8 was
admitted while its worst-case peak plus the interpreter overran RLIMIT_AS
(e.g. 15 MiB maxLogBytes against 128 MiB). Reserve a fixed
INTERPRETER_BASELINE_BYTES (64 MiB) before the multiple claims the rest, so each
budget times 8 must fit the room LEFT after the baseline.

The host gate validates against the CONFIGURED addressSpaceMb, but a launch
environment can inherit a stricter RLIMIT_AS (a ulimit -v wrapper below
addressSpaceMb) that _clamped lowers the effective limit to, leaving the budgets
sized for a ceiling the child never gets. bootstrap.py now 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 rather than letting a
near-budget output OOM mid-run.

Add regression tests for both (the load gate against a 256 MiB address space
covering both budgets, and a ulimit -v wrapper for the inherited-limit re-check);
register the tail-copy test in the note Testing section; sync the zh pair. Merges
origin/feat/code-runtime-python-protocol to resolve the DIRTY base.
2026-08-31 14:22:37 +08:00
Chinesezjc 86c6d9345e test(code-runtime-python): size the tail-copy repro so the model can build its own string
The tail-copy regression built `"first\n" + "A" * 200 MiB`, whose construction
alone peaks near 400 MiB (the string plus the concat temporary) and OOMs under
the 384 MiB addressSpaceMb before the log path under test runs — a MemoryError in
the model, not the defect. Build the tail in a variable and concatenate only the
newline (peak ~2x150 MiB = 300 MiB, under the address space), so the model's own
allocation fits; the pre-fix code then buffered the whole 150 MiB tail again,
pushing past 384 MiB, while the sliced prefix does not.
2026-08-31 14:22:37 +08:00
Chinesezjc d9307ae2a4 fix(code-runtime-python): size the output-budget/address-space gate by worst-case Unicode and gate both budgets
The load-time addressSpaceMb gate used a 1/8 fraction derived for ASCII, but the
child ledgers trigger on character count against a serialized-byte budget: an
astral character is one character yet ~4 bytes stored and ~4 encoded, live at
once, so the true worst-case peak is ~8x the budget, not ~2x. Replace the
fraction with an explicit OUTPUT_BUDGET_WORST_CASE_ADDRESS_SPACE_MULTIPLE (8)
and a strict `>`, and gate maxValueBytes the same way as maxLogBytes — the value
path builds and encodes a near-budget completion under the same RLIMIT_AS, so
the incompatible pair was previously admitted there too.

Slice the newline branch's unterminated tail to a budget-sized prefix: it
buffered the whole text[pos:] before the flush trigger could bound it, so an
early newline plus a huge tail made a second full copy of the model's string —
an RLIMIT_AS death the config gate cannot cover since the tail can far exceed
maxLogBytes.

Disclose the cross-field constraint in the maxLogBytes/maxValueBytes/addressSpaceMb
JSDoc (regenerating config-catalog); refresh the note's stale
Buffer.byteLength(JSON.stringify) reference; reconcile the arrival-order rebuttal
with the seam's "in order" logs JSDoc (within-stream, cross-stream best-effort).
Extend the load-rejection test to both budgets and add a tail-copy regression;
sync the zh pair.
2026-08-31 14:22:37 +08:00
Chinesezjc 2df88b5bbe fix(code-runtime-python): reject an oversized maxLogBytes at load instead of metering log capture at runtime
The child log ledger encodes an admitted entry to UTF-8 once to charge its
serialized cost, so a maxLogBytes approaching addressSpaceMb lets a legitimate
near-budget log entry breach RLIMIT_AS and die as worker-exit instead of
truncating. Two runtime fixes were tried and both traded one resource bound for
another: an exact serialized-cost check is either a full encode (the allocation
being avoided) or a per-character Python loop that burns the CPU budget (a 10 MB
write hits SIGXCPU under cpuSeconds:1). The breach is a property of the
maxLogBytes/addressSpaceMb pair, not any write, so reject the incompatible pair
at load — maxLogBytes must stay within one eighth of the addressSpaceMb byte
count — and revert _LogStream to its original character-count buffering, which
is memory-safe once the budget fits the address space. The check runs on every
platform since the incompatibility is a config-value property, not a runtime one.

Replace the child-flood regression tests (which asserted the reverted runtime
behavior) with a load-rejection test. The host-side accrueStrayCost UTF-8
per-lead validation and its tests are unaffected. Update the note and zh pair.
2026-08-31 14:22:37 +08:00
Chinesezjc c24e1e991b fix(code-runtime-python): charge structurally-valid-but-illegal UTF-8 and newline-path logs by decoded cost
accrueStrayCost accepted any 0x80-0xBF continuation, so a CESU-8 surrogate
(ED A0 80) or overlong (E0 80 80) — structurally well-formed but illegal, and
as cheap to flood as 0xFF — was charged its structural width 3 while
toString('utf8') renders each byte as its own U+FFFD (cost 9). Validate each
lead's first-continuation range (WHATWG E0/ED/F0/F4 bounds) and charge 3 per
byte of any sequence outside it, folding a broken prefix to one U+FFFD.

The child _LogStream newline path had the same char-vs-serialized gap the
newline-free trigger had: its per-line fit checks (first reconstructed line and
each subsequent line) compared character count against the serialized-byte
budget, so a control-char line passed and _logs.push encoded it whole, breaching
RLIMIT_AS. Route every check through _fragment_cost_upto, which sums per-char
costs from _json_char_cost over a start/end sub-range without slicing or
encoding and stops at the budget.

Decline arrival-order stray flushing: the two pipes' data events interleave
nondeterministically and logs carries no cross-pipe ordering guarantee, so a
fixed drain order is as valid as any and an arrival-tick branch could not be
covered without a flaky test.

Add CESU-8/overlong, newline-path-flood, and all-lead-class reassembly
regression tests; fix the note's now-inaccurate CESU/illegal-byte claims and a
fixture byte-count comment; sync the zh pair.
2026-08-31 14:22:37 +08:00
Chinesezjc 5c43621ed2 fix(code-runtime-python): weigh the child log flush by per-fragment serialized cost, allocation-free
The prior child-flush fix measured each fragment with chunk.encode('utf-8'),
which copies the whole write — under a tight addressSpaceMb a single 340 MiB
write died on that encode (the exact allocation _push_bounded_prefix exists to
avoid), and re-scanning the whole pending list per write was quadratic under a
daemon-thread flood (the concurrent-write test timed out at 28s). Compute each
fragment's serialized cost with _fragment_cost_upto, which walks the str via a
new _json_char_cost (code point to escaped width, no encode) and stops once the
running total passes the budget, and accumulate it into _pending_cost once per
write. The early-flush trigger reads that accumulator: still charges control
chars their full serialized width (a NUL is 6 bytes), but never encodes a whole
write and never re-scans the buffer, so the 340 MiB single-write and
daemon-thread tests pass alongside the NUL-flood one.

Rework the child NUL-flood regression to write in 1 MiB chunks under a 512 MiB
address space so its own argument construction is not the allocation under test.
2026-08-31 14:22:37 +08:00
Chinesezjc d9f2fa1b04 test(code-runtime-python): drive the child NUL-flood test without a single huge argument
The child-log-flood regression built one 30M-char argument string, which under
the 64 MB addressSpaceMb died on RLIMIT_AS during construction (exit 120) before
the flush trigger under test could run, so it failed on Linux CI. Write the
flood in 1 MiB chunks under a 512 MiB address space instead: the argument str is
never itself the allocation under test, the fixed serialized-cost trigger keeps
the pending tail bounded to a few MiB, and the run completes at the marker; the
pre-fix char-count trigger accumulates the whole ~200 MiB and its ~1.2 GiB
settlement encode breaches RLIMIT_AS. Mirrors the addressSpaceMb budget the
existing oversized-completion tests use.
2026-08-31 14:22:37 +08:00