Files
deepseek-harness/packages/tasks/tasks
Yichen Jiang 59e759ce13 fix(tasks-local): layer control surfaces and listeners by registering scope
One host registry serves every composition in the process, so its two
service-wide collections answered per-owner questions process-wide. `start()`
asked only whether SOME surface was attached, so an agent whose own composition
loads no `tool-tasks` could start work it has no tool to collect or stop as soon
as any other preset attached one — and the answer changed depending on which
sessions happened to be open. `settle()` walked every registered listener, so a
task settling without a waiter injected one completion notice per mounted
preset into the same owner.

Both collections now sit in `ScopedLayers`, the layered-registry primitive
`tools` and `skills` already use: a registration files into its registering
context's scope, and a read unions the global layer with the owner's scope
chain. A surface or listener registered from an unscoped context lands in the
global layer and serves every owner, which is exactly the host-plane
composition's own controls, so the TUI path is unchanged without a special
case.

This supersedes the consumer-side filter in the previous commit. That filter
produced the right notices but sat in the wrong layer: it left the `start()`
gate process-wide, it could not be enforced against a producer that resolves
the registry directly, and it made a Consumer carry scope knowledge that the
other layered registries keep in the registry. `tool-tasks` is scope-agnostic
again and the `dsh-scope` edge moves to `tasks-local`.

`start()`'s refusal is now owner-relative, so its model-visible text names the
agent rather than the process. The shipped `minimal` preset keeps
`enableRunInBackground: false`, no longer as the safety boundary — the registry
owns that now — but so an agent that could never collect a task is not offered
the parameter at all.

Refs #2141
2026-08-10 16:12:41 +08:00
..

@deepseek-ai/dsh-tasks

English | 中文

The background task registry contract (ctx.tasks). The abstract TaskService and its vocabulary types give long-running producers shared ids, owner isolation, reads, cancellation, waiting, notices, and cleanup under one contract; the process-local registry lives in dsh-tasks-local. Producer plugins extend TaskKindMap with their opaque id namespace.

Service contract

  • start(spec): TaskId validates the control surface, spec, exact live owner, and optional positive outputLimitBytes before calling the producer's run() once. A starter throw leaves nothing registered; successful return commits without another failable step.
  • get(id, caller?) and list(caller?) return non-consuming snapshots. Listing includes only caller-owned and unowned tasks.
  • read(id, caller?) consumes the single cursor for stream tasks and reads terminal output idempotently for final-output tasks.
  • kill(id, caller?, reason?) invokes producer cancellation before changing status. A cancellation throw leaves the task running; success changes it to stopping and marks terminal delivery reported.
  • wait(id, timeoutMs, caller?, signal?) returns a terminal snapshot or the live snapshot at timeout. Aborting stops only the wait; settlement wins once it has committed terminal delivery to that waiter.
  • onTaskDone(listener) observes each terminal record with the exact owner. Listener throws and rejections are contained; listener work is not awaited.
  • attachSurface(name) declares a control surface for its effect lifetime. start() fails before producer execution when no attached surface serves the spec's owner.

Both registrations are owner-relative, because one registry serves every composition in the process. A surface or listener registered from an unscoped context serves every owner; one registered under an agent composition's scope serves exactly the agents composed under it. So a composition that loads no control surface cannot start background work on the strength of another composition's controls, and one settlement notifies only the listeners its owner's composition registered.

Owned access compares the task's SessionId with the caller's. Ids such as bash-1 are predictable, so this fence is the boundary. Unowned tasks are open to callers and last until service disposal.

outputLimitBytes is producer-owned model-presentation policy carried unchanged into snapshots. A control surface applies it after adding status or notice metadata; the registry does not rewrite producer output or invent a default for producers that omit it.

Implementations also owe the lifecycle semantics of the contract: registrations outlive producer and control-surface fibers, owner and service disposal cancel live work and await compliant producers, and settlement is first-wins — one terminal record, one round of contained listener notification, released waiters.

See the task type catalog, the runtime Agent Note, and the seam Agent Note.

Model Experience

Indirectly, through producer plugins and dsh-tool-tasks, which render task ids, output, status, cancellation, and completion notices.

KV Cache effect

No direct invalidation; the named consumer owns any request-prefix changes.

Known Limitations and Deferred Work

  • Stream output has one consuming cursor — independent observers need a cursor or snapshot API.
  • Foreground work cannot be promoted — producers choose foreground or background before starting.
  • The contract is in-processTaskStart.run() passes callbacks and exact Agent objects; a durable or cross-process backend must reshape identity, restart, ownership, and observation semantics before it can implement this seam.