Skip to content

feat(native-chat): the chat strip and the sidebar read the host's child records - #22614

Open
brennanb2025 wants to merge 11 commits into
brennanb2025/c7-shared-subagent-rowfrom
brennanb2025/c5-read-child-records
Open

brennanb2025 wants to merge 11 commits into
brennanb2025/c7-shared-subagent-rowfrom
brennanb2025/c5-read-child-records

Conversation

@brennanb2025

@brennanb2025 brennanb2025 commented Sep 24, 2026 •

Copy link
Copy Markdown
Contributor
Files Added Deleted Net
Test 25 $\color{#1a7f37}{\Huge{\mathbf{+}}}$​1639 $\color{#cf222e}{\Huge{\mathbf{−}}}$​254 $\color{#1a7f37}{\Huge{\mathbf{+}}}$​1385
Prod 54 $\color{#1a7f37}{\Huge{\mathbf{+}}}$​892 $\color{#cf222e}{\Huge{\mathbf{−}}}$​246 $\color{#1a7f37}{\Huge{\mathbf{+}}}$​646

ELI5

A chat session's subagents show up in two places: as indented rows under the session in the sidebar, and as rows in the chat's background-task strip. Until now both were drawn from a task list the provider adapter published, and the sidebar's copy was converted again in the renderer, losing facts on the way. The previous PRs in this stack gave the host one record per child (what it is doing, how it ended, when) and one row model for both surfaces. This PR is the switch: the host now sends those records to both surfaces, both surfaces draw from them, and the old adapter-to-client task feed is removed. A finished subagent now stays listed with how it ended until the parent's next turn, a subagent whose shell is still running reads "Monitoring background tasks" in both places, and when Orca loses contact with the host both surfaces stop vouching for the child in the same words.

Stacked on #22565 (the shared row, head 3422ccaf4c), which is stacked on #22553 → #22536 → #22521. The base branch is brennanb2025/c7-shared-subagent-row.

Merge order

What Changed

Delivery: the host publishes its child records (new optional fields only)

  • Status summary (agentSession.subscribeStatus, read by every session list): gains children?: AgentChildWorkView[], the host's records for the session, live and recently settled, read from the canonical store through the status sink the session's row landed in (StructuredAgentSessionStatusSink.readChildWork → AgentHookServer.getStructuredChildWorkViews, wired at both entry points, desktop main and orcad). backgroundTasks is still published, now derived from the same views (live rows only, as is subagents: an older client never folds a settled child into the parent's state, so a settled row shows children with its records and an empty subagents).
  • Chat strip channel (agentSession.subscribe / history backgroundTasks): gains children?, and tasks / settledTasks are derived from the same views. The channel no longer reads the adapter's task DTO: it reads the records, and it republishes when a child-work ingest changed them (an unchanged roster sends nothing).
  • Parent row: ingestStructuredStatus folds child liveness from canonicalStatusStore.getChildren(subject), not from the summary's task list. structuredAgentSessionAgentStatus takes childWork (records or views) instead of backgroundTasks; the fold itself is untouched. After every child-work ingest the feed re-reads the records and republishes, which is what re-folds the parent row.
  • Retention: a finished child stays listed, with its outcome, until the session's own next turn at the latest. It goes earlier when the provider ends the session (its session-ended evidence removes every record, e.g. a provider exit or the post-exit release), when the host closes or releases the session (the store forgets the row and its records, and the retained summary drops them), or when it is the oldest past the 32-settled-record cap. The feed watches the newest root turn id in the journal (a subagent's turn never counts) and, when a new one appears, sends a new turn-started evidence edge; the reducer removes settled records then, keeping any that still owns live work (the same exclusion as the 32-record cap). No timer.
  • A closed session's retained summary drops children / backgroundTasks: the store forgets them with the row.

Readers

  • Renderer bridge (StructuredAgentSessionStatusBridge): copies summary.children verbatim onto the row as entry.children (decoded permissively, below). subagents is derived from those views through the shared projection; the renderer's own conversion of the task DTO remains only for an older host that publishes no views. Liveness for the row reads the views.
  • Store: the live-entry builder carries children, reusing the previous array when a summary repeats it (liveEntryChildFields), and a done row's retention change now counts children.
  • Chat strip: NativeChatStructuredSessionStatus passes the channel's views as childViews and childRowContext, built from the session's own status row exactly as the sidebar builds it: agentChildRowContextForParent(entry, isExplicitAgentStatusFresh(entry, now, AGENT_STATUS_STALE_AFTER_MS)), where the row's subagentObservation is the status feed's getSessionObservation. So a lost or stale parent reads the same on both surfaces. A roster of finished children is shown but no longer counts as live work (the monitoring indicator and the conversation-command block read live views only).
  • Full sidebar mode still renders child rows through the literal CLI row component, fed by the model (unchanged from refactor(sidebar): one subagent row for CLI and structured children, shared with the chat strip #22565).

Writer removed

  • The adapters' onBackgroundTasksChanged publication path is deleted end to end (Claude and Codex adapters, their close/teardown funnels, the runtime wiring, host.publishBackgroundTaskState). The strip, the summary and conversation-command admission no longer read the provider tracker's roster, and backgroundTaskState is no longer on the adapter contract (see Architecture review).
  • The renderer no longer converts a new host's task DTO into child rows.
  • New adapter method backgroundTaskStops(sessionId) supplies the provider's stop capability (Claude: targeted; Codex: none), which the DTO used to carry.

Wire and permissive decoding

  • decodeAgentChildWorkViews (src/shared/agent-status-child-work-view-wire.ts) is the client-side reader: unknown keys are ignored; an unknown kind degrades to unknown, state to unverifiable, outcome to unknown, operation.basis to reported, membership to a live row that asserts nothing; stoppable must be true to offer a stop; only a row with no id, clocks or fence is dropped. The strict record codec stays host-only.
  • Codex id mapping: a Codex agent's view providerId is its bare thread id (the journal linkage key). Every legacy shape derived from views maps it back to codex-agent:<thread> (structuredChildWorkLegacyTasks / structuredChildWorkLegacySubagents), so old clients keep their row ids. On a new client the view path keys rows by childWorkId, so row keys change once at the switch; Codex agents are not stoppable, so no stop path sees the difference.
  • New capability agent-session.background-task-child-views.v1, advertised by the desktop renderer, remote and web clients. At the RPC boundary (structured-agent-session-background-task-capability.ts), a reader that does not advertise it never receives children and never receives a roster whose rows are all settled (it would read that as live work: animate the indicator and refuse conversation commands); it gets its pre-feature view. The existing unstoppable-row withholding now applies only when the host offers a targeted stop (a reader draws per-row Stop only then), so a stop-only reader keeps Codex rows.

Status-summary churn (measured)

The summary goes to every session list, remote ones included, so it compares child views with usage stripped and the child's evidence clocks (observedAt, operation.observedAt) tolerated up to 60 s (SUMMARY_CHILD_CLOCK_TOLERANCE_MS, the precision of the "No update in Nm" reading) — the same shape as the existing totalTokens exclusion. The strip channel still carries every tick. Measured with a throwaway harness (real Claude adapter over agentSession.*, real hook server as the sink), 5 background children, 20 rounds of task_progress each (100 frames):

phase summary broadcasts, base (3422ccaf4c) summary broadcasts, this PR strip frames, both
spawn 5 children 10 10 5
100 progress ticks, tool name unchanged 0 5 100
100 progress ticks, tool name changes every 5 ticks per child 0 24 100
100 progress ticks, tool name changes on most ticks 0 53 100
5 completions 5 5 5

A tick that changes only usage or a clock inside 60 s republishes nothing; the 5 heartbeat-only republishes are one per child when its clock crossed 60 s. What does republish is a child's current tool changing — the fact the sidebar row now shows. The base republished 0 because the task DTO carried no operation at all.

Why

A child had up to three materializations: the adapter's task DTO (strip), a renderer conversion of it (sidebar), and the hook-lane roster (CLI). Each hop dropped facts (monitoring collapsed to working, failed became blocked, the running tool was discarded). #22521–#22553 built one host record per child; #22565 built one row model. Neither changes anything until both surfaces read the record, and the old writer is gone — this PR does both at once so there is never a window where the two surfaces read different sources.

Alternatives considered:

  • Lift the main-window filters and let the hook server's canonical row reach the renderer, deleting the bridge. Not done here: a remote runtime's rows do not reach the local hook server, so the bridge (which subscribes to whichever host owns the worktree) is still the only renderer path for remote sessions. The host now authors the child content; the bridge copies it verbatim. The filters and their pinning test are unchanged (see docs/reference/agent-status-store.md).
  • A separate sibling field on the subscribe frames instead of children inside the roster. Rejected: more frame surface for the same effect; the existing RPC-boundary projection already hands older readers their pre-feature roster.
  • Keep republishing the summary on every tick. Rejected by measurement risk (plan risk 10); per-tick freshness belongs to the channel only an open chat subscribes to.
  • Retention on a clock, or by comparing settle times with the turn's start. Rejected: a timer is excluded by decision, and turn startedAt is provider time. The journal's root turn identity is the parent's own fact.

Linked Issue

None — part of the structured chat status/orchestration program.

Visual Proof

Captured with the $electron skill: a dev instance of this branch launched with ORCA_BACKGROUND_LAUNCH=1 on a throwaway profile, Playwright over CDP, CDP screenshots of the hidden window; no window was shown and no OS input was used. This is a renderer-seeded fixture, not a live provider session: the session's status row was written through the real setAgentStatus path with the payload the bridge writes (views, the derived subagents, the observation), using this branch's shared modules, and the real NativeChatStructuredSessionStatus (this branch's version, reading the parent's verdict from the store by pane key) was mounted over a roster built by the real structuredSessionBackgroundTasksView. Children: working with Bash: npm test -- parser; finished while its npm run dev shell runs; finished; failed.

Fixture screenshots (sidebar card and strip, before and after losing contact with the host):

  • 01-live-window.png, 02-live-sidebar-card.png, 03-live-strip.png: sidebar and strip agree row for row — Summarize the grammar - Grammar has 42 productions (finished, green), Monitoring background tasks - Start the dev server (yellow monitoring icon; strip nests npm run dev under it), Fuzz the tokenizer - Exit code 1: fuzz target crashed (failed), Run the parser tests - Bash: npm test -- parser (working); strip header "4 agents — 1 working, 1 monitoring, 1 blocked, 1 done".
  • 04–06-lost-transport-*.png: the same session after the status feed reports the host unverifiable. Both surfaces: Start the dev server - No update in 3m, Run the parser tests - No update in 0m; settled outcomes unchanged.
Sidebar card Chat strip
Live 02-live-sidebar-card 03-live-strip
Lost transport 05-lost-transport-sidebar-card 06-lost-transport-strip

Live run (real providers)

A design-check pass ran this head against real provider CLIs: $electron with Playwright over CDP, a hidden dev instance (ORCA_BACKGROUND_LAUNCH=1, throwaway profile, no OS input), three sessions.

  • Claude (Opus), one Task subagent. While it ran, the strip read 1 agent — working with the child's own current tool: Run sleep and echo · Bash: sleep 40; echo CHILD_DONE. The subagent moved its sleep to the background, and that shell appeared as a command owned by the agent. When the subagent returned, the agent settled succeeded and its killed background shell settled cancelled (shown with the interrupted dot, by design; see Review). The sidebar showed the session row with an indented Run sleep and echo - general-purpose child row, the same name and state as the strip. Both stayed listed with their outcomes after the turn ended, and on the next user message both surfaces cleared within one 3 s poll.
  • Codex multi-agent v2 (GPT-5.6-Terra). sleep_child read working · Agent, with its shell /bin/zsh -lc 'sleep 45; echo CHILD_DONE' nested under it on its own clock, then settled as 1 agent — done / sleep_child · CHILD_REPLY … ended 0s ago. The sidebar showed the same child row.
  • Codex default mode (GPT-5.5). No agent row on either surface (see Review; feat(native-chat): register Codex default-mode helpers as subagents #22619 adds it). The helper's shell appeared as an unowned command row, 1 shell command — 18s, then — done, retained. The raw codex item:collabAgentToolCall rows in the transcript are pre-existing and are feat(native-chat): register Codex default-mode helpers as subagents #22619's scope, not this PR's.
Arm While running After
Claude, strip claude1-03 claude1-05
Claude, sidebar, then next turn claude1-sidebar-expanded claude1-next-turn-01
Codex v2 c5-v2try1-08 c5-v2try1-18
Codex default mode c5-deftry1-05 c5-deftry1-11

Not covered live: Linux, Windows, SSH and mobile; Claude waiting/blocked children (not produced; see Review); a merged tree with #22619.

Testing

All run with env -u ORCA_STRUCTURED_SESSION, macOS.

  • pnpm tc:node, tc:web, tc:cli: exit 0.
  • New tests:
    • src/main/runtime/structured-child-records-switch.test.ts — end to end on a host with no renderer: the production runtime, a Codex session, and a real AgentHookServer as the status sink. At each step the status summary's views and the strip channel's views carry the same children (kind, label, state, membership, outcome, current tool, owner), with literal expectations (deliberately not compared against the tracker's DTO, which shares the executions with the producer — feat(native-chat): Codex sessions write their subagents into the host status store #22553's caveat). Also: the parent row reads working while the subagent runs after the parent's turn, working/monitoring when only the child's dev server remains, done after it exits; an older client's task list holds live work only; finished children stay until the next root turn, then leave both surfaces; provider exit removes the records.
    • src/renderer/src/components/native-chat/structured-child-records-switch.test.tsx — wired renderer parity (real bridge, real store, real NativeChatStructuredSessionStatus, real sidebar child rows): views copied verbatim, legacy subagents derived with the Codex id, parent fold from views; entry identity reused on a repeated summary; and one child reads the same on both surfaces live, after the status stream drops (lost transport), and when the parent row is stale.
    • structured-agent-session-status-feed-children.test.ts (7), agent-status-child-work-view-wire.test.ts (5), structured-session-background-tasks-view.test.ts (3), retention in the reducer suite, child-view gating in the RPC capability suite (+4), records-not-summary in the ingest suite.
  • Existing suites rewritten for the removed path (the adapter-publication tests now assert the evidence that reaches the records; the ingest tests seed the store): Claude close, Codex background tasks, both producer ordering tests, the status-feed tests, the ingest tests.
  • Ablations (deleting or neutering one mechanism, exact-once edit, restored from HEAD):
Deleted Red
Strip channel re-pointed at the adapter's DTO 1 (end-to-end agreement)
Ingest liveness read from the summary's task list 4 (ingest suite + end-to-end parent row)
Retention call in the feed 2 (feed retention + end-to-end)
turn-started reducer arm 2 (reducer + end-to-end)
Old-reader gate at the RPC boundary 2
Summary clock tolerance 1
Bridge copying views onto the row 4 (all wired renderer tests)
Strip's parent verdict (childRowContext) 2 (lost and stale parity)
Finished roster read as live work 1
Codex legacy id mapping 5
Closed session keeps children 1
Summary republish after child work 1
Settled rows leaking into the summary's task list (substitution) 2
Decoder rejecting an unknown arm 1
Strip channel republish on ingest 1
  • Regression: src/main/{claude,codex,native-chat,agent-hooks,runtime,ipc,orcad,startup}, src/shared, renderer native-chat, sidebar, dashboard, store, runtime, lib, web, and refactor(sidebar): one subagent row for CLI and structured children, shared with the chat strip #22565's parity test: 4,458 files, 44,356 tests: 44,141 passed, 1 expected-fail, 208 skipped, 6 failed. 5 are the real-Claude-binary files (claude-structured-real-cli, claude-tui-resume-real-binary), which fail identically on the base per feat(native-chat): Claude sessions write their subagents into the host status store #22536/feat(native-chat): Codex sessions write their subagents into the host status store #22553. The 6th (web-runtime-client capability list) was this PR's and is fixed, as is the CLI transport list (8/8). Timing-only failures seen once under load (refusal-retry, WorktreeCard.compact-hover, proven-dead-retry) pass alone.

  • Renderer-root tests (src/renderer/src/*.test.ts, missing from the sweep above, found in review): at the first pushed head renderer-node-builtin-boundary.test.ts was red (1 of 2). Its raw-text scan read the word "imports" in the header comment of the new agent-status-child-work-view-wire.ts as the start of the import block, turning the type-only import type {…} from './agent-status-child-work' into a value edge that reaches node:crypto. Fixed by rewording that comment (commit 9acc0c1d79): now 4 files, 36/36; with the old header restored the boundary test is red again (positive control).

  • Cross-version against the newest release tag: cross-version-agent-session-wire 14/14 and cross-version-terminal-wire 10/10, each run alone. Running the whole folder at once timed out extracting several baselines in parallel (tar -xf timed out after 45000ms), an environment limit; the two suites that cover this surface pass alone.

  • Quality: check:code-quality:changed against the base: 0 new findings in every category. Full oxlint on every changed TS/TSX file: 0 (it did report max-lines on two files during development, both split). audit:anti-slop exit 0. No new docs; pnpm-lock.yaml absent from the range; no new i18n strings.

  • I manually tested these changes locally

  • Automated tests added/updated, or explained why not below

Review

Not verified:

  • Live coverage is limited to three sessions on macOS (Claude Opus, Codex v2, Codex default mode; see Visual Proof → Live run). Beyond those, the host path is proven by the end-to-end test with a scripted Codex app-server and a real hook server.
  • Codex default multi-agent mode shows the helper's shell but no agent row until feat(native-chat): register Codex default-mode helpers as subagents #22619. Codex 0.155's default mode (GPT-5.5; the model picks the mode) emits collabAgentToolCall and no subAgentActivity, and feat(native-chat): Codex sessions write their subagents into the host status store #22553 registers agent children only from the latter — so no agent child record exists: no agent row in the strip or the sidebar, and the strip header counts no agents. The helper's shell still reaches the records as an unowned command row (1 shell command — …), retained like any settled child. Verified live with GPT-5.5 at this head. Codex row parity here holds for subAgentActivity (v2) sessions, verified live with GPT-5.6-Terra; default-mode agent rows arrive with feat(native-chat): register Codex default-mode helpers as subagents #22619, whose records flow through the same store read this PR's surfaces use.
  • Raw collab rows in the transcript are not this PR's surface. A live Codex session at this head still renders item:collabAgentToolCall as raw provider status rows in the transcript, in both multi-agent modes. The transcript is not this PR's surface; feat(native-chat): register Codex default-mode helpers as subagents #22619 renders those calls as named tool rows.
  • Waiting and blocked Claude children are not produced — deferral, not faked. The Claude lane (feat(native-chat): Claude sessions write their subagents into the host status store #22536) never links an SDK permission request to its subagent (untraced), and a live task_updated.patch.error is not mapped to blocked, so a Claude child waiting on approval reads working in both surfaces (the pane-level wait still shows on the parent). Codex produces waiting from the child thread's status (feat(native-chat): Codex sessions write their subagents into the host status store #22553) and both surfaces render it. No state parity is claimed for Claude waiting/blocked. Producing them belongs to the Claude producer lane and is deferred to a follow-up PR (not open yet), gated on a captured transcript that proves the attribution.
  • A cancelled child reads "interrupted", by design. refactor(sidebar): one subagent row for CLI and structured children, shared with the chat strip #22565 maps a settled cancelled outcome to the interrupted dot, as a CLI row does. In the live Claude run, the background shell killed when its subagent returned shows that dot; it is the recorded outcome, not a defect.
  • Mobile tests not run (Expo is not installed here). Mobile reads no background-task roster or child rows (it only imports reducer types); it now advertises the child-views capability via the shared remote list, which it never reads.
  • No Linux, Windows or SSH run. SSH/remote correctness rests on the design: records live on the execution host; clients mirror them over the existing feeds; contact loss is the client's unverifiable, never a record write.
  • The provider tracker still resolves which ids a stop reaches (Claude: claude-structured-control-actions.ts reads the tracker's stoppable ids). Conversation-command admission no longer reads it (see Architecture review).
  • Strip before the parent row lands: the strip's children read the verdict of the session's status row. In the moment before that row exists in the renderer store (the status feed has not delivered it yet), there is no verdict, so the strip shows the claims as reported, the same as its behaviour before this PR. Not changed: deciding a verdict with no row would be a second rule beside the row's own.
  • The main-window filters that keep hook-server structured rows away from the renderer stay, and so does their test: the bridge remains the one renderer writer of a structured row (it is also the only path for remote runtimes).
  • The mainAgent lane PR (feat(agent-status): publish the main agent's own state beside the combined row state #22452) is not on main; this is implemented against today's structuredAgentSessionAgentStatus inputs and changes only its liveness input. The fold, mainAgent, and their consumers are untouched; rebase if feat(agent-status): publish the main agent's own state beside the combined row state #22452 lands first.
  • childrenRevision from the plan is not added: equality is derived from the views, so there is no second counter to keep in step.

Architecture review

An architecture pass on this PR asked for four changes. What changed and why:

  • One owner for "live background work blocks a conversation command". The host used to refuse /clear, /compact and rewind on the provider tracker's own roster, while the strip and the client-side block read the host's child records. Two rule sets fold the same frames, so if they ever drift, the host could refuse with "Stop background tasks" while the strip shows no row and no Stop button. Admission now reads the same records through the same read as the strip (readChildWork), uses the same liveness fold as the strip's monitoring indicator (a finished row blocks nothing), and asks for a stop only when the strip actually renders one: a per-row stop on a live, stoppable, addressable row when the provider targets stops, else its single untargeted stop. Otherwise it says "Wait for background tasks to finish". The per-row rule is now one shared function used by both the row model and admission. backgroundTaskState is removed from the adapter contract and the router, so no host code can read the tracker's roster; the concrete Claude and Codex adapters keep it only for the tests that check the tracker against the records. Tests: structured-conversation-command-admission.test.ts (7) and a host-level structured-conversation-command-child-work.test.ts, where the adapter's roster claims live work throughout and the refusal still follows only what the strip lists. Ablations at the final head: removing the records check fails 5 of 8; restoring the tracker-roster read fails 3 of 8; asking for a stop based only on the provider's stop capability fails 1 of 8.
  • When the legacy child shapes are deleted. The header of structured-agent-session-child-work-legacy.ts now gives the condition. Once MIN_COMPATIBLE_RUNTIME_CLIENT_VERSION goes above the protocol version in force when agent-session.background-task-child-views.v1 shipped (3), every client the host accepts reads children. At that point the summary's backgroundTasks derivation, the channel's tasks/settledTasks, the Codex codex-agent: id remap and the capability gate's withoutChildViews are deleted together. The summary derivation and the gate both point to that header.
  • Retention wording. The retention bullet above now lists the earlier deaths. Keeping settled children in the closed session's retained summary was considered and not done: on close the store forgets the records, so the strip would show nothing while the session list still showed them. On provider exit, session-ended removes the records before close, so retaining them at close would not keep them in that case anyway.
  • Clock tolerance. SUMMARY_CHILD_CLOCK_TOLERANCE_MS now states its rule. Anything that reads the summary's child clocks must show staleness no more precisely than that (today it shows whole minutes). A reader that needs finer freshness uses the background-task channel. The equality compares children by position, so it relies on the store returning them in insertion order.

Recorded deviations:

  • Stops still resolve through the provider tracker. A targeted or untargeted stop reaches only the ids in the tracker's roster. If the tracker and the records ever drift, the strip could offer a Stop that does nothing. Moving stop resolution onto the records (the host passes the live, stoppable provider ids from the records) changes the adapter's stop contract and is not in this PR.
  • Settled children are not kept across a restart. The store is in memory. After an app restart, only the transcript still shows what a finished fleet did. The records can be rebuilt from the journal in principle, but no restart path rebuilds them. This is accepted, not a match.

Agent skill upstream boundary

  • Not applicable, or this change follows docs/reference/agent-skill-sharing-upstream-boundary.md and copies or mechanically translates no upstream skill-installer source, tests, fixtures, registry entries, path tables, comments, or documentation.

Notes

  • Mixed versions. New host → old client: old clients ignore children; they keep reading backgroundTasks / tasks / settledTasks / subagents, derived from the views (live rows in the summary; Codex agents keep codex-agent:<thread>), never a settled-only roster, never the views. New client → old host: no children anywhere, so the bridge converts the old task list as before and the strip reads tasks; pixels are the refactor(sidebar): one subagent row for CLI and structured children, shared with the chat strip #22565 fallback. Nothing new is persisted (structured rows are never saved). tests/e2e/cross-version-wire exercises the agent-session surface in both skew directions.
  • Folder workspaces / SSH / WSL. The subject carries the full execution scope; the store read is keyed by the subject the row landed under. Nothing assumes a git worktree.
  • Performance. Per summary build: one store read of the session's children and their aliases. Summary republishes measured above.
  • One tracked reference doc sentence updated (agent-status-store.md: the structured row folds the store's child records).

Checklist

  • This PR is small and focused
  • I explained what changed and why (ELI5, the user-facing before/after, the mechanism, and why over the alternatives)
  • Before/after screenshots or videos attached for UI changes, or N/A with reason
  • Self-reviewed for correctness, security, and performance
  • Cross-platform, SSH/remote, and path/shortcut impact considered (or N/A)
  • pnpm lint, pnpm typecheck, pnpm test, and pnpm build pass (or CI will cover; local preferred)

@pullfrog pullfrog Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

✅ No new issues found.

Reviewed changes

  • Host publishes its child records. The status summary gains optional children?: AgentChildWorkView[], with backgroundTasks now derived from the same views (live rows only); the chat strip channel reads readChildWork from the host's status sink instead of the adapter's task DTO, republishes only on a changed fingerprint, and sends an explicit null once a live provider has no rows. The parent row ingests canonicalStatusStore.getChildren(subject) rather than the summary's task list.
  • Retention without a timer. The status feed tracks the newest root turn id in the journal (subagent turns excluded) and, when it changes, admits a new turn-started evidence edge; the reducer removes settled children that no longer own live work, so a finished child stays listed with its outcome until the session's next turn.
  • Both readers draw from the records. The renderer bridge decodes summary.children permissively, copies them onto the row verbatim, derives the legacy subagents roster from them, and reuses the child array across repeated summaries; the strip consumes the same views plus a childRowContext built exactly as the sidebar builds it, so a lost or stale parent reads the same on both surfaces.
  • The old writer is gone. onBackgroundTasksChanged is deleted end to end (Claude/Codex adapters, close/teardown funnels, runtime wiring, host.publishBackgroundTaskState); a new adapter seam backgroundTaskStops supplies the provider's stop capability for provider-side decisions only.
  • Wire compatibility. New capability agent-session.background-task-child-views.v1 is advertised by the shared remote list and the desktop renderer; the RPC boundary withholds children and settled-only rosters from a reader that does not advertise it. decodeAgentChildWorkViews ignores unknown keys and degrades unknown enum arms rather than dropping rows, and Codex view ids map back to codex-agent:<thread> for legacy shapes.

I read the complete diff (all 66 files) and ran the new and rewritten suites touching this surface — shared view-wire/reconciliation, the status-feed and feed-children suites, the end-to-end structured-child-records-switch, the RPC capability suite, and the renderer switch/background-tasks-view tests — all green. The abstraction is coherent: one store read feeds one projection, and the legacy shapes are derived from it rather than maintained in parallel, which is the failure mode this PR exists to remove. No blocking or non-blocking findings.

Pullfrog  | View workflow run | Using DeepSeek Flash (free via Pullfrog for OSS) | 𝕏

@pullfrog pullfrog Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

✅ No new issues found.

Reviewed changes

This run reviewed the delta since the previous Pullfrog review. The branch was rebased from base 3422ccaf4c onto 78e73c646a, which now carries the mainAgent lane; git range-diff shows the patch content is otherwise unchanged, so the re-review is the rebase integration plus the header-comment fix.

  • Rebased onto the mainAgent lane. structuredAgentSessionAgentStatus now takes childWork (records or views) instead of the summary's backgroundTasks, while returning the base's mainAgent. The only PR edit to that function is the input swap, so the host's parent row, the renderer bridge, and the CLI still fold one liveness source.
  • Held-open verdict preserved. The ingest and bridge date a child-held row with isAgentStatusHeldOpenByChildWork({ state, mainAgent }), which is exactly the removed fromChildWork (mainAgent.state === leadState && state !== 'done'); the parent row's freshness clock is unchanged.
  • Readers still draw from the views. The bridge copies summary.children onto the row and derives the legacy roster from them; the live-entry builder reuses the child array across repeated summaries; the strip counts only live views toward the monitoring indicator.
  • Wire and retention unchanged. The agent-session.background-task-child-views.v1 capability, the RPC-boundary withholding of children and settled-only rosters, the turn-started retention edge, and the header-comment reword all stand as previously reviewed.

I read the complete diff end to end, plus the base versions of the files the rebase touched, and traced the fromChildWork → isAgentStatusHeldOpenByChildWork equivalence. No new findings.

Pullfrog  | View workflow run | Using DeepSeek Flash (free via Pullfrog for OSS) | 𝕏

@pullfrog pullfrog Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

✅ No new issues found.

Reviewed changes

This run reviewed the delta since the previous pullfrog review (8dac1b1 → bb15e1c): two commits that move conversation-command admission off the provider adapter's own task roster and onto the host's child records. One non-code note: the PR description's Review section still lists conversation-command admission among the adapter-tracker readers, which this delta no longer holds.

  • Admission reads the strip's records. conversationCommandBlocked now takes childWork and folds it through agentChildWorkLiveness, the same fold the strip's monitoring indicator uses, so a settled roster blocks nothing and only work the strip lists refuses a command.
  • One owner for the stop-offer rule. The new shared agentChildWorkViewOffersStop is used by both the row model's canStop and admission's stripOffersStop, and stripOffersStop mirrors the strip's own controls: a per-row stop where supportsTaskStop, otherwise the single untargeted stop where supportsStopAll.
  • readChildWork threaded to the decision. StructuredAgentSessionMutationContext gains readChildWork (wired from clientDelivery.readChildWork); both runStructuredConversationCommand and rewindStructuredAgentSession pass it.
  • Adapter interface trimmed. backgroundTaskState leaves StructuredAgentSessionAdapter and the router; the concrete Claude/Codex adapters keep it as a test-only accessor for holding the tracker roster beside the records.
  • Tests. A new end-to-end test proves a tracker roster that claims live work refuses nothing while the strip is empty, refuses Stop… for a live record, and admits once the record settles; the unit suite covers the targeted / untargeted / no-stop matrix with the tracker present throughout.
  • Comments only. The legacy death condition, the summary clock-tolerance invariant, and the retention lifecycles.

I read the full diff and the delta end to end, traced stripOffersStop against the strip's render gating (supportsStop / supportsStopAll and the per-row stopId), confirmed agentChildWorkViewOffersStop is exactly the old canStop predicate, and ran the admission unit suite (7/7) and the new integration suite (1/1) green, plus pnpm tc:node and oxlint on the changed files.

Pullfrog  | View workflow run | Using DeepSeek Flash (free via Pullfrog for OSS) | 𝕏

@brennanb2025
brennanb2025 force-pushed the brennanb2025/c7-shared-subagent-row branch from b4cf0d2 to 5406ab2 Compare September 24, 2026 21:38
@brennanb2025
brennanb2025 force-pushed the brennanb2025/c5-read-child-records branch from bb15e1c to 254db2d Compare September 24, 2026 22:08
@brennanb2025
brennanb2025 force-pushed the brennanb2025/c7-shared-subagent-row branch from 5406ab2 to 359483b Compare September 25, 2026 00:02
…mary and the chat strip

The status summary and the background-task channel now read a session's child
records from the host's canonical store, through the status sink its row
landed in, and derive the legacy task and subagent shapes from the same views.
The parent row folds its child-work liveness from those records at ingest,
not from the summary's task list. The adapters no longer push their task DTO
to clients: the onBackgroundTasksChanged path is gone, and a child-work ingest
is what republishes both the summary and the strip. Finished children stay
listed until the session's own next turn starts. A reader that predates child
views never receives a roster whose rows are all settled.
… its parent's verdict

The strip's roster now renders from the child views its channel carries, and
passes the verdict the session's own status row gives its children, built the
way the sidebar builds it (the row's freshness and the status feed's
observation). So one child reads the same in the strip and the sidebar, live,
after the transport drops, and once the row goes stale. A roster of finished
children stays shown until the next turn but no longer animates the monitoring
indicator or blocks conversation commands.

Tests: an end-to-end run on a host with no renderer (a real hook server as the
status sink) shows the summary and the strip channel carrying the same records
at every step, the parent row folded from them, retention, and an older
client's task list holding live work only; a wired renderer test shows both
surfaces agree when live, lost and stale.
…s its live roster, a finished roster holds nothing open

- The view decoder ignores unknown keys, degrades unknown kinds, states,
  outcomes and memberships, and drops only rows it cannot identify.
- At the RPC boundary a reader that predates child views gets no strip for a
  roster of finished children and never the views themselves; a stop-only
  reader keeps rows whose host offers no targeted stop.
- The strip shows finished children without reading them as live work.
- The row keeps its child list's identity when a summary repeats it.
…lue import to the renderer boundary

The renderer node-builtin boundary test scans raw text, so a header comment
that said "imports" ahead of the import block turned the type-only import of
agent-status-child-work into a value edge that reaches node:crypto.
…ts own module

The status feed crossed the file-size limit once the summary gained the main agent's turn
outcome beside the child views. Which summary changes reach every session list now lives in
structured-agent-session-status-summary-equality.ts.
A conversation command was refused on the provider tracker's own roster
while the strip read the host's child records, so a drift between the two
rule sets could refuse /clear with a stop instruction the strip had no
button for. Admission now reads the same records through the same read as
the strip, uses the strip's liveness fold, and asks for a stop only when
the strip renders one. The adapter contract no longer exposes the tracker
roster, so no host decision can read it.

Also records when the legacy child shapes die, every earlier death of a
settled child, and the display-precision invariant behind the summary's
clock tolerance.
@brennanb2025
brennanb2025 force-pushed the brennanb2025/c5-read-child-records branch from 254db2d to 20bff9f Compare September 25, 2026 01:06

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant