fix(code-runtime-python): correct the open-merge cap arithmetic on both sides

The review's arithmetic checks: the closing-frame walk used cap
logBudget - openCost, so a compliant merged entry (58-byte wire cost under a
64-byte budget) could see a negative cap and truncate; the first-fragment cap
used logBudget instead of the ledger's logBudget - 1, so an open frame costing
63 was admitted with a bill of 64, pushing the ledger negative and letting a
subsequent empty frame ride in one byte past the configured cap; and the child
billed a closing frame as a fresh entry (quotes+separator again) instead of the
merged tail, truncating an exact-fit 30+30 entry.

Fixes: first-fragment cap logBudget - 1 (matching admit), continuation and
closing-frame cap logBudget + 2 (billed without quotes), jsonStringCostUpTo
returns undefined below 2 bytes, and the child's split billing keys off
_open_started alone (a closing frame pays content only) with the cheaper bound
len(text) while a merge is open. Regression cases cover all three arithmetic
paths.
This commit is contained in:
Chinesezjc
2026-08-31 15:03:21 +08:00
committed by Tianyi Cui
parent 4fd0068fb7
commit 3001cc23be
3 changed files with 75 additions and 17 deletions
@@ -167,7 +167,7 @@ class LogBuffer:
# above the budget truncates without ever encoding it — the full encode
# would allocate a second equally large string and could turn a
# truncatable log into an RLIMIT_AS death.
if (len(text) + 3 if not open or not self._open_started else len(text) + 1) > self._remaining:
if (len(text) + 3 if not self._open_started else len(text)) > self._remaining:
self._truncated = True
self._sink(log_truncation_marker(self._max_bytes), truncated=True)
return
@@ -193,11 +193,13 @@ class LogBuffer:
# instead of emitting the truncation marker. The +1 also floors an empty
# entry above zero, so a flood of blank ``print()`` lines exhausts the
# budget instead of emitting unbounded zero-cost log frames.
# Split billing for an `open` entry: the first fragment pays the full
# JSON-string cost plus the separator; each continuation pays only its
# content (the quotes and the separator were billed on the first
# fragment). A closed entry pays the full cost as before.
if open and self._open_started:
# Split billing for a merged entry: the FIRST fragment pays the full
# JSON-string cost plus the separator; every later fragment — a
# continuation OR the closing frame (it is the merged entry's tail, not
# a new entry) — pays only its content, since the quotes and separator
# were billed on the first fragment. A standalone closed entry (no open
# in progress) pays the full cost as before.
if self._open_started:
cost = _json_string_cost(raw) - 2
if cost < 0:
cost = 0