@gotgenes/pi-subagents 6.14.1 → 6.16.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,34 @@ 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
+ ## [6.16.0](https://github.com/gotgenes/pi-packages/compare/pi-subagents-v6.15.0...pi-subagents-v6.16.0) (2026-05-23)
9
+
10
+
11
+ ### Features
12
+
13
+ * add session and outputFile convenience getters to AgentRecord ([#144](https://github.com/gotgenes/pi-packages/issues/144)) ([b451894](https://github.com/gotgenes/pi-packages/commit/b451894d101b43be9115dcf6d45725a09da81df8))
14
+
15
+
16
+ ### Documentation
17
+
18
+ * mark Step L complete, remove resolved smells from architecture doc ([#144](https://github.com/gotgenes/pi-packages/issues/144)) ([36ab7a5](https://github.com/gotgenes/pi-packages/commit/36ab7a51d0320666b71d6c9e20c3bbf63b7c43c5))
19
+ * plan consolidate observation model ([#144](https://github.com/gotgenes/pi-packages/issues/144)) ([9aa2c85](https://github.com/gotgenes/pi-packages/commit/9aa2c8508079c6ae847662631afd223e8966e12e))
20
+ * **retro:** add retro notes for issue [#145](https://github.com/gotgenes/pi-packages/issues/145) ([2d23081](https://github.com/gotgenes/pi-packages/commit/2d230817d53357a62eb752b56f8b1c8ce4af718c))
21
+
22
+ ## [6.15.0](https://github.com/gotgenes/pi-packages/compare/pi-subagents-v6.14.1...pi-subagents-v6.15.0) (2026-05-23)
23
+
24
+
25
+ ### Features
26
+
27
+ * extract resolveSpawnConfig pure function ([#145](https://github.com/gotgenes/pi-packages/issues/145)) ([e89724a](https://github.com/gotgenes/pi-packages/commit/e89724a87480713529160d0fa23975becbcfe162))
28
+
29
+
30
+ ### Documentation
31
+
32
+ * plan decompose execute and push ctx to boundary ([#145](https://github.com/gotgenes/pi-packages/issues/145)) ([aae7d7b](https://github.com/gotgenes/pi-packages/commit/aae7d7b4e04ab0dddedd2a0f9f2b806719956ced))
33
+ * update architecture doc for completed Step M ([#145](https://github.com/gotgenes/pi-packages/issues/145)) ([33ec0c7](https://github.com/gotgenes/pi-packages/commit/33ec0c73479076c180381dcc1cb4106ba635f33f))
34
+ * update plan with injected collaborators for ctx elimination ([#145](https://github.com/gotgenes/pi-packages/issues/145)) ([76bb57b](https://github.com/gotgenes/pi-packages/commit/76bb57b4b5190078ded8685907f0878640031e13))
35
+
8
36
  ## [6.14.1](https://github.com/gotgenes/pi-packages/compare/pi-subagents-v6.14.0...pi-subagents-v6.14.1) (2026-05-23)
9
37
 
10
38
 
@@ -617,13 +617,11 @@ Phase 9 targets the next layer: observation model consolidation, `ExtensionConte
617
617
 
618
618
  | Smell | Location | Evidence | Severity |
619
619
  | ------------------------------------------------ | --------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------- |
620
- | Dual observation | `record-observer.ts`, `ui-observer.ts` | Both independently count tool uses and accumulate lifetime usage from the same session events; consumers use `activity?.toolUses ?? record.toolUses` fallbacks | High |
621
620
  | `execute` does config resolution for its callees | `agent-tool.ts` (145-line `execute`) | ~60 lines unpack config, resolve model, compute metadata, repack into 16-field bags for spawners; `ctx` threaded 4 layers deep | Medium |
622
621
  | Wide `ctx` in menu handlers | `agent-menu.ts`, `agent-config-editor.ts`, `agent-creation-wizard.ts` | Functions declare `ctx: ExtensionContext` but only call `ctx.ui.select/confirm/input/notify/editor`; 43 `ctx as any` casts across 3 test files | Medium |
623
- | `record.execution?.session` traversal | 15+ callsites across tools, notification, widget, menu | Callers reach through `ExecutionState` to access session and outputFile - Law of Demeter violation | Medium |
624
622
  | Direct SDK import in `conversation-viewer.ts` | `conversation-viewer.test.ts` | Hoisted `vi.mock("@earendil-works/pi-tui")` to intercept `wrapTextWithAnsi` | Low |
625
623
  | Widget mixes rendering, lifecycle, and state | `agent-widget.ts` (370 lines) | `renderWidget` is ~109 lines mixing data collection, formatting, and overflow layout; constructor takes 3 concrete collaborators | Low |
626
- | `deps.` prefix noise in function bodies | 12 modules across tools, UI, notification, service-adapter | Functions accept a `deps` bag and access every field as `deps.foo`; hides real dependencies and lengthens every call line | Low |
624
+ | `deps.` prefix noise in function bodies | remaining modules across tools, UI, service-adapter | Functions accept a `deps` bag and access every field as `deps.foo`; hides real dependencies and lengthens every call line | Low |
627
625
 
628
626
  ### Dependency bag convention
629
627
 
@@ -634,9 +632,9 @@ Applied incrementally as each step touches a module:
634
632
 
635
633
  This eliminates the `deps.` prefix noise across ~124 callsites in 12 modules.
636
634
 
637
- ### Step L: Consolidate observation model (#144)
635
+ ### Step L: Consolidate observation model (#144)
638
636
 
639
- Remove `_toolUses` and `_lifetimeUsage` from `AgentActivityTracker`.
637
+ Removed `_toolUses` and `_lifetimeUsage` from `AgentActivityTracker`.
640
638
  UI consumers read stats from `AgentRecord` instead of the tracker.
641
639
  The UI observer retains event subscriptions for re-render triggers but no longer accumulates stats independently.
642
640
 
@@ -647,38 +645,24 @@ Apply the dependency bag convention to touched modules: `NotificationDeps` (4 fi
647
645
 
648
646
  Impact: eliminates dual counting; removes `??` fallback pattern from widget and conversation viewer; hides `ExecutionState` structure from consumers.
649
647
 
650
- ### Step M: Decompose execute and push ExtensionContext to the boundary (#145)
648
+ ### Step M: Decompose execute and push ExtensionContext to the boundary (#145)
651
649
 
652
- `execute` is 145 lines with three responsibilities mixed together:
650
+ Extracted config resolution into `resolveSpawnConfig` (pure function in `spawn-config.ts`).
651
+ Injected three collaborators (`buildSnapshot`, `getModelInfo`, `getSessionInfo`) into `createAgentTool` so `execute` no longer reads `ctx` beyond `ctx.ui` (already delegated to `widget.setUICtx`).
652
+ `AgentManager.spawn()` and `spawnAndWait()` accept `ParentSnapshot` instead of `ExtensionContext`.
653
+ `service-adapter.ts` calls `buildParentSnapshot(session.ctx)` at its boundary.
654
+ `foreground-runner` and `background-spawner` receive `ResolvedSpawnConfig` + domain values (`snapshot`, `parentSessionFile`, `parentSessionId`) instead of `ctx`.
653
655
 
654
- 1. **Boundary extraction** (~5 lines) - read `ctx.model`, `ctx.modelRegistry`, `ctx.ui`, `ctx.sessionManager`, call `buildParentSnapshot(ctx)`.
655
- 2. **Config resolution** (~60 lines) - resolve agent type, merge invocation config, resolve model, compute max turns, build tags and display metadata.
656
- 3. **Dispatch** (~80 lines) - resume / background / foreground, each passing 14-16 field parameter bags.
657
-
658
- The config resolution section is working for the dependencies: manually unpacking `resolvedConfig` field by field, computing derived values, then repacking everything into massive objects for `spawnBackground` and `runForeground`.
659
- The 16-field bags are the symptom - they exist because the resolution happened in the wrong place.
660
-
661
- The fix has two parts:
662
-
663
- 1. **Extract config resolution** into a pure function (e.g. `resolveSpawnConfig`) that accepts the raw tool params, registry, model info, and settings, and returns a single `ResolvedSpawnConfig` object.
664
- `execute` becomes: extract ctx → resolve config → dispatch.
665
- `spawnBackground` and `runForeground` receive `ResolvedSpawnConfig` instead of 16 individual fields.
666
- 2. **Push `ctx` to the boundary.**
667
- `execute` extracts everything from `ctx` in its first few lines.
668
- `foreground-runner.ts` and `background-spawner.ts` receive domain values (`snapshot`, `parentSessionFile`, `parentSessionId`) instead of `ctx`.
669
- `AgentManager.spawn()` and `spawnAndWait()` accept `ParentSnapshot` instead of `ExtensionContext`.
670
- `service-adapter.ts` calls `buildParentSnapshot(session.ctx)` at its boundary.
656
+ Dissolved `ForegroundDeps`, `BackgroundDeps`, and `AdapterDeps` into plain parameters.
657
+ `AgentToolDeps` is destructured in the `createAgentTool` signature.
671
658
 
672
659
  After this step, `ExtensionContext` appears only in:
673
660
 
674
- - `agent-tool.ts execute` (SDK callback - unavoidable)
661
+ - `index.ts` closures (wired at extension startup)
675
662
  - `service-adapter.ts` (cross-extension boundary)
676
- - `index.ts` (extension entry point)
677
663
  - Menu handlers (addressed by Step N)
678
664
 
679
- Apply the dependency bag convention to touched modules: `ForegroundDeps` (3 fields) and `BackgroundDeps` (3 fields) become plain parameters; `AdapterDeps` (3 fields) becomes plain parameters; `AgentToolDeps` (6 fields) is destructured in the signature.
680
-
681
- Impact: `execute` drops from ~145 to ~30 lines; eliminates 16-field parameter bags; eliminates 1 `vi.mock()` call in `agent-manager.test.ts`; `foreground-runner` and `background-spawner` tests no longer need `ctx` mocks; `AgentManager` operates entirely on domain types.
665
+ Impact: `execute` dropped from ~145 to ~25 lines; eliminated 16-field parameter bags; eliminated `vi.mock("../src/parent-snapshot.js")` in `agent-manager.test.ts`; foreground/background runner tests no longer need `ctx` mocks; `AgentManager` operates entirely on domain types.
682
666
 
683
667
  ### Step N: Narrow UI context for menu handlers (#146)
684
668
 
@@ -690,7 +674,7 @@ Creation wizard’s `spawnAndWait` call changes: the narrow `AgentMenuManager.sp
690
674
 
691
675
  Apply the dependency bag convention to touched modules: `AgentConfigEditorDeps` (4 fields), `SteerToolDeps` (4 fields), and `GetResultDeps` (4 fields) become plain parameters; `AgentMenuDeps` (8 fields) and `AgentCreationWizardDeps` (5 fields) are destructured in the signature.
692
676
 
693
- After Steps M and N, `ExtensionContext` appears only at true boundaries: `agent-tool.ts execute` (SDK callback), `service-adapter.ts` (cross-extension bridge), and `index.ts` (extension entry point).
677
+ After Steps M and N, `ExtensionContext` appears only at true boundaries: `index.ts` closures, `service-adapter.ts` (cross-extension bridge), and `index.ts` (extension entry point).
694
678
 
695
679
  Impact: eliminates ~43 `ctx as any` casts across menu, editor, and wizard test files; tests construct a plain object satisfying `MenuUI` with no cast.
696
680
 
@@ -0,0 +1,263 @@
1
+ ---
2
+ issue: 144
3
+ issue_title: "Consolidate observation model (Phase 9, Step L)"
4
+ ---
5
+
6
+ # Consolidate observation model
7
+
8
+ ## Problem Statement
9
+
10
+ `record-observer.ts` and `ui-observer.ts` independently count tool uses and accumulate lifetime usage from the same session events.
11
+ `AgentRecord` owns `_toolUses` and `_lifetimeUsage` (accumulated by the record observer), while `AgentActivityTracker` maintains its own `_toolUses` and `_lifetimeUsage` (accumulated by the UI observer).
12
+ Consumers use `activity?.toolUses ?? record.toolUses` fallbacks to paper over the ambiguity.
13
+
14
+ Separately, 15+ callsites navigate `record.execution?.session` and `record.execution?.outputFile`, leaking the `ExecutionState` structure to every consumer.
15
+
16
+ Finally, `NotificationDeps` (4 fields) is a dependency bag on a class where every method uses every field — plain constructor parameters are simpler.
17
+
18
+ ## Goals
19
+
20
+ - Remove `_toolUses` and `_lifetimeUsage` from `AgentActivityTracker` so stats have a single source of truth on `AgentRecord`.
21
+ - Update UI consumers (widget, conversation viewer, notification, foreground runner) to read stats from `AgentRecord` instead of the tracker.
22
+ - Remove the `onToolEnd` counter and `onUsageUpdate` accumulator from `AgentActivityTracker`, keeping only live UI state (active tools, response text, turn count).
23
+ - Stop the UI observer from duplicating `tool_execution_end` counting and `message_end` usage accumulation.
24
+ - Add `session` and `outputFile` convenience getters on `AgentRecord` to hide the `execution?.` traversal.
25
+ - Replace `NotificationDeps` interface with plain parameters on `NotificationManager` constructor.
26
+
27
+ ## Non-Goals
28
+
29
+ - Splitting `AgentWidget` rendering into pure functions (Step P / #148 — depends on this work).
30
+ - Narrowing `ExtensionContext` for menu handlers (Step N / #146 — independent track).
31
+ - Injecting text wrapping into `ConversationViewer` (Step O / #147 — independent track).
32
+
33
+ ## Background
34
+
35
+ ### Dual counting
36
+
37
+ Both observers subscribe to the same session events:
38
+
39
+ | Event | `record-observer.ts` | `ui-observer.ts` |
40
+ | --------------------- | ---------------------------- | ------------------------------------- |
41
+ | `tool_execution_end` | `record.incrementToolUses()` | `tracker.onToolEnd()` → `_toolUses++` |
42
+ | `message_end` (usage) | `record.addUsage(delta)` | `tracker.onUsageUpdate(delta)` |
43
+
44
+ The widget reads `bg?.toolUses ?? a.toolUses` and the conversation viewer reads `this.activity?.toolUses ?? this.record.toolUses`, preferring the tracker when alive and falling back to the record.
45
+ After this change, there is one source: `AgentRecord`.
46
+
47
+ ### `execution?.` traversal
48
+
49
+ `ExecutionState` holds `session` and `outputFile`.
50
+ These are set once when the agent session is created (`agent-manager.ts` line 276).
51
+ Consumers reach through `record.execution?.session` and `record.execution?.outputFile` in 12 distinct locations across 7 files.
52
+ Convenience getters on `AgentRecord` eliminate the traversal and hide the `ExecutionState` structure.
53
+
54
+ ### `NotificationDeps` bag
55
+
56
+ `NotificationManager` receives 4 fields via `NotificationDeps`: `sendMessage`, `agentActivity`, `markFinished`, `updateWidget`.
57
+ Every method on the class uses every field.
58
+ Per code-design convention (dependency width), a 4-field interface where every consumer uses every field is fine as a bag, but the architecture doc explicitly calls for plain parameters here.
59
+ This is a mechanical change.
60
+
61
+ ## Design Overview
62
+
63
+ ### AgentActivityTracker changes
64
+
65
+ Remove `_toolUses`, `_lifetimeUsage`, `onToolEnd`, `onUsageUpdate`, `toolUses` getter, and `lifetimeUsage` getter.
66
+ Retain: `_activeTools`, `_toolKeySeq`, `_responseText`, `_session`, `_turnCount`, `_maxTurns`, and their associated transition methods and getters.
67
+
68
+ `onToolStart` stays (tracks active tools for the widget spinner).
69
+ A new `onToolDone(toolName)` method replaces `onToolEnd` — it removes the tool from `_activeTools` without incrementing any counter.
70
+ This rename clarifies that the method only manages the active-tool display set.
71
+
72
+ ### UI observer changes
73
+
74
+ The UI observer stops calling counter/accumulator methods:
75
+
76
+ - `tool_execution_end` handler: calls `tracker.onToolDone(name)` (active-tool tracking only).
77
+ - `message_end` handler: removed entirely (no more usage accumulation).
78
+ The `onUpdate?.()` call for usage re-render moves to the record observer path via the existing widget update flow.
79
+
80
+ ### AgentRecord convenience getters
81
+
82
+ ```typescript
83
+ get session(): AgentSession | undefined {
84
+ return this.execution?.session;
85
+ }
86
+
87
+ get outputFile(): string | undefined {
88
+ return this.execution?.outputFile;
89
+ }
90
+ ```
91
+
92
+ Callers change from `record.execution?.session` to `record.session`.
93
+ The `execution` field remains writable (set by `AgentManager`), but consumers no longer need to know about `ExecutionState`.
94
+
95
+ ### Consumer migration
96
+
97
+ | File | Before | After |
98
+ | -------------------------- | -------------------------------------------------- | ------------------------------------------------ |
99
+ | `agent-widget.ts` | `bg?.toolUses ?? a.toolUses` | `a.toolUses` |
100
+ | `agent-widget.ts` | `getLifetimeTotal(bg?.lifetimeUsage)` | `getLifetimeTotal(a.lifetimeUsage)` |
101
+ | `agent-widget.ts` | `getSessionContextPercent(bg?.session)` | `getSessionContextPercent(a.session)` |
102
+ | `conversation-viewer.ts` | `this.activity?.toolUses ?? this.record.toolUses` | `this.record.toolUses` |
103
+ | `conversation-viewer.ts` | `getLifetimeTotal(this.activity?.lifetimeUsage)` | `getLifetimeTotal(this.record.lifetimeUsage)` |
104
+ | `conversation-viewer.ts` | `getSessionContextPercent(this.activity?.session)` | `getSessionContextPercent(this.record.session)` |
105
+ | `notification.ts` | `record.execution?.session` | `record.session` |
106
+ | `notification.ts` | `record.execution?.outputFile` | `record.outputFile` |
107
+ | `tools/get-result-tool.ts` | `record.execution?.session` | `record.session` |
108
+ | `tools/steer-tool.ts` | `record.execution?.session` | `record.session` |
109
+ | `agent-menu.ts` | `record.execution?.session` | `record.session` |
110
+ | `service-adapter.ts` | `record.execution?.session` | `record.session` |
111
+ | `agent-manager.ts` | `record.execution?.session` | `record.session` (in dispose paths) |
112
+ | `foreground-runner.ts` | `fgState.toolUses` | `record.toolUses` (in final result) |
113
+ | `foreground-runner.ts` | `formatLifetimeTokens(fgState)` | `formatLifetimeTokens(record)` (in final result) |
114
+
115
+ ### NotificationManager refactoring
116
+
117
+ Replace:
118
+
119
+ ```typescript
120
+ export interface NotificationDeps { … }
121
+ constructor(private deps: NotificationDeps) {}
122
+ ```
123
+
124
+ With:
125
+
126
+ ```typescript
127
+ constructor(
128
+ private sendMessage: NotificationDeps["sendMessage"],
129
+ private agentActivity: Map<string, AgentActivityTracker>,
130
+ private markFinished: (id: string) => void,
131
+ private updateWidget: () => void,
132
+ ) {}
133
+ ```
134
+
135
+ Internal references change from `this.deps.sendMessage(…)` to `this.sendMessage(…)`.
136
+ The `NotificationDeps` interface is removed.
137
+
138
+ ### Foreground runner streaming
139
+
140
+ During foreground execution, `streamUpdate` reads `fgState.toolUses` and `formatLifetimeTokens(fgState)` for live spinner updates.
141
+ After removing these from the tracker, `streamUpdate` must read from the record instead.
142
+ The record is available only after `spawnAndWait` returns the record reference via `onSessionCreated`.
143
+ Before that callback fires, tool uses and usage are both zero — the tracker never had meaningful data at that point either.
144
+ `streamUpdate` will capture a `let recordRef: AgentRecord | undefined` and read `recordRef?.toolUses ?? 0` during the spinner phase.
145
+ After `onSessionCreated`, `recordRef` is set and subsequent spinner ticks read live values from the record.
146
+
147
+ ## Module-Level Changes
148
+
149
+ ### Modified files
150
+
151
+ 1. `src/ui/agent-activity-tracker.ts` — Remove `_toolUses`, `_lifetimeUsage`, `onToolEnd`, `onUsageUpdate`, `toolUses` getter, `lifetimeUsage` getter.
152
+ Rename remaining tool-end logic to `onToolDone`.
153
+ Remove `addUsage` and `UsageDelta` imports.
154
+ 2. `src/ui/ui-observer.ts` — Call `tracker.onToolDone` instead of `tracker.onToolEnd`.
155
+ Remove `message_end` usage accumulation block.
156
+ 3. `src/agent-record.ts` — Add `get session()` and `get outputFile()` convenience getters.
157
+ Import `AgentSession` type for the return type.
158
+ 4. `src/notification.ts` — Remove `NotificationDeps` interface.
159
+ Change `NotificationManager` constructor to plain parameters.
160
+ Replace `this.deps.*` with `this.*`.
161
+ Change `record.execution?.session` → `record.session`, `record.execution?.outputFile` → `record.outputFile`.
162
+ 5. `src/ui/agent-widget.ts` — Read `a.toolUses`, `a.lifetimeUsage`, `a.session` instead of `bg?.toolUses ?? a.toolUses` etc.
163
+ 6. `src/ui/conversation-viewer.ts` — Read `this.record.toolUses`, `this.record.lifetimeUsage`, `this.record.session` instead of fallback pattern.
164
+ 7. `src/tools/get-result-tool.ts` — `record.execution?.session` → `record.session`.
165
+ 8. `src/tools/steer-tool.ts` — `record.execution?.session` → `record.session`.
166
+ 9. `src/ui/agent-menu.ts` — `record.execution?.session` → `record.session`.
167
+ 10. `src/service-adapter.ts` — `record.execution?.session` → `record.session`.
168
+ 11. `src/agent-manager.ts` — `record.execution?.session` → `record.session` in dispose paths.
169
+ Keep the `record.execution = { session, outputFile }` assignment unchanged (that's the write path).
170
+ 12. `src/tools/foreground-runner.ts` — Read `record.toolUses` and `formatLifetimeTokens(record)` for final result.
171
+ Capture `recordRef` for streaming phase.
172
+ 13. `src/tools/helpers.ts` — `buildDetails`: read `record.toolUses` and `record.lifetimeUsage` (already does); remove optional `activity` parameter's usage of `toolUses`/`lifetimeUsage` if present (verify).
173
+ 14. `src/index.ts` — Update `NotificationManager` construction from bag to plain parameters.
174
+ 15. `src/tools/background-spawner.ts` — `record?.execution?.outputFile` → `record?.outputFile`.
175
+
176
+ ### Test files modified
177
+
178
+ 1. `test/ui/agent-activity-tracker.test.ts` — Remove tests for `toolUses`, `lifetimeUsage`, `onToolEnd`, `onUsageUpdate`.
179
+ Add tests for `onToolDone` (active-tool removal without counting).
180
+ 2. `test/ui/ui-observer.test.ts` — Update `tool_execution_end` expectations (calls `onToolDone` not `onToolEnd`).
181
+ Remove `message_end` usage accumulation tests.
182
+ 3. `test/notification.test.ts` — Update `makeDeps()` factory to use plain parameters.
183
+ Or update `NotificationManager` construction call to match new signature.
184
+ 4. `test/agent-record.test.ts` — Add tests for `session` and `outputFile` getters.
185
+ 5. `test/conversation-viewer.test.ts` — Remove mock activity tracker usage for stats; tests use record stats directly.
186
+
187
+ ## Test Impact Analysis
188
+
189
+ ### New unit tests enabled
190
+
191
+ 1. `AgentRecord.session` and `AgentRecord.outputFile` getters — straightforward getter tests on the record class.
192
+ 2. `AgentActivityTracker.onToolDone` — verifies active-tool removal without side effects on a counter.
193
+
194
+ ### Existing tests that become simpler
195
+
196
+ 1. `agent-activity-tracker.test.ts` — 4 test blocks for `toolUses`, `lifetimeUsage`, `onToolEnd`, `onUsageUpdate` can be removed.
197
+ The `onToolDone` replacement needs fewer assertions (no counter check).
198
+ 2. `ui-observer.test.ts` — The `message_end` usage accumulation tests can be removed.
199
+ The `tool_execution_end` test simplifies (verifies active-tool removal only).
200
+ 3. `notification.test.ts` — The `makeDeps()` helper changes shape but stays the same size.
201
+ 4. `conversation-viewer.test.ts` — Tests that mock `activity.toolUses` and `activity.lifetimeUsage` can simplify to reading from the record.
202
+
203
+ ### Existing tests that must stay
204
+
205
+ 1. `record-observer.test.ts` — All tests remain as-is.
206
+ The record observer is the sole source of `incrementToolUses` and `addUsage` calls now.
207
+ 2. `agent-record.test.ts` — All existing tests for `incrementToolUses`, `addUsage`, `incrementCompactions` remain.
208
+ 3. `notification.test.ts` — Pure helper tests (`escapeXml`, `getStatusLabel`, `formatTaskNotification`, `buildNotificationDetails`, `buildEventData`) remain unchanged.
209
+
210
+ ## TDD Order
211
+
212
+ 1. **Red→Green: `AgentRecord` convenience getters.**
213
+ Add tests for `record.session` and `record.outputFile` (returns `undefined` when no execution, delegates when set).
214
+ Implement the getters.
215
+ Commit: `feat: add session and outputFile convenience getters to AgentRecord (#144)`
216
+
217
+ 2. **Green→Green: Migrate callsites from `execution?.` to convenience getters.**
218
+ Update all 12 callsites across 7 files (`notification.ts`, `agent-widget.ts`, `agent-menu.ts`, `get-result-tool.ts`, `steer-tool.ts`, `service-adapter.ts`, `agent-manager.ts`, `background-spawner.ts`).
219
+ No test changes needed — existing tests pass.
220
+ Run `pnpm run check` to verify.
221
+ Commit: `refactor: use AgentRecord.session and .outputFile convenience getters (#144)`
222
+
223
+ 3. **Red→Green: Rename `onToolEnd` to `onToolDone` and remove counter.**
224
+ Update `agent-activity-tracker.test.ts`: remove `onToolEnd` counter tests, add `onToolDone` tests (active-tool removal only, no `toolUses` assertion).
225
+ Implement: rename method, remove `_toolUses++`.
226
+ Update `ui-observer.ts` to call `onToolDone`.
227
+ Update `ui-observer.test.ts` expectations.
228
+ Commit: `refactor: rename AgentActivityTracker.onToolEnd to onToolDone (#144)`
229
+
230
+ 4. **Red→Green: Remove `_toolUses` and `_lifetimeUsage` from tracker.**
231
+ Update `agent-activity-tracker.test.ts`: remove `toolUses` getter, `lifetimeUsage` getter, and `onUsageUpdate` test blocks.
232
+ Implement: remove `_toolUses`, `_lifetimeUsage`, `toolUses` getter, `lifetimeUsage` getter, `onUsageUpdate` method, `UsageDelta` type, `addUsage` import.
233
+ Update `ui-observer.ts`: remove `message_end` usage accumulation block.
234
+ Update `ui-observer.test.ts`: remove `message_end` usage tests.
235
+ Run `pnpm run check` to catch any remaining references.
236
+ Commit: `refactor: remove duplicate stats from AgentActivityTracker (#144)`
237
+
238
+ 5. **Green→Green: Migrate UI consumers to read stats from `AgentRecord`.**
239
+ Update `agent-widget.ts`: replace `bg?.toolUses ?? a.toolUses` → `a.toolUses`, `bg?.lifetimeUsage` → `a.lifetimeUsage`, `bg?.session` → `a.session`.
240
+ Update `conversation-viewer.ts`: replace `activity?.toolUses ?? record.toolUses` → `record.toolUses`, `activity?.lifetimeUsage` → `record.lifetimeUsage`, `activity?.session` → `record.session`.
241
+ Update `foreground-runner.ts`: read stats from record instead of tracker for final result.
242
+ Capture `recordRef` for streaming phase.
243
+ Update `conversation-viewer.test.ts` if any tests mock activity stats.
244
+ Commit: `refactor: read stats from AgentRecord in UI consumers (#144)`
245
+
246
+ 6. **Red→Green: Replace `NotificationDeps` with plain parameters.**
247
+ Update `notification.test.ts`: change `makeDeps()` → individual parameters in `NotificationManager` constructor calls.
248
+ Implement: remove `NotificationDeps` interface, change constructor to plain parameters, replace `this.deps.*` with `this.*`.
249
+ Update `index.ts`: change `NotificationManager` construction to pass individual arguments.
250
+ Commit: `refactor: dissolve NotificationDeps into plain constructor parameters (#144)`
251
+
252
+ ## Risks and Mitigations
253
+
254
+ | Risk | Mitigation |
255
+ | ------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
256
+ | Foreground streaming reads stale `recordRef` before `onSessionCreated` fires | Before `onSessionCreated`, both tracker and record have zero stats — behavior is unchanged. Capture `recordRef` as `undefined` initially and guard with `??`. |
257
+ | Widget reads `a.toolUses` but `a` is the snapshot from `listAgents()` — might be stale | `listAgents()` returns live `AgentRecord` references (not copies), so `a.toolUses` reflects the latest record-observer increment. |
258
+ | Removing `lifetimeUsage` from tracker breaks `formatLifetimeTokens(fgState)` in foreground runner | Step 5 explicitly migrates this callsite to `formatLifetimeTokens(record)`. The TDD order places the removal (step 4) before the migration (step 5), but step 4's `pnpm run check` will catch the type error and both steps can be merged if needed. |
259
+ | `buildDetails` in `helpers.ts` accepts an `activity` parameter for `turnCount`/`maxTurns` | `turnCount` and `maxTurns` remain on the tracker — only `toolUses` and `lifetimeUsage` are removed. `buildDetails` reads `record.toolUses` and `record.lifetimeUsage` already; it reads `activity?.turnCount` and `activity?.maxTurns` which remain valid. |
260
+
261
+ ## Open Questions
262
+
263
+ - None — the architecture doc prescribes the exact changes and the design is straightforward.