mirror of
https://github.com/deepseek-ai/deepseek-harness
synced 2026-08-15 21:04:50 +00:00
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).
@deepseek-ai/dsh-tasks
The background task registry (ctx.tasks): a runtime-global, CONCRETE service (no interface/implementation split — one sensible in-process implementation exists; a durable job backend would own that extraction) that gives every long-running tool the same ids, isolation, and lifecycle.
Service API
register(registration): TaskId— a producer hands over running work:kind(also the id prefix),label, optionalowner: Agent,cancel(reason?),done: Promise<TaskOutcome>(settles at QUIESCENCE, never rejects), optionalreadOutput()(stream kinds; absence = final-output-only). Throws while no control surface is attached — the loud fence against a deployment exposingrun_in_backgroundwith no way to collect or stop the work — and is ATOMIC: a failed registration mutates nothing (no stored task, no counter bump, no owner-cleanup bookkeeping), so producers can reliably cancel their just-started work and rethrow.get(id, caller?)/list(caller?)— non-consuming snapshots;listreturns only caller-owned plus unowned tasks (a global listing would leak foreign labels).read(id, caller?): TaskRead— stream kinds consume the per-task cursor (v1's single intended reader is the owning model — a non-consuming multi-reader surface would be a cursor/snapshot API extension, not areadchange); final kinds read the terminal output idempotently.kill(id, caller?, reason?)—'requested'(live task: producercancelruns first — a throw fails the kill loud and leaves the task untouched — thenstopping) or'already-terminal'. Every successful kill marks the taskreported(the killer saw the end → completion notice suppressed).wait(id, timeoutMs, caller?, signal?)— resolves with the terminal snapshot (markedreported), or the live snapshot at timeout; an aborted signal rejects the WAIT only. Timing is adsh-timeoutdeadline()scoped to theTASK_WAIT_TIMEOUTcode, so a nested foreign deadline never misreads as a wait timeout.onTaskDone(listener)— exactly once per task with the terminal snapshot; effect-scoped, per-listener containment, silent after service disposal.attachSurface(name)— declares a control surface exists (the model tools, or a deployment's custom surface); effect-scoped.
Every read/kill/wait/get compares the task's owner session (owner.session.header.id) with the caller's and rejects a foreign one — ids are predictable (bash-1), so the fence, not id secrecy, is the isolation boundary.
Lifecycle
- Registrations are NOT effect-scoped to the registering fiber: tasks belong to their owning agent + producing backend, so producer/surface HMR reloads never touch them.
- An owned task attaches (once per owner) an awaited cleanup via
ctx.agents.onCleanup: on the owner's disposal the registry cancels its live tasks, awaits eachdone, and drops the snapshots —AgentHandle.dispose()resolves only after quiescence. - Service disposal closes the listener registry first (late teardown kills stay silent), cancels every live task with containment, and awaits settlement.
Non-goals (v1)
Durable/cross-restart tasks, non-consuming observation cursors, and foreground→background promotion are deliberate deferrals — see the runtime RFC § Alternatives.