docs: tighten prose audit after master retarget
This commit is contained in:
@@ -1,43 +1,6 @@
|
||||
/**
|
||||
* The host half of one worker-engine run: spawn the Worker, bridge its child
|
||||
* RPC onto the holder-bound subagent service, fan its observer messages into
|
||||
* the engine's events, and own cancellation, the settle-within-grace
|
||||
* guarantee, and child cleanup. The worker's lifetime IS the run's lifetime:
|
||||
* `dispose()` always ends with `worker.terminate()`, so no thread outlives its
|
||||
* run.
|
||||
*
|
||||
* The run's `result` promise settles exactly once, from whichever of these
|
||||
* lands first: receipt of the worker's `result` message, an unexpected worker
|
||||
* death (`error`/`messageerror`/premature `exit` → `stopReason: 'error'`, or
|
||||
* `'cancelled'` when a cancel was in flight), or the post-cancel grace timer (a
|
||||
* script that never settles is force-settled `cancelled` and its worker
|
||||
* terminated — the real kill an in-process engine could not perform). At
|
||||
* `result` receipt the host snapshots whether caller/signal/dispose
|
||||
* cancellation is already in flight: an earlier cancellation overrides a
|
||||
* non-cancelled report; otherwise the report wins before settlement-only child
|
||||
* cleanup invokes arbitrary provider callbacks. Worker death uses the same
|
||||
* boundary: it claims `error` (or a previously requested `cancelled`) before
|
||||
* reaping children, so cleanup callbacks cannot rewrite the outcome. That
|
||||
* first signal also closes inbound message admission: Node may emit `error`,
|
||||
* then deliver queued messages, then emit `exit`, but those late messages may
|
||||
* neither create work nor narrate after settlement. If Result or grace already
|
||||
* owns the outcome, death preserves it while still cleaning resources; the
|
||||
* eventual exit performs a final disposal-only sweep without repeating child
|
||||
* cancellation.
|
||||
*
|
||||
* Provider starts and published children are tracked separately. Every start
|
||||
* receives one shared per-run abort signal; the provider owns partial setup
|
||||
* until its promise fulfills. If admission closes while a start is pending,
|
||||
* the signal aborts it; a late fulfillment is disposed without publication to
|
||||
* the worker. Ready runs enter a callId registry whose memoized disposal is
|
||||
* shared by graceful worker RPC, public disposal, normal-settlement reap, and
|
||||
* worker-death cleanup. Quiescence requires both pending starts and published
|
||||
* children to drain. Lifecycle pairing is host-guaranteed independently:
|
||||
* every forwarded `agent-start` enters a ledger, and a dead or terminated
|
||||
* worker's missing `agent-end` is synthesized exactly once as cancelled. On a
|
||||
* termination path `agentsStarted` reports the host-observed child-start count;
|
||||
* calls still queued worker-side for a concurrency slot are unknowable.
|
||||
*
|
||||
* Host side of one workflow run. Owns the worker, child RPC, first-outcome
|
||||
* settlement, cancellation grace, lifecycle pairing, and quiescent cleanup.
|
||||
* @module @deepseek-ai/dsh-workflow-workerthread/host
|
||||
*/
|
||||
|
||||
@@ -63,29 +26,10 @@ interface ChildRecord {
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve the worker entry and spawn options for the current runtime shape.
|
||||
* Unbuilt (tsx demos, vitest — `import.meta.url` points into `src/`), the
|
||||
* entry is a JavaScript data-URL bootstrap. That bootstrap runs INSIDE the
|
||||
* user worker, registers tsx's ESM AND CommonJS transforms there, and only
|
||||
* then imports the TypeScript sibling. The whole mixed-module source graph
|
||||
* therefore receives TypeScript transformation and the tsconfig paths map in
|
||||
* the worker's own module-loader realm. A worker inherits no
|
||||
* transform pipeline from vitest (vite transforms in-process), and a parent
|
||||
* `--import tsx` registration is not a contract that user workers share on
|
||||
* every supported Node line. Built (`lib/index.js`), the entry is the sibling
|
||||
* bundle the package tsdown config emits and no loader is needed (`execArgv`
|
||||
* pinned empty in both shapes — hermetic, like the environment).
|
||||
*
|
||||
* Both shapes spawn with an EMPTY environment (`env: {}`): the documented vm
|
||||
* escape reaches `process`, and the harness's ambient credentials
|
||||
* (`DEEPSEEK_API_KEY` et al.) must not ride along — the same stance as
|
||||
* `dsh-code-runtime-worker`, stronger than the scrubbed env the
|
||||
* defensive-patterns rule requires for spawned commands (a shell needs PATH;
|
||||
* this worker needs nothing). Sole exception: the unbuilt shape forwards
|
||||
* `TSX_TSCONFIG_PATH` when the parent carries it (loader plumbing the paths
|
||||
* map depends on outside the repo cwd, not a secret). This closes the
|
||||
* AMBIENT channel only — an escapee still holds process-wide privileges
|
||||
* like fs access (the README's trust premise stands).
|
||||
* Resolve a built worker bundle or an unbuilt bootstrap that installs both tsx
|
||||
* transforms inside the worker. Both shapes clear `execArgv` and the ambient
|
||||
* environment; the unbuilt shape forwards only `TSX_TSCONFIG_PATH` for path
|
||||
* resolution.
|
||||
* @param init - the run payload, passed as `workerData`.
|
||||
* @returns the entry URL and the Worker options to spawn it with.
|
||||
*/
|
||||
@@ -94,15 +38,7 @@ function resolveWorkerSpawn(init: WorkerInit): { entry: URL; options: WorkerOpti
|
||||
if (!import.meta.url.endsWith('.ts')) {
|
||||
return { entry: new URL('./worker.js', import.meta.url), options: { workerData: init, env: {}, execArgv: [] } }
|
||||
}
|
||||
// Resolve tsx lazily: only the unbuilt shape executes this arm, so a built
|
||||
// consumer never needs the dev-only loader installed. A JavaScript entry is
|
||||
// essential — it can install tsx's ESM and CommonJS hooks from INSIDE the
|
||||
// user worker before any TypeScript enters Node's native strip-only parser.
|
||||
// Both hooks are load-bearing because the source graph crosses both module
|
||||
// shapes on supported Node lines. TSX_TSCONFIG_PATH is
|
||||
// the one variable forwarded through the scrub: a parent running outside
|
||||
// the repo cwd (the ACP snapshot harness is the real case) pins the paths
|
||||
// map through it. Loader plumbing, not a secret.
|
||||
// Resolve tsx only for unbuilt consumers and install it before importing TS.
|
||||
const workerEntry = new URL('./worker.ts', import.meta.url)
|
||||
const tsxEsmApiEntry = import.meta.resolve('tsx/esm/api')
|
||||
const tsxCjsApiEntry = import.meta.resolve('tsx/cjs/api')
|
||||
|
||||
@@ -1,39 +1,7 @@
|
||||
/**
|
||||
* Per-run execution state for the engine's THREAD side: the script's vm
|
||||
* context and its injected hooks (`agent`/`parallel`/`pipeline`/`phase`/
|
||||
* `log`/`args`), the concurrency semaphore and caps, cancellation, and the
|
||||
* drive loop that turns a script settlement into a {@link WorkflowResult}.
|
||||
* Children are started by RPC to the host through a {@link ChildPort}, so
|
||||
* this module never touches a cordis context — it runs inside the worker
|
||||
* thread.
|
||||
*
|
||||
* Value boundary (the trust premise lives in ./realm.ts): values ENTERING the
|
||||
* worker-side host code from the script (hook options, schemas, the return
|
||||
* value) are materialized by `materializeFromRealm` — a plain walk that
|
||||
* rejects loud everything JSON cannot carry, which also makes every value
|
||||
* safe for the later postMessage hop. Values ENTERING the realm (`args`,
|
||||
* `agent()` results, hook promises and their failures, combinator arrays) are
|
||||
* handed over DIRECTLY as worker-realm values: the script is model-written
|
||||
* and trusted, so outer prototypes are not a leak. `args` is cloned once at
|
||||
* start so a script scribbling on it cannot mutate the session's init object
|
||||
* (a benign-bug guard; the postMessage clone already isolated the caller).
|
||||
*
|
||||
* Failure discipline: fatal {@link WorkflowError}s (bad hook arguments,
|
||||
* unsupported options/schemas, tripped caps, synchronous start refusal,
|
||||
* provider-start failure, ready-child result rejection, and
|
||||
* cancellation) ALWAYS propagate through
|
||||
* `parallel`/`pipeline` — recognized by `instanceof` against this realm's
|
||||
* class, which a script inside the vm context cannot forge — and the per-item
|
||||
* `null` is reserved for child-run failures and ordinary in-stage script
|
||||
* errors. Every hook-returned promise gets a no-op rejection consumer, so a
|
||||
* dropped promise cannot surface an unhandled rejection (which would kill the
|
||||
* worker and read as an engine fault).
|
||||
*
|
||||
* There is deliberately NO worker-side abandon channel: a script that never
|
||||
* settles after a cancel simply never posts a result, and the HOST enforces
|
||||
* the settles-within-grace guarantee by force-settling `cancelled` and
|
||||
* terminating the worker — the real kill an in-process engine could not have.
|
||||
*
|
||||
* Worker-side workflow runtime: vm hooks, child RPC, limits, value
|
||||
* materialization, cancellation, and result shaping. Host termination enforces
|
||||
* the cancellation deadline.
|
||||
* @module @deepseek-ai/dsh-workflow-workerthread/runtime
|
||||
*/
|
||||
|
||||
|
||||
@@ -16,28 +16,10 @@ function fakeParent(): Agent {
|
||||
return { id: AgentId('workflow-parent'), options: {} } as unknown as Agent
|
||||
}
|
||||
|
||||
// Worker-thread startup is CPU-bound (a fresh thread compiles the runtime on
|
||||
// every start): on a contended CI runner it regularly blows past vitest's 5s
|
||||
// default test timeout, observed repeatedly on the coverage lane.
|
||||
// Allow cold worker startup on contended CI runners.
|
||||
vi.setConfig({ testTimeout: 30_000 })
|
||||
|
||||
/**
|
||||
* `vi.waitFor` with a contention-proof default timeout: the 1s default
|
||||
* flaked repeatedly on the CI coverage lane, where worker-thread cold start
|
||||
* (CPU-bound — a fresh thread compiles the runtime) competes with three
|
||||
* sibling vitest workers for CPU. The 10s default is for exactly those
|
||||
* races — waiting for a worker to start, run its first script line, or
|
||||
* deliver an async child-registration message to the host. It is NOT for a
|
||||
* wait that asserts the HOST reacted PROMPTLY to something that already
|
||||
* happened (a settled result, an observed worker death): those keep an
|
||||
* explicit tight override below, or the generous default would silently
|
||||
* accept a multi-second regression in host-side reap latency as passing
|
||||
* (proven by injecting a 6s delay into one such reap and watching the
|
||||
* un-overridden version of this helper still pass in ~6s).
|
||||
* @param assertion - retried until it stops throwing or the timeout elapses.
|
||||
* @param timeout - override for a wait that must stay deliberately tight.
|
||||
* @returns resolves when the assertion passes.
|
||||
*/
|
||||
/** Retry an assertion until it passes or the timeout elapses. */
|
||||
function waitFor(assertion: () => void, timeout = 10_000): Promise<void> {
|
||||
return vi.waitFor(assertion, { timeout, interval: 50 })
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user