mirror of
https://github.com/deepseek-ai/deepseek-harness.git
synced 2026-09-11 04:00:38 +00:00
fix(code-runtime-python): merge a flushed unterminated line into the next log entry
The review's remaining warning: an explicit flush of an unterminated line
(print(..., end='', flush=True)) pushed a full log frame, so the following
print() landed in a second entry and logs.join('\n') rendered 'a\nb' for what
the program printed as one line — a model-visible output defect. The flush
frame now carries an flag (LogMessage gains the optional field on both
sides and in the mirror test), the host holds it and appends the next log frame
to the same entry, and finish() admits the residual if the run ends with it
still open. The settlement note registers the decimal-context fix from the
previous commit.
This commit is contained in:
@@ -1025,6 +1025,10 @@ export class PythonCodeRuntime extends CodeRuntime {
|
||||
return new Promise<CodeRunResult>((resolve) => {
|
||||
let settled = false
|
||||
const logs: string[] = []
|
||||
// An unterminated line flushed with the `open` flag: the next log frame
|
||||
// appends to it (no fake newline between entries), and finish() admits
|
||||
// the residual if the run ends with it still open.
|
||||
let openLog: string | undefined
|
||||
|
||||
// One host-side ledger covers normal frames, forged frames, and stray stdout bytes.
|
||||
// The ledger starts one byte below maxLogBytes: each entry is charged its
|
||||
@@ -1496,7 +1500,17 @@ export class PythonCodeRuntime extends CodeRuntime {
|
||||
}
|
||||
return
|
||||
}
|
||||
admit(message.text)
|
||||
if (message.open === true) {
|
||||
// An explicit flush of an unterminated line: hold it so the next
|
||||
// frame appends to the SAME entry (print('a', end='', flush=True)
|
||||
// followed by print('b') reads back as one 'ab' entry, not a fake
|
||||
// newline). The residual is admitted by finish() if the run ends
|
||||
// with it still open.
|
||||
openLog = (openLog ?? '') + message.text
|
||||
return
|
||||
}
|
||||
admit((openLog ?? '') + message.text)
|
||||
openLog = undefined
|
||||
return
|
||||
case 'done': {
|
||||
if (message.error) {
|
||||
@@ -1876,6 +1890,12 @@ export class PythonCodeRuntime extends CodeRuntime {
|
||||
// A spawn failure (ENOENT, EACCES) never produced a pid, so there is no
|
||||
// process to kill: settle now. Its `close` still fires later and reaches
|
||||
// the idempotent settle() again as a no-op.
|
||||
// An unterminated flushed line never got a closing frame; admit it so
|
||||
// the committed flush is not lost from logs.
|
||||
if (openLog !== undefined) {
|
||||
admit(openLog)
|
||||
openLog = undefined
|
||||
}
|
||||
if (child.pid === undefined) {
|
||||
settle(result)
|
||||
return
|
||||
|
||||
@@ -102,6 +102,13 @@ interface LogMessage {
|
||||
* and keeps exactly one marker in `logs`.
|
||||
*/
|
||||
truncated?: boolean
|
||||
/**
|
||||
* Set on the frame an explicit `flush()` (or the settlement flush) pushes for
|
||||
* an UNTERMINATED line: the host holds it and 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 between two entries.
|
||||
*/
|
||||
open?: boolean
|
||||
}
|
||||
|
||||
/** The failure carried on a {@link DoneMessage}: one of three kinds plus text. */
|
||||
@@ -227,7 +234,7 @@ const WIRE_FRAME_FIELD_ROLES = {
|
||||
RunMessage: { type: 'required', program: 'required' },
|
||||
BootAckMessage: { type: 'required' },
|
||||
CallMessage: { type: 'required', id: 'required', global: 'required', name: 'required', args: 'required' },
|
||||
LogMessage: { type: 'required', text: 'required', truncated: 'optional' },
|
||||
LogMessage: { type: 'required', text: 'required', truncated: 'optional', open: 'optional' },
|
||||
DoneErrorField: { kind: 'required', message: 'required' },
|
||||
DoneMessage: { type: 'required', value: 'optional', error: 'optional' },
|
||||
ErrorClass: { name: 'required', memberNameProperty: 'required' },
|
||||
@@ -611,8 +618,13 @@ export function validateChildFrame(raw: unknown): ChildToHost | undefined {
|
||||
if (typeof m.text !== 'string') return undefined
|
||||
// Rebuilt, not passed through: a forged `truncated` of any other type
|
||||
// would reach the host as a truthy value and silence capture for the
|
||||
// rest of the run. Only the literal `true` counts.
|
||||
return { type: 'log', text: m.text, ...m.truncated === true ? { truncated: true } : {} }
|
||||
// rest of the run. Only the literal `true` counts; `open` likewise.
|
||||
return {
|
||||
type: 'log',
|
||||
text: m.text,
|
||||
...m.truncated === true ? { truncated: true } : {},
|
||||
...m.open === true ? { open: true } : {},
|
||||
}
|
||||
case 'call': {
|
||||
// The id must be a finite number: it is echoed verbatim into the reply
|
||||
// frame, and a forged `1e400` id (Infinity after JSON.parse) would make
|
||||
|
||||
Reference in New Issue
Block a user