refactor(tasks): drive wait() timing through dsh-timeout
ctx.tasks.wait arms a deadline() fusing the caller's abort with the wait timeout and classifies the outcome with timeoutOf scoped to the new TASK_WAIT_TIMEOUT code: a wait timeout resolves to the live snapshot (the task keeps running), a caller abort rejects the wait — same contract, no hand-rolled timer/listener plumbing, and a nested foreign deadline can no longer misread as a wait timeout. task_output deliberately declares NO ToolDefinition.timeoutMs: timeout-policy turns a timed-out call into a structured TOOL_TIMEOUT failure, but a timed-out wait is a SUCCESS that must still report [status: running] (decision recorded in the runtime RFC alternatives).
This commit is contained in:
@@ -30,6 +30,7 @@
|
||||
|
||||
import { Context, Service } from 'cordis'
|
||||
import type { Agent, AgentId } from '@deepseek-ai/dsh-agent'
|
||||
import { deadline, timeoutOf } from '@deepseek-ai/dsh-timeout'
|
||||
import { TaskId } from './types.ts'
|
||||
import type { TaskDoneListener, TaskOutcome, TaskRead, TaskRegistration, TaskSnapshot, TaskStatus } from './types.ts'
|
||||
|
||||
@@ -49,6 +50,14 @@ declare module 'cordis' {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* The `dsh-timeout` code stamped on a {@link TaskService.wait} deadline's
|
||||
* `TimeoutReason`. A wait timeout only ends the WAIT (the task keeps running
|
||||
* and the live snapshot is returned) — scoping `timeoutOf` to this code keeps
|
||||
* a foreign (outer, nested) deadline's timeout from being misread as ours.
|
||||
*/
|
||||
export const TASK_WAIT_TIMEOUT = 'TASK_WAIT_TIMEOUT'
|
||||
|
||||
/** The registry's mutable per-task record (never handed out — see {@link TaskService.snapshot}). */
|
||||
interface TrackedTask {
|
||||
id: TaskId
|
||||
@@ -273,15 +282,22 @@ export class TaskService extends Service {
|
||||
if (signal?.aborted) throw new Error('wait aborted')
|
||||
task.waiters += 1
|
||||
try {
|
||||
// The dsh-timeout deadline fits wait() exactly because both only
|
||||
// NOTIFY: a wait timeout returns the live snapshot (the task keeps
|
||||
// running — nothing is terminated), and timeoutOf scoped to our own
|
||||
// code tells that timeout apart from a caller abort, which rejects
|
||||
// the wait. `using` clears the timer on every exit path.
|
||||
using d = deadline(signal, timeoutMs, TASK_WAIT_TIMEOUT)
|
||||
await new Promise<void>((resolve, reject) => {
|
||||
const cleanup = (): void => {
|
||||
clearTimeout(timer)
|
||||
signal?.removeEventListener('abort', onAbort)
|
||||
const onAbort = (): void => {
|
||||
if (timeoutOf(d.signal, TASK_WAIT_TIMEOUT) !== undefined) resolve()
|
||||
else reject(new Error('wait aborted'))
|
||||
}
|
||||
const timer = setTimeout(() => { cleanup(); resolve() }, timeoutMs)
|
||||
const onAbort = (): void => { cleanup(); reject(new Error('wait aborted')) }
|
||||
signal?.addEventListener('abort', onAbort, { once: true })
|
||||
void task.settled.then(() => { cleanup(); resolve() })
|
||||
d.signal.addEventListener('abort', onAbort, { once: true })
|
||||
void task.settled.then(() => {
|
||||
d.signal.removeEventListener('abort', onAbort)
|
||||
resolve()
|
||||
})
|
||||
})
|
||||
} finally {
|
||||
task.waiters -= 1
|
||||
|
||||
Reference in New Issue
Block a user