|
|
|
|
@@ -65,7 +65,7 @@ Registrations are NOT effect-scoped to the registering fiber: a task belongs to
|
|
|
|
|
|
|
|
|
|
## Authorization and the service surface
|
|
|
|
|
|
|
|
|
|
Cross-session isolation lives IN the runtime so every consumer gets the same rule for free: read/kill/wait/get take the caller (`Agent | undefined`), and a task whose owner session differs from the caller's session is rejected (`!== undefined` comparison — an unowned task is open, a no-agent caller cannot match an owned task). `list(caller)` returns only the caller-visible tasks (owned-by-caller or unowned) — a global listing would leak other sessions' labels. Owner identity is `session.header.id`, the canonical id every other subsystem keys on; because both sides of the comparison come from live `Agent`s, the freestanding `OwnerToken` brand the bash seam used to carry became internal state rather than a seam type.
|
|
|
|
|
Cross-session isolation lives IN the runtime so every consumer gets the same rule for free: read/kill/wait/get take the caller (`Agent | undefined`), and a task whose owner session differs from the caller's session is rejected (`!== undefined` comparison — an unowned task is open, a no-agent caller cannot match an owned one). `list(caller)` returns only the caller-visible tasks (owned-by-caller or unowned) — a global listing would leak other sessions' labels. The snapshot carries that owner as the canonical branded `SessionId`, not a package-local token or bare string. Lifecycle ownership is checked independently at start: the supplied owner must be the exact live `Agent` instance currently registered under its id, so an old object cannot attach its session's work to a replacement agent's cleanup after id reuse.
|
|
|
|
|
|
|
|
|
|
```ts ignore-check
|
|
|
|
|
class TaskService extends Service { // ctx.tasks
|
|
|
|
|
@@ -80,7 +80,7 @@ class TaskService extends Service { // ctx.tasks
|
|
|
|
|
}
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
`TaskSnapshot` is the read-only projection: id, kind, label, owner session, status, detail, started/finished timestamps, and the `reported` notice-suppression flag (below). `wait` resolves with the terminal snapshot, or with the still-`running` snapshot on timeout; aborting the wait cancels only the wait — unless the task already settled, in which case the wait still delivers the terminal snapshot (settlement suppressed the completion notice on this live waiter's behalf, and an aborted waiter un-counts itself synchronously so a same-tick settlement never suppresses a notice nobody will deliver).
|
|
|
|
|
`TaskSnapshot` is the read-only projection: id, kind, label, branded owner `SessionId`, status, detail, started/finished timestamps, and the `reported` notice-suppression flag (below). `wait` resolves with the terminal snapshot, or with the still-`running` snapshot on timeout; aborting the wait cancels only the wait — unless the task already settled, in which case the wait still delivers the terminal snapshot (settlement suppressed the completion notice on this live waiter's behalf, and an aborted waiter un-counts itself synchronously so a same-tick settlement never suppresses a notice nobody will deliver).
|
|
|
|
|
|
|
|
|
|
**Misconfiguration fails loud**: a deployment that loads a background-capable producer without any control surface would let the model start tasks it can never read or stop — the half-loaded failure mode the subagent RFC's first draft reshaped a whole plugin to avoid. The fence is `attachSurface()`: `dsh-tool-tasks` attaches (effect-scoped) on load, and `start()` throws `background tasks unavailable: no control surface is attached (load @deepseek-ai/dsh-tool-tasks)` when none is attached — the earliest self-contained moment, since concurrent plugin start makes a load-time check racy. The registry stays ignorant of tool names; a deployment with a custom (non-model) surface attaches its own.
|
|
|
|
|
|
|
|
|
|
@@ -100,7 +100,7 @@ Completion notices stay durable context, not a wake-up (`agent.inject()` appends
|
|
|
|
|
|
|
|
|
|
## 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, the 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.
|
|
|
|
|
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.
|
|
|
|
|
|
|
|
|
|
## The awaited owner-cleanup seam
|
|
|
|
|
|
|
|
|
|
@@ -109,7 +109,7 @@ A contract-compliant background task must not outlive its owner: the subagent ca
|
|
|
|
|
- `AgentRegistry.onCleanup(agentId, cleanup: () => Promise<void>): () => void` — a per-agent cleanup registry (registrations are effects; the disposer unregisters).
|
|
|
|
|
- The loop's composite disposal chain carries one link for it: after stop-and-drain and before unregister, `await ctx.agents.drainCleanups(agent.id)` runs every registered cleanup with per-cleanup containment (a throwing cleanup is logged and never starves later cleanups or the rest of the chain). This is a documented `dsh-agent-loop` change; running cleanups is part of the `AgentFactory` dispose contract so a replacement loop honors it too.
|
|
|
|
|
|
|
|
|
|
`dsh-tasks` consumes the seam: the first task registered for an owner attaches one cleanup that cancels the owner's still-live tasks, normally awaits each task's `done` (quiescence), and drops the owner's snapshots. For contract-compliant producers, `AgentHandle.dispose()` therefore resolves only after the owner's background children are actually gone, and the guarantee composes transitively: a background subagent that started background tasks of its own drains them when its child agent disposes inside the parent task's settlement path (the cascade OpenCode implements with explicit parent-chain walking falls out of the seam here). A producer whose teardown cancel throws is the explicit degradation: its record settles `failed` with a possible-orphan detail and cleanup continues. An ownerless task is the sanctioned way for healthy work to outlive an agent, and a future durable-job RFC is the way to outlive the runtime.
|
|
|
|
|
`dsh-tasks` consumes the seam: after validating the exact registered owner instance, the first task for that owner attaches one cleanup that cancels the owner's still-live tasks, normally awaits each task's `done` (quiescence), and drops the owner's snapshots. For contract-compliant producers, `AgentHandle.dispose()` therefore resolves only after the owner's background children are actually gone, and the guarantee composes transitively: a background subagent that started background tasks of its own drains them when its child agent disposes inside the parent task's settlement path (the cascade OpenCode implements with explicit parent-chain walking falls out of the seam here). A producer whose teardown cancel throws is the explicit degradation: its record settles `failed` with a possible-orphan detail and cleanup continues. An ownerless task is the sanctioned way for healthy work to outlive an agent, and a future durable-job RFC is the way to outlive the runtime.
|
|
|
|
|
|
|
|
|
|
## Bash migration
|
|
|
|
|
|
|
|
|
|
@@ -169,7 +169,7 @@ Everything model-visible already lands in the log: starts and reads are tool cal
|
|
|
|
|
|
|
|
|
|
## Testing
|
|
|
|
|
|
|
|
|
|
Unit coverage pins the registry lifecycle (register/read/kill/wait/list, owner isolation including no-agent callers, stream-vs-final read semantics, listener containment, notice suppression after an explicit kill or terminal read/wait, the `attachSurface` fence, start atomicity — a failed preflight mutates nothing and burns no counter; a model-facing failed `cancel` leaves the task untouched; a teardown failed `cancel` force-fails the record once without awaiting `done` — ordinary disposal quiescence, per-kind id counters), the `onCleanup` drain ordering + containment (including mid-drain registration and effect self-release), both producers' start mapping plus the structural no-uncollectable-work guarantee (a failed preflight means the producer's `run()` — the spawn — was never invoked), and unchanged foreground bash/subagent behavior. Snapshot coverage pins the task tool schemas and the prompt section through the pinned-header fixture.
|
|
|
|
|
Unit coverage pins the registry lifecycle (register/read/kill/wait/list, owner isolation including no-agent callers and stale owner objects after id reuse, stream-vs-final read semantics, listener containment, notice suppression after an explicit kill or terminal read/wait, the `attachSurface` fence, start atomicity — a failed preflight mutates nothing and burns no counter; a model-facing failed `cancel` leaves the task untouched; a teardown failed `cancel` force-fails the record once without awaiting `done` — ordinary disposal quiescence, per-kind id counters), the branded `SessionId` snapshot boundary, the `onCleanup` drain ordering + containment (including mid-drain registration and effect self-release), both producers' start mapping plus the structural no-uncollectable-work guarantee (a failed preflight means the producer's `run()` — the spawn — was never invoked), and unchanged foreground bash/subagent behavior. Snapshot coverage pins the task tool schemas and the prompt section through the pinned-header fixture.
|
|
|
|
|
|
|
|
|
|
## Consequences
|
|
|
|
|
|
|
|
|
|
|