mirror of
https://github.com/deepseek-ai/deepseek-harness
synced 2026-08-15 21:04:50 +00:00
fix(tasks): contain async completion listeners
TaskService contained synchronous listener throws but discarded returned promises. An async TaskDoneListener could therefore reject as an unhandled process rejection even though the API promises per-listener containment. Allow listeners to return PromiseLike<void> and attach a rejection handler to each result without awaiting it. This logs asynchronous failures independently while preserving the observation-only contract: later listeners, task waiters, and teardown are not delayed. Add a regression test and synchronize the public docs and generated API declaration.
This commit is contained in:
@@ -75,7 +75,7 @@ class TaskService extends Service { // ctx.tasks
|
||||
read(id: TaskId, caller?: Agent): TaskRead // delta (stream kinds, consuming) or final output (final kinds, idempotent) + snapshot
|
||||
kill(id: TaskId, caller?: Agent, reason?: string): 'requested' | 'already-terminal'
|
||||
wait(id: TaskId, timeoutMs: number, caller?: Agent, signal?: AbortSignal): Promise<TaskSnapshot>
|
||||
onTaskDone(listener: (snapshot: TaskSnapshot, owner: Agent | undefined) => void): () => void // exact lifecycle owner; effect-scoped, contained
|
||||
onTaskDone(listener: (snapshot: TaskSnapshot, owner: Agent | undefined) => void | PromiseLike<void>): () => void // exact owner; effect-scoped, contained
|
||||
attachSurface(name: string): () => void // the misconfiguration fence, below
|
||||
}
|
||||
```
|
||||
@@ -98,6 +98,8 @@ One system-prompt section (order 106, next to `tool:bash`) teaches the cross-cal
|
||||
|
||||
Completion notices stay durable context, not a wake-up (`agent.inject()` appends a logged `context/message` the next model request sees; it does not run the model): on `onTaskDone`, `dsh-tool-tasks` injects `background task <id> (<kind>: <label>) finished [status: …]. Read its output with task_output.` through the exact owner captured at start, never a replacement found through a reused agent/session id, with the disposed-race contained. Notices are deduplicated the way Claude Code and Kimi Code both learned to: a task the model explicitly killed, or whose terminal state a read/wait already returned (including a wait pending at the moment of settlement), is marked `reported` and its notice suppressed — never a redundant "finished" for work the model just collected or ended. The model-visible ⟺ logged invariant holds with no new session event type.
|
||||
|
||||
Completion listeners are observation-only: each synchronous throw and returned promise rejection is logged independently, later listeners still run, and listener promises are not awaited before waiters or task teardown continue.
|
||||
|
||||
## Producer opt-in and schema exposure
|
||||
|
||||
Whether a producer tool offers `run_in_background` is that producer's own defaulted config: `enableRunInBackground?: boolean` on `dsh-tool-bash` and on each `dsh-tool-subagent` instance (both default `true` — bash keeps its always-exposed behavior, and a deployment disables either per instance from cordis.yml, no code edit). A bundle forwards the configs of the child plugins it owns: `dsh-agent-core` exposes `toolBash` for its built-in producer and `toolTasks` for the generic control surface, while independently composed producers such as subagent instances receive config directly. This forwarding is config reachability, not producer registration: future background-capable tools do not become `agent-core` fields unless that bundle also chooses to own them. A disabled producer omits the parameter from its schema entirely — and, because the arg validator deliberately allows undeclared keys, its `execute` ALSO refuses a forced `run_in_background: true` loud (the omission is advertising; the execution-time check is the enforcement). `ctx.tasks` plays no part in schema shaping — it never rewrites or decorates a producer's tool schema (Kimi Code regex-rewrites its bash description when background is disabled; config-owns-the-schema makes that trick unnecessary) — it only provides runtime registration. The two halves compose fail-loud: the producer's config decides what the model sees, and a background call that still reaches `start()` without a control surface throws the load-this-package error. `start()` preflights every failable check (the fence, validation, exact live owner instance, and owner-cleanup attach) BEFORE invoking the producer's `run()` and commits atomically after — background work started without a collectable id is structurally impossible, not a producer rollback obligation.
|
||||
|
||||
Reference in New Issue
Block a user