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).
This commit is contained in:
Chinesezjc
2026-08-31 14:24:59 +08:00
committed by Tianyi Cui
parent 9d9525549d
commit 9a8663cc4c
6 changed files with 65 additions and 16 deletions
@@ -890,6 +890,16 @@ async def _run(channel: ProtocolChannel) -> None:
exec(code, ns) # noqa: S102 -- defines __dsh_main__; executing model code is the point
value = await ns["__dsh_main__"]()
die_if_cpu_exhausted(cpu_seconds)
# Flush the log buffers BEFORE metering and framing the completion value.
# `_done_with_value` materializes the value's escaped JSON form to meter
# it, and `send_done` encodes the frame — several copies of a near-budget
# value live at once (see OUTPUT_BUDGET_WORST_CASE_ADDRESS_SPACE_MULTIPLE).
# Any unflushed log pending would add its own bytes to that peak, so a
# `maxLogBytes` and a `maxValueBytes` each admitted alone by the load gate
# could together breach RLIMIT_AS. Flushing first frees the log pending so
# the value frame's peak stands alone against the address space.
flush_out()
flush_err()
done = _done_with_value(value, max_value_bytes)
except BaseException as exc: # noqa: BLE001 -- report every failure to host
done = {
@@ -911,7 +921,9 @@ async def _run(channel: ProtocolChannel) -> None:
# Flush any print output not terminated by a newline (a traceback always
# ends in one, but `print(x, end="")` or a bare write may not), so the
# final partial line is not silently dropped.
# final partial line is not silently dropped. The success path already
# flushed before framing the value; this is an idempotent no-op there and
# the flush the exception path needs.
flush_out()
flush_err()
reply_task.cancel()