@gotgenes/pi-subagents 16.5.0 → 16.6.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,13 @@ 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
+ ## [16.6.0](https://github.com/gotgenes/pi-packages/compare/pi-subagents-v16.5.0...pi-subagents-v16.6.0) (2026-06-17)
9
+
10
+
11
+ ### Features
12
+
13
+ * read widget activity off subagent records ([#421](https://github.com/gotgenes/pi-packages/issues/421)) ([df09e03](https://github.com/gotgenes/pi-packages/commit/df09e03d122c391212507c4c5d8436cd4a0561ba))
14
+
8
15
  ## [16.5.0](https://github.com/gotgenes/pi-packages/compare/pi-subagents-v16.4.0...pi-subagents-v16.5.0) (2026-06-17)
9
16
 
10
17
 
@@ -953,16 +953,18 @@ Step 8 is the user-driven UI reconsideration the disentangled core finally makes
953
953
  The deeper target in the [first-principles refinement](#first-principles-refinement-and-the-deeper-target) — metrics as a pure observer projection rather than mutable fields — is deliberately **not** forced here.
954
954
  Folding the live activity onto the record (the single owner of run state, consistent with Phase 17's `SubagentState`) removes the duplication without inventing the asynchronous-observation seam the `improvement-discovery` skill warns is essential, not structural.
955
955
 
956
- 1. **Fold run metrics and live activity onto the core record (pure addition).**
956
+ 1. **✅ Fold run metrics and live activity onto the core record (pure addition) — complete (v16.5.0).**
957
957
  ([#420]) Target: `lifecycle/subagent-state.ts`, `observation/record-observer.ts`, `lifecycle/subagent.ts`.
958
958
  Extend the single owned run-state value object with `turnCount`, active tools, and response text; have the already-subscribed `record-observer` handle `turn_end`, `tool_execution_start`, `message_start`, `message_update`; expose read-only `turnCount`/`maxTurns`/`activeTools`/`responseText` getters on `Subagent`.
959
959
  `AgentActivityTracker` still exists; nothing reads the new getters yet (tidy-first).
960
960
  Smell: Category C. Outcome: `Subagent` is the single home for all run state; getters available for migration.
961
- 2. **Migrate every activity reader to the record getters.**
961
+ Landed: `SubagentState` owns `turnCount`/`activeTools`/`responseText` plus their transition methods; `record-observer` populates them on a single subscription; `Subagent` exposes the four read-only getters; +27 tests (1031 → 1058).
962
+ 2. **✅ Migrate every activity reader to the record getters — complete.**
962
963
  ([#421]) Target: `ui/widget-renderer.ts`, `ui/conversation-viewer.ts`, `ui/agent-menu.ts`, `tools/foreground-runner.ts`, `observation/notification.ts`.
963
964
  Switch each reader from `AgentActivityTracker` to the record getters added in Step 1 (widget-renderer reads activity off `listAgents()`; viewer/menu drop the `activity` param; notification reads `turnCount`/`maxTurns` off the record).
964
965
  Smell: Category C (Law of Demeter).
965
966
  Outcome: no consumer references `AgentActivityTracker`.
967
+ Landed: `WidgetAgent` folds the live-activity fields and a `contextPercent` projection; `AgentWidget` projects records; `ConversationViewer`, `AgentsMenuHandler`, `buildDetails`, and `buildNotificationDetails` read off the record; `NotificationSystem.cleanupCompleted` removed; `SubagentEventsObserver` returns early on consumed results; +8 tests (1058 → 1066).
966
968
  3. **Delete `AgentActivityTracker` and `ui-observer`; drop the activity map from the runtime and spawn tools.**
967
969
  ([#422]) Target: `ui/agent-activity-tracker.ts` (delete), `ui/ui-observer.ts` (delete), `runtime.ts`, `tools/foreground-runner.ts`, `tools/background-spawner.ts`.
968
970
  The spawn tools stop constructing trackers, subscribing, and populating maps; `SubagentRuntime.agentActivity` is removed.
@@ -1006,8 +1008,8 @@ Folding the live activity onto the record (the single owner of run state, consis
1006
1008
 
1007
1009
  ```mermaid
1008
1010
  flowchart TB
1009
- S1["1 — Fold metrics + activity onto record (#420)"]
1010
- S2["2 — Migrate readers to record getters (#421)"]
1011
+ S1["1 — Fold metrics + activity onto record (#420)"]
1012
+ S2["2 — Migrate readers to record getters (#421)"]
1011
1013
  S3["3 — Delete tracker + ui-observer, drop activity map (#422)"]
1012
1014
  S4["4 — Widget self-drives on events (#423)"]
1013
1015
  S5["5 — Drop widget dep from subagent tool (#424)"]
@@ -1058,7 +1060,7 @@ Detailed records are preserved in per-phase history files:
1058
1060
  | 15 | Domain model evolution | Complete | [phase-15-domain-model-evolution.md](history/phase-15-domain-model-evolution.md) |
1059
1061
  | 16 | Invert dependencies (extensions on a minimal core) | Complete | [phase-16-invert-dependencies.md](history/phase-16-invert-dependencies.md) |
1060
1062
  | 17 | Core consolidation | Complete | [phase-17-core-consolidation.md](history/phase-17-core-consolidation.md) |
1061
- | 18 | Reconsider UI (first principles) | Proposed | this document, [Phase 18](#phase-18-reconsider-ui-from-first-principles) |
1063
+ | 18 | Reconsider UI (first principles) | In progress | this document, [Phase 18](#phase-18-reconsider-ui-from-first-principles) |
1062
1064
 
1063
1065
  ### Structural refactoring issues
1064
1066
 
@@ -0,0 +1,309 @@
1
+ ---
2
+ issue: 421
3
+ issue_title: "pi-subagents: migrate activity-tracker readers to the record getters"
4
+ ---
5
+
6
+ # Migrate activity-tracker readers to the record getters
7
+
8
+ ## Problem Statement
9
+
10
+ Phase 18 Step 1 ([#420]) folded the live-activity fields (`turnCount`, `activeTools`, `responseText`, `maxTurns`) onto the single owned run-state value object and exposed them as read-only getters on `Subagent`, but nothing reads them yet.
11
+ Five consumers still reach into a separate UI `AgentActivityTracker` for data the record now exposes directly — a Law-of-Demeter coupling that keeps the activity tier entangled with the core.
12
+ This step switches each reader from the tracker to the record getters so that no reader reads activity off `AgentActivityTracker`, unblocking the tracker's deletion in Step 3 ([#422]).
13
+
14
+ This is Phase 18 Step 2 of the architecture roadmap (`docs/architecture/architecture.md`).
15
+ It is a reader migration: the producer plumbing that still constructs trackers, subscribes `ui-observer`, and populates the runtime activity map (`foreground-runner`, `background-spawner`, `runtime.ts`, `ui-observer.ts`) is left in place and removed in Step 3.
16
+ The reader-held map dependencies in `notification`, `agent-widget`, and `agent-menu` — files Step 3 does not touch — are removed here.
17
+
18
+ ## Goals
19
+
20
+ - Switch `widget-renderer` to read live activity off the `Subagent` records returned by `listAgents()` rather than a parallel `activityMap`, folding the `WidgetActivity` interface into `WidgetAgent`.
21
+ - Drop the `activity` constructor param from `ConversationViewer`; read `activeTools` / `responseText` off `this.record`.
22
+ - Drop the `AgentActivityReader` dependency from `AgentsMenuHandler`.
23
+ - Migrate `runForeground` and `buildDetails` to read streaming state (`turnCount`, `maxTurns`, `activeTools`, `responseText`) off the `Subagent` they already hold.
24
+ - Migrate `buildNotificationDetails` to read `turnCount` / `maxTurns` off the record, and drop the `agentActivity` map dependency (and the now-vestigial `cleanupCompleted` method) from `NotificationManager`.
25
+ - Project each `Subagent` into a pure-data `WidgetAgent` snapshot inside `AgentWidget` (with a `contextPercent` field), keeping `widget-renderer` a pure function of plain data.
26
+
27
+ This change is **not breaking**.
28
+ The published service surface (`src/service/service.ts`) exposes `SubagentRecord` / `SubagentStatus`, neither of which changes.
29
+ There is one accepted observable behavior change in the widget (below), but no API, config, or default changes on upgrade without a user edit.
30
+
31
+ ### Accepted behavior change
32
+
33
+ Today the widget shows **no** turn count on finished-agent lines because the activity tracker is deleted on completion (foreground deletes it after `spawnAndWait`; `NotificationManager.sendCompletion` deletes it for background agents).
34
+ The record's `turnCount` getter persists, so once `widget-renderer` reads off `listAgents()` records, finished lines will show `⟳N`.
35
+ This is **accepted** (confirmed with the operator): the deletion-on-completion behavior was a UI-state artifact, and showing the turn count consistently is the more correct outcome.
36
+ Preserving the old behavior would require the widget to track which finished agents were "cleaned up" — reintroducing exactly the coupling this phase removes.
37
+
38
+ ## Non-Goals
39
+
40
+ - Deleting `AgentActivityTracker` / `ui-observer`, or removing the producer plumbing in `foreground-runner` / `background-spawner` / `runtime.ts` that still constructs trackers, subscribes, and populates the activity map — Step 3 ([#422]).
41
+ - Dropping the `widget` and `agentActivity` constructor params from `AgentTool` — Step 5 ([#424]).
42
+ - Making the widget self-drive its timer from lifecycle events instead of spawn-tool calls — Step 4 ([#423]).
43
+ - Reconciling the public event contract (`SUBAGENT_EVENTS.ACTIVITY`) — Step 6 ([#425]).
44
+ - Changing `SubagentState`, `record-observer`, or the getters themselves — landed in Step 1 ([#420]).
45
+
46
+ ## Background
47
+
48
+ Relevant modules and how they relate:
49
+
50
+ - `src/lifecycle/subagent.ts` — exposes the four read-only getters added in Step 1: `turnCount` and `activeTools` and `responseText` delegate to the owned `SubagentState`; `maxTurns` returns `this.execution.maxTurns`.
51
+ Also exposes `getContextPercent(): number | null` (delegates to `subagentSession.getContextPercent()`), the live context-window utilization.
52
+ - `src/ui/widget-renderer.ts` — pure rendering functions.
53
+ `renderFinishedLine` / `renderRunningLines` take a `WidgetAgent` plus a `WidgetActivity | undefined`; `renderWidgetLines` takes an `activityMap: ReadonlyMap<string, WidgetActivity>`.
54
+ `renderRunningLines` computes the context percent via `activity?.session ? getSessionContextPercent(activity.session) : null` — a reach into session stats from a module documented as taking no SDK types.
55
+ - `src/ui/agent-widget.ts` — `AgentWidget` holds the `agentActivity` map and passes `manager.listAgents()` (Subagents, structurally accepted as `WidgetAgent`) plus the map to `renderWidgetLines`.
56
+ - `src/ui/conversation-viewer.ts` — `ConversationViewer` takes an `activity: AgentActivityTracker | undefined`; reads `activity.activeTools` / `activity.responseText` in `buildContentLines` for the running streaming indicator.
57
+ - `src/ui/agent-menu.ts` — defines `AgentActivityReader` (`get(id): AgentActivityTracker | undefined`), takes it as a constructor param, and reads `this.agentActivity.get(record.id)` in `viewAgentConversation` to pass to `ConversationViewer`.
58
+ - `src/tools/foreground-runner.ts` — constructs `fgState = new AgentActivityTracker(...)`, subscribes `ui-observer`, populates the activity map, and reads `fgState.turnCount` / `fgState.maxTurns` / `fgState.activeTools` / `fgState.responseText` in `streamUpdate`; passes `fgState` to `buildDetails`.
59
+ - `src/tools/helpers.ts` — `buildDetails(base, record, activity?, overrides?)` reads `activity?.turnCount` and `activity?.maxTurns`.
60
+ Two call sites: `runForeground` (passes `fgState`) and `AgentTool` resume (no activity arg).
61
+ - `src/observation/notification.ts` — `buildNotificationDetails(record, resultMaxLen, activity?)` reads `activity?.turnCount` / `activity?.maxTurns`.
62
+ `NotificationManager` holds the `agentActivity` map and deletes from it in `sendCompletion` and `cleanupCompleted`.
63
+ - `src/observation/subagent-events-observer.ts` — `onSubagentCompleted` calls `notifications.cleanupCompleted(record.id)` when `record.notification?.resultConsumed`, else `notifications.sendCompletion(record)`.
64
+ - `src/index.ts` — constructs `NotificationManager`, `AgentWidget`, and `AgentsMenuHandler`, each currently passed `runtime.agentActivity`.
65
+
66
+ AGENTS.md / code-design constraints that apply:
67
+
68
+ - `widget-renderer.ts` is a pure rendering module ("stateless: they receive data and return formatted strings.
69
+ No timers, no SDK types, no side effects").
70
+ The migration keeps it pure and **narrows** it: the precomputed `contextPercent` field lets it drop the `SessionLike` / `getSessionContextPercent` imports, removing the session reach-through.
71
+ - `AgentWidget` is the adapter between the live manager/records and the pure renderer; the explicit `Subagent` → `WidgetAgent` projection belongs there (DIP, narrow data boundary).
72
+ - `getSessionContextPercent` and `SessionLike` stay in `usage.ts` — still used by `subagent-session.ts` and `getSessionTokens`, so removing the widget-renderer import creates no dead code.
73
+
74
+ ## Design Overview
75
+
76
+ ### `WidgetAgent` absorbs the activity fields (widget-renderer)
77
+
78
+ `WidgetActivity` is removed; its fields move onto `WidgetAgent` as plain data, plus a precomputed context percent:
79
+
80
+ ```typescript
81
+ export interface WidgetAgent {
82
+ readonly id: string;
83
+ readonly type: SubagentType;
84
+ readonly status: string;
85
+ readonly description: string;
86
+ readonly toolUses: number;
87
+ readonly startedAt: number;
88
+ readonly completedAt?: number;
89
+ readonly error?: string;
90
+ readonly lifetimeUsage?: Readonly<LifetimeUsage>;
91
+ readonly compactionCount: number;
92
+ // Folded-in live activity (was WidgetActivity)
93
+ readonly turnCount: number;
94
+ readonly maxTurns?: number;
95
+ readonly activeTools: ReadonlyMap<string, string>;
96
+ readonly responseText: string;
97
+ readonly contextPercent: number | null;
98
+ }
99
+ ```
100
+
101
+ `renderFinishedLine(agent, registry, theme)` and `renderRunningLines(agent, registry, spinnerFrame, theme)` drop the `WidgetActivity | undefined` parameter and read `agent.turnCount` / `agent.maxTurns` / `agent.activeTools` / `agent.responseText` directly.
102
+ `renderRunningLines` uses `agent.contextPercent` in place of `activity?.session ? getSessionContextPercent(activity.session) : null`.
103
+ `renderWidgetLines` and `buildSections` drop the `activityMap` parameter.
104
+ Because every `WidgetAgent` now carries activity, the old "no activity provided → omit turn count / show `thinking…`" branch is gone — `describeActivity(emptyMap, "")` already returns the `thinking…` string for an idle running agent, so the running-line text is preserved.
105
+
106
+ ### `AgentWidget` projects records into snapshots
107
+
108
+ `AgentWidget` drops its `agentActivity` constructor param and field.
109
+ `renderWidget` maps each record through a local projection before handing the array to the pure renderer:
110
+
111
+ ```typescript
112
+ private toWidgetAgent(record: Subagent): WidgetAgent {
113
+ return {
114
+ id: record.id,
115
+ type: record.type,
116
+ status: record.status,
117
+ description: record.description,
118
+ toolUses: record.toolUses,
119
+ startedAt: record.startedAt,
120
+ completedAt: record.completedAt,
121
+ error: record.error,
122
+ lifetimeUsage: record.lifetimeUsage,
123
+ compactionCount: record.compactionCount,
124
+ turnCount: record.turnCount,
125
+ maxTurns: record.maxTurns,
126
+ activeTools: record.activeTools,
127
+ responseText: record.responseText,
128
+ contextPercent: record.getContextPercent(),
129
+ };
130
+ }
131
+ ```
132
+
133
+ `renderWidget` then calls `renderWidgetLines({ agents: this.manager.listAgents().map(r => this.toWidgetAgent(r)), registry, … })` with no `activityMap`.
134
+ `assembleWidgetState` keeps consuming raw records via its `AgentSummary` shape (id / status / completedAt) — unchanged.
135
+ This makes the snapshot boundary explicit: the renderer depends only on plain data; `AgentWidget` owns the projection (Tell-Don't-Ask — it asks each record once and hands forward a value).
136
+
137
+ ### `ConversationViewer` reads off the record
138
+
139
+ The `activity` constructor param and field are removed.
140
+ `buildContentLines` reads the streaming indicator inputs off the record it already holds:
141
+
142
+ ```typescript
143
+ if (this.record.status === "running") {
144
+ lines.push(...formatStreamingIndicator(
145
+ this.record.activeTools,
146
+ this.record.responseText,
147
+ width,
148
+ th,
149
+ ));
150
+ }
151
+ ```
152
+
153
+ The `&& this.activity` guard is dropped — a running record always exposes `activeTools` / `responseText` (empty / `""` when idle), and `describeActivity` renders the idle case as `thinking…`, matching the prior no-tracker fallback.
154
+
155
+ ### `AgentsMenuHandler` drops the reader
156
+
157
+ The `AgentActivityReader` interface and the `agentActivity` constructor param are removed.
158
+ `viewAgentConversation` no longer looks up a tracker; it constructs `ConversationViewer` without an `activity` field.
159
+
160
+ ### `buildDetails` / `runForeground` read off the record
161
+
162
+ `buildDetails` drops its `activity?: AgentActivityTracker` parameter; the structural `record` param gains `turnCount?: number` and `maxTurns?: number`, and the body reads `record.turnCount` / `record.maxTurns`:
163
+
164
+ ```typescript
165
+ export function buildDetails(
166
+ base: Pick<AgentDetails, "displayName" | "description" | "subagentType" | "modelName" | "tags">,
167
+ record: {
168
+ toolUses: number; startedAt: number; completedAt?: number; status: string;
169
+ error?: string; id?: string; lifetimeUsage: LifetimeUsage;
170
+ turnCount?: number; maxTurns?: number;
171
+ },
172
+ overrides?: Partial<AgentDetails>,
173
+ ): AgentDetails { … turnCount: record.turnCount, maxTurns: record.maxTurns … }
174
+ ```
175
+
176
+ `Subagent` satisfies this structurally (the getters supply `turnCount` / `maxTurns`).
177
+ In `runForeground`, `streamUpdate` reads `recordRef?.turnCount ?? 1`, `recordRef?.activeTools ?? new Map()`, `recordRef?.responseText ?? ""`, and `execution.effectiveMaxTurns` for `maxTurns` (the runner already holds this constant; `recordRef` is undefined until `onSessionCreated`).
178
+ The final `buildDetails(presentation.detailBase, record, fgState, { tokens })` becomes `buildDetails(presentation.detailBase, record, { tokens })`.
179
+ The `fgState` tracker, its `subscribeUIObserver` subscription, and the `agentActivity.set` / `delete` calls remain (producer plumbing removed in Step 3) but are no longer read.
180
+
181
+ ### `buildNotificationDetails` / `NotificationManager`
182
+
183
+ `buildNotificationDetails(record, resultMaxLen)` drops the `activity?` param and reads `record.turnCount` / `record.maxTurns`.
184
+ `NotificationManager` drops the `agentActivity` constructor param and field; `sendCompletion` keeps only the nudge scheduling.
185
+ `cleanupCompleted` only ever deleted a map entry, so it becomes vestigial and is **removed** from both `NotificationSystem` and `NotificationManager`.
186
+ `SubagentEventsObserver.onSubagentCompleted` updates its `resultConsumed` branch to simply `return` (skip the nudge) instead of calling `cleanupCompleted`.
187
+
188
+ ### Consumer call-site check (index.ts)
189
+
190
+ ```typescript
191
+ const notifications = new NotificationManager((msg, opts) => pi.sendMessage(msg, opts));
192
+ const widget = new AgentWidget(manager, registry);
193
+ const agentsMenu = new AgentsMenuHandler(manager, registry, settings, new FsAgentFileOps(), …);
194
+ ```
195
+
196
+ All three constructions drop the `runtime.agentActivity` argument.
197
+ `runtime.agentActivity` itself stays (still written by the spawn tools; removed in Step 3).
198
+
199
+ ## Module-Level Changes
200
+
201
+ - `src/ui/widget-renderer.ts` — remove the `WidgetActivity` interface; fold its fields plus `contextPercent` into `WidgetAgent`; drop the `WidgetActivity | undefined` param from `renderFinishedLine` / `renderRunningLines`; drop `activityMap` from `buildSections` / `renderWidgetLines`; remove the `SessionLike` and `getSessionContextPercent` imports (keep `getLifetimeTotal`).
202
+ - `src/ui/agent-widget.ts` — drop the `agentActivity` constructor param and field; add the `toWidgetAgent` projection; map `listAgents()` through it in `renderWidget`; drop the `AgentActivityTracker` import.
203
+ - `src/ui/conversation-viewer.ts` — drop the `activity` field and `ConversationViewerOptions.activity`; read `activeTools` / `responseText` off `this.record`; drop the `AgentActivityTracker` import.
204
+ - `src/ui/agent-menu.ts` — remove the `AgentActivityReader` interface and the `agentActivity` constructor param; drop the tracker lookup in `viewAgentConversation`; drop the `AgentActivityTracker` import.
205
+ - `src/tools/helpers.ts` — `buildDetails` drops the `activity?` param and reads `record.turnCount` / `record.maxTurns` (param type gains the two optional fields); drop the `AgentActivityTracker` import.
206
+ - `src/tools/foreground-runner.ts` — `streamUpdate` and the final `buildDetails` call read off `recordRef` / `record`; the `fgState` construction, `subscribeUIObserver`, and map writes stay (Step 3).
207
+ - `src/observation/notification.ts` — `buildNotificationDetails` drops the `activity?` param and reads off the record; `NotificationManager` drops the `agentActivity` param/field and the `delete` calls; remove `cleanupCompleted` from `NotificationSystem` and the class; drop the `AgentActivityTracker` import.
208
+ - `src/observation/subagent-events-observer.ts` — `onSubagentCompleted` `resultConsumed` branch returns without calling `cleanupCompleted`.
209
+ - `src/index.ts` — drop `runtime.agentActivity` from the `NotificationManager`, `AgentWidget`, and `AgentsMenuHandler` constructions.
210
+ - `.pi/skills/package-pi-subagents/SKILL.md` — update the module-dependency-flow line `widget ─polls─→ AgentActivityTracker map` to reflect that the widget now polls `listAgents()` records.
211
+ - `docs/architecture/architecture.md` — add a `Landed:` line to the Phase 18 Step 2 entry during implementation (the entry text already describes the work).
212
+
213
+ Test files (see TDD Order for which step each lands in):
214
+
215
+ - `test/helpers/make-subagent.ts` — add `turnCount` / `activeTools` / `responseText` / `maxTurns` shorthands to `createTestSubagent` so reader tests can seed activity on records.
216
+ - `test/widget-renderer.test.ts` — remove `WidgetActivity` / `makeActivity`; fold activity defaults into `makeAgent` (`turnCount`, `maxTurns`, `activeTools`, `responseText`, `contextPercent`); drop the `activity` arg from render calls and `activityMap` from `renderWidgetLines` calls; update the former "omit turn count when no activity" cases to assert the turn count now renders; convert the session-stats context test to a `contextPercent` field.
217
+ - `test/ui/agent-widget.test.ts` — drop the `agentActivity` arg from `new AgentWidget(...)`; add a projection test asserting `renderWidget` reads activity off records.
218
+ - `test/conversation-viewer.test.ts` — drop `activity` from `TestViewerOptions` / `createTestViewer`; seed the running-indicator test's `activeTools` / `responseText` via `createTestSubagent`; drop the `AgentActivityTracker` import.
219
+ - `test/ui/agent-menu.test.ts` — drop the `agentActivity` arg from `new AgentsMenuHandler(...)`.
220
+ - `test/tools/helpers.test.ts` — drop the `activity` arg from `buildDetails` calls; seed `turnCount` / `maxTurns` via `createTestSubagent`; drop the `AgentActivityTracker` import.
221
+ - `test/tools/foreground-runner.test.ts` — adjust any `details.turnCount` assertions to the record's value (turn count `1` default matches the prior tracker default, so most cases are unchanged); the activity-map registration test stays (producer plumbing remains).
222
+ - `test/observation/notification.test.ts` — drop `agentActivity` from `makeArgs` and `new NotificationManager(...)`; drop the `activity` arg from `buildNotificationDetails` calls and seed `turnCount` / `maxTurns` via `createTestSubagent`; remove the two map-cleanup assertions and the `cleanupCompleted` test.
223
+ - `test/observation/subagent-events-observer.test.ts` — update any assertion that `cleanupCompleted` is called on `resultConsumed` to assert no nudge is sent instead.
224
+
225
+ `test/runtime.test.ts` and `test/helpers/make-deps.ts` keep their `AgentActivityTracker` map usage — the runtime map and the `AgentTool` `agentActivity` access survive this step (removed in Steps 3 / 5).
226
+ Completed historical plans under `docs/plans/` are point-in-time records and are not edited.
227
+
228
+ ## Test Impact Analysis
229
+
230
+ 1. **New unit tests enabled.**
231
+ `agent-widget.test.ts` can now assert the projection directly — that `renderWidget` surfaces a record's `turnCount` / `activeTools` / `contextPercent` without any external map — because activity lives on the record the manager stub returns.
232
+ `widget-renderer.test.ts` tests get simpler: a single `makeAgent` carries everything, removing the two-object (`WidgetAgent` + `WidgetActivity`) arrangement.
233
+ 2. **Tests that become redundant / simplified.**
234
+ The `widget-renderer` "omits turn count when no activity provided" cases lose their premise (there is no activity-absent path); they convert to asserting the turn count renders.
235
+ The `notification` "sendCompletion cleans up activity" and "cleanupCompleted removes activity" cases lose their subject (the map is gone) and are removed.
236
+ `ui-observer.test.ts` and `agent-activity-tracker.test.ts` stay — they pin the producer behavior still wired until Step 3.
237
+ 3. **Tests that must stay as-is.**
238
+ `foreground-runner.test.ts`'s activity-map registration test (the tracker is still constructed and registered), and `runtime.test.ts`'s `agentActivity` map presence test, genuinely exercise the producer plumbing this step does not remove.
239
+
240
+ ## Invariants at risk
241
+
242
+ - **Step 1 ([#420]) outcome — `Subagent` is the single home for run state; getters available for migration.**
243
+ This step is the consumer of that outcome; the getters' parity with the old tracker (turn count starts at `1`, `name_seq` active-tool keying, reset-then-append response text) is what makes the swap behavior-preserving for running agents.
244
+ Pinned by the existing `subagent-state.test.ts` / `record-observer.test.ts` cases from Step 1 plus the reader tests migrated here.
245
+ - **`widget-renderer` purity — pure functions, plain data, no SDK types** (established when the renderer was split out, plan 0148).
246
+ The migration preserves and strengthens it: the `contextPercent` field removes the `getSessionContextPercent(session)` reach-through, so the renderer no longer touches session stats.
247
+ Pinned by `widget-renderer.test.ts` constructing plain `WidgetAgent` objects (no class instances).
248
+ - **`NotificationManager` construction graph stays a cycle-free DAG with no widget dependency** (index.ts comment).
249
+ Dropping the `agentActivity` map narrows its deps further; no new dependency is added.
250
+
251
+ No earlier phase step's documented `Outcome:` is regressed — the producer plumbing Step 1's tidy-first overlap left in place is untouched here.
252
+
253
+ ## TDD Order
254
+
255
+ 1. **Preparatory: seed activity on test records.**
256
+ Surface: `test/helpers/make-subagent.ts` (+ a focused assertion in `test/helpers/make-deps.test.ts` or `make-subagent` coverage if one exists).
257
+ Add `turnCount` / `activeTools` / `responseText` / `maxTurns` shorthands to `createTestSubagent` (drive `state.incrementTurnCount` / `addActiveTool` / `appendResponseText`; thread `maxTurns` into `makeStubExecution`).
258
+ Pure test-helper addition — no production change; makes the reader-migration steps easy ("make the change that makes the change easy").
259
+ Commit: `test: seed live-activity on createTestSubagent (#421)`.
260
+ 2. **Migrate the widget reader.**
261
+ Surface: `test/widget-renderer.test.ts`, `test/ui/agent-widget.test.ts`.
262
+ Red→green: update `makeAgent` to carry activity, drop the `activity` arg / `activityMap`, assert finished lines now render the turn count, assert the `AgentWidget` projection.
263
+ Implement the `WidgetAgent` fold + `contextPercent`, the render-signature changes, and the `AgentWidget.toWidgetAgent` projection; update the `new AgentWidget(...)` call in `index.ts` and the SKILL.md flow line.
264
+ This step lands the accepted behavior change (finished-agent turn count), so the commit is `feat:`, not `refactor:`.
265
+ Run `pnpm run check` after this commit — `renderWidgetLines` is the shared signature changed.
266
+ Commit: `feat: read widget activity off subagent records (#421)`.
267
+ 3. **Migrate the conversation viewer and `/agents` menu.**
268
+ Surface: `test/conversation-viewer.test.ts`, `test/ui/agent-menu.test.ts`.
269
+ Red→green: drop `activity` from the viewer factory and seed the running-indicator inputs on the record; drop the `agentActivity` arg from the menu handler construction.
270
+ Implement: drop `ConversationViewer.activity`; read off `this.record`; remove `AgentActivityReader` and the `viewAgentConversation` lookup; update the `new AgentsMenuHandler(...)` call in `index.ts`.
271
+ The viewer constructor signature and the menu constructor signature both change with their sole call sites in the same commit.
272
+ Commit: `refactor: read conversation-viewer activity off the record (#421)`.
273
+ 4. **Migrate `buildDetails` and the foreground runner.**
274
+ Surface: `test/tools/helpers.test.ts`, `test/tools/foreground-runner.test.ts`.
275
+ Red→green: drop the `activity` arg from `buildDetails` calls and seed `turnCount` / `maxTurns` on the record; confirm the foreground `details` still report the record's turn count.
276
+ Implement: `buildDetails` param/body change (both call sites — `runForeground` and the `AgentTool` resume path — compile in the same commit), and `runForeground`'s `streamUpdate` / final `buildDetails` reads.
277
+ Commit: `refactor: build agent details from the record, not the tracker (#421)`.
278
+ 5. **Migrate notifications and drop the activity map there.**
279
+ Surface: `test/observation/notification.test.ts`, `test/observation/subagent-events-observer.test.ts`.
280
+ Red→green: drop `agentActivity` from the notification fixtures and the `activity` arg from `buildNotificationDetails`; remove the map-cleanup and `cleanupCompleted` cases; assert the observer's `resultConsumed` branch sends no nudge.
281
+ Implement: `buildNotificationDetails` reads off the record; `NotificationManager` drops the map param/field and `cleanupCompleted`; `NotificationSystem` drops `cleanupCompleted`; `SubagentEventsObserver` returns in the `resultConsumed` branch; update the `new NotificationManager(...)` call in `index.ts`.
282
+ Run `pnpm run check` after this commit — `NotificationSystem` is a shared interface and `index.ts` is its construction site.
283
+ Commit: `refactor: build notification details from the record (#421)`.
284
+
285
+ After Step 5, no reader reads activity off `AgentActivityTracker`; the tracker, `ui-observer`, and the runtime map remain only as inert producer plumbing for Step 3 ([#422]).
286
+
287
+ ## Risks and Mitigations
288
+
289
+ - **Risk: the running-line idle text changes.**
290
+ Once `activeTools` / `responseText` always come off the record, an idle running agent renders via `describeActivity(new Map(), "")` instead of the old no-tracker `thinking…` fallback.
291
+ Mitigation: `describeActivity` already returns `thinking…` for an empty map and blank text, so the output is preserved; the migrated `widget-renderer` test pins it.
292
+ - **Risk: transient background-tracker map leak between Steps 2 and 3.**
293
+ `NotificationManager` no longer deletes finished agents' trackers from the runtime map, and background agents are no longer cleaned up by anyone until the map is removed.
294
+ Mitigation: after this step the map is write-only and read by nobody, so the leak is inert and short-lived; Step 3 ([#422]) removes the map entirely.
295
+ - **Risk: `streamUpdate` runs before `recordRef` is set (foreground).**
296
+ The initial `streamUpdate()` and the first spinner ticks fire before `onSessionCreated` assigns `recordRef`.
297
+ Mitigation: read with defaults that match the tracker's initial values (`turnCount ?? 1`, empty `activeTools`, `"" `responseText, `execution.effectiveMaxTurns` for `maxTurns`).
298
+ - **Risk: removing `cleanupCompleted` changes the observer's consumed-result path.**
299
+ Mitigation: the method only deleted a map entry; the observer's intent (skip the nudge when the result was already consumed) is preserved by an early `return`, pinned by the updated observer test.
300
+
301
+ ## Open Questions
302
+
303
+ - Whether the `NotificationSystem` interface should be narrowed further (e.g. to a two-method completion notifier) now that `cleanupCompleted` is gone is deferred — it is orthogonal to this step and best judged once Steps 3–5 settle the surrounding wiring.
304
+
305
+ [#420]: https://github.com/gotgenes/pi-packages/issues/420
306
+ [#422]: https://github.com/gotgenes/pi-packages/issues/422
307
+ [#423]: https://github.com/gotgenes/pi-packages/issues/423
308
+ [#424]: https://github.com/gotgenes/pi-packages/issues/424
309
+ [#425]: https://github.com/gotgenes/pi-packages/issues/425
@@ -43,3 +43,48 @@ Full suite green; type check and lint clean; zero dead code.
43
43
  - WARN finding: `package-pi-subagents` SKILL.md `Observation` domain row description ("Session-event stats") is now slightly incomplete for `record-observer` (it also accumulates live-activity fields).
44
44
  Intentionally deferred per the planning-stage decision — no symbol removed, and the description will be updated in Step 2 ([#421]) when the observer's role is fully defined after the reader migration.
45
45
  - Pre-completion reviewer verdict: **WARN** (one non-blocking finding, deferred per retro).
46
+
47
+ ## Stage: Final Retrospective (2026-06-17T17:40:39Z)
48
+
49
+ ### Session summary
50
+
51
+ Shipped Phase 18 Step 1 across planning, TDD (3 red→green→commit cycles, +27 tests), and release (`pi-subagents` v16.5.0).
52
+ The code work was clean and rework-free; all friction was in markdown-lint surfacing late — two separate doc-lint failures (one pre-existing, one introduced) escaped the baseline and post-TDD gates and were caught only by the pre-completion reviewer and the ship-stage lint, each requiring a fix commit.
53
+
54
+ ### Observations
55
+
56
+ #### What went well
57
+
58
+ - The `pre-completion-reviewer` safety net earned its keep: it caught the pre-existing `MD051` broken-fragment links in `phase-17-core-consolidation.md` (introduced earlier by `e4d92535`, unrelated to this issue) before the push, returning FAIL on the first run.
59
+ Without it the broken links would have shipped.
60
+ - TDD execution was textbook: each of the 3 steps followed red→green→commit with no rework to production code, the `maxTurns`-via-`execution` delegation decision from planning held, and the "vacuously-passing" observer tests (verified in the retro) correctly converted to real assertions once the observer was implemented.
61
+
62
+ #### What caused friction (agent side)
63
+
64
+ - `instruction-violation` (gate-caught, at ship) — wrote a second `[#421]:` link-reference definition into the retro file when appending the TDD stage entry via `Write` full-content, tripping `MD053` (duplicate definition).
65
+ The `markdown-conventions` skill states this rule verbatim ("Link reference definitions are file-scoped … a duplicate trips MD053").
66
+ Impact: one fix commit at ship time (`80d4d050`), caught by the root `pnpm run lint` pre-push gate rather than during TDD (no lint runs after stage-notes writing).
67
+ - `missing-context` (self-identified during ship investigation) — the TDD green-baseline lint was run package-scoped (`pnpm --filter @gotgenes/pi-subagents run lint`), which **silently passes** on `MD051` cross-file fragment failures.
68
+ Verified empirically: package `lint:md` (`rumdl check *.md docs/**/*.md`) returns "No issues" on the broken fragment, while root `rumdl check .` exits 1 on it — the cross-file fragment target (`../architecture.md`) is only resolved on a repo-root tree walk.
69
+ Impact: the pre-existing `MD051` failure slipped past the baseline gate (whose job is to catch exactly that) and surfaced mid-TDD via the reviewer, costing a fix commit (`86ed0c81`) and a reviewer FAIL/re-dispatch cycle.
70
+
71
+ #### What caused friction (user side)
72
+
73
+ - None.
74
+ The session ran end-to-end without user correction; the operator-authored, roadmap-aligned issue meant no clarification was needed.
75
+
76
+ ### Diagnostic details
77
+
78
+ - **Model-performance correlation** — two `pre-completion-reviewer` subagent dispatches ran on the agent's configured reviewer model and performed judgment-heavy review work (acceptance criteria, design review, cross-step invariants); appropriate match.
79
+ No high-cost model was spent on mechanical work and no reasoning-weak model on judgment work.
80
+ - **Feedback-loop gap analysis** — the lint feedback loop fired at the wrong scope and the wrong time: package-scoped at baseline (missed `MD051`), and absent after retro stage-notes writing (missed `MD053` until ship).
81
+ `pnpm run check` and `pnpm run test` were run incrementally per step and caught everything they should; only the markdown-lint loop was misconfigured.
82
+ - **Escalation-delay / unused-tool** — no `rabbit-hole` friction; no lens-2 or lens-3 findings.
83
+
84
+ ### Changes made
85
+
86
+ 1. `.pi/prompts/tdd-plan.md` — "Verify green baseline" step 2 and "After the last TDD step" step 3 now specify `pnpm run lint` runs **from the repo root**, with a one-clause rationale that package-scoped lint silently passes on `MD051` cross-file fragments and cross-package issues (Proposal 1).
87
+ 2. Proposal 2 (an `MD053` duplicate-link-definition reminder in the retro-writing prompts) was declined — the rule already exists verbatim in the `markdown-conventions` skill, so a prompt clause would duplicate it.
88
+ 3. Follow-up (post-retro): `#420` completed Phase 18 Step 1 but the roadmap Steps section was not marked done until the operator noticed.
89
+ Added a roadmap-step-status check so this is caught going forward: a WARN bullet in `pre-completion-reviewer.md` §2c (backstop), and a proactive step in `.pi/prompts/tdd-plan.md` step 7 and `.pi/prompts/build-plan.md` "After the last step" — mark the completed roadmap step done (`✅`/`Landed:`) and update the phase status row.
90
+ Also marked Step 1 complete in `architecture.md` (Steps list, dependency diagram node, phase table row → "In progress").
@@ -0,0 +1,56 @@
1
+ ---
2
+ issue: 421
3
+ issue_title: "pi-subagents: migrate activity-tracker readers to the record getters"
4
+ ---
5
+
6
+ # Retro: #421 — Migrate activity-tracker readers to the record getters
7
+
8
+ ## Stage: Planning (2026-06-17T00:00:00Z)
9
+
10
+ ### Session summary
11
+
12
+ Produced a 5-step TDD plan for Phase 18 Step 2 — migrating the five activity readers (`widget-renderer`, `conversation-viewer`, `agent-menu`, `foreground-runner`, `notification`) off `AgentActivityTracker` and onto the `Subagent` getters added in Step 1 ([#420]).
13
+ The plan files at `packages/pi-subagents/docs/plans/0421-migrate-activity-readers-to-record-getters.md`.
14
+
15
+ ### Observations
16
+
17
+ - **Two design forks surfaced and were resolved with the operator** via `ask_user`. (1) Finished agents currently show **no** turn count because the tracker is deleted on completion; reading off the record (which persists `turnCount`) makes finished lines show `⟳N`.
18
+ Operator accepted this as the more-correct behavior — so the widget step is `feat:`, not `refactor:`. (2) How `WidgetAgent` exposes context percent: chose a precomputed `contextPercent` **field** + a `Subagent` → `WidgetAgent` projection in `AgentWidget` (over a `getContextPercent()` method on the snapshot), honoring the renderer's pure-data contract and dropping its `getSessionContextPercent(session)` reach-through.
19
+ Operator's steer: "no sacred cows, especially in the UI — make the change that makes the change easy, then make the easy change."
20
+ - **Scope boundary with Step 3 ([#422]).**
21
+ The producer plumbing (`foreground-runner` / `background-spawner` constructing trackers, `subscribeUIObserver`, the `runtime.agentActivity` map) stays — it is removed in Step 3.
22
+ But the reader-held map deps in `notification`, `agent-widget`, and `agent-menu` are removed **here**, because Step 3's target list does not include those files.
23
+ This means `cleanupCompleted` (which only deleted a map entry) becomes vestigial and is removed from `NotificationSystem`, rippling to `SubagentEventsObserver`.
24
+ - **Transient map leak between Steps 2 and 3** is noted as an accepted risk: after this step the runtime map is write-only and read by nobody, so it is inert until Step 3 deletes it.
25
+ - **Prep step (tidy-first):** Step 1 adds `turnCount` / `activeTools` / `responseText` / `maxTurns` shorthands to `createTestSubagent` so the reader-migration tests can seed activity on records.
26
+ - **Living-doc updates identified:** `.pi/skills/package-pi-subagents/SKILL.md` line 56 (`widget ─polls─→ AgentActivityTracker map`) and the architecture roadmap's Phase 18 Step 2 `Landed:` line.
27
+ Completed historical plans under `docs/plans/` are point-in-time records and are not edited.
28
+ - **Shared-signature commits** flagged for `pnpm run check`: the widget step (`renderWidgetLines`) and the notification step (`NotificationSystem` + `index.ts` construction).
29
+ Both `buildDetails` call sites and both viewer/menu constructor call sites must land in the same commit as their signature changes.
30
+
31
+ [#420]: https://github.com/gotgenes/pi-packages/issues/420
32
+ [#422]: https://github.com/gotgenes/pi-packages/issues/422
33
+
34
+ ## Stage: Implementation — TDD (2026-06-17T17:00:00Z)
35
+
36
+ ### Session summary
37
+
38
+ All 5 TDD cycles completed in one session.
39
+ The five activity readers (`widget-renderer`, `conversation-viewer`, `agent-menu`, `foreground-runner`, `notification`) were migrated off `AgentActivityTracker` and onto the `Subagent` record getters added in Phase 18 Step 1.
40
+ Test count went from 1058 to 1066 (+8).
41
+
42
+ ### Observations
43
+
44
+ - **Step 2 (widget)** introduced the `WidgetAgent` → activity-field fold and the `AgentWidget.toWidgetAgent` projection.
45
+ The accepted behavior change (finished agents now show `⟳N` from the persisted record) landed as `feat:` as planned.
46
+ `renderWidgetLines` drops `activityMap`; renderer is now a pure function of plain data with no SDK-type reach.
47
+ - **Step 3 (viewer + menu)** was straightforward once `ConversationViewer` dropped `activity?: AgentActivityTracker`; the streaming indicator now reads `this.record.activeTools` / `this.record.responseText` directly.
48
+ `AgentActivityReader` interface removed; `AgentsMenuHandler` constructor lost one parameter.
49
+ - **Step 4 (buildDetails + foreground runner)** required care around the pre-`onSessionCreated` phase: `streamUpdate` falls back to `recordRef?.turnCount ?? 1`, `execution.effectiveMaxTurns`, empty `Map()`, and `""` before the record reference is assigned.
50
+ The `AgentTool` resume call site (`buildDetails(base, record)`) was already correct and needed no change.
51
+ - **Step 5 (notifications)** removed `NotificationSystem.cleanupCompleted` entirely (it only deleted a map entry).
52
+ `SubagentEventsObserver.onSubagentCompleted` now returns early on `resultConsumed` rather than calling the removed method.
53
+ The `NotificationManager` constructor drops the `agentActivity: Map` argument; `index.ts` needed one arg removed.
54
+ - **Post-commit SKILL.md fix**: the pre-completion reviewer (WARN) flagged that `subagent-events-observer.ts` was missing from the Observation domain table; fixed in a follow-up `docs:` commit.
55
+ - **Pre-completion reviewer**: WARN (non-blocking).
56
+ Reviewer warning: SKILL.md Observation domain table listed 4 modules and omitted `subagent-events-observer.ts`; corrected before writing these notes.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gotgenes/pi-subagents",
3
- "version": "16.5.0",
3
+ "version": "16.6.0",
4
4
  "type": "module",
5
5
  "exports": {
6
6
  ".": {
package/src/index.ts CHANGED
@@ -62,7 +62,6 @@ export default function (pi: ExtensionAPI) {
62
62
  // widget dependency — keeping the construction graph a cycle-free DAG.
63
63
  const notifications = new NotificationManager(
64
64
  (msg, opts) => pi.sendMessage(msg, opts),
65
- runtime.agentActivity,
66
65
  );
67
66
 
68
67
  // Settings: owns all three in-memory values and handles load/save/emit.
@@ -130,7 +129,7 @@ export default function (pi: ExtensionAPI) {
130
129
 
131
130
  // Live widget: constructed after the manager (it polls listAgents()) and
132
131
  // injected directly into its consumers — no post-construction field write.
133
- const widget = new AgentWidget(manager, runtime.agentActivity, registry);
132
+ const widget = new AgentWidget(manager, registry);
134
133
 
135
134
  // Grab UI context from first tool execution + clear lingering widget on new turn
136
135
  const toolStart = new ToolStartHandler(widget);
@@ -157,7 +156,6 @@ export default function (pi: ExtensionAPI) {
157
156
  const agentsMenu = new AgentsMenuHandler(
158
157
  manager,
159
158
  registry,
160
- runtime.agentActivity,
161
159
  settings,
162
160
  new FsAgentFileOps(),
163
161
  join(getAgentDir(), "agents"),
@@ -1,7 +1,6 @@
1
1
  import { debugLog } from "#src/debug";
2
2
  import { getLifetimeTotal } from "#src/lifecycle/usage";
3
3
  import type { Subagent } from "#src/types";
4
- import type { AgentActivityTracker } from "#src/ui/agent-activity-tracker";
5
4
 
6
5
  /** Details attached to custom notification messages for visual rendering. */
7
6
  export interface NotificationDetails {
@@ -77,7 +76,6 @@ export function formatTaskNotification(record: Subagent, resultMaxLen: number):
77
76
  export function buildNotificationDetails(
78
77
  record: Subagent,
79
78
  resultMaxLen: number,
80
- activity?: AgentActivityTracker,
81
79
  ): NotificationDetails {
82
80
  const totalTokens = getLifetimeTotal(record.lifetimeUsage);
83
81
 
@@ -86,8 +84,8 @@ export function buildNotificationDetails(
86
84
  description: record.description,
87
85
  status: record.status,
88
86
  toolUses: record.toolUses,
89
- turnCount: activity?.turnCount ?? 0,
90
- maxTurns: activity?.maxTurns,
87
+ turnCount: record.turnCount,
88
+ maxTurns: record.maxTurns,
91
89
  totalTokens,
92
90
  durationMs: record.completedAt ? record.completedAt - record.startedAt : 0,
93
91
  outputFile: record.outputFile,
@@ -127,7 +125,6 @@ export function buildEventData(record: Subagent) {
127
125
  export interface NotificationSystem {
128
126
  cancelNudge: (key: string) => void;
129
127
  sendCompletion: (record: Subagent) => void;
130
- cleanupCompleted: (id: string) => void;
131
128
  dispose: () => void;
132
129
  }
133
130
 
@@ -141,7 +138,6 @@ export class NotificationManager implements NotificationSystem {
141
138
  msg: { customType: string; content: string; display: boolean; details?: unknown },
142
139
  opts?: { triggerTurn?: boolean; deliverAs?: "steer" | "followUp" | "nextTurn" },
143
140
  ) => void,
144
- private agentActivity: Map<string, AgentActivityTracker>,
145
141
  ) {}
146
142
 
147
143
  cancelNudge(key: string): void {
@@ -153,14 +149,9 @@ export class NotificationManager implements NotificationSystem {
153
149
  }
154
150
 
155
151
  sendCompletion(record: Subagent): void {
156
- this.agentActivity.delete(record.id);
157
152
  this.scheduleNudge(record.id, () => this.emitIndividualNudge(record));
158
153
  }
159
154
 
160
- cleanupCompleted(id: string): void {
161
- this.agentActivity.delete(id);
162
- }
163
-
164
155
  dispose(): void {
165
156
  for (const timer of this.pendingNudges.values()) clearTimeout(timer);
166
157
  this.pendingNudges.clear();
@@ -193,7 +184,7 @@ export class NotificationManager implements NotificationSystem {
193
184
  customType: "subagent-notification",
194
185
  content: notification + footer,
195
186
  display: true,
196
- details: buildNotificationDetails(record, 500, this.agentActivity.get(record.id)),
187
+ details: buildNotificationDetails(record, 500),
197
188
  },
198
189
  { deliverAs: "followUp", triggerTurn: true },
199
190
  );
@@ -65,10 +65,7 @@ export class SubagentEventsObserver implements SubagentManagerObserver {
65
65
  });
66
66
 
67
67
  // Skip notification if result was already consumed via get_subagent_result.
68
- if (record.notification?.resultConsumed) {
69
- this.notifications.cleanupCompleted(record.id);
70
- return;
71
- }
68
+ if (record.notification?.resultConsumed) return;
72
69
 
73
70
  this.notifications.sendCompletion(record);
74
71
  }
@@ -70,11 +70,15 @@ export async function runForeground(
70
70
  ...presentation.detailBase,
71
71
  toolUses,
72
72
  tokens: recordRef ? formatLifetimeTokens(recordRef) : "",
73
- turnCount: fgState.turnCount,
74
- maxTurns: fgState.maxTurns,
73
+ // Read activity off the record; fall back to safe defaults before onSessionCreated fires
74
+ turnCount: recordRef?.turnCount ?? 1,
75
+ maxTurns: recordRef?.maxTurns ?? execution.effectiveMaxTurns,
75
76
  durationMs: Date.now() - startedAt,
76
77
  status: "running",
77
- activity: describeActivity(fgState.activeTools, fgState.responseText),
78
+ activity: describeActivity(
79
+ recordRef?.activeTools ?? new Map(),
80
+ recordRef?.responseText ?? "",
81
+ ),
78
82
  spinnerFrame: spinnerFrame % SPINNER.length,
79
83
  };
80
84
  onUpdate?.({
@@ -136,7 +140,7 @@ export async function runForeground(
136
140
  }
137
141
 
138
142
  const tokenText = formatLifetimeTokens(record);
139
- const details = buildDetails(presentation.detailBase, record, fgState, { tokens: tokenText });
143
+ const details = buildDetails(presentation.detailBase, record, { tokens: tokenText });
140
144
 
141
145
  const fallbackNote = identity.fellBack
142
146
  ? `Note: Unknown agent type "${identity.rawType}" — using general-purpose.\n\n`
@@ -1,6 +1,5 @@
1
1
  import type { AgentConfigLookup } from "#src/config/agent-types";
2
2
  import { getLifetimeTotal, type LifetimeUsage } from "#src/lifecycle/usage";
3
- import { AgentActivityTracker } from "#src/ui/agent-activity-tracker";
4
3
  import { type AgentDetails, formatTokens } from "#src/ui/display";
5
4
 
6
5
  /** Parenthetical status note for completed agent result text. */
@@ -28,16 +27,18 @@ export function buildDetails(
28
27
  error?: string;
29
28
  id?: string;
30
29
  lifetimeUsage: LifetimeUsage;
30
+ /** Live-activity counters — exposed as getters on Subagent (Phase 18 Step 2). */
31
+ turnCount?: number;
32
+ maxTurns?: number;
31
33
  },
32
- activity?: AgentActivityTracker,
33
34
  overrides?: Partial<AgentDetails>,
34
35
  ): AgentDetails {
35
36
  return {
36
37
  ...base,
37
38
  toolUses: record.toolUses,
38
39
  tokens: formatLifetimeTokens(record),
39
- turnCount: activity?.turnCount,
40
- maxTurns: activity?.maxTurns,
40
+ turnCount: record.turnCount,
41
+ maxTurns: record.maxTurns,
41
42
  durationMs: (record.completedAt ?? Date.now()) - record.startedAt,
42
43
  status: record.status as AgentDetails["status"],
43
44
  agentId: record.id,
@@ -5,7 +5,6 @@ import type { ParentSnapshot } from "#src/lifecycle/parent-snapshot";
5
5
  import { type ModelRegistry, resolveModel } from "#src/session/model-resolver";
6
6
  import { getModelLabelFromConfig } from "#src/tools/helpers";
7
7
  import type { AgentConfig, Subagent } from "#src/types";
8
- import type { AgentActivityTracker } from "#src/ui/agent-activity-tracker";
9
8
  import { AgentConfigEditor } from "#src/ui/agent-config-editor";
10
9
  import { AgentCreationWizard } from "#src/ui/agent-creation-wizard";
11
10
  import type { AgentFileOps } from "#src/ui/agent-file-ops";
@@ -36,14 +35,6 @@ export interface AgentMenuSettings {
36
35
  applyGraceTurns(n: number): { message: string; level: "info" | "warning" };
37
36
  }
38
37
 
39
- /**
40
- * Read-only interface for the agent-menu's agentActivity access.
41
- * Only the conversation viewer needs to read a tracker by agent ID.
42
- */
43
- export interface AgentActivityReader {
44
- get(id: string): AgentActivityTracker | undefined;
45
- }
46
-
47
38
  // ---- Narrow UI context types ----
48
39
 
49
40
  /** Narrow UI interface — only the ctx.ui methods menu handlers actually call. */
@@ -70,7 +61,6 @@ export class AgentsMenuHandler {
70
61
  constructor(
71
62
  private readonly manager: AgentMenuManager,
72
63
  private readonly registry: AgentTypeRegistry,
73
- private readonly agentActivity: AgentActivityReader,
74
64
  private readonly settings: AgentMenuSettings,
75
65
  fileOps: AgentFileOps,
76
66
  personalAgentsDir: string,
@@ -263,14 +253,12 @@ export class AgentsMenuHandler {
263
253
  const { ConversationViewer, VIEWPORT_HEIGHT_PCT } = await import(
264
254
  "./conversation-viewer"
265
255
  );
266
- const activity = this.agentActivity.get(record.id);
267
256
 
268
257
  await ui.custom<undefined>(
269
258
  (tui: any, theme: any, _keybindings: any, done: any) => {
270
259
  return new ConversationViewer({
271
260
  tui,
272
261
  record,
273
- activity,
274
262
  theme,
275
263
  done,
276
264
  registry: this.registry,
@@ -7,10 +7,10 @@
7
7
  */
8
8
 
9
9
  import { AgentTypeRegistry } from "#src/config/agent-types";
10
+ import type { Subagent } from "#src/lifecycle/subagent";
10
11
  import type { SubagentManager } from "#src/lifecycle/subagent-manager";
11
- import type { AgentActivityTracker } from "#src/ui/agent-activity-tracker";
12
12
  import { ERROR_STATUSES, type Theme } from "#src/ui/display";
13
- import { renderWidgetLines } from "#src/ui/widget-renderer";
13
+ import { renderWidgetLines, type WidgetAgent } from "#src/ui/widget-renderer";
14
14
 
15
15
  // ---- Types ----
16
16
 
@@ -79,7 +79,6 @@ export class AgentWidget {
79
79
 
80
80
  constructor(
81
81
  private manager: SubagentManager,
82
- private agentActivity: Map<string, AgentActivityTracker>,
83
82
  private registry: AgentTypeRegistry,
84
83
  ) {}
85
84
 
@@ -129,11 +128,31 @@ export class AgentWidget {
129
128
  }
130
129
  }
131
130
 
131
+ /** Project a live Subagent record onto a pure-data WidgetAgent snapshot. */
132
+ private toWidgetAgent(record: Subagent): WidgetAgent {
133
+ return {
134
+ id: record.id,
135
+ type: record.type,
136
+ status: record.status,
137
+ description: record.description,
138
+ toolUses: record.toolUses,
139
+ startedAt: record.startedAt,
140
+ completedAt: record.completedAt,
141
+ error: record.error,
142
+ lifetimeUsage: record.lifetimeUsage,
143
+ compactionCount: record.compactionCount,
144
+ turnCount: record.turnCount,
145
+ maxTurns: record.maxTurns,
146
+ activeTools: record.activeTools,
147
+ responseText: record.responseText,
148
+ contextPercent: record.getContextPercent(),
149
+ };
150
+ }
151
+
132
152
  /** Delegate rendering to the pure widget-renderer module. */
133
153
  private renderWidget(tui: any, theme: Theme): string[] {
134
154
  return renderWidgetLines({
135
- agents: this.manager.listAgents(),
136
- activityMap: this.agentActivity,
155
+ agents: this.manager.listAgents().map(r => this.toWidgetAgent(r)),
137
156
  registry: this.registry,
138
157
  spinnerFrame: this.widgetFrame,
139
158
  terminalWidth: tui.terminal.columns,
@@ -9,7 +9,6 @@ import { type Component, matchesKey, type TUI, truncateToWidth, visibleWidth } f
9
9
  import type { AgentConfigLookup } from "#src/config/agent-types";
10
10
  import { getLifetimeTotal } from "#src/lifecycle/usage";
11
11
  import type { Subagent } from "#src/types";
12
- import type { AgentActivityTracker } from "#src/ui/agent-activity-tracker";
13
12
  import { buildInvocationTags, formatDuration, formatSessionTokens, getDisplayName, getPromptModeLabel, type Theme } from "#src/ui/display";
14
13
  import { formatMessage, formatStreamingIndicator } from "#src/ui/message-formatters";
15
14
 
@@ -24,7 +23,6 @@ export const VIEWPORT_HEIGHT_PCT = 70;
24
23
  export interface ConversationViewerOptions {
25
24
  tui: TUI;
26
25
  record: Subagent;
27
- activity: AgentActivityTracker | undefined;
28
26
  theme: Theme;
29
27
  done: (result: undefined) => void;
30
28
  registry: AgentConfigLookup;
@@ -40,7 +38,6 @@ export class ConversationViewer implements Component {
40
38
 
41
39
  private tui: TUI;
42
40
  private record: Subagent;
43
- private activity: AgentActivityTracker | undefined;
44
41
  private theme: Theme;
45
42
  private done: (result: undefined) => void;
46
43
  private registry: AgentConfigLookup;
@@ -49,7 +46,6 @@ export class ConversationViewer implements Component {
49
46
  constructor({
50
47
  tui,
51
48
  record,
52
- activity,
53
49
  theme,
54
50
  done,
55
51
  registry,
@@ -57,7 +53,6 @@ export class ConversationViewer implements Component {
57
53
  }: ConversationViewerOptions) {
58
54
  this.tui = tui;
59
55
  this.record = record;
60
- this.activity = activity;
61
56
  this.theme = theme;
62
57
  this.done = done;
63
58
  this.registry = registry;
@@ -231,11 +226,11 @@ export class ConversationViewer implements Component {
231
226
  needsSeparator = true;
232
227
  }
233
228
 
234
- // Streaming indicator for running agents
235
- if (this.record.status === "running" && this.activity) {
229
+ // Streaming indicator for running agents — read activity off the record
230
+ if (this.record.status === "running") {
236
231
  lines.push(...formatStreamingIndicator(
237
- this.activity.activeTools,
238
- this.activity.responseText,
232
+ this.record.activeTools,
233
+ this.record.responseText,
239
234
  width,
240
235
  th,
241
236
  ));
@@ -7,8 +7,8 @@
7
7
 
8
8
  import { truncateToWidth } from "@earendil-works/pi-tui";
9
9
  import type { AgentConfigLookup } from "#src/config/agent-types";
10
- import type { LifetimeUsage, SessionLike } from "#src/lifecycle/usage";
11
- import { getLifetimeTotal, getSessionContextPercent } from "#src/lifecycle/usage";
10
+ import type { LifetimeUsage } from "#src/lifecycle/usage";
11
+ import { getLifetimeTotal } from "#src/lifecycle/usage";
12
12
  import type { SubagentType } from "#src/types";
13
13
  import {
14
14
  describeActivity,
@@ -35,15 +35,13 @@ export interface WidgetAgent {
35
35
  readonly error?: string;
36
36
  readonly lifetimeUsage?: Readonly<LifetimeUsage>;
37
37
  readonly compactionCount: number;
38
- }
39
-
40
- /** Read-only activity snapshot for widget rendering. */
41
- export interface WidgetActivity {
42
- readonly activeTools: ReadonlyMap<string, string>;
43
- readonly responseText: string;
38
+ // Live activity (folded from the former WidgetActivity — precomputed by AgentWidget)
44
39
  readonly turnCount: number;
45
40
  readonly maxTurns?: number;
46
- readonly session?: SessionLike;
41
+ readonly activeTools: ReadonlyMap<string, string>;
42
+ readonly responseText: string;
43
+ /** Context-window utilisation (0–100), or null when unavailable. */
44
+ readonly contextPercent: number | null;
47
45
  }
48
46
 
49
47
  // ── Per-agent rendering ──────────────────────────────────────────────────────
@@ -51,7 +49,6 @@ export interface WidgetActivity {
51
49
  /** Render a single finished agent line (no tree connector prefix). */
52
50
  export function renderFinishedLine(
53
51
  agent: WidgetAgent,
54
- activity: WidgetActivity | undefined,
55
52
  registry: AgentConfigLookup,
56
53
  theme: Theme,
57
54
  ): string {
@@ -81,7 +78,7 @@ export function renderFinishedLine(
81
78
  }
82
79
 
83
80
  const parts: string[] = [];
84
- if (activity) parts.push(formatTurns(activity.turnCount, activity.maxTurns));
81
+ parts.push(formatTurns(agent.turnCount, agent.maxTurns));
85
82
  if (agent.toolUses > 0) parts.push(`${agent.toolUses} tool use${agent.toolUses === 1 ? "" : "s"}`);
86
83
  parts.push(duration);
87
84
 
@@ -92,7 +89,6 @@ export function renderFinishedLine(
92
89
  /** Render a single running agent as header + activity line pair (no tree connector prefix). */
93
90
  export function renderRunningLines(
94
91
  agent: WidgetAgent,
95
- activity: WidgetActivity | undefined,
96
92
  registry: AgentConfigLookup,
97
93
  spinnerFrame: number,
98
94
  theme: Theme,
@@ -103,18 +99,17 @@ export function renderRunningLines(
103
99
  const elapsed = formatMs(Date.now() - agent.startedAt);
104
100
 
105
101
  const tokens = getLifetimeTotal(agent.lifetimeUsage);
106
- const contextPercent = activity?.session ? getSessionContextPercent(activity.session) : null;
107
- const tokenText = tokens > 0 ? formatSessionTokens(tokens, contextPercent, theme, agent.compactionCount) : "";
102
+ const tokenText = tokens > 0 ? formatSessionTokens(tokens, agent.contextPercent, theme, agent.compactionCount) : "";
108
103
 
109
104
  const parts: string[] = [];
110
- if (activity) parts.push(formatTurns(activity.turnCount, activity.maxTurns));
105
+ parts.push(formatTurns(agent.turnCount, agent.maxTurns));
111
106
  if (agent.toolUses > 0) parts.push(`${agent.toolUses} tool use${agent.toolUses === 1 ? "" : "s"}`);
112
107
  if (tokenText) parts.push(tokenText);
113
108
  parts.push(elapsed);
114
109
  const statsText = parts.join(" · ");
115
110
 
116
111
  const frame = SPINNER[spinnerFrame % SPINNER.length];
117
- const activityText = activity ? describeActivity(activity.activeTools, activity.responseText) : "thinking\u2026";
112
+ const activityText = describeActivity(agent.activeTools, agent.responseText);
118
113
 
119
114
  const header = `${theme.fg("accent", frame)} ${theme.bold(name)}${modeTag} ${theme.fg("muted", agent.description)} ${theme.fg("dim", "·")} ${theme.fg("dim", statsText)}`;
120
115
  const activityLine = theme.fg("dim", ` \u23BF ${activityText}`);
@@ -157,7 +152,6 @@ interface WidgetSections {
157
152
  /** Render each agent bucket into pre-formatted lines with ├─ tree connectors. */
158
153
  function buildSections(
159
154
  categories: AgentCategories,
160
- activityMap: ReadonlyMap<string, WidgetActivity>,
161
155
  registry: AgentConfigLookup,
162
156
  spinnerFrame: number,
163
157
  theme: Theme,
@@ -165,12 +159,12 @@ function buildSections(
165
159
  ): WidgetSections {
166
160
  const finishedLines: string[] = [];
167
161
  for (const a of categories.finished) {
168
- finishedLines.push(truncate(theme.fg("dim", "\u251C\u2500") + " " + renderFinishedLine(a, activityMap.get(a.id), registry, theme)));
162
+ finishedLines.push(truncate(theme.fg("dim", "\u251C\u2500") + " " + renderFinishedLine(a, registry, theme)));
169
163
  }
170
164
 
171
165
  const runningLines: [string, string][] = [];
172
166
  for (const a of categories.running) {
173
- const [header, act] = renderRunningLines(a, activityMap.get(a.id), registry, spinnerFrame, theme);
167
+ const [header, act] = renderRunningLines(a, registry, spinnerFrame, theme);
174
168
  runningLines.push([
175
169
  truncate(theme.fg("dim", "\u251C\u2500") + ` ${header}`),
176
170
  truncate(theme.fg("dim", "\u2502 ") + act),
@@ -259,14 +253,13 @@ function assembleOverflow(
259
253
  /** Pure rendering of the widget body. Returns lines to display. */
260
254
  export function renderWidgetLines(params: {
261
255
  agents: readonly WidgetAgent[];
262
- activityMap: ReadonlyMap<string, WidgetActivity>;
263
256
  registry: AgentConfigLookup;
264
257
  spinnerFrame: number;
265
258
  terminalWidth: number;
266
259
  theme: Theme;
267
260
  shouldShowFinished: (agentId: string, status: string) => boolean;
268
261
  }): string[] {
269
- const { agents, activityMap, registry, spinnerFrame, terminalWidth, theme, shouldShowFinished } = params;
262
+ const { agents, registry, spinnerFrame, terminalWidth, theme, shouldShowFinished } = params;
270
263
 
271
264
  const { running, queued, finished } = categorizeAgents(agents, shouldShowFinished);
272
265
 
@@ -281,7 +274,6 @@ export function renderWidgetLines(params: {
281
274
 
282
275
  const { finishedLines, runningLines, queuedLine } = buildSections(
283
276
  { running, queued, finished },
284
- activityMap,
285
277
  registry,
286
278
  spinnerFrame,
287
279
  theme,