Address ds-review-bot on #2127:
- assistant-output: the rule has ONE implementation, the incremental
AssistantOutputFold (push/pushText/collect); finalAssistantOutput folds a
complete suffix, the SDK backend folds notification events, and the ACP
backend folds raw chunk text into the same streamed fallback.
- subagent/end.lastAssistantMessage: 'no output' is encoded once — absent,
never [], on both lifecycle shapes (observeRun now omits empty output).
- tool-subagent: a non-completed foreground result stays isError but appends
the child's preserved partial text after the stop-reason headline.
- Authored keyless snapshot scenario subagent-max-tokens-partial pins the
assembled transcript: the child's committed log carries the usage-only
empty message and the parent's tool result carries the partial answer.
- Rule-boundary sentence (message wins over later streamed text) and the
consumer half recorded in the Agent Note; comments trimmed to pointers.
- E2BFileSystem.readBytes now short-circuits on the stat size before any
content transfer and streams the remote object, cancelling at the first
chunk past the cap, honoring the seam's bounded-buffering contract; the
fs-e2b README pair documents the new primitive.
- The tool-fs HMR test now proves the attachments-scoped registration:
disposing the store withdraws read_image while read/write/edit stay,
remounting restores it, and disposing the plugin withdraws everything.
- read_image caps reads at the smaller of maxImageBytes and
maxMessageImageBytes, records an absent observation for a missing
target like its sibling read, widens the mismatch remedy to cover
out-of-family formats, and renames the gate's parameter to
requestedPath; module/apply/registration JSDoc now match the shipped
composition. zh terminology aligned; the examples manifest keeps its
literal arrow.
Moving the task registry to the host plane put every preset's `tool-tasks`
listener on ONE `LocalTaskService`. `settle()` computes a single snapshot and
walks every registered listener with no scope filter, and it marks `reported`
only when a waiter is present — so a task settling without a waiter reached
each mount's listener with `reported` false and every one of them injected the
same completion into the same owner. Three shipped presets carry `tool-tasks`,
and a preset file edit adds a second generation of the same mount, so an agent
read N copies of one notice as model-visible durable context.
A mount now claims an owner only when the owner's scope chain reaches the
mount's own scope. An unscoped mount is the host-plane instance that serves
every agent, which keeps the TUI composition and every existing test intact.
Registry-side ownership was the alternative: mark `reported` once the first
listener claims it. It is wrong because `onTaskDone` is not a notice-only
seam — the `dsh-tasks` invariant companion registers a validating listener —
so first-claim-wins would silence observers that are not delivering anything.
The regression test mounts two scoped `tool-tasks` over one registry and
settles an unowned-wait task, which is the only path that reaches the notice
listeners at all: the shipped-composition e2e uses `wait: true`, and a waiter
marks `reported` before settlement, so that test structurally cannot cover it.
Also corrects the standing-mounts Agent Note, which still listed `tasks-local`
among the stateful PRESET plugins.
Refs #2141
The generated matrix under-reports client-face listeners because the program
is seeded from the host aggregate alone, so `session/preset-changed` lists
`ui-command` without `ui-skill` — the same shape as the existing
`connection/reset` and `models/changed` rows. Record it where the collector
lives, with what closing it actually takes.
The frame carried `agentPreset` for surfaces that label the session, but
nothing consumed it: `noteAgentPreset` ran only in the switching tab's RPC
callback, so a second connected client refetched its catalogs while its
session row — the header label's source, and the hero chip's no-op input —
kept the composition the session had replaced.
`SessionManager.handleHostEnvelope` now folds the frame like the other
session frames. Re-applying the switching tab's own frame is a no-op: the
merge lowers `blank` only and keeps the row's `updatedAt`.
The model reads a PNG/JPEG/WebP/GIF file, the bytes commit through the
durable attachment lifecycle, and the tool result carries the real
ImageBlock so the image enters context from the next request onward.
FileSystem gains a bounded readBytes primitive (local + E2B providers);
registration is conditional on the attachment store, and a strict
execution gate refuses routes that do not declare image input, so a
text route's durable history stays free of image blocks. llm-replay
models may declare inputModalities, letting keyless ACP snapshots pin
both the sha256-referenced success and the verbatim refusal.
Supersedes the withdrawn route-scoped design of PR #598; the decision
record is .agents/notes/implemented/feature/2026-08-10-minimal-read-image-tool.md.
- 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.