@gotgenes/pi-subagents 17.1.0 → 17.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -5,6 +5,27 @@ All notable changes to this project will be documented in this file.
5
5
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
6
6
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
7
 
8
+ ## [17.3.0](https://github.com/gotgenes/pi-packages/compare/pi-subagents-v17.2.0...pi-subagents-v17.3.0) (2026-06-22)
9
+
10
+
11
+ ### Features
12
+
13
+ * add /subagent-sessions read-only navigation command ([#445](https://github.com/gotgenes/pi-packages/issues/445)) ([341385c](https://github.com/gotgenes/pi-packages/commit/341385cf1f0c9f5ae1d3035c5b3b34bc3e636c92))
14
+ * add subagent session selection and live transcript source ([#445](https://github.com/gotgenes/pi-packages/issues/445)) ([7173647](https://github.com/gotgenes/pi-packages/commit/71736478c37af2c6ceaebc3ce0ee5a85e75ab1cb))
15
+ * add typed agentMessages accessor on subagent record ([#445](https://github.com/gotgenes/pi-packages/issues/445)) ([3bd49e3](https://github.com/gotgenes/pi-packages/commit/3bd49e37feea2cca52c706f7e765b8ae934ab9fe))
16
+
17
+ ## [17.2.0](https://github.com/gotgenes/pi-packages/compare/pi-subagents-v17.1.0...pi-subagents-v17.2.0) (2026-06-20)
18
+
19
+
20
+ ### Features
21
+
22
+ * shrink agent widget to background runs only ([#444](https://github.com/gotgenes/pi-packages/issues/444)) ([76463b4](https://github.com/gotgenes/pi-packages/commit/76463b47227961226dbce5efb70a71d596fe092e))
23
+
24
+
25
+ ### Documentation
26
+
27
+ * note background-only widget in README and roadmap ([#444](https://github.com/gotgenes/pi-packages/issues/444)) ([437332f](https://github.com/gotgenes/pi-packages/commit/437332fa4a1fbb2f0a0358495454847bc597ae13))
28
+
8
29
  ## [17.1.0](https://github.com/gotgenes/pi-packages/compare/pi-subagents-v17.0.1...pi-subagents-v17.1.0) (2026-06-20)
9
30
 
10
31
 
package/README.md CHANGED
@@ -61,7 +61,7 @@ Background agents return an ID immediately and notify you on completion.
61
61
 
62
62
  ## UI
63
63
 
64
- The extension renders a persistent widget above the editor showing all active agents:
64
+ The extension renders a persistent widget above the editor showing active background agents (foreground runs are rendered inline by the `subagent` tool's progress stream):
65
65
 
66
66
  ```text
67
67
  ● Agents
@@ -500,6 +500,9 @@ Everything else — permissions, worktree/workspace isolation, UI, telemetry —
500
500
 
501
501
  The rationale and the full reasoning chain that led here are recorded in [`docs/decisions/0002-extensions-on-a-minimal-core.md`](../decisions/0002-extensions-on-a-minimal-core.md).
502
502
 
503
+ A separate, longer-horizon note — [`client-server-opportunities.md`](./client-server-opportunities.md) — records what Pi's eventual client-server split (Mario Zechner's session-sync unification) would unlock for pi-subagents: viewing live subagent sessions, viewing suspended ones, and operators interacting with a subagent through an editor.
504
+ That architecture is not on the near-term roadmap; the note captures the opportunity so it is on record.
505
+
503
506
  ### Two extension surfaces
504
507
 
505
508
  Extensions attach through exactly two surfaces, distinguished by the direction of information flow.
@@ -966,7 +969,7 @@ Outcome: new `subagents-settings.ts` (~80 LOC) and focused command registered; `
966
969
 
967
970
  `Release: independent`
968
971
 
969
- ### Step 3 — Shrink widget to background agents only ([#444])
972
+ ### Step 3 — Shrink widget to background agents only ([#444])
970
973
 
971
974
  Smell: Category C (coupling) — the widget shows all agents including foreground ones, duplicating the `subagent` tool's inline `onUpdate` stream for foreground runs.
972
975
  Target files:
@@ -990,16 +993,28 @@ Smell: Category C (coupling) — the bespoke `ConversationViewer` re-implements
990
993
  This step adds the new surface alongside the existing viewer; it does not touch `agent-menu.ts`.
991
994
  Target files:
992
995
 
993
- - New `src/ui/session-navigator.ts` (or a widget gesture, per Step 1's answer to parallel-agent navigation) operator picks one of N background agents and views its persisted session via the spike-chosen mechanism, keyed on `record.outputFile`.
994
- - `src/index.ts` register the new navigation surface (command and/or widget gesture, per the spike).
996
+ - New `src/ui/session-navigator.ts` a flat command that lists any subagent with a live record or a persisted session file (foreground included, never background-filtered), lets the operator pick one, and renders that child's transcript read-only.
997
+ - New typed accessor on `Subagent`/`SubagentSession` returning `record.messages` as `AgentMessage[]` (the boundary currently widens it to `readonly unknown[]`).
998
+ - `src/index.ts` — register the new command; the background widget ([#444]) is an optional secondary selection gesture, not a dependency.
995
999
 
996
1000
  ADR-0004 Decision B: "Tell-Don't-Ask — hand Pi the session path; Pi owns the viewer."
997
- Mechanism (confirmed by the Step 1 spike, [ADR-0004] addendum): a **read-only** transcript rendered from `parseSessionEntries(readFileSync(record.outputFile, "utf8"))`, surfaced through a flat command — **not** `switchSession` (a full takeover that invalidates the root's in-flight turn) and **not** `loadEntriesFromFile` (a test-only export of the SDK's `core/session-manager` module that the package's public barrel does not re-export, in both `0.79.1` and `0.79.8`).
1001
+ Mechanism (confirmed by the Step 1 spike and revised by [ADR-0004] Addendum 2): a **read-only** (non-interactive) transcript **dual-sourced by liveness**, rendered through Pi's own public entry components (no bespoke renderer).
1002
+
1003
+ - **Tracked agent** (still in `manager.listAgents()`) — render live from the in-memory record: `record.messages` for history, `record.subscribeToUpdates()` to re-render on streaming updates, and `record.activeTools` / `record.responseText` for the running-agent streaming indicator.
1004
+ - **Evicted / untracked agent** — render from the file snapshot: `parseSessionEntries(readFileSync(record.outputFile, "utf8"))` → drop the `SessionHeader` → `buildSessionContext(...).messages`.
1005
+
1006
+ Both sources yield `AgentMessage[]`, so one Pi-component renderer serves both: Pi's public entry components (`AssistantMessageComponent` / `ToolExecutionComponent` / …) or `serializeConversation` (see the [ADR-0004] addendum, Findings 0 and 1).
1007
+ Neither `switchSession` (a full takeover that invalidates the root's in-flight turn) nor `loadEntriesFromFile` (a test-only export the package's public barrel does not re-export, in both `0.79.1` and `0.79.8`) is used.
998
1008
  `Subagent.outputFile` already exposes the persisted child session JSONL path via `subagentSession?.outputFile` — no new SDK dependency.
999
- Rendering uses Pi's own public APIs (no bespoke renderer): `parseSessionEntries` → drop the `SessionHeader` → `buildSessionContext(...).messages` → Pi's public entry components (`AssistantMessageComponent` / `ToolExecutionComponent` / …) or `serializeConversation` (see the [ADR-0004] addendum, Findings 0 and 1).
1000
1009
  The new surface stands up while the old `viewAgentConversation`/`ConversationViewer` path still works; the bespoke viewer is removed only by the terminal cut (Step 5).
1001
1010
 
1002
- Outcome: operator views a child agent's persisted session through Pi's native machinery; the new surface coexists with the old viewer until Step 5.
1011
+ Outcome: operator views any subagent's session through Pi's native machinery — live for a running agent, a file snapshot for an evicted one; the new surface coexists with the old viewer until Step 5.
1012
+
1013
+ Landed ([#445], sliced): #445 shipped the first releasable vertical slice — the `/subagent-sessions` command (`src/ui/session-navigator.ts`), the pure selection/sourcing/text-render core (`src/ui/session-navigation.ts`), and the typed `agentMessages` accessor (`SessionMessage` on `SubagentSession`/`Subagent`).
1014
+ It is **live-source only** behind a renderer-agnostic `TranscriptSource` seam, rendered via Pi's `serializeConversation` text.
1015
+ With the `manager.listAgents()`-only candidate set, no listed record is ever session-disposed (dispose-and-delete are atomic), so the file-snapshot branch has no reachable caller and was deferred to keep `fallow dead-code` clean.
1016
+ Two follow-ups complete the step behind the same seam: (a) upgrade the renderer from `serializeConversation` text to Pi's per-entry TUI components; (b) broaden the candidate set to evicted agents and add the file-snapshot source (`parseSessionEntries` → `buildSessionContext`).
1017
+ The step heading stays unchecked until both land.
1003
1018
 
1004
1019
  `Release: independent` (spike-gated)
1005
1020
 
@@ -1063,7 +1078,7 @@ Outcome: test clone groups ≤ 10 (from 16); `subagent-manager.test.ts` uses sha
1063
1078
  flowchart LR
1064
1079
  S1["✅ Step 1 - Spike (#446)"]
1065
1080
  S2["✅ Step 2 - Settings command (#447)"]
1066
- S3["Step 3 - Background widget (#444)"]
1081
+ S3["Step 3 - Background widget (#444)"]
1067
1082
  S4["Step 4 - Native session nav (#445)"]
1068
1083
  S5["Step 5 - Dissolve /agents + viewer (#442)"]
1069
1084
  S6["Step 6 - Remove definition mgmt (#441)"]
@@ -0,0 +1,127 @@
1
+ # Client-server architecture: opportunities for pi-subagents
2
+
3
+ This note is forward-looking.
4
+ Pi's client-server split is **not on the near-term roadmap** — it is a long-term vision recorded in Mario Zechner's [pi session sync unification][session-sync] plan.
5
+ This document captures what that architecture would let pi-subagents do, so the opportunity is on record alongside the existing [Target architecture](./architecture.md#target-architecture) section.
6
+
7
+ It assumes the session-sync plan's shape:
8
+
9
+ - `AgentSession` owns all authoritative intra-session state and emits all sync events.
10
+ - `AgentSessionServer` owns multiple `AgentSession` runtimes, session lifecycle, client subscriptions, snapshots, and deltas.
11
+ - `AgentSessionClient` is the only thing a renderer (interactive mode, web mode) talks to.
12
+ - The sync model is **snapshot plus delta**: a client joins a session, receives one canonical snapshot, then a stream of deltas.
13
+ - Commands go up; state deltas come down.
14
+ - A client can `watch` (read-only) or `join` (interactive) a session, and can hold synchronized caches for many sessions at once.
15
+ - Session lifecycle (`new`, `resume`, `fork`, `import`, `join`, `leave`, `watch`, `unwatch`) is normal server API, not hidden local runtime replacement.
16
+
17
+ ## The key realization
18
+
19
+ A subagent **is** a child `AgentSession`. pi-subagents already creates one via `createSubagentSession`, drives its turn loop through `SubagentSession`, and disposes it.
20
+
21
+ Today that child session is an in-process object visible only through machinery pi-subagents had to invent because Pi has no session-sync client:
22
+
23
+ - `record-observer` re-derives live activity from raw session events.
24
+ - the 80 ms `SubagentManager.listAgents()` widget poll.
25
+ - the [#277] Law-of-Demeter accessors (`Subagent.getConversation()`, `.messages`, `.subscribeToUpdates()`, `.getContextPercent()`) that re-expose session internals.
26
+ - the bespoke `ConversationViewer`, and [ADR-0004]'s replacement, native session navigation.
27
+ - [ADR-0004]'s dual-source-by-liveness split (tracked agent → in-memory record; evicted → file snapshot).
28
+
29
+ Every one of those is pi-subagents reinventing an `AgentSessionClient`.
30
+ The session-sync plan supplies the real one.
31
+ If subagent child sessions are registered as first-class sessions in the `AgentSessionServer`, all three target capabilities reduce to the same primitive the operator's own session already uses: `watch`/`join` a session id, receive a snapshot, then a delta stream.
32
+
33
+ ## Capability 1 — viewing live subagent sessions
34
+
35
+ `watchSession(subagentSessionId)` gives the operator's client a canonical `SessionSnapshot` (current streaming message, pending tool executions with partial results, queue contents, context usage, active tools) followed by the live delta stream (`message_update`, `tool_execution_start/update/end`, `turn_start/end`).
36
+
37
+ What changes versus today:
38
+
39
+ 1. **Late-join correctness.**
40
+ The snapshot carries in-flight state, so an operator attaching to an already-running subagent immediately sees its current streaming message and pending tools — not just future events.
41
+ Today the conversation viewer only catches future deltas plus whatever happens to be in the in-memory record.
42
+ 2. **The widget and viewer become thin renderers** of synchronized session state.
43
+ The 80 ms poll, `record-observer`, and the [#277] accessors disappear — they were all substitutes for `subscribeSession`.
44
+ 3. **A unified, reconnect-safe live session list.**
45
+ The global event scope (`session_created`, `session_status_changed` idle/busy) means the operator sees every subagent the instant it spawns, with a live status badge per agent — replacing both `listAgents()` polling and the hand-rolled `subagents:*` broadcast tier.
46
+ 4. **Multi-session client state** lets one operator client hold synchronized caches for N subagents at once.
47
+ This is the clean foundation for the parallel-agent navigation gesture that [ADR-0004]'s spike (entry-criterion #3) struggled to design — tabbed or split views of multiple live subagents fall out of the model with no redesign.
48
+
49
+ ## Capability 2 — viewing suspended subagent sessions
50
+
51
+ Today "suspended" is a dead end: once a background subagent completes and `disposeSession()` fires, the live `AgentSession` is gone, and the only artifact is the persisted JSONL read one-shot via `parseSessionEntries`.
52
+ That is a static file dump with no liveness and no path back to interaction.
53
+
54
+ Under the server model, the live/suspended distinction collapses at the client.
55
+ Session lifecycle is normal server API (`resume_session`, `join_session`, `import_session`), so a suspended subagent is just a session the server can rehydrate from `SessionManager` on demand:
56
+
57
+ 1. The operator calls `joinSession(id)` (or the server lazily reloads the `AgentSession` from JSONL when a client joins a dormant session), gets a snapshot, and renders it through the same components as a live session.
58
+ [ADR-0004]'s two-code-path split (in-memory record vs file snapshot) stops being the client's problem — whether the session is resident or rehydrated is the server's private concern.
59
+ 2. A genuinely paused subagent (one parked by the `ConcurrencyLimiter` awaiting capacity, or deliberately held) becomes representable: the server holds the runtime idle, the operator can `watch` its frozen state, and then sees it transition to `busy` via `session_status_changed` when capacity frees up.
60
+ 3. Subagents can outlive the operator's TUI.
61
+ Because the server is a separate process, if the operator's interactive client restarts it reconnects and re-lists or re-joins subagent sessions that survived — impossible in today's single-process model.
62
+
63
+ ## Capability 3 — operators interacting via an editor and submitting messages
64
+
65
+ Today the only inbound channel to a running subagent is `steer_subagent` — a single buffer-or-deliver message through `Subagent.steer`.
66
+ No editor, no takeover, no real conversation.
67
+
68
+ The plan's `join` versus `watch` split is the right primitive:
69
+
70
+ 1. **`watch` is read-only** (Capability 1).
71
+ **`join` is interactive participation**: the operator's client sends session commands scoped to the subagent's session id — `prompt`, `abort`, `set model`, `set thinking level`, `set active tools`, `navigate tree`, `compact`, `run bash`.
72
+ 2. **Editor flow.**
73
+ The operator opens the subagent session in their client, types in the editor (editor text, focus, and overlays are explicitly client-local UI state, never synchronized), and submits a `prompt` command targeting the subagent's session id.
74
+ The command goes up; the server applies it to the subagent's `AgentSession` (which already owns prompt submission, queued steering, and follow-up messages); canonical deltas flow back down to all subscribers.
75
+ This is precisely "commands go up, state deltas come down," applied to a subagent session instead of the operator's own.
76
+ 3. **`steer` generalizes into the full command surface.**
77
+ pi-subagents' bespoke buffer-or-deliver logic (`Subagent.steer` rejecting when not running) is subsumed by `AgentSession`'s queued-steering machinery, and the custom buffering becomes redundant.
78
+ 4. **Multiple clients on one session.**
79
+ The plan supports this explicitly, with deltas broadcast to all subscribers including the initiator, so an operator can jump into a subagent the parent agent spawned, inject guidance, and the parent's view stays consistent.
80
+ 5. **Fork and rewind, not just append.**
81
+ `navigate tree` plus `fork_session` let an operator rewind a subagent or fork it at an entry — full session editing, far beyond appending a steer message.
82
+
83
+ ## What pi-subagents itself becomes
84
+
85
+ This validates and sharpens the direction the architecture doc is already heading. pi-subagents' stated core job — spawn a child session derived from the parent, run the turn loop, track and stream and collect the result, gate concurrency, support resume, and publish its lifecycle — is almost exactly the `AgentSessionServer`'s job for child sessions.
86
+
87
+ - **The observation tier dissolves.**
88
+ This confirms [Phase 18]'s own finding that "the activity/metrics push tier is provisional" — under the server it genuinely does not need to exist in pi-subagents.
89
+ - **The recursion principle gets its real substrate.**
90
+ The architecture doc's "a subagent is a recursive Pi" framing maps directly onto the plan's session model: a subagent session is just another session in the server, with the same snapshot, delta, and command surface.
91
+ - **pi-subagents keeps only its semantic layer** — agent type, description, result text, token totals, concurrency admission, and the recursion guard — the things a generic session server does not know.
92
+ The liveness and activity channel rides on session sync instead of a hand-rolled broadcast.
93
+
94
+ ### Bespoke machinery the server would retire
95
+
96
+ | Today (pi-subagents reinvents it) | Under the server architecture |
97
+ | ------------------------------------------------------------------------ | ---------------------------------------------------------- |
98
+ | `record-observer` re-deriving live activity | `subscribeSession` delta stream |
99
+ | 80 ms `listAgents()` widget poll | `session_created` / `session_status_changed` global events |
100
+ | [#277] accessors (`messages`, `subscribeToUpdates`, `getContextPercent`) | `SessionSnapshot` + deltas |
101
+ | `ConversationViewer` / native session navigation | client renders the snapshot through Pi's own components |
102
+ | dual-source-by-liveness split | server rehydrates; client sees one session shape |
103
+ | `steer_subagent` buffer-or-deliver | `join` + `prompt` / queued-steering commands |
104
+ | `subagents:*` activity broadcasts | session sync (semantic events stay; liveness moves) |
105
+
106
+ ## Prerequisites and open questions
107
+
108
+ Being honest about the constraints the plan itself raises:
109
+
110
+ 1. **Registry-local provider state is a hard prerequisite** (the plan states this explicitly).
111
+ Subagents run in-process under a multi-session server — exactly the scenario where process-global provider registration would corrupt one session's model view from another's extension load. pi-subagents is well-positioned (it already snapshots model and registry into `ParentSnapshot`), but the underlying Pi change must land first.
112
+ 2. **The recursion guard stays pi-subagents' invariant.**
113
+ A subagent session created through the server must still have pi-subagents' three tools stripped, and an operator-joined subagent must not be able to spawn observers of itself in a loop.
114
+ 3. **Workspace context must travel in the snapshot.**
115
+ Subagents can run in a different cwd or git worktree (`pi-subagents-worktrees`).
116
+ The plan's `SessionSnapshot.cwd` field already accounts for this, so an operator's client renders and interacts in the correct workspace.
117
+ 4. **Operator-submitted versus parent-submitted command permissions.**
118
+ Commands an operator joins and submits flow through the subagent's `AgentSession`, so pi-permission-system's in-child gating still applies — but whether an operator's `run bash` or `prompt` should carry a different permission posture than the parent agent's is a genuine open design question, not something to assume.
119
+
120
+ ## Net assessment
121
+
122
+ This architecture turns all three capabilities from bespoke, fragile features pi-subagents must build and maintain into thin clients of one uniform session-sync surface — and in doing so lets pi-subagents shed most of the observation infrastructure it only built because that surface did not exist yet.
123
+
124
+ [session-sync]: https://jot.mariozechner.at/s/zgzbq9n4f4mfck
125
+ [ADR-0004]: ../decisions/0004-reconsider-ui-direction.md
126
+ [Phase 18]: ./architecture.md#phase-18-complete
127
+ [#277]: https://github.com/gotgenes/pi-packages/issues/277
@@ -214,6 +214,40 @@ Reject the tentative `/subagents:settings` and the `/agents-settings` alternativ
214
214
 
215
215
  This keeps bespoke transcript rendering out of the package (rendering is Pi's own public components), adds no inbound call from the UI to the core, and preserves the Phase 18 spine invariants (#422–#425).
216
216
 
217
+ ## Addendum 2 (2026-06-20): Step 4 sourcing — live record plus file snapshot ([#445])
218
+
219
+ This addendum revises the first addendum's Criterion 2 answer for Step 4 ([#445]).
220
+ The "strictly read-only, file-only" sourcing and the "not a live overlay" framing are superseded by the dual-source decision below; everything else from the spike stands.
221
+
222
+ **Why revise.**
223
+ The spike's mechanism — `parseSessionEntries(readFileSync(record.outputFile))` read once — serves a _completed_ subagent well but is a frozen snapshot for a _running_ one: it shows only what was flushed to disk at open time and does not stream.
224
+ The clarified desired behavior is to see the **live** activity of any subagent _and_ the activity of a completed subagent.
225
+ The bespoke `ConversationViewer` being removed in Step 5 already delivers both — it subscribes to the live in-memory record (`record.subscribeToUpdates()`) and renders `record.messages` plus a streaming indicator built from `record.activeTools` / `record.responseText`.
226
+ That liveness comes from the in-memory record, not the persisted file, so a file-only viewer drops it.
227
+
228
+ **Decision — dual-source, one renderer.**
229
+ Step 4 sources the transcript by liveness and renders both sources through the same Pi public entry components (Finding 1 holds — no bespoke renderer):
230
+
231
+ - **Tracked agent (still in `manager.listAgents()`)** — render from the live in-memory record: `record.messages` for history, `record.subscribeToUpdates()` to re-render on streaming updates, and `record.activeTools` / `record.responseText` for the running-agent streaming indicator.
232
+ This is live.
233
+ - **Evicted / untracked agent** — render from the file snapshot: `parseSessionEntries(readFileSync(record.outputFile, "utf8"))` → drop the `SessionHeader` → `buildSessionContext(...).messages` (Findings 0 and 1).
234
+
235
+ Both sources yield `AgentMessage[]`, so a single Pi-component renderer serves both.
236
+
237
+ **Type-boundary note.**
238
+ `SubagentSession.messages` is deliberately widened to `readonly unknown[]` at the core boundary (`src/lifecycle/subagent-session.ts`), even though the underlying `_session.messages` is the SDK's `AgentMessage[]`.
239
+ Step 4 should add a typed accessor that returns `AgentMessage[]` (or narrow at the boundary) rather than feeding `unknown[]` into Pi's components.
240
+ This is a read accessor on the existing record — it adds no inbound call from the UI to the core and does not regress the Phase 18 spine invariants.
241
+
242
+ **Still read-only (non-interactive).**
243
+ The viewer remains strictly non-interactive: Criterion 2's anti-redundant-steering rationale stands (steering lives in the `steer_subagent` tool and the widget), and Criterion 1's rejection of `switchSession` is unchanged — "live" here means the in-memory subscription, not an active-session takeover.
244
+
245
+ **Candidate set (revises Criterion 3).**
246
+ The selection command lists **any subagent with a live record or a persisted session file** — foreground agents included, not only running background agents.
247
+ This matches the bespoke viewer's current reach (gated on `record.isSessionReady()`, never background-filtered) and avoids an interim regression.
248
+ A foreground agent is navigable only _after_ it completes — while it runs, the root turn is blocked on it — whereas a background agent is navigable live.
249
+ The background widget ([#444]) remains the optional secondary selection gesture for background agents; the command is the primary, unit-testable surface.
250
+
217
251
  [#444]: https://github.com/gotgenes/pi-packages/issues/444
218
252
  [#445]: https://github.com/gotgenes/pi-packages/issues/445
219
253
  [#446]: https://github.com/gotgenes/pi-packages/issues/446
@@ -0,0 +1,156 @@
1
+ ---
2
+ issue: 444
3
+ issue_title: "pi-subagents: shrink the agent widget to background runs only"
4
+ ---
5
+
6
+ # Shrink the agent widget to background runs only
7
+
8
+ ## Release Recommendation
9
+
10
+ **Release:** ship independently
11
+
12
+ Phase 19 Step 3 ([architecture roadmap][arch]) is tagged `Release: independent` — it is not a member of any release batch.
13
+ The change is self-contained to `agent-widget.ts` and its tests, touches no public surface, and ADR-0004 explicitly notes it is "independent of the spike."
14
+
15
+ ## Problem Statement
16
+
17
+ The above-editor agent widget currently renders every agent — foreground and background alike.
18
+ For foreground runs the `subagent` tool's inline `onUpdate` stream already renders live progress, so the widget duplicates that display (ADR-0004 Decision A).
19
+ The widget should survive only as the **background-agent status surface**: the one place with no inline tool-call display, which must represent N parallel background agents at once.
20
+
21
+ There is also a latent inconsistency to fix.
22
+ `AgentWidget` calls `manager.listAgents()` at two sites — `update()` (which feeds `seedFinishedAgents`, `assembleWidgetState`, and `clearWidget`) and `renderWidget()` (the tree map).
23
+ Filtering at only one site would leave the other rendering foreground agents.
24
+
25
+ ## Goals
26
+
27
+ - The widget shows only background agents (`record.invocation?.runInBackground === true`); foreground agents never appear.
28
+ - Both `listAgents()` call sites are funneled through a single private accessor that applies the background predicate once at the source.
29
+ - The foreground/widget duplication called out by ADR-0004 Decision A is eliminated.
30
+ - This is a UI behavior refinement, not a public-API or config change — it is **non-breaking** (no exported surface, default, or config shape changes on upgrade).
31
+
32
+ ## Non-Goals
33
+
34
+ - Relabeling the widget heading from `Agents` to `Background agents` (see Open Questions) — not requested; keep scope tight.
35
+ - Any change to `widget-renderer.ts` rendering logic — it has no foreground-specific path; the fix lives entirely at the data source in `agent-widget.ts`.
36
+ - Other `listAgents()` consumers (`agent-menu.ts`) — those are removed wholesale by Phase 19 Step 5 ([#442]), out of scope here.
37
+ - Native session navigation ([#445], Step 4) and the `/subagents-settings` extraction ([#447], Step 2).
38
+
39
+ ## Background
40
+
41
+ Relevant modules:
42
+
43
+ - `src/ui/agent-widget.ts` — `AgentWidget implements SubagentManagerObserver`; self-drives an 80 ms render loop from lifecycle events (Step 4 self-drive, [#423]).
44
+ `update()` reads `manager.listAgents()` into a local `allAgents`, then passes it to `seedFinishedAgents()`, `assembleWidgetState()`, and (on the idle path) `clearWidget()`.
45
+ `renderWidget(tui, theme)` independently calls `manager.listAgents().map(r => this.toWidgetAgent(r))` to build the tree.
46
+ - `src/ui/widget-renderer.ts` — pure rendering; `renderWidgetLines()` categorizes agents into running/queued/finished and emits tree lines.
47
+ It has no knowledge of foreground vs. background — it renders whatever list it receives.
48
+ - `Subagent.invocation` (`src/lifecycle/subagent.ts`) — a readonly `AgentInvocation | undefined` set once at construction.
49
+ `AgentInvocation.runInBackground` (`src/types.ts`) is the reliable signal: set by `spawn-config.ts` → `AgentInvocation.runInBackground` → stored on `Subagent.invocation`.
50
+ - `AgentSummary` (in `agent-widget.ts`) — the minimal `{ id, status, completedAt }` shape consumed by `assembleWidgetState`/`clearWidget`.
51
+ `Subagent` is structurally assignable to it, so the accessor can return `Subagent[]` without an interface change.
52
+
53
+ Constraint from AGENTS.md / package skill: pi-subagents is a minimal core; this is a consumer-side UI behavior change with no policy or core impact.
54
+
55
+ ## Design Overview
56
+
57
+ Introduce one private accessor on `AgentWidget` and route both existing call sites through it:
58
+
59
+ ```typescript
60
+ /** Background agents only — the widget's sole audience (ADR-0004 Decision A). */
61
+ private listBackgroundAgents(): Subagent[] {
62
+ return this.manager
63
+ .listAgents()
64
+ .filter(record => record.invocation?.runInBackground === true);
65
+ }
66
+ ```
67
+
68
+ Call-site changes:
69
+
70
+ - `update()` — replace `const allAgents = this.manager.listAgents();` with `const backgroundAgents = this.listBackgroundAgents();` and rename the downstream references (`seedFinishedAgents`, `assembleWidgetState`, `clearWidget`).
71
+ - `renderWidget()` — replace `this.manager.listAgents().map(...)` with `this.listBackgroundAgents().map(...)`.
72
+
73
+ The predicate `record.invocation?.runInBackground === true` is applied exactly once, at the funnel.
74
+ `undefined` invocation (or `runInBackground` absent/false) is treated as foreground and excluded.
75
+
76
+ Edge cases verified:
77
+
78
+ - **`clearWidget` stale-purge:** it deletes `finishedTurnAge` entries for IDs "no longer in the list."
79
+ Because `seedFinishedAgents` only ever seeds from the background-filtered list, no foreground agent is ever tracked, so purging against the background-only list cannot drop a still-relevant entry — no behavior regression.
80
+ - **Queued agents:** the concurrency limiter queues only background agents (foreground bypasses the queue), so they carry `runInBackground === true` and remain visible; the queued-count display stays accurate.
81
+ - **All-foreground case:** `update()` sees an empty background list → `assembleWidgetState` reports no active/finished → `clearWidget` → widget never registers. `renderWidget()` is consequently never invoked.
82
+ - **Mixed case (the latent bug):** one background running + one foreground running → `update()` registers the widget (1 background active); `renderWidget()` must also filter, or it renders both.
83
+ Routing both sites through the accessor closes this.
84
+
85
+ ## Module-Level Changes
86
+
87
+ - `src/ui/agent-widget.ts` — add private `listBackgroundAgents()`; route `update()` and `renderWidget()` through it; rename the `update()` local `allAgents` → `backgroundAgents`.
88
+ - `src/ui/widget-renderer.ts` — **no change.**
89
+ Verified there is no foreground-specific rendering path; filtering at the data source is sufficient.
90
+ Listed only to record the verification.
91
+ - `test/ui/agent-widget.test.ts` — migrate existing widget fixtures to set `invocation: { runInBackground: true }` (otherwise the new filter excludes them); add background-only filtering tests.
92
+ - `packages/pi-subagents/README.md` — line ~64, change "showing all active agents" → "showing active background agents" (stale prose once foreground is excluded).
93
+ - `packages/pi-subagents/docs/architecture/architecture.md` — mark Step 3 ✅ with a `Landed`/`Outcome` note; the `src/ui/agent-widget.ts` tree caption at line ~342 stays accurate (generic "live status widget").
94
+
95
+ Grep sweep performed for stale references to "all agents" / "showing all active agents" across `src/`, `test/`, `.pi/skills/package-pi-subagents/SKILL.md`, and `docs/`:
96
+
97
+ - `README.md:64` — stale, updated above.
98
+ - `.pi/skills/package-pi-subagents/SKILL.md:57` ("widget ─polls─→ Subagent records (listAgents)") — still accurate (it does poll `listAgents`, now filtered); no edit.
99
+ - `docs/comparison-with-upstream.md:29`, `architecture.md:342` — generic "live above-editor widget"; remain accurate.
100
+
101
+ ## Test Impact Analysis
102
+
103
+ 1. **New tests enabled by the change:**
104
+ - `update()` excludes foreground agents — an all-foreground agent list leaves the widget unregistered (`lastContent()` undefined).
105
+ - `renderWidget()` excludes foreground agents — a mixed background+foreground list renders only the background agent's description in the tree (the foreground description is absent).
106
+ This pins the previously-latent two-site inconsistency.
107
+ 2. **Tests that become redundant:** none are removed.
108
+ The existing widget-behavior fixtures (`makeWidget` in the two `describe` blocks, plus the projection test's `createTestSubagent`) are migrated to set `invocation: { runInBackground: true }` so they continue to exercise the same lifecycle/seeding/self-drive behavior under the new filter.
109
+ 3. **Tests that must stay as-is:** the `assembleWidgetState` pure-function suite — it calls the pure function directly with `AgentSummary[]`, not through the accessor, so it is unaffected by the filter and continues to pin the counting logic.
110
+
111
+ ## Invariants at risk
112
+
113
+ This step touches `agent-widget.ts`, which prior phases already refactored.
114
+ The fixture migration must keep these invariants pinned (the tests stay, only their `invocation` field is added):
115
+
116
+ - **Self-drive from lifecycle ([#423]):** `onSubagentStarted`/`onSubagentCreated` start the 80 ms timer and render; `onSubagentCompleted`/`onSubagentCompacted` render.
117
+ Pinned by the "self-drives from lifecycle notifications" describe block — fixtures migrated to background invocation so the manager stub's agents survive the filter.
118
+ - **`seedFinishedAgents` idempotency / linger aging ([#374]):** finished agents seed once, age out after 1 turn (errors after 2), and `update()` never advances the age without a turn.
119
+ Pinned by the "update self-seeds finished agents" describe block — fixtures migrated to background invocation.
120
+
121
+ A later step must not regress these with a green suite; the migration preserves the assertions verbatim apart from the `invocation` field.
122
+
123
+ ## TDD Order
124
+
125
+ 1. **`test:` migrate widget fixtures to background invocation (preparatory / tidy-first).**
126
+ Surface: `test/ui/agent-widget.test.ts`.
127
+ Add `invocation: { runInBackground: true }` to the `makeWidget` agent stubs (both `describe` blocks) and to the projection test's `createTestSubagent` call.
128
+ No production change — the filter does not exist yet, so the field is inert and the suite stays green.
129
+ This makes the next step a clean addition rather than a mass fixture rewrite.
130
+ Commit: `test: set background invocation on agent-widget fixtures (#444)`.
131
+ 2. **`feat:` filter the widget to background agents only.**
132
+ Red: add (a) an all-foreground list leaves the widget unregistered, and (b) a mixed background+foreground list renders only the background agent in `renderWidget()`.
133
+ Green: add the private `listBackgroundAgents()` accessor; route both `update()` and `renderWidget()` through it; rename the `update()` local to `backgroundAgents`.
134
+ Commit: `feat: shrink agent widget to background runs only (#444)`.
135
+ 3. **`docs:` update widget docs for background-only behavior.**
136
+ Update `README.md` ("all active agents" → "active background agents") and mark Phase 19 Step 3 ✅ with a `Landed`/`Outcome` note in `docs/architecture/architecture.md`.
137
+ Commit: `docs: note background-only widget in README and roadmap (#444)`.
138
+
139
+ ## Risks and Mitigations
140
+
141
+ - **Mass test breakage when the filter lands** — every `update()`-driven fixture would be filtered out and assertions would fail.
142
+ Mitigation: TDD step 1 migrates all fixtures first (inert until the filter exists), so step 2 only adds new tests.
143
+ - **A foreground agent the operator expected to see disappears** — intended per ADR-0004 Decision A; the inline `onUpdate` stream is authoritative for foreground runs.
144
+ - **`clearWidget` stale-purge dropping a relevant entry** — analyzed in Design Overview; foreground agents are never seeded, so purging against the background-only list is correct.
145
+
146
+ ## Open Questions
147
+
148
+ - Should the widget heading read `Background agents` instead of `Agents`?
149
+ Deferred — not in the issue scope; revisit if the bare label reads as ambiguous in practice.
150
+
151
+ [arch]: ../architecture/architecture.md
152
+ [#442]: https://github.com/gotgenes/pi-packages/issues/442
153
+ [#445]: https://github.com/gotgenes/pi-packages/issues/445
154
+ [#447]: https://github.com/gotgenes/pi-packages/issues/447
155
+ [#423]: https://github.com/gotgenes/pi-packages/issues/423
156
+ [#374]: https://github.com/gotgenes/pi-packages/issues/374