@gotgenes/pi-subagents 17.2.0 → 17.4.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,24 @@ 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.4.0](https://github.com/gotgenes/pi-packages/compare/pi-subagents-v17.3.0...pi-subagents-v17.4.0) (2026-06-23)
9
+
10
+
11
+ ### Features
12
+
13
+ * add getToolDefinition accessor on subagent record ([#462](https://github.com/gotgenes/pi-packages/issues/462)) ([e0bfdac](https://github.com/gotgenes/pi-packages/commit/e0bfdacec33120d0f52e6294f3676f4776aee4af))
14
+ * expose getToolDefinition on the transcript source seam ([#462](https://github.com/gotgenes/pi-packages/issues/462)) ([669f5ff](https://github.com/gotgenes/pi-packages/commit/669f5ff71523122ebb011adc6875b54d7001d53a))
15
+ * render /subagent-sessions transcript with Pi per-entry components ([#462](https://github.com/gotgenes/pi-packages/issues/462)) ([b832a43](https://github.com/gotgenes/pi-packages/commit/b832a437c97378f701033ef8e98c6f7805d7e7a8))
16
+
17
+ ## [17.3.0](https://github.com/gotgenes/pi-packages/compare/pi-subagents-v17.2.0...pi-subagents-v17.3.0) (2026-06-22)
18
+
19
+
20
+ ### Features
21
+
22
+ * 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))
23
+ * 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))
24
+ * 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))
25
+
8
26
  ## [17.2.0](https://github.com/gotgenes/pi-packages/compare/pi-subagents-v17.1.0...pi-subagents-v17.2.0) (2026-06-20)
9
27
 
10
28
 
@@ -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.
@@ -915,9 +918,9 @@ The end state deletes `agent-menu.ts` — the god-command that bundles four unre
915
918
  Rather than surgically mutate that doomed module (and the #1 churn hotspot `index.ts`) once per option, Phase 19 first stands up the replacement surfaces additively, then removes the now-orphaned subtree in a single terminal cut.
916
919
  This keeps every responsibility's old surface live until its replacement exists (ADR-0004's no-interim-regression invariant), turns the three replacement steps into genuinely parallel work (none touch `agent-menu.ts`), and reduces `index.ts` edits from four surgical removals to one deregistration.
917
920
 
918
- Seven steps in three phases:
921
+ Seven numbered steps in three phases, plus two follow-ups (Steps 4a–4b) carved from the #445 slice:
919
922
 
920
- - **Phase A — stand up replacements (additive):** spike, settings command, background widget, native session navigation (Steps 1–4).
923
+ - **Phase A — stand up replacements (additive):** spike, settings command, background widget, native session navigation and its renderer/source follow-ups (Steps 1–4, 4a–4b).
921
924
  - **Phase B — dissolve `/agents` (terminal cut):** delete the orphaned subtree in two deletion commits, one per subtree (Steps 5–6).
922
925
  - **Phase C — test health:** consolidate the test clones that survive the cut (Step 7).
923
926
 
@@ -984,25 +987,67 @@ Outcome: widget shows only background agents; foreground/widget duplication elim
984
987
 
985
988
  `Release: independent`
986
989
 
987
- ### Step 4 — Implement native session navigation ([#445])
990
+ ### Step 4 — Implement native session navigation ([#445])
988
991
 
989
992
  Smell: Category C (coupling) — the bespoke `ConversationViewer` re-implements session-transcript rendering when Pi's own machinery targets the already-persisted child session JSONL.
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
+ Step 4 (#445, the slice) is complete and released (`pi-subagents` v17.3.0); the remaining work is now tracked as two follow-up steps behind the same seam: Step 4a ([#462]) upgrades the renderer from `serializeConversation` text to Pi's per-entry TUI components (gates Step 5 for rendering parity); Step 4b ([#463]) broadens the candidate set to evicted agents and adds the file-snapshot source (`parseSessionEntries` → `buildSessionContext`, independent).
1003
1017
 
1004
1018
  `Release: independent` (spike-gated)
1005
1019
 
1020
+ ### ✅ Step 4a — Upgrade native-navigation renderer to Pi TUI components ([#462])
1021
+
1022
+ Smell: Category C (coupling) — the #445 slice renders the transcript as `serializeConversation` plain text, while the bespoke `ConversationViewer` it replaces renders richer per-message formatting.
1023
+ Until the native renderer reaches parity, the terminal cut (Step 5) cannot delete the bespoke viewer without a fidelity regression.
1024
+ This step swaps the renderer behind the existing `TranscriptSource` seam (`src/ui/session-navigation.ts` / `session-navigator.ts`) for Pi's per-entry components (`AssistantMessageComponent` / `ToolExecutionComponent` / …); selection and sourcing are untouched.
1025
+
1026
+ Gates Step 5: per [ADR-0004]'s no-interim-regression invariant, the native navigator must reach rendering parity with the bespoke viewer before Step 5 deletes it.
1027
+
1028
+ Outcome: native session navigation renders at parity with the removed `ConversationViewer`; Step 5 can delete the bespoke viewer with no fidelity regression.
1029
+
1030
+ Landed ([#462]): the renderer now mounts Pi's per-entry components (`AssistantMessageComponent` / `ToolExecutionComponent` / `BashExecutionComponent` / `UserMessageComponent` / `CompactionSummaryMessageComponent` / `BranchSummaryMessageComponent` / `SkillInvocationMessageComponent`) into a `Container`, mirroring Pi's own `renderSessionContext` mapping.
1031
+ The `TranscriptOverlay` caches that `Container` and rebuilds it on source change only (Pi's `rebuildChatFromMessages` path), keeping the lightweight `◍` streaming indicator.
1032
+ Tool calls render with their real `ToolDefinition`, resolved through a dependency-safe `getToolDefinition` read accessor on the record (mirroring `agentMessages`) surfaced on the `TranscriptSource` seam — no inbound call into the core.
1033
+ The pure `session-navigation.ts` sheds `renderTranscriptLines`/`serializeConversation`; rendering now lives in the SDK/TUI `session-navigator.ts`, which threads `cwd` from the command context.
1034
+ `custom`-role messages are skipped (the bespoke viewer never rendered them either).
1035
+ Selection and sourcing are untouched; native navigation now renders at parity, unblocking Step 5 for rendering fidelity.
1036
+
1037
+ `Release: independent`
1038
+
1039
+ ### Step 4b — File-snapshot source for evicted agents ([#463])
1040
+
1041
+ Smell: Category C (coupling) — the #445 slice sources transcripts live from `manager.listAgents()` only; an agent evicted by the 10-minute cleanup sweep has a persisted session JSONL but no live record, so it is unreachable.
1042
+ This step adds the file-snapshot `TranscriptSource` branch (`parseSessionEntries(readFile(outputFile))` → drop the `SessionHeader` → `buildSessionContext(...).messages`) and broadens the candidate set to enumerate evicted agents, behind the same seam; the renderer is untouched.
1043
+
1044
+ Independent: this is a new capability the bespoke viewer never had, so it gates nothing and is not a Step 5 prerequisite.
1045
+ Best sequenced after Step 4a (shared renderer), but carries no hard dependency.
1046
+
1047
+ Outcome: the operator can view a fully-evicted agent's transcript from its persisted session file; the dual-source design recorded in [ADR-0004] Addendum 2 is fully realized.
1048
+
1049
+ `Release: independent`
1050
+
1006
1051
  ### Step 5 — Dissolve `/agents` and remove the conversation-viewer subtree ([#442])
1007
1052
 
1008
1053
  Smell: Category A (dead subsystem) plus Category B (oversized) — once Steps 2–4 re-home all four menu responsibilities, the `/agents` command and everything reachable only from `agent-menu.ts` is an unreferenced subtree.
@@ -1064,20 +1109,25 @@ flowchart LR
1064
1109
  S1["✅ Step 1 - Spike (#446)"]
1065
1110
  S2["✅ Step 2 - Settings command (#447)"]
1066
1111
  S3["✅ Step 3 - Background widget (#444)"]
1067
- S4["Step 4 - Native session nav (#445)"]
1112
+ S4["Step 4 - Native session nav slice (#445)"]
1113
+ S4a["✅ Step 4a - Renderer to TUI components (#462)"]
1114
+ S4b["Step 4b - File-snapshot source (#463)"]
1068
1115
  S5["Step 5 - Dissolve /agents + viewer (#442)"]
1069
1116
  S6["Step 6 - Remove definition mgmt (#441)"]
1070
1117
  S7["Step 7 - Test clones (#443)"]
1071
1118
 
1072
1119
  S1 --> S4
1120
+ S4 --> S4a
1121
+ S4 --> S4b
1073
1122
  S2 --> S5
1074
1123
  S3 --> S5
1075
- S4 --> S5
1124
+ S4a --> S5
1076
1125
  S5 --> S6
1077
1126
  S6 --> S7
1078
1127
  ```
1079
1128
 
1080
- The terminal cut (Step 5) depends on all three replacements — settings (Step 2), widget (Step 3), and session navigation (Step 4) — because each of the four `/agents` options must have its responsibility re-homed before its branch can die.
1129
+ The terminal cut (Step 5) depends on all three replacements — settings (Step 2), widget (Step 3), and session navigation **at rendering parity** (Step 4a, which completes the #445 slice) — because each of the four `/agents` options must have its responsibility re-homed, and the viewer replacement at parity, before its branch can die.
1130
+ Step 4b (file-snapshot source) is a new capability and gates nothing.
1081
1131
  The old `S1 → S6 → S7` chain hid the widget dependency; this diagram makes it explicit.
1082
1132
 
1083
1133
  ### Parallel tracks
@@ -1085,6 +1135,7 @@ The old `S1 → S6 → S7` chain hid the widget dependency; this diagram makes i
1085
1135
  - **Track A — Replacements (Steps 1–4):** the spike gates session navigation (Step 1 → Step 4); settings (Step 2) and the background widget (Step 3) are independent of the spike and of each other.
1086
1136
  None of these steps edits `agent-menu.ts`, so they carry no shared-file collision on the menu — genuinely parallelizable, unlike the prior plan's Steps 2/3/5, which all collided on `agent-menu.ts` and `index.ts`.
1087
1137
  Steps 2 and 4 each append a command registration to `index.ts` (additive, low-conflict).
1138
+ Steps 4a (renderer parity) and 4b (file-snapshot source) complete the #445 slice behind its `TranscriptSource` seam; Step 4a gates Step 5, Step 4b is independent.
1088
1139
  - **Track B — Dissolution (Steps 5 → 6):** the terminal cut, gated on all of Track A landing.
1089
1140
  Hub-first ordering is forced: Step 5 deletes `agent-menu.ts` (orphaning the leaves), then Step 6 `git rm`s the now-orphaned definition-management subtree.
1090
1141
  - **Track C — Test health (Step 7):** clone consolidation, run after the cut so no surviving helper is extracted into a doomed file.
@@ -1093,7 +1144,7 @@ The old `S1 → S6 → S7` chain hid the widget dependency; this diagram makes i
1093
1144
 
1094
1145
  - **Batch "dissolve-agents":** Steps 5, 6 (ship together; tail = Step 6).
1095
1146
  Depends on Steps 2, 3, 4 already merged.
1096
- - Independently releasable: Steps 1, 2, 3, 4, 7.
1147
+ - Independently releasable: Steps 1, 2, 3, 4, 4a, 4b, 7.
1097
1148
 
1098
1149
  ## Refactoring history
1099
1150
 
@@ -1215,5 +1266,7 @@ The upstream test suite is run periodically as a regression canary for the sessi
1215
1266
  [#445]: https://github.com/gotgenes/pi-packages/issues/445
1216
1267
  [#446]: https://github.com/gotgenes/pi-packages/issues/446
1217
1268
  [#447]: https://github.com/gotgenes/pi-packages/issues/447
1269
+ [#462]: https://github.com/gotgenes/pi-packages/issues/462
1270
+ [#463]: https://github.com/gotgenes/pi-packages/issues/463
1218
1271
  [ADR-0002]: ../decisions/0002-extensions-on-a-minimal-core.md
1219
1272
  [ADR-0004]: ../decisions/0004-reconsider-ui-direction.md
@@ -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