mirror of
https://github.com/deepseek-ai/deepseek-harness.git
synced 2026-09-11 04:00:38 +00:00
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.
This commit is contained in:
@@ -51,11 +51,13 @@ _MAX_FALLBACK_NAME_CHARS = 200
|
||||
# Mirror of the host's output-budget/address-space gate (src/index.ts's
|
||||
# OUTPUT_BUDGET_WORST_CASE_ADDRESS_SPACE_MULTIPLE and INTERPRETER_BASELINE_BYTES),
|
||||
# re-applied against the EFFECTIVE RLIMIT_AS after inheritance clamping. An astral
|
||||
# character is one character but ~4 bytes of str storage and ~4 UTF-8 bytes, live
|
||||
# at once while the ledger charges and frames it, so a budget's worst-case peak is
|
||||
# eight times its byte count; the interpreter's own footprint is reserved on top.
|
||||
# Kept in sync with the host constants by the shared reasoning, not a wire field.
|
||||
_OUTPUT_BUDGET_WORST_CASE_MULTIPLE = 8
|
||||
# character is one character but ~4 bytes of str storage and ~4 UTF-8 bytes, and
|
||||
# three such copies are live at the peak — the caller's write argument, the line
|
||||
# slice or joined pending handed to push, and the encode copy push takes — so a
|
||||
# budget's worst-case peak is twelve times its byte count; the interpreter's own
|
||||
# footprint is reserved on top. Kept in sync with the host constants by the shared
|
||||
# reasoning, not a wire field.
|
||||
_OUTPUT_BUDGET_WORST_CASE_MULTIPLE = 12
|
||||
_INTERPRETER_BASELINE_BYTES = 64 * 1024 * 1024
|
||||
|
||||
|
||||
@@ -350,9 +352,16 @@ class _LogStream(io.TextIOBase):
|
||||
# against them.
|
||||
with self._logs.lock:
|
||||
if self._pending:
|
||||
self._logs.push("".join(self._pending))
|
||||
# Join, drop the chunks, THEN push — the same order the newline
|
||||
# path uses (:232-235). 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
|
||||
# encode, matching that path's peak.
|
||||
line = "".join(self._pending)
|
||||
self._pending = []
|
||||
self._pending_chars = 0
|
||||
self._logs.push(line)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
@@ -664,7 +673,7 @@ async def _run(channel: ProtocolChannel) -> None:
|
||||
if effective_soft != resource.RLIM_INFINITY:
|
||||
budgetable = effective_soft - _INTERPRETER_BASELINE_BYTES
|
||||
for _budget_key in ("maxLogBytes", "maxValueBytes"):
|
||||
if int(boot[_budget_key]) * _OUTPUT_BUDGET_WORST_CASE_MULTIPLE > budgetable:
|
||||
if int(boot[_budget_key]) * _OUTPUT_BUDGET_WORST_CASE_MULTIPLE >= budgetable:
|
||||
raise ValueError(
|
||||
"config.%s is too large for the inherited RLIMIT_AS of %d bytes "
|
||||
"(a near-budget output would breach it during encode); "
|
||||
|
||||
Reference in New Issue
Block a user