mirror of
https://github.com/deepseek-ai/deepseek-harness.git
synced 2026-09-11 04:00:38 +00:00
fix(code-runtime-python): make the write-path pre-checks open-aware; document the open flag in zh
The review's warning: while an open entry accumulates, the newline pre-checks in the write path still charged a NEW entry's +3 cheap-bound overhead (quotes + separator), so an exact-fit merged TAIL was truncated (or the pre-check over-rejected it and flushed a truncated prefix). Both pre-checks now charge the overhead only when no open entry is in progress, matching _push_locked's open-aware bound. A regression case (the review's recipe: flush an open fragment, then write one exact-fit newline-terminated line) is verified to truncate when the +3 is restored. The zh README's wire-contract section now describes the open flag like the en side (the fd-3 Agent Note holds the split-billing arithmetic; a cross-doc link was omitted to keep the bilingual link sequence aligned).
This commit is contained in:
@@ -2,5 +2,5 @@
|
||||
# side as of the last confirmed-consistent state. Both languages carry equal authority;
|
||||
# after editing either side, bring the other along and re-record with:
|
||||
# pnpm run verify-translation-pairing --write packages/code-runtime/code-runtime-python/README.md
|
||||
README.md: 97a4b6d4ccf51cf999dcef237aac1480a532bce6
|
||||
README.zh.md: 649ca8ddd4c1ad02f27b8131c60f22511801c1e9
|
||||
README.md: 3734639760e51c31ee3ba467e57f7e025ef1907c
|
||||
README.zh.md: 233cf01c3e1f8ad97f181e7f65ce009438e2366d
|
||||
|
||||
@@ -33,7 +33,7 @@ The package's default export is the `PythonCodeRuntime` plugin. Its public surfa
|
||||
|
||||
### The wire
|
||||
|
||||
Frames travel on the child's fd 3 as JSON-lines — one object per line — so stdout/stderr stay clear for the program's own output. Child → host: `boot-ack`, `call`, `log`, `done`. Host → child: `boot` (first frame, carrying every cap and the namespace declarations), `run` (after `boot-ack`, carrying only the program body), and one `reply` per `call`. A forged frame can carry both `value` and `error` on `done`, so a consumer must check `error` first and ignore `value` when it is set. A `log` frame's `open` flag marks an unterminated line committed by an explicit flush: the host appends the next log frame to the same entry, so `print('a', end='', flush=True); print('b')` reads back as one `'ab'` entry rather than a fake newline.
|
||||
Frames travel on the child's fd 3 as JSON-lines — one object per line — so stdout/stderr stay clear for the program's own output. Child → host: `boot-ack`, `call`, `log`, `done`. Host → child: `boot` (first frame, carrying every cap and the namespace declarations), `run` (after `boot-ack`, carrying only the program body), and one `reply` per `call`. A forged frame can carry both `value` and `error` on `done`, so a consumer must check `error` first and ignore `value` when it is set. A `log` frame's `open` flag marks an unterminated line committed by an explicit flush: the host appends the next log frame to the same entry, so `print('a', end='', flush=True); print('b')` reads back as one `'ab'` entry rather than a fake newline (the split-billing arithmetic lives in the fd-3 protocol Agent Note's wire-contract section).
|
||||
|
||||
### What can go wrong
|
||||
|
||||
|
||||
@@ -55,7 +55,7 @@ kind: "package-reference"
|
||||
|
||||
### wire 契约
|
||||
|
||||
帧为 `boot`/`run`(宿主 → 子进程)与 `boot-ack`/`call`/`log`/`done` 加每个 call 一个 `reply`(子进程 → 宿主)。`log` 帧的 `truncated` 标志标记的就是子进程账本自己的截断标记帧,因此宿主在与子进程相同的点停止捕获,而不是从自己的预算推断。`done.error.kind` 为 `exception`、`invalid-output`、`output-limit` 之一;墙钟/CPU 预算、中止与基底死亡在宿主侧观察,不以帧形式携带。
|
||||
帧为 `boot`/`run`(宿主 → 子进程)与 `boot-ack`/`call`/`log`/`done` 加每个 call 一个 `reply`(子进程 → 宿主)。`log` 帧的 `truncated` 标志标记的就是子进程账本自己的截断标记帧,因此宿主在与子进程相同的点停止捕获,而不是从自己的预算推断。`log` 帧的 `open` 标志标记由显式 flush 提交的未结束行:宿主把下一个 log 帧合并进同一条目,因此 `print('a', end='', flush=True); print('b')` 读回为一条 `'ab'` 条目而不是假换行(拆分计费算术在 fd-3 协议 Agent Note 的 wire-contract 段)。`done.error.kind` 为 `exception`、`invalid-output`、`output-limit` 之一;墙钟/CPU 预算、中止与基底死亡在宿主侧观察,不以帧形式携带。
|
||||
|
||||
### 无损 JSON 跨越
|
||||
|
||||
|
||||
@@ -296,7 +296,14 @@ class _LogStream(io.TextIOBase):
|
||||
pos = 0
|
||||
if self._pending or self._pending_blocks:
|
||||
newline = text.index("\n")
|
||||
if self._pending_chars + newline + 3 > self._logs.remaining:
|
||||
# The +3 cheap-bound overhead (quotes + separator) belongs to a
|
||||
# NEW entry. While an `open` entry is accumulating, the closing
|
||||
# line is that entry's TAIL: its cheap bound is the content
|
||||
# length alone, matching `_push_locked`'s open-aware bound.
|
||||
# Charging +3 here truncates an exact-fit merged tail or
|
||||
# over-rejects it, then flushes a truncated prefix instead.
|
||||
overhead = 3 if not self._logs._open_started else 0
|
||||
if self._pending_chars + newline + overhead > self._logs.remaining:
|
||||
# The reconstructed first line cannot fit the ledger, so
|
||||
# LogBuffer would reject it whole: copy only the prefix that
|
||||
# fails its cheap bound and drop the chunks. The slice is
|
||||
@@ -331,7 +338,8 @@ class _LogStream(io.TextIOBase):
|
||||
# prefix, which push still rejects on its own cheap bound (the
|
||||
# prefix is longer than `remaining`), so the marker is emitted
|
||||
# and the oversized line is never materialized.
|
||||
if newline - pos + 3 > self._logs.remaining:
|
||||
overhead = 3 if not self._logs._open_started else 0
|
||||
if newline - pos + overhead > self._logs.remaining:
|
||||
self._logs.push(text[pos:pos + self._logs.remaining + 4])
|
||||
break
|
||||
self._logs.push(text[pos:newline])
|
||||
|
||||
@@ -1995,6 +1995,28 @@ describe('PythonCodeRuntime — programs and bindings', () => {
|
||||
expect(result.logs).toEqual(['a'.repeat(30) + 'b'.repeat(30)])
|
||||
}, 15_000)
|
||||
|
||||
it('does not over-reject an exact-fit closing line while an open entry accumulates', async () => {
|
||||
// The write-path pre-check's cheap bound used +3 (quotes + separator) even
|
||||
// while an open entry was accumulating, so an exact-fit merged TAIL was
|
||||
// truncated: print('a'*29, flush) bills 32 (ledger 31 left), then
|
||||
// print('b'*28, end=''); print('c') merges 29 more chars whose cheap bound
|
||||
// is 29, not 32 — the +3 form saw 28 + 1 + 3 = 32 > 31 and truncated a
|
||||
// line that fits (merged cost 2 + 58 + 1 = 61 <= 63).
|
||||
const { runtime } = await setup({ maxLogBytes: 64 })
|
||||
const result = await runtime.run({
|
||||
program: [
|
||||
'import sys',
|
||||
"sys.stdout.write('a' * 29)",
|
||||
'sys.stdout.flush()',
|
||||
"sys.stdout.write('b' * 30 + chr(10))",
|
||||
'return "done"',
|
||||
].join('\n'),
|
||||
bindings: [],
|
||||
})
|
||||
expect(result.error).toBeUndefined()
|
||||
expect(result.logs).toEqual(['a'.repeat(29) + 'b'.repeat(30)])
|
||||
}, 15_000)
|
||||
|
||||
it('rejects a new open entry once the ledger has only two bytes left', async () => {
|
||||
// The jsonStringCostUpTo sub-2-byte guard: forged open frames drive the
|
||||
// host ledger down to 1 byte, then a new open entry's first-fragment cap
|
||||
|
||||
Reference in New Issue
Block a user