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
This commit is contained in:
Yichen Jiang
2026-08-10 16:12:41 +08:00
parent 37ebe87087
commit 59e759ce13
36 changed files with 232 additions and 97 deletions

View File

@@ -2,5 +2,5 @@
# side as of the last confirmed-consistent state. Both languages carry equal authority;
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write .agents/notes/implemented/architecture/2026-07-26-task-registry-seam.md
2026-07-26-task-registry-seam.md: 45801505f1729ec6094900acf94b4c17ed92c3b1
2026-07-26-task-registry-seam.zh.md: 8dd90b34d2da1d22caba13fe8c388dab4a29be0d
2026-07-26-task-registry-seam.md: 4487bd9c53595fa8b4eed588b294ceafe3ab58dc
2026-07-26-task-registry-seam.zh.md: 6195dc809e84852c7e0f63ac101ba0ed6a46853e

View File

@@ -12,7 +12,7 @@ The [background-task runtime](2026-06-20-generic-long-running-tool-runtime.md) s
`tasks/` is now a three-package capability family in the bash-trio shape:
- **`@deepseek-ai/dsh-tasks` (Service Definition)** — the abstract `TaskService extends Service` owning `ctx.tasks`, the eight-method contract (`start`, `list`, `get`, `read`, `kill`, `wait`, `onTaskDone`, `attachSurface`), all vocabulary types (`TaskId`, `TaskKindMap`, `TaskStart`, `TaskHooks`, `TaskOutcome`, `TaskSnapshot`, `TaskRead`, `TaskDoneListener`), and the snapshot invariant companion. The class-level JSDoc states the semantics every Service provider owes: registrations outlive producer and surface fibers, owned access is session-fenced, settlement is first-wins with contained listeners, and `start` refuses work while no control surface is attached.
- **`@deepseek-ai/dsh-tasks` (Service Definition)** — the abstract `TaskService extends Service` owning `ctx.tasks`, the eight-method contract (`start`, `list`, `get`, `read`, `kill`, `wait`, `onTaskDone`, `attachSurface`), all vocabulary types (`TaskId`, `TaskKindMap`, `TaskStart`, `TaskHooks`, `TaskOutcome`, `TaskSnapshot`, `TaskRead`, `TaskDoneListener`), and the snapshot invariant companion. The class-level JSDoc states the semantics every Service provider owes: registrations outlive producer and surface fibers, owned access is session-fenced, settlement is first-wins with contained listeners, and `start` refuses work while no attached control surface serves the spec's owner (surfaces and listeners are scope-layered, so one process-wide registry answers both questions per owner).
- **`@deepseek-ai/dsh-tasks-local` (Service provider)** — `LocalTaskService`, the process-local registry moved verbatim: the in-memory store, per-kind counters, waiter bookkeeping, `TASK_WAIT_TIMEOUT` deadline code, owner-cleanup effects, and force-fail teardown. The `dsh-timeout` dependency moves here with it; the Service Definition package has no provider dependencies.
- **`@deepseek-ai/dsh-tool-tasks` (Consumer)** — unchanged; it injects `'tasks'` and never imports provider types.

View File

@@ -12,7 +12,7 @@ Status: implemented
`tasks/` 如今是一个 bash 三件套形态的三包能力家族:
- **`@deepseek-ai/dsh-tasks`Service Definition**——抽象的 `TaskService extends Service`,拥有 `ctx.tasks`、八个方法的约定(`start``list``get``read``kill``wait``onTaskDone``attachSurface`)、全部词汇类型(`TaskId``TaskKindMap``TaskStart``TaskHooks``TaskOutcome``TaskSnapshot``TaskRead``TaskDoneListener`),以及快照不变式配套插件。类级 JSDoc 陈述了每个 Service provider 都必须兑现的语义:注册的存续期长于生产方与控制接口的 fiber有所有者的访问以会话为界结算遵循首次结果优先且监听器错误被隔离并且没有附加任何控制接口时 `start` 拒绝启动工作
- **`@deepseek-ai/dsh-tasks`Service Definition**——抽象的 `TaskService extends Service`,拥有 `ctx.tasks`、八个方法的约定(`start``list``get``read``kill``wait``onTaskDone``attachSurface`)、全部词汇类型(`TaskId``TaskKindMap``TaskStart``TaskHooks``TaskOutcome``TaskSnapshot``TaskRead``TaskDoneListener`),以及快照不变式配套插件。类级 JSDoc 陈述了每个 Service provider 都必须兑现的语义:注册的存续期长于生产方与控制接口的 fiber有所有者的访问以会话为界结算遵循首次结果优先且监听器错误被隔离并且没有任何已附加的控制接口服务于 spec 的所有者时 `start` 拒绝启动工作(控制接口与监听器按 scope 分层,因此一个进程级注册表能逐所有者地回答这两个问题)
- **`@deepseek-ai/dsh-tasks-local`Service provider**——`LocalTaskService`,即原样迁移的进程内注册表:内存存储、按 kind 划分的计数器、等待方簿记、`TASK_WAIT_TIMEOUT` deadline 代码、所有者清理 effect以及强制失败的拆除。`dsh-timeout` 依赖随之迁入此包Service Definition 包不含任何提供方依赖。
- **`@deepseek-ai/dsh-tool-tasks`Consumer**——保持不变;它注入 `'tasks'`,从不导入提供方类型。