# Architecture `dsh-subagent-inspector` is one DSH bundle with a Host observer and one Web client module. It is deliberately read-only: no tool definition, provider, scheduler hook, permission handler, Session mutation, or telemetry transport is registered. ## Data flow ```text Session / agent / subagent events │ ▼ Host normalization + folds ── ctx.sessionPersistence.inspect() │ ctx.subagents.listDescendants() ├── GET /snapshot ├── GET /history └── GET /stream (SSE) │ ▼ conversation.session.header.actions │ ▼ Read-only responsive drawer ``` The snapshot is authoritative for tree membership. Each SSE connection receives a fresh snapshot first, then normalized frames. The client deduplicates timeline events by `sessionId + seq`, so reconnect replay cannot create duplicate rows. ## Host The Host listens to `session/created`, `session/event`, `session/disposed`, `agent/created`, `agent/status`, and `agent/disposed`. Each live parent agent also receives scoped `subagent/start` and `subagent/end` listeners, which makes lifecycle-only remote providers observable without pretending that they own a local Session. `ctx.subagents.listDescendants(rootSessionId)` supplies the durable tree. `ctx.sessionPersistence.inspect(sessionId)` supplies the canonical log. For forked children, events below `header.seedLength` belong to the inherited seed and are discarded before normalization, duration folding, usage folding, or pagination. Known Session events are normalized into `InspectorTimelineEvent`; unknown event kinds return `null` and are not forwarded. Raw assistant chunks remain observable until the final `assistant/message` for the same Turn/Step arrives. The client then replaces the chunk aggregate with the canonical final message. Tool calls and results are joined by `callId`. ## Protocol ### `InspectorAgentSummary` Contains identity, parent, depth, durable label and mode, execution activity and phase, child presence, timing, token usage, current tool, model, and terminal or diagnostic reason. A remote row carries only fields observed from the lifecycle pair. ### `InspectorTimelineEvent` Contains `sessionId`, durable `seq`, event time, optional Turn/Step coordinates, a normalized `kind`, and a JSON payload. Text and reasoning remain separate content block types. ### `InspectorStreamFrame` One of `snapshot`, `agent-upsert`, `session-event`, `status`, `removed`, or `error`. The root Session is used only as the membership anchor; its own events are never emitted as child timeline events. ## HTTP boundary All three routes are exact, same-origin, cache-disabled, and GET-only. Session IDs must match a conservative bounded identifier pattern. History access additionally proves that the requested Session is a local descendant of the selected root. No `Access-Control-Allow-Origin` header is set. The SSE endpoint holds one response open and emits a comment heartbeat every 15 seconds. Subscribers are removed when the request closes or the plugin is disposed. ## Client The client registers one additive action at `conversation.session.header.actions` and uses the native `SessionRuntime.openSubagent()` address when the user asks to open a local row. It does not replace the official `ui-subagent` lineage/navigation registration. All log values are rendered as React text nodes or `
` text. The plugin never uses `dangerouslySetInnerHTML`. Tool arguments and results use closed `
` elements by default. Below 760 px the drawer occupies the full viewport. ## Privacy and limitations - Complete local logs can contain sensitive prompts, source, commands, and tool results. - Same-origin is not an authentication layer. The routes inherit the Harness Web listener boundary. - A provider without a local Session cannot expose prompts, reasoning, tools, tokens, or results through the Session APIs, so the UI shows an explicit lifecycle placeholder. - The plugin shows recorded reasoning only; it never reconstructs hidden chain-of-thought.