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:
@@ -10,7 +10,6 @@ import type { Context } from 'cordis'
|
||||
import z from 'schemastery'
|
||||
import { boundContextSummary, createUserMessage, type ContentBlock } from '@deepseek-ai/dsh-llm'
|
||||
import { TextRetainer } from '@deepseek-ai/dsh-retention'
|
||||
import { scopeChainOf, scopeOf } from '@deepseek-ai/dsh-scope'
|
||||
import { defineTool } from '@deepseek-ai/dsh-tools'
|
||||
import type { GenericCallView, ToolDefinition, ToolExecution } from '@deepseek-ai/dsh-tools'
|
||||
import { TaskId } from '@deepseek-ai/dsh-tasks'
|
||||
@@ -227,21 +226,16 @@ export function apply(ctx: Context, config: Config): void {
|
||||
text: 'Track every background task id you start. You are notified in-session when a task finishes — do not busy-poll or sleep on one; keep working on independent steps and do not duplicate a running task\'s work. Before giving a final answer, collect every still-relevant task with task_output (set wait: true only when you are genuinely blocked on it), and task_kill tasks that stopped mattering.',
|
||||
})
|
||||
|
||||
// Use the exact lifecycle owner; reusable ids could resolve to a replacement.
|
||||
// Delivery targets the exact lifecycle owner. The notice waits in its
|
||||
// next-step inbox until another step claims it; disposal before that
|
||||
// boundary discards it with the owner.
|
||||
//
|
||||
// One host registry can carry SEVERAL mounts of this plugin — one per agent
|
||||
// preset — and `settle()` broadcasts a single snapshot to every registered
|
||||
// listener with no scope filter of its own. Each mount must therefore claim
|
||||
// only the owners composed under it, or every mounted preset injects the
|
||||
// same completion into the same agent and the model reads N copies of one
|
||||
// notice. An unscoped mount is the host-plane instance that serves every
|
||||
// agent, so it claims all of them.
|
||||
const mountScope = scopeOf(ctx)
|
||||
// The registry routes each settlement to the listeners its owner's scope
|
||||
// chain reaches, so a mount under one preset never sees another preset's
|
||||
// agents; this listener owns delivery, not the choice of whom to deliver to.
|
||||
ctx.tasks.onTaskDone((snapshot, owner) => {
|
||||
if (snapshot.reported || owner === undefined) return
|
||||
if (mountScope !== undefined && !scopeChainOf(scopeOf(owner.ctx)).includes(mountScope)) return
|
||||
owner.inject(createUserMessage({
|
||||
content: [{
|
||||
type: 'text',
|
||||
|
||||
Reference in New Issue
Block a user