refactor(subagent): unify async readiness and cancellation

This commit is contained in:
Tianyi Cui
2026-07-12 22:41:59 +08:00
parent 02ca71db57
commit bb3f6bd736
49 files changed
+1350 -4147

No files matched your search

@@ -14,11 +14,6 @@
* A `cancel` arriving instead of `go` still releases the gate: `drive()`
* sees the cancelled state and settles without running the body.
*
* Terminal ordering is Result first, settlement cleanup second. The session
* queues the Result message before asking the execution to reap stray children;
* MessagePort FIFO therefore lets the host atomically claim the result before a
* cleanup ChildCancel can invoke arbitrary provider code.
*
* @module @deepseek-ai/dsh-workflow-workerthread/session
*/
@@ -64,10 +59,6 @@ class RpcChildHandle implements ChildHandle {
this.result = entry.settled.promise
}
cancel(reason?: string): void {
this.post(WorkerToHostType.ChildCancel, { callId: this.callId, reason })
}
dispose(): Promise<void> {
this.post(WorkerToHostType.ChildDispose, { callId: this.callId })
return this.entry.disposed.promise
@@ -76,7 +67,7 @@ class RpcChildHandle implements ChildHandle {
/**
* The worker-side child-RPC bridge ({@link ChildPort}): allocates callIds,
* posts the start/cancel/dispose RPCs, and owns the per-call pending
* posts the start/dispose RPCs, and owns the per-call pending
* book-keeping the session's message handler settles via the `onChild*`
* entry points.
*/
@@ -94,10 +85,10 @@ class ChildRpcBridge implements ChildPort {
settled: Promise.withResolvers<ChildResult>(),
disposed: Promise.withResolvers<void>(),
}
// Containment: when synchronous start or asynchronous readiness fails (or
// Containment: when asynchronous provider start fails (or
// the run is torn down), the settled promise may never gain a consumer —
// it must not surface as an unhandled rejection and kill the worker.
entry.settled.promise.catch(() => { /* consumed: unconsumed child settlement after failed start/readiness */ })
entry.settled.promise.catch(() => { /* consumed: unconsumed child settlement after failed start */ })
this.pending.set(callId, entry)
this.post(WorkerToHostType.ChildStart, { callId, request })
const childId = await entry.started.promise
@@ -109,7 +100,7 @@ class ChildRpcBridge implements ChildPort {
this.pending.get(callId)?.started.resolve(childId)
}
/** Synchronous start or asynchronous readiness failed; reject and retire the pending RPC. */
/** Asynchronous provider start failed; reject and retire the pending RPC. */
onChildStartError(callId: number, rendered: string): void {
const entry = this.pending.get(callId)
this.pending.delete(callId)
@@ -213,12 +204,5 @@ export async function runWorkerSession(port: MessagePort, init: WorkerInit): Pro
post(WorkerToHostType.Ready, {})
await gate.promise
const result = await execution.drive()
try {
// This post is the worker's terminal claim. Queue it BEFORE aborting stray
// children: MessagePort FIFO then guarantees the host claims Result before
// any settlement-only ChildCancel can invoke arbitrary provider callbacks.
post(WorkerToHostType.Result, { result })
} finally {
execution.reapAfterResult()
}
post(WorkerToHostType.Result, { result })
}