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.
This commit is contained in:
Chinesezjc
2026-08-31 14:22:37 +08:00
committed by Tianyi Cui
parent 2df88b5bbe
commit d9307ae2a4
7 changed files with 144 additions and 64 deletions
@@ -248,7 +248,17 @@ class _LogStream(io.TextIOBase):
pos = newline + 1
if pos < length:
if self._logs.remaining > 0:
tail = text[pos:]
# Buffer only a budget-sized PREFIX of the tail, not the whole
# `text[pos:]`: an early newline followed by a huge unterminated
# tail (`"\n" + "A" * 30 MiB`) would otherwise copy the entire
# tail into `_pending` here — a second full copy of the model's
# own string, the RLIMIT_AS death this path exists to avoid —
# before the newline-free trigger below could bound it. Anything
# past `remaining` characters cannot be admitted (the char count
# is a lower bound on the serialized cost), so a
# `remaining + 4`-character prefix is all that can ever survive;
# the flush trigger below rejects it and emits the marker.
tail = text[pos:pos + self._logs.remaining + 4]
self._pending.append(tail)
self._pending_chars = len(tail)
else: