mirror of
https://github.com/deepseek-ai/deepseek-harness.git
synced 2026-09-11 04:00:38 +00:00
feat(code-runtime-python): add the CPython subprocess backend
Land the PythonCodeRuntime implementation on top of the fd-3 protocol seam: python3 -I per run, binding namespace over fd 3, RLIMIT_CPU/AS, wall-clock timer, and SIGTERM->grace->SIGKILL process-group teardown, with the real-subprocess integration suite. Fixes three defects surfaced on the source PR's review before they ship: - boot-write failure resolved a worker-exit through finish()/settle() that read wallTimer/onAbort/live in their TDZ, rejecting run() instead; the boot write now runs after those bindings and the v8-ignore that hid the branch is removed. - log capture serialized against settlement with no lock while model daemon threads keep writing; LogBuffer now owns one shared re-entrant lock taken by write/flush_line/push. - the fd-3 line residual was a subarray view pinning the whole joined frame; it is copied into a right-sized Buffer via detachResidual so pendingBytes measures what is retained.
This commit is contained in:
@@ -33,10 +33,17 @@
|
||||
"license": "MIT",
|
||||
"peerDependencies": {
|
||||
"@deepseek-ai/dsh-invariants": "workspace:^",
|
||||
"@deepseek-ai/dsh-session": "workspace:^",
|
||||
"@deepseek-ai/dsh-timeout": "workspace:^",
|
||||
"@deepseek-ai/cordis": "workspace:^"
|
||||
},
|
||||
"dependencies": {
|
||||
"@deepseek-ai/schemastery": "workspace:^"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@deepseek-ai/dsh-invariants": "workspace:^",
|
||||
"@deepseek-ai/dsh-session": "workspace:^",
|
||||
"@deepseek-ai/dsh-timeout": "workspace:^",
|
||||
"@deepseek-ai/cordis": "workspace:^"
|
||||
}
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,67 @@
|
||||
import { EventEmitter } from 'node:events'
|
||||
import { PassThrough } from 'node:stream'
|
||||
import { afterEach, describe, expect, it, vi } from 'vitest'
|
||||
import { Context } from 'cordis'
|
||||
|
||||
/**
|
||||
* A synchronous `proto.write` throw on the fd-3 pipe is the one boot path a real
|
||||
* subprocess cannot be coerced into from a test: the pipe accepts queued bytes
|
||||
* until the kernel buffer fills, and a same-tick EPIPE needs fd 3 already closed
|
||||
* before the first write. `spawn` is mocked so fd 3 throws on the boot frame,
|
||||
* which is exactly the branch that regressed. The mock is confined to this file
|
||||
* so the real-subprocess suite in runtime.spec.ts is untouched.
|
||||
*/
|
||||
const { spawnMock } = vi.hoisted(() => ({ spawnMock: vi.fn() }))
|
||||
vi.mock('node:child_process', async importOriginal => ({
|
||||
...(await importOriginal<typeof import('node:child_process')>()),
|
||||
spawn: spawnMock,
|
||||
}))
|
||||
|
||||
const { PythonCodeRuntime } = await import('../src/index.ts')
|
||||
|
||||
/** A `child_process.ChildProcess` stand-in whose fd-3 pipe rejects every write. */
|
||||
function fakeChildWithThrowingFd3(): EventEmitter {
|
||||
const child = new EventEmitter() as EventEmitter & {
|
||||
pid?: number
|
||||
stdout: PassThrough
|
||||
stderr: PassThrough
|
||||
stdio: unknown[]
|
||||
}
|
||||
// Leave `pid` absent: `finish()` still runs its `clearTimeout(wallTimer)` /
|
||||
// `removeEventListener(onAbort)` prologue (the TDZ site) before short-
|
||||
// circuiting on `child.pid === undefined` to `settle` instead of waiting on a
|
||||
// `close` this fake never emits, so the run resolves promptly.
|
||||
child.stdout = new PassThrough()
|
||||
child.stderr = new PassThrough()
|
||||
// A duplex whose `write` throws synchronously, standing in for an fd-3 pipe
|
||||
// that fails the moment the boot frame is issued.
|
||||
const proto = new PassThrough()
|
||||
proto.write = () => { throw Object.assign(new Error('EPIPE: broken pipe, write'), { code: 'EPIPE' }) }
|
||||
child.stdio = [new PassThrough(), child.stdout, child.stderr, proto]
|
||||
return child
|
||||
}
|
||||
|
||||
afterEach(() => {
|
||||
spawnMock.mockReset()
|
||||
})
|
||||
|
||||
describe('PythonCodeRuntime — boot-write failure', () => {
|
||||
it('resolves a worker-exit when the fd-3 boot write throws (no TDZ ReferenceError)', async () => {
|
||||
// Before the fix, the boot-write block ran BEFORE `wallTimer`, `onAbort`,
|
||||
// and `live` were initialized, so its `finish()` (which clears `wallTimer`,
|
||||
// removes `onAbort`, and — through `settle` — deletes `live`) hit the
|
||||
// temporal dead zone and threw a ReferenceError. That escaped the Promise
|
||||
// executor and REJECTED run() instead of resolving the worker-exit the catch
|
||||
// constructs. This test would see that rejection; the fix makes it resolve.
|
||||
spawnMock.mockImplementation(() => fakeChildWithThrowingFd3())
|
||||
const ctx = new Context()
|
||||
const fiber = await ctx.plugin(PythonCodeRuntime)
|
||||
const runtime = ctx.codeRuntime as InstanceType<typeof PythonCodeRuntime>
|
||||
|
||||
const result = await runtime.run({ program: 'return 1', bindings: [] })
|
||||
|
||||
expect(result.error?.kind).toBe('worker-exit')
|
||||
expect(result.error?.message).toContain('failed to boot python subprocess')
|
||||
await fiber.dispose()
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,29 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { detachResidual } from '../src/index.ts'
|
||||
|
||||
describe('detachResidual — fd-3 residual detachment', () => {
|
||||
it('returns a copy that does NOT share the source frame allocation', () => {
|
||||
// Simulate the data handler's state: one large joined frame from
|
||||
// Buffer.concat, sliced past its newline to leave a small residual VIEW.
|
||||
const joined = Buffer.alloc(1024 * 1024, 0x61) // 1 MiB backing allocation
|
||||
joined[512] = 0x0a // a newline partway through
|
||||
const residual = joined.subarray(513) // a view onto `joined`'s backing store
|
||||
|
||||
// Before the fix the handler carried this view forward verbatim, pinning the
|
||||
// whole 1 MiB `joined` allocation behind a residual that reports far fewer
|
||||
// bytes. A right-sized copy must not point back into `joined`.
|
||||
const [carried] = detachResidual(residual)
|
||||
|
||||
expect(carried).toBeDefined()
|
||||
expect(carried!.length).toBe(residual.length)
|
||||
expect(carried!.equals(residual)).toBe(true)
|
||||
// The copy's backing store is its own, sized to its content — not the 1 MiB
|
||||
// frame. A subarray view would report the source's full byteLength here.
|
||||
expect(carried!.buffer.byteLength).toBe(carried!.length)
|
||||
expect(carried!.buffer).not.toBe(joined.buffer)
|
||||
})
|
||||
|
||||
it('carries nothing forward for an empty residual', () => {
|
||||
expect(detachResidual(Buffer.alloc(0))).toEqual([])
|
||||
})
|
||||
})
|
||||
File diff suppressed because it is too large
Load Diff
@@ -14,8 +14,20 @@
|
||||
{
|
||||
"path": "../../../vendor/cordis"
|
||||
},
|
||||
{
|
||||
"path": "../../../vendor/schemastery"
|
||||
},
|
||||
{
|
||||
"path": "../code-runtime"
|
||||
},
|
||||
{
|
||||
"path": "../../core/session"
|
||||
},
|
||||
{
|
||||
"path": "../../runtime-diagnostics/invariants"
|
||||
},
|
||||
{
|
||||
"path": "../../util/timeout"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user