- child-agent.ts declares both policy-service augmentations as explicit
empty type imports, so removing the ApprovalPolicy import cannot
silently degrade ctx.get('approval') typing.
- dsh-subagent-inprocess no longer consumes the policy services in src,
so its optional peers and tsconfig references are dropped; both
policy-inheritance Agent Notes state the current ownership.
- The continuable suite pins that a later child-side switch beats the
delegation snapshot and that an unswitched fork parent seeds no
policy events.
Presets own the rows that decide what a session's `/` menu contains, but
both browser catalogs cache per session and had no invalidation edge for a
recompose: `commands/changed` is registry-wide and recomposing registers
nothing, so the menu kept serving the composition the session no longer ran.
The host stream now frames the logged `agent-preset/selected` commit as
`host/session-preset-changed`; the runtime bridges it to the typed
`session/preset-changed` event, `ui-command` soft-refreshes that session's
directory key and `ui-skill` invalidates its catalog entry.
Reaching the host on a second switch was a separate defect: the list-row
identity guard compared every summary field except `agentPreset`, and the
merge keeps the row's `updatedAt`, so a switched row looked unchanged and
served its cached instance forever. The hero chip compares the pick against
that row, so switching back to the creation-time preset sent no RPC at all.
`tool-bash` resolves the background-task registry with `ctx.get('tasks')`,
and it sits at the preset's top level. The registry sat inside an
entry-local `isolate: { tasks: true }` realm, which is invisible to every
sibling row outside it, while the Web surface disabled the host row — so
both lookups missed and every `run_in_background` call answered
"background tasks unavailable" with `task_output`, `task_list`, and
`task_kill` still listed in the catalog. `task_list` returning
"(no background tasks)" is what made the outage read as an empty queue
rather than a severed producer.
That is the `goals` criterion read from inside the preset: a Service a row
outside its realm READS belongs to the plane both can see. `tasks` already
keys access by owning agent (`assertAccess` compares `task.owner.id`) and
mints an independent token per `attachSurface` call, so one host instance
serves every session exactly as before presets — the per-preset-standing-mounts
note records that sharing `tasks-local` is a return to its design.
`minimal` mounts no `tool-tasks`, and the `start()` control-surface gate is
a service-wide set that another preset's controls would open for it, so its
`tool-bash` disables `run_in_background` and drops the parameter from the
schema.
Fixes#2141
Review found the `onTasksChanged` teardown reasoning inverted. The comment
claimed every registration is an effect on the registry's own fiber, so
listeners would be gone before service disposal empties the store — but the
traceable proxy rebinds `this.ctx` to the CALLER, which this package's own
HMR-safety test already proves. The only shipped consumer registers from the
api-proxy mux stream, so it was still listening and simply kept the rows it
last received. Service disposal now announces the emptied set, and teardown
announces its stopping transition immediately instead of leaving an observer
on `running` for however long a slow producer takes to release.
Two documentation claims were false in the opposite direction: the Agent Note
and the ui-task README both said an unowned task is invisible in the header,
while `list(caller)` returns unowned tasks to every caller, the carrier fans
their changes out to every subscribed session, and this PR's own test asserts
exactly that. The note even contradicted itself two sections earlier. Both
sides now state the real asymmetries — another session's tasks, and the
process-local registry emptying on restart.
The "no Web path calls the consuming `ctx.tasks.read()`" invariant claimed a
test that did not exist; the carrier suite's producer had no `readOutput` at
all, so a stray read would have failed nothing. Its producer now counts cursor
consumption and the lifecycle and baseline paths both assert zero.
Also: a session created after the mux opened now receives the task baseline it
missed, the popover samples its clock when it opens rather than at mount, and
a failed task's unbounded producer detail elides instead of widening the row.
The arrow shorthand implicitly returns rmSync's void, which the
no-confusing-void-expression rule forbids; the block body keeps the
fallback without the violation.
A continuable background child (the default backgroundMode for both
delegation tools) never received the parent session's explicit
sandbox/approval overrides: materialization applied only child
composition, so a danger-full-access parent produced workspace-write
children whose every out-of-workspace operation raised an approval
prompt.
Move the one-shot driver's capture/append pair into the shared
child-agent module (captureDelegatedPolicyOverrides /
appendDelegatedPolicyOverrides) and call it from both paths:
startContinuable captures before its first await, only fresh
materialization appends the source-tagged events (after any fork seed),
and a cold resume replays the persisted delegation events instead of
re-capturing the parent.
Adds the continuable inheritance unit suite, the ACP snapshot scenario
subagent-continuable-inheritance (fails without the fix), the
continuable policy-inheritance Agent Note, and the seam-level README
contract, with bilingual counterparts.
Fixes#1692
The auto-merge kept recompose on the removed open re-link. It now moves
the binding this roster kept from the agent's mount; an agent that never
composed one has nothing to re-link, so the switch is its first bind —
exactly a mount — and once bound only the kept binding can move it.
setScopeParent could re-link any key from anywhere, leaving the
blank-session-only recompose rule entirely to caller discipline. The
relation now binds once — a second bind throws — and re-linking exists
only on the ScopeParentBinding returned to the original binder, the
private-capability shape the package conventions prescribe for a
single-caller operation. The preset roster keeps each composed agent's
binding in a WeakMap keyed by the agent, making it the sole authority
that can move an agent between standing compositions; the blank-session
contract itself stays with the gateway, which alone can see what a
session logged.
A max-tokens step that assembled only tool-call blocks appends an
EMPTY-content assistant/message (the usage host). Three consumers each
hand-rolled output selection and all let it erase the child's real
answer: the in-process readResult and the Activation subagent/end
capture took the last message unfiltered, and the SDK backend let any
message beat its streamed-text fallback; the in-process driver also had
no streamed-text fallback for cancelled turns.
dsh-subagent now owns the canonical rule in src/assistant-output.ts
(last non-empty assistant message, else the accumulated text-delta
stream) and all three consumers apply it. Regression tests in all three
packages fail under the previous selections.
Closes#1514