feat: add the worker-thread code runtime (dsh-code-runtime-worker)

The shipped backend of the code-execution seam, per the Code Mode RFC's
worker-thread section: one fresh Node worker per run, executing the
model's TypeScript after a host-side type-strip (wrapped in an
async-function shell so top-level return/await parse, sliced back out
position-preserved), bindings bridged over the message port under
hostile-peer rules (own-property name lookup, at-most-once replies,
post-settlement drops, null-prototype namespaces), logs streamed eagerly
with an in-band truncation marker, and two independent budgets — measured
event-loop busy time (computeMs) plus a never-pausing wall ceiling
(maxWallMs) — funneling into worker.terminate(). env: {} and execArgv: []
keep the isolate hermetic; disposal aborts in-flight runs and awaits
worker exits.

The worker entry loads unbuilt via Node's native type stripping
(src/worker.ts, erasable-only) and ships built as a sibling tsdown bundle
(lib/worker.js); tests/built-lib.e2e.ts pins the built load path under
plain node and joins the built-artifact smoke gate. Unit suites cover the
bootstrap in-process (fake port) and the runtime over real workers,
per-file 100%.
This commit is contained in:
Tianyi Cui
2026-07-08 11:07:14 +08:00
parent 15a3431913
commit 583704ac1d
26 changed files with 1486 additions and 9 deletions
@@ -0,0 +1,257 @@
/**
* Worker-side execution logic, written as plain functions over an injected
* port so the unit suite can run every line IN-PROCESS against a fake port
* (a real worker thread is a separate V8 isolate the coverage provider
* cannot observe). The real worker entry (`worker.ts`) is a thin
* self-executing glue file over {@link runWorkerMain}, excluded from
* coverage the same way `bin.ts` entrypoints are, and exercised end-to-end
* by the integration tests that spawn real workers.
*
* @module @deepseek-ai/dsh-code-runtime-worker/src/bootstrap
*/
import { inspect } from 'node:util'
import type { CodeLogEntry } from '@deepseek-ai/dsh-code-runtime'
import type { DoneMessage, ReplyMessage, WorkerBootData, WorkerToHost } from './protocol.ts'
/** The port surface the bootstrap needs — satisfied by `parentPort` and by the tests' fake. */
export interface BootstrapPort {
postMessage(message: WorkerToHost): void
on(event: 'message', listener: (message: ReplyMessage) => void): void
}
/**
* A writable stream's `write` slot, as the bootstrap patches it (see
* {@link captureStreamWrites}). Method-typed so the real
* `process.stdout`/`process.stderr` (narrower chunk parameters) remain
* assignable.
*/
export interface PatchableStream {
write(chunk: unknown, ...rest: unknown[]): boolean
}
/**
* Ordered log capture under one shared byte budget, delivered to a sink as
* each entry lands (the real sink streams entries over the port eagerly, so
* captured output survives a mid-run termination). Once the budget is
* exhausted it emits exactly one in-band marker entry (on the `stderr`
* diagnostics channel) and silently drops everything after — the cap is a
* blast-radius bound, so "how much was lost" intentionally stays unmeasured.
*/
export class LogBuffer {
private remaining: number
private truncated = false
// Explicit fields, not constructor parameter properties: this module loads
// under Node's native strip-only mode, which rejects non-erasable syntax —
// and parameter properties are non-erasable.
private readonly maxBytes: number
private readonly sink: (entry: CodeLogEntry) => void
constructor(maxBytes: number, sink: (entry: CodeLogEntry) => void) {
this.maxBytes = maxBytes
this.sink = sink
this.remaining = maxBytes
}
/**
* Emit one entry to the sink, charging its text against the budget (drops + marks once exhausted).
* @param entry - the log entry to deliver.
*/
push(entry: CodeLogEntry): void {
if (this.truncated) return
const cost = Buffer.byteLength(entry.text, 'utf8')
if (cost > this.remaining) {
this.truncated = true
this.sink({ source: 'stderr', text: `[dsh-code-runtime-worker] log capture truncated at ${this.maxBytes} bytes` })
return
}
this.remaining -= cost
this.sink(entry)
}
}
/** The five console methods the shim captures, in the seam's level vocabulary. */
const CONSOLE_LEVELS = ['log', 'info', 'warn', 'error', 'debug'] as const
/**
* A `console` replacement whose five leveled methods render their arguments
* `util.inspect`-style (matching real console formatting closely enough for
* a model to recognize its own output) into the buffer. Only these five
* exist — the program gets a deliberately small console, not Node's full
* surface.
* @param logs - the buffer every rendered line is pushed into.
* @returns the five-method console object handed to the program.
*/
export function makeConsoleShim(logs: LogBuffer): Record<(typeof CONSOLE_LEVELS)[number], (...args: unknown[]) => void> {
const render = (args: unknown[]): string =>
args.map(arg => typeof arg === 'string' ? arg : inspect(arg, INSPECT_OPTIONS)).join(' ')
const shim = Object.create(null) as Record<(typeof CONSOLE_LEVELS)[number], (...args: unknown[]) => void>
for (const level of CONSOLE_LEVELS) {
shim[level] = (...args: unknown[]) => { logs.push({ source: 'console', level, text: render(args) }) }
}
return shim
}
/**
* Redirect a stream's `write` into the log buffer (the program-visible
* `process.stdout`/`process.stderr` in the real worker), so raw writes land
* in emission order alongside console output instead of racing down a pipe.
* @param logs - the buffer captured writes are pushed into.
* @param stream - the stream whose `write` slot is patched.
* @param source - the log source the captured writes are attributed to.
* @returns the restore function (the in-process tests un-patch; the real
* worker never needs to).
*/
export function captureStreamWrites(logs: LogBuffer, stream: PatchableStream, source: 'stdout' | 'stderr'): () => void {
// The slot's VALUE is stored for restore and reassigned — never invoked
// detached, so the unbound-method concern does not apply.
// eslint-disable-next-line @typescript-eslint/unbound-method
const original = stream.write
stream.write = (chunk: unknown): boolean => {
logs.push({ source, text: typeof chunk === 'string' ? chunk : String(chunk) })
return true
}
return () => { stream.write = original }
}
/** Bounded inspect options: deep enough to be useful, bounded so a pathological value cannot explode the rendering. */
const INSPECT_OPTIONS = { depth: 4, maxArrayLength: 100, maxStringLength: 10_000 } as const
/**
* Prepare the program's completion value for the done message: a
* structured-clone-safe value whose rendering fits `maxValueBytes` crosses
* raw; anything else (non-cloneable, or oversized) is REPLACED by its
* bounded `util.inspect` rendering, truncated with an in-band marker — the
* seam contract's "a non-transferable value is replaced by a string
* rendering", extended to oversized ones so a huge return cannot flood the
* host.
* @param value - the program's completion value.
* @param maxValueBytes - the byte cap for the rendered value.
* @returns the done-message fragment: `{}` for `undefined`, else `{ value }`.
*/
export function prepareValue(value: unknown, maxValueBytes: number): { value?: unknown } {
if (value === undefined) return {}
const rendered = typeof value === 'string' ? value : inspect(value, INSPECT_OPTIONS)
let cloneable = true
try {
structuredClone(value)
} catch {
// Only the verdict matters: the value has parts structured clone rejects
// (functions, classes, …) and must cross as its rendering instead.
cloneable = false
}
if (cloneable && Buffer.byteLength(rendered, 'utf8') <= maxValueBytes) return { value }
const capped = rendered.length > maxValueBytes ? `${rendered.slice(0, maxValueBytes)}… [truncated]` : rendered
return { value: capped }
}
/** One awaited binding call's settlement handles, keyed by call id in the pending map. */
export interface PendingCall {
resolve(value: unknown): void
reject(error: Error): void
}
/**
* Route host replies into the pending-call map: each reply settles its call
* at most once, and a reply for an unknown id (stray, or a duplicate answer
* to an id already settled) is ignored. Shared wiring between
* {@link runWorkerMain} and the tests that exercise {@link makeNamespaces}
* standalone.
* @param port - the port whose `message` events carry the replies.
* @param pending - the id-keyed map of unsettled binding calls.
*/
export function wireReplies(port: BootstrapPort, pending: Map<number, PendingCall>): void {
port.on('message', (message: ReplyMessage) => {
const entry = pending.get(message.id)
if (!entry) return
pending.delete(message.id)
if (message.ok) entry.resolve(message.value)
else entry.reject(new Error(message.message))
})
}
/**
* Build the binding namespace objects the program sees: one null-prototype
* global per namespace, each declared name an own enumerable async function
* that bridges over the port (`__proto__`/`constructor`/`toString` are
* ordinary keys, never prototype collisions). A non-cloneable argument
* rejects that one call with a descriptive error; the host's reply (`ok`
* false) rejects it likewise, so a failed tool call surfaces in the program
* as an ordinary promise rejection.
* @param data - the boot payload's namespace declarations (globals + names).
* @param port - the port binding calls are posted to.
* @param pending - the id-keyed map each posted call parks its handles in.
* @param nextId - the shared mutable id counter (worker-issued correlation ids).
* @returns one namespace object per declaration, in declaration order.
*/
export function makeNamespaces(
data: Pick<WorkerBootData, 'namespaces'>,
port: BootstrapPort,
pending: Map<number, PendingCall>,
nextId: { value: number },
): Record<string, unknown>[] {
return data.namespaces.map(({ global, names }) => {
const namespace = Object.create(null) as Record<string, unknown>
for (const name of names) {
Object.defineProperty(namespace, name, {
enumerable: true,
value: (args: unknown): Promise<unknown> => new Promise((resolve, reject) => {
const id = nextId.value++
pending.set(id, { resolve, reject })
try {
port.postMessage({ type: 'call', id, global, name, args })
} catch (error: unknown) {
pending.delete(id)
reject(new Error(`binding arguments must be structured-cloneable: ${error instanceof Error ? error.message : String(error)}`))
}
}),
})
}
return namespace
})
}
/**
* Run one program to settlement and post the {@link DoneMessage}: wires the
* reply handler, materializes the namespaces and console shim, compiles the
* type-stripped body as an async function (top-level `await`/`return`
* work), and reports a thrown program error as the done message's `error`
* field. Exactly one done message is ever posted.
* @param port - the message port to the host (the real `parentPort`, or the tests' fake).
* @param data - the boot payload the host sent.
* @param streams - the stream objects whose `write` is captured (the real
* `process.stdout`/`process.stderr` in the worker; fakes in tests).
* @returns resolves after the done message is posted (the tests await it;
* the real entry lets the worker exit naturally).
*/
export async function runWorkerMain(
port: BootstrapPort,
data: WorkerBootData,
streams: { stdout: PatchableStream; stderr: PatchableStream },
): Promise<void> {
const logs = new LogBuffer(data.maxLogBytes, (entry) => { port.postMessage({ type: 'log', entry }) })
captureStreamWrites(logs, streams.stdout, 'stdout')
captureStreamWrites(logs, streams.stderr, 'stderr')
const pending = new Map<number, PendingCall>()
wireReplies(port, pending)
const nextId = { value: 1 }
const namespaces = makeNamespaces(data, port, pending, nextId)
const consoleShim = makeConsoleShim(logs)
let done: DoneMessage
try {
// The async function constructor, reached through an instance because
// `AsyncFunction` is not a global. The program body is strict-mode.
/* v8 ignore next -- the arrow exists only to reach the AsyncFunction constructor; it is never invoked. */
const AsyncFunction = (async () => {}).constructor as new (...args: string[]) => (...fnArgs: unknown[]) => Promise<unknown>
const fn = new AsyncFunction(...data.namespaces.map(namespace => namespace.global), 'console', `'use strict';\n${data.code}`)
const value = await fn(...namespaces, consoleShim)
done = { type: 'done', ...prepareValue(value, data.maxValueBytes) }
} catch (error: unknown) {
const message = error instanceof Error ? error.stack ?? error.message : String(error)
done = { type: 'done', error: { message } }
}
port.postMessage(done)
}
@@ -0,0 +1,350 @@
/**
* Worker-thread implementation of the code-execution seam: one fresh Node
* worker per run, executing the model's TypeScript after a host-side
* type-strip, with bindings bridged over the message port. Containment, not
* a security boundary (bash-equivalent trust — see the Code Mode RFC's
* trust-posture section): the worker gets an EMPTY environment, a heap cap,
* and two independent budgets — `computeMs` metered on the worker's
* measured event-loop busy time (a hot loop cannot hide behind a pending
* binding call) and a never-pausing `maxWallMs` ceiling — all funneling
* into `worker.terminate()`, which ends hot synchronous loops too.
*
* @module @deepseek-ai/dsh-code-runtime-worker
*/
import { Worker } from 'node:worker_threads'
import { stripTypeScriptTypes } from 'node:module'
import { Context } from 'cordis'
import z from 'schemastery'
import { CodeRuntime } from '@deepseek-ai/dsh-code-runtime'
import type { CodeBindingFunction, CodeLogEntry, CodeRunFailure, CodeRunRequest, CodeRunResult } from '@deepseek-ai/dsh-code-runtime'
import type { ReplyMessage, WorkerBootData, WorkerToHost } from './protocol.ts'
export type { BootstrapPort, PatchableStream } from './bootstrap.ts'
export type { CallMessage, DoneMessage, ReplyMessage, WorkerBootData, WorkerToHost } from './protocol.ts'
/** Plugin config: every execution cap, changeable from `cordis.yml` (no hardcoded tunables). */
export interface Config {
/**
* Busy-time budget in milliseconds: the run fails with kind `'timeout'`
* once the worker's MEASURED event-loop active time
* (`worker.performance.eventLoopUtilization()`) exceeds this. Metering
* measured busy time — not wall time, not host-side pending-call
* bookkeeping — is what makes the budget both fair (a program awaiting a
* slow tool accrues nothing) and ungameable (a hot loop accrues whether
* or not a decoy dispatch is in flight).
*/
computeMs?: number
/**
* Wall-clock ceiling in milliseconds; never pauses for anything. The
* backstop for what busy-time cannot see (a program awaiting a promise
* nobody will resolve).
*/
maxWallMs?: number
/** Shared byte budget for captured log text (console + raw stream writes), truncation marked in-band. */
maxLogBytes?: number
/** Byte cap for the rendered completion value; an oversized or non-cloneable value crosses as a capped string rendering. */
maxValueBytes?: number
/** The worker's max old-generation heap in MiB (`resourceLimits`); overflow kills the worker, surfacing as kind `'worker-exit'`. */
maxOldGenerationSizeMb?: number
}
/** {@link Config} after schemastery fills the defaults (every field present). */
type ResolvedConfig = Required<Config>
/**
* How often the host samples the worker's event-loop utilization for the
* `computeMs` budget. An internal cadence, not config: the only effect of
* the interval is budget-expiry granularity (a run can overshoot by up to
* one interval), and nothing a deployment could tune here improves that
* without burning host CPU.
*/
const ELU_POLL_INTERVAL_MS = 25
/** ECMAScript reserved words that cannot be async-function parameter names — rejected as binding globals. */
const RESERVED_WORDS = new Set([
'await', 'break', 'case', 'catch', 'class', 'const', 'continue', 'debugger', 'default', 'delete', 'do',
'else', 'enum', 'export', 'extends', 'false', 'finally', 'for', 'function', 'if', 'import', 'in',
'instanceof', 'new', 'null', 'return', 'super', 'switch', 'this', 'throw', 'true', 'try', 'typeof',
'var', 'void', 'while', 'with', 'yield', 'let', 'static', 'implements', 'interface', 'package',
'private', 'protected', 'public', 'arguments', 'eval',
])
/** Valid async-function parameter name (the binding global becomes one). */
const IDENTIFIER = /^[A-Za-z_$][A-Za-z0-9_$]*$/
/**
* The shell a program is wrapped in for the type-strip, matching the
* grammatical context it will execute in (an async function body, where
* top-level `return` and `await` are legal — a bare module parse would
* reject the `return`). Strip mode is position-preserving (removed syntax
* becomes whitespace, nothing shifts), so the wrapper survives the strip
* byte-identical and the body slices back out with the model's own
* line/column positions intact.
*/
const STRIP_WRAP = { prefix: 'async function __dsh_program__() {\n', suffix: '\n}' } as const
/** One in-flight run's host-side state, tracked for disposal. */
interface LiveRun {
worker: Worker
settle(failure: CodeRunFailure): void
finished: Promise<void>
}
/**
* The worker entry module. Source runs unbuilt (`src/worker.ts`, loadable
* directly on this repo's Node range via native type stripping — the file
* is erasable-only with type-only relative imports); the built package
* ships it as a sibling bundle (`lib/worker.js`, its own tsdown entry).
* The URL *pathname*'s extension says which world this module is in —
* pathname, because dev-time module runners (vitest) may suffix
* `import.meta.url` with a query string; relative resolution drops it.
*/
/* v8 ignore next -- the './worker.js' arm is the built-lib world, unreachable unbuilt by construction; the built-lib e2e pins it. */
const WORKER_URL = new URL(new URL(import.meta.url).pathname.endsWith('.ts') ? './worker.ts' : './worker.js', import.meta.url)
/** Render an unknown thrown value as a message, `Error` or not. */
function messageOf(error: unknown): string {
return error instanceof Error ? error.message : String(error)
}
/**
* The shipped {@link CodeRuntime} backend (`ctx.codeRuntime`). Registers as
* the `codeRuntime` service; every cap comes from validated config. See the
* module doc for the containment model and the class JSDoc on the seam for
* the contract this implements (error-as-field, hostile-peer port,
* no cross-run state, dispose to quiescence).
*/
export class WorkerCodeRuntime extends CodeRuntime {
static Config: z<Config> = z.object({
computeMs: z.number().default(60_000),
maxWallMs: z.number().default(600_000),
maxLogBytes: z.number().default(65_536),
maxValueBytes: z.number().default(32_768),
maxOldGenerationSizeMb: z.number().default(512),
})
readonly language = 'typescript'
readonly isolation = 'worker-thread'
private readonly config: ResolvedConfig
private readonly live = new Set<LiveRun>()
private disposed = false
constructor(ctx: Context, config: Config) {
super(ctx)
// Schemastery filled the defaults; the cast records that. Positivity is a
// semantic check the schema's plain number type does not carry.
this.config = config as ResolvedConfig
for (const [key, value] of Object.entries(this.config)) {
if (!(Number.isFinite(value) && value > 0)) throw new Error(`dsh-code-runtime-worker: config.${key} must be a positive number, got ${String(value)}`)
}
ctx.effect(() => () => this.teardown(), 'worker code-runtime teardown')
}
/**
* Dispose to quiescence: mark the service unusable, fail every in-flight
* run as aborted, and AWAIT each worker's exit so no worker outlives the
* fiber.
*/
private async teardown(): Promise<void> {
this.disposed = true
const runs = [...this.live]
for (const run of runs) run.settle({ kind: 'abort', message: 'runtime disposed' })
await Promise.all(runs.map(run => run.finished))
}
/**
* Execute one program in a fresh worker. Program outcomes — including a
* type-strip syntax error, which never spawns a worker — resolve with
* `result.error`; the method rejects only for seam misuse (a disposed
* runtime, an invalid binding namespace).
* @param request - the program, its bindings, and the abort signal.
* @returns the run's outcome per the seam contract.
*/
async run(request: CodeRunRequest): Promise<CodeRunResult> {
if (this.disposed) throw new Error('dsh-code-runtime-worker: run() after disposal')
const bindings = this.validateBindings(request)
if (request.signal?.aborted) {
return { logs: [], error: { kind: 'abort', message: String(request.signal.reason) } }
}
let code: string
try {
const stripped = stripTypeScriptTypes(STRIP_WRAP.prefix + request.program + STRIP_WRAP.suffix)
code = stripped.slice(STRIP_WRAP.prefix.length, stripped.length - STRIP_WRAP.suffix.length)
} catch (error: unknown) {
// A program that does not survive the type-strip (syntax error,
// non-erasable syntax like `enum`) is a program failure, reported the
// same way a thrown exception would be — and no worker ever spawns.
return { logs: [], error: { kind: 'exception', message: messageOf(error) } }
}
return await this.execute(request, code, bindings)
}
/** Reject (seam misuse) malformed binding namespaces: non-identifier or reserved globals, duplicates, and the `console` collision. */
private validateBindings(request: CodeRunRequest): Map<string, Record<string, CodeBindingFunction>> {
const bindings = new Map<string, Record<string, CodeBindingFunction>>()
for (const namespace of request.bindings) {
if (!IDENTIFIER.test(namespace.global) || RESERVED_WORDS.has(namespace.global)) {
throw new Error(`dsh-code-runtime-worker: binding global ${JSON.stringify(namespace.global)} is not a usable identifier`)
}
if (namespace.global === 'console' || bindings.has(namespace.global)) {
throw new Error(`dsh-code-runtime-worker: duplicate binding global ${JSON.stringify(namespace.global)}`)
}
bindings.set(namespace.global, namespace.functions)
}
return bindings
}
/** Spawn the worker for one validated, type-stripped run and drive it to settlement. */
private execute(
request: CodeRunRequest,
code: string,
bindings: Map<string, Record<string, CodeBindingFunction>>,
): Promise<CodeRunResult> {
const bootData: WorkerBootData = {
code,
namespaces: [...bindings].map(([global, functions]) => ({ global, names: Object.keys(functions) })),
maxLogBytes: this.config.maxLogBytes,
maxValueBytes: this.config.maxValueBytes,
}
const worker = new Worker(WORKER_URL, {
workerData: bootData,
// Model code gets NO ambient environment — stronger than the scrubbed
// env the defensive-patterns rule requires for spawned commands.
env: {},
// Hermetic flags too: without this the worker inherits the host
// process's execArgv (a test runner's or tsx's loader hooks), which a
// bare isolate with an empty environment cannot satisfy. The entry
// needs nothing beyond native type stripping, on this repo's whole
// Node range.
execArgv: [],
resourceLimits: { maxOldGenerationSizeMb: this.config.maxOldGenerationSizeMb },
// Backstop capture: the bootstrap patches JS-level writes into its own
// ordered buffer, so these pipes normally stay silent; anything that
// still arrives (native-level writes) is appended after the done logs.
stdout: true,
stderr: true,
})
return new Promise<CodeRunResult>((resolve) => {
let settled = false
const answered = new Set<number>()
const logs: CodeLogEntry[] = []
const strayLogs: CodeLogEntry[] = []
let strayBudget = this.config.maxLogBytes
const captureStray = (source: 'stdout' | 'stderr') => (chunk: Buffer) => {
if (settled || strayBudget <= 0) return
const text = chunk.toString('utf8').slice(0, strayBudget)
strayBudget -= Buffer.byteLength(text, 'utf8')
strayLogs.push({ source, text })
}
worker.stdout.on('data', captureStray('stdout'))
worker.stderr.on('data', captureStray('stderr'))
// Settlement: exactly one outcome wins; every path funnels through
// here, cleans up the timers/listeners, terminates the worker, and
// resolves only after the worker actually exited (quiescence). Logs
// streamed eagerly before the settlement are kept — a timed-out or
// killed program still shows the model what it printed.
let finishResolve!: () => void
const finished = new Promise<void>((done) => { finishResolve = done })
const finish = (result: Omit<CodeRunResult, 'logs'>): void => {
if (settled) return
settled = true
clearInterval(eluTimer)
clearTimeout(wallTimer)
request.signal?.removeEventListener('abort', onAbort)
this.live.delete(live)
void worker.terminate().then(() => {
finishResolve()
resolve({ ...result, logs: [...logs, ...strayLogs] })
})
}
const onDone = (message: WorkerToHost): void => {
if (message.type !== 'done') return
finish({
...message.value !== undefined ? { value: message.value } : {},
...message.error ? { error: { kind: 'exception' as const, message: message.error.message } } : {},
})
}
const onCall = (message: WorkerToHost): void => {
if (message.type !== 'call' || settled) return
// Hostile-peer rules: a duplicate id is ignored, an unknown name is
// answered with a failure, and a binding throw/reject becomes the
// program-side rejection — contained here, never a host crash.
if (answered.has(message.id)) return
answered.add(message.id)
const reply = (payload: ReplyMessage): void => {
if (settled) return
try {
worker.postMessage(payload)
} catch {
// The reply value failed structured clone; renegotiate as an error
// reply, which is always clone-plain. Nothing else throws here.
worker.postMessage({ type: 'reply', id: message.id, ok: false, message: 'binding resolution is not structured-cloneable' })
}
}
const record = bindings.get(message.global)
// Own-property lookup only: a forged name like 'constructor' or
// 'hasOwnProperty' must not walk the record's prototype chain and
// reach a callable the consumer never declared.
const fn = record && Object.hasOwn(record, message.name) ? record[message.name] : undefined
if (typeof fn !== 'function') {
reply({ type: 'reply', id: message.id, ok: false, message: `unknown binding ${JSON.stringify(`${message.global}.${message.name}`)}` })
return
}
void (async () => {
try {
reply({ type: 'reply', id: message.id, ok: true, value: await fn(message.args) })
} catch (error: unknown) {
reply({ type: 'reply', id: message.id, ok: false, message: messageOf(error) })
}
})()
}
worker.on('message', (message: WorkerToHost) => {
if (message.type === 'log' && !settled) logs.push(message.entry)
onCall(message)
onDone(message)
})
worker.on('error', (error: Error) => {
finish({ error: { kind: 'worker-exit', message: `worker error: ${error.message}` } })
})
worker.on('exit', (exitCode: number) => {
finish({ error: { kind: 'worker-exit', message: `worker exited with code ${exitCode} before completing` } })
})
// The compute budget reads the worker's own measured busy time, so a
// hot loop expires it no matter what dispatches are in flight, while a
// program idling on a slow binding accrues nothing.
const eluTimer = setInterval(() => {
const elu = worker.performance.eventLoopUtilization()
if (elu.active > this.config.computeMs) {
finish({ error: { kind: 'timeout', message: `compute budget exhausted (${this.config.computeMs}ms busy)` } })
}
}, ELU_POLL_INTERVAL_MS)
const wallTimer = setTimeout(() => {
finish({ error: { kind: 'timeout', message: `wall-clock ceiling reached (${this.config.maxWallMs}ms)` } })
}, this.config.maxWallMs)
const onAbort = (): void => {
finish({ error: { kind: 'abort', message: String(request.signal?.reason) } })
}
request.signal?.addEventListener('abort', onAbort, { once: true })
const live: LiveRun = {
worker,
finished,
settle: (failure: CodeRunFailure) => { finish({ error: failure }) },
}
this.live.add(live)
})
}
}
export default WorkerCodeRuntime
@@ -0,0 +1,65 @@
/**
* Wire protocol between the host runtime and the worker bootstrap. Everything
* crossing the message port is structured-clone-plain and versionless — both
* ends ship in this package, always at the same version. The host treats
* inbound traffic as HOSTILE (the worker runs model code, which can reach
* `parentPort` via `import('node:worker_threads')` and forge any of these
* shapes); the worker treats inbound traffic as trusted.
*
* @module @deepseek-ai/dsh-code-runtime-worker/src/protocol
*/
import type { CodeLogEntry } from '@deepseek-ai/dsh-code-runtime'
/** What the host hands the worker at spawn, via `workerData`. */
export interface WorkerBootData {
/** The type-stripped (plain JS) program body. */
code: string
/** Binding namespaces to materialize: the global name plus the function names (functions themselves stay host-side). */
namespaces: { global: string; names: string[] }[]
/** Shared byte budget for captured log text; exceeding it drops further entries after one in-band marker. */
maxLogBytes: number
/** Byte cap for the rendered completion value (see the value-preparation contract in bootstrap.ts). */
maxValueBytes: number
}
/** Worker → host: one bridged binding call. */
export interface CallMessage {
type: 'call'
/** Worker-issued correlation id; the host answers each id at most once and ignores duplicates. */
id: number
/** The namespace global the call targets. */
global: string
/** The function name within the namespace. */
name: string
/** The single argument, structured-clone-plain. */
args: unknown
}
/** Worker → host: one captured log entry, streamed eagerly so output survives a mid-run termination (timeout, abort, OOM). */
export interface LogMessage {
type: 'log'
entry: CodeLogEntry
}
/**
* Worker → host: the program settled. `error` carries a program exception
* (the only failure the bootstrap itself can report — budgets, aborts, and
* substrate death are observed host-side). `value` is present only on a
* clean completion that produced one (already size-capped and
* clone-safe per the bootstrap's value preparation). Logs are NOT carried
* here — they streamed eagerly as {@link LogMessage}s.
*/
export interface DoneMessage {
type: 'done'
value?: unknown
error?: { message: string }
}
/** Every message the worker sends. */
export type WorkerToHost = CallMessage | LogMessage | DoneMessage
/** Host → worker: the answer to one {@link CallMessage}. */
export type ReplyMessage =
| { type: 'reply'; id: number; ok: true; value: unknown }
| { type: 'reply'; id: number; ok: false; message: string }
@@ -0,0 +1,20 @@
/**
* The worker-thread entrypoint: self-executing glue over
* `bootstrap.ts`'s {@link runWorkerMain}, kept to the spawn wiring alone.
* Like `bin.ts` CLI entrypoints, this file executes only inside a spawned
* worker isolate — a place the coverage provider cannot observe — so it is
* excluded from the coverage gate while every line of actual logic lives in
* `bootstrap.ts`, unit-tested in-process; the real spawn path is pinned by
* the integration tests that run genuine workers.
*
* @module @deepseek-ai/dsh-code-runtime-worker/src/worker
*/
import { parentPort, workerData } from 'node:worker_threads'
import { runWorkerMain } from './bootstrap.ts'
import type { WorkerBootData } from './protocol.ts'
// A worker always has a parent port; guard loudly rather than run detached.
if (!parentPort) throw new Error('dsh-code-runtime-worker: worker entry loaded outside a worker thread')
await runWorkerMain(parentPort, workerData as WorkerBootData, { stdout: process.stdout, stderr: process.stderr })