mirror of
https://github.com/deepseek-ai/deepseek-harness.git
synced 2026-09-11 04:00:38 +00:00
fix(code-runtime-python): drop a late binding resolution before snapshotting it
`sendReply` already refuses to write after the run settled, but only after `snapshotJsonValue` walked and copied the resolution. Binding resolution carries no seam-level byte cap, so a binding resolving a wide value after `maxWallMs`, an abort, or dispose settled the run spent host heap building a frame that was then discarded. The check moves ahead of the snapshot. Also in this change: - `readProcessStart` moved after `messageOf`. Inserting it between `messageOf`'s JSDoc and its body left that function undocumented and the orphaned block reading as a second doc for the reader; `verify-export-jsdoc` does not catch it because `messageOf` is not exported. - The README pair adds the disposed-runtime rejection to `run()`'s public contract, which `src/index.ts` has enforced all along. - Known Limitations records three deferred constraints that until now existed only in review discussion: the combined log-and-value peak the load gate does not model, the host-side per-member expansion of a wide binding reply (owned by `packages/core/session`, and shared with the worker-thread backend), and the absence of fd-3 backpressure for concurrent replies. - The Agent Note's same-group section records the teardown identity guard and its two rulings, including why an ABSENT start-time reading proceeds rather than withholding the signal, and that reading it as a mismatch is what turned the three same-group heartbeat cases red on Linux.
This commit is contained in:
@@ -3832,6 +3832,37 @@ describe('PythonCodeRuntime — hostile peer', () => {
|
||||
expect(result.value).toBe(reply.length)
|
||||
}, 90_000)
|
||||
|
||||
it('drops a late binding resolution before snapshotting it', async () => {
|
||||
// `sendReply` checks `settled`, but only after the resolution has been walked
|
||||
// and copied by `snapshotJsonValue`. Binding resolution carries no seam-level
|
||||
// byte cap, so a binding that resolves a wide value AFTER the run already
|
||||
// settled (here on `maxWallMs`) spent host heap building a frame that is then
|
||||
// discarded. The check now runs before the snapshot.
|
||||
//
|
||||
// The binding resolves well after the 1s wall clock with a 2M-element array;
|
||||
// the run must still report `timeout`, and the late value must not appear.
|
||||
let resolvedLate = false
|
||||
const { runtime } = await setup({ maxWallMs: 1_000 })
|
||||
const result = await runtime.run({
|
||||
program: 'return await tools.slow({})',
|
||||
bindings: [{
|
||||
global: 'tools',
|
||||
functions: {
|
||||
slow: async () => {
|
||||
await new Promise(resolve => setTimeout(resolve, 2_500))
|
||||
resolvedLate = true
|
||||
return Array.from({ length: 2_000_000 }, () => 0)
|
||||
},
|
||||
},
|
||||
}],
|
||||
})
|
||||
expect(result.error?.kind).toBe('timeout')
|
||||
expect(result.value).toBeUndefined()
|
||||
// Pin that the late path actually ran, so the assertion above is not vacuous.
|
||||
await new Promise(resolve => setTimeout(resolve, 2_000))
|
||||
expect(resolvedLate).toBe(true)
|
||||
}, 90_000)
|
||||
|
||||
it('bounds a flood of zero-byte log lines through the per-entry separator charge', async () => {
|
||||
// Blank print() lines carry zero content bytes; without the +1 separator
|
||||
// charge they would bypass maxLogBytes entirely and grow the retained
|
||||
|
||||
Reference in New Issue
Block a user