@gotgenes/pi-subagents 6.1.0 → 6.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,454 @@
1
+ ---
2
+ issue: 100
3
+ issue_title: "Replace callback threading with direct session-event subscription"
4
+ ---
5
+
6
+ # Replace callback threading with direct session-event subscription
7
+
8
+ ## Problem Statement
9
+
10
+ `SpawnOptions` carries 6 `on*` callback fields that thread through three layers: `agent-tool.ts` creates activity-tracking callbacks → `AgentManager.startAgent()` wraps each to update the record, then forwards → `runner.run()` subscribes to session events and translates them into callback invocations.
11
+ The session already emits all of these events via `session.subscribe()`.
12
+ Three layers reimplement what two independent subscriptions could provide.
13
+
14
+ The same pattern repeats in `resume()`, which hand-rolls 3 of the 4 callback wrappers.
15
+
16
+ ## Goals
17
+
18
+ - Replace the 3-layer callback chain with two direct session subscribers: a **record observer** (AgentManager, updates record stats) and a **UI observer** (agent-tool, streams widget state).
19
+ - Drop 5 `on*` callback fields from `SpawnOptions` (keep `onSessionCreated` as the session-delivery mechanism).
20
+ - Drop 5 `on*` callback fields from `RunOptions`.
21
+ - Drop 3 `on*` callback fields from `ResumeOptions`.
22
+ - Simplify `startAgent()` from ~80 lines of callback wiring to ~20 lines of observer setup.
23
+ - Eliminate duplicated callback wiring between `startAgent()` and `resume()`.
24
+
25
+ ## Non-Goals
26
+
27
+ - Extracting turn-limit enforcement from the runner — it stays as the runner's own subscription, just simplified.
28
+ A follow-up could extract it into a `turn-limiter.ts` module.
29
+ - Changing the public `SubagentsService` API in `service.ts` — the public `SpawnOptions` type is unaffected.
30
+ - Removing `onSessionCreated` — it remains as the session-delivery bridge between the runner and external subscribers.
31
+ - Changing `collectResponseText` or `forwardAbortSignal` in the runner — those are the runner's own concerns.
32
+
33
+ ## Background
34
+
35
+ ### Prerequisite status
36
+
37
+ - Issue #98 (AgentRecord state machine) — **done**.
38
+ `AgentRecord` is a class with encapsulated transition methods (`markRunning`, `markCompleted`, `markError`, etc.).
39
+ The record observer uses public mutable fields (`toolUses`, `lifetimeUsage`, `compactionCount`) and the `addUsage` helper.
40
+ - Issue #99 (ParentSnapshot) — **done**.
41
+ `runner.run()` accepts `ParentSnapshot`, `SpawnArgs` stores a snapshot, `AgentManager` has `exec: ShellExec` injected.
42
+
43
+ ### Current callback threading
44
+
45
+ In `startAgent()`, AgentManager wraps 4 of the 6 callbacks to interleave record mutations:
46
+
47
+ | Callback | Record mutation | Forwarding |
48
+ | ------------------ | ------------------------------------------------------------- | ------------------------------------- |
49
+ | `onToolActivity` | `record.toolUses++` on end | Forward to `options.onToolActivity` |
50
+ | `onAssistantUsage` | `addUsage(record.lifetimeUsage, …)` | Forward to `options.onAssistantUsage` |
51
+ | `onCompaction` | `record.compactionCount++` + `this.onCompact?.(record, info)` | Forward to `options.onCompaction` |
52
+ | `onSessionCreated` | Store session, capture file, flush steers | Forward to `options.onSessionCreated` |
53
+ | `onTextDelta` | None | Pass-through |
54
+ | `onTurnEnd` | None | Pass-through |
55
+
56
+ In `resume()`, 3 callbacks are wired manually (onToolActivity, onAssistantUsage, onCompaction) with no forwarding — only record mutations.
57
+
58
+ The runner's big `session.subscribe()` block (lines 323–370 of `agent-runner.ts`) mixes turn-limit enforcement with event→callback translation for all 6 callback types.
59
+
60
+ ### Code-design constraints
61
+
62
+ - **Parameter relay** (code-design skill): the 5 UI callbacks thread through `SpawnOptions` → `startAgent()` wrapping → `RunOptions` → runner subscription.
63
+ The intermediary (`startAgent`) only uses them to interleave record mutations.
64
+ - **Scattered resets** (design-review): `resume()` hand-rolls 3 of the same 4 callback wrappers from `startAgent()`.
65
+
66
+ ### Relevant modules
67
+
68
+ | Module | Role in this change |
69
+ | ------------------------- | -------------------------------------------------------------------- |
70
+ | `src/agent-manager.ts` | Owns `SpawnOptions`, `startAgent()`, `resume()` — primary target |
71
+ | `src/agent-runner.ts` | Owns `RunOptions`, `ResumeOptions`, runner subscription — simplifies |
72
+ | `src/tools/agent-tool.ts` | Owns `createActivityTracker()` — replaced by UI observer |
73
+ | `src/ui/agent-widget.ts` | Defines `AgentActivity` interface — unchanged |
74
+ | `src/agent-record.ts` | `AgentRecord` class — unchanged (observer writes to public fields) |
75
+ | `src/usage.ts` | `addUsage` helper — unchanged, used by record observer |
76
+
77
+ ## Design Overview
78
+
79
+ ### Two independent observers
80
+
81
+ ```text
82
+ session.subscribe()
83
+
84
+ ┌─────────────┼─────────────┐
85
+ │ │
86
+ Record observer UI observer
87
+ (accumulates stats on record) (updates widget state)
88
+ managed by AgentManager managed by agent-tool
89
+ subscribes in onSessionCreated subscribes in onSessionCreated
90
+ unsubscribes in .then/.catch unsubscribes on completion
91
+ ```
92
+
93
+ Both subscribe to the same session but update independent state.
94
+ Neither wraps or forwards the other's events.
95
+
96
+ ### Record observer
97
+
98
+ New module `src/record-observer.ts`:
99
+
100
+ ```typescript
101
+ export interface RecordObserverOptions {
102
+ onCompact?: (record: AgentRecord, info: CompactionInfo) => void;
103
+ }
104
+
105
+ export function subscribeRecordObserver(
106
+ session: AgentSession,
107
+ record: AgentRecord,
108
+ options?: RecordObserverOptions,
109
+ ): () => void;
110
+ ```
111
+
112
+ Handles three event types:
113
+
114
+ | Session event | Record mutation |
115
+ | ------------------------------------------- | ---------------------------------------------------- |
116
+ | `tool_execution_end` | `record.toolUses++` |
117
+ | `message_end` (assistant, with usage) | `addUsage(record.lifetimeUsage, usage)` |
118
+ | `compaction_end` (not aborted, with result) | `record.compactionCount++`, call `options.onCompact` |
119
+
120
+ These are the exact mutations currently scattered across `startAgent()` and `resume()` callback wrappers.
121
+
122
+ The returned function unsubscribes from the session.
123
+
124
+ ### UI observer
125
+
126
+ New module `src/ui/ui-observer.ts`:
127
+
128
+ ```typescript
129
+ export function subscribeUIObserver(
130
+ session: AgentSession,
131
+ state: AgentActivity,
132
+ onUpdate?: () => void,
133
+ ): () => void;
134
+ ```
135
+
136
+ Handles six event types — the same events currently translated by `createActivityTracker`'s callbacks:
137
+
138
+ | Session event | State mutation |
139
+ | ------------------------------------- | --------------------------------------------------- |
140
+ | `tool_execution_start` | Add to `state.activeTools` |
141
+ | `tool_execution_end` | Remove from `state.activeTools`, `state.toolUses++` |
142
+ | `message_start` | Reset `state.responseText` |
143
+ | `message_update` (text_delta) | Append to `state.responseText` |
144
+ | `turn_end` | `state.turnCount++` |
145
+ | `message_end` (assistant, with usage) | `addUsage(state.lifetimeUsage, usage)` |
146
+
147
+ Calls `onUpdate?.()` after each mutation (matching current `onStreamUpdate` behavior for foreground rendering).
148
+
149
+ The returned function unsubscribes from the session.
150
+
151
+ ### SpawnOptions after the change
152
+
153
+ ```typescript
154
+ export interface SpawnOptions {
155
+ description: string;
156
+ model?: Model<any>;
157
+ maxTurns?: number;
158
+ isolated?: boolean;
159
+ inheritContext?: boolean;
160
+ thinkingLevel?: ThinkingLevel;
161
+ isBackground?: boolean;
162
+ bypassQueue?: boolean;
163
+ isolation?: IsolationMode;
164
+ invocation?: AgentInvocation;
165
+ signal?: AbortSignal;
166
+ parentSessionFile?: string;
167
+ parentSessionId?: string;
168
+ /** Called when the session is created — the one remaining callback. */
169
+ onSessionCreated?: (session: AgentSession) => void;
170
+ }
171
+ ```
172
+
173
+ Drops: `onToolActivity`, `onTextDelta`, `onTurnEnd`, `onAssistantUsage`, `onCompaction`.
174
+
175
+ ### RunOptions after the change
176
+
177
+ ```typescript
178
+ export interface RunOptions {
179
+ exec: ShellExec;
180
+ model?: Model<any>;
181
+ maxTurns?: number;
182
+ signal?: AbortSignal;
183
+ isolated?: boolean;
184
+ thinkingLevel?: ThinkingLevel;
185
+ cwd?: string;
186
+ parentSessionFile?: string;
187
+ parentSessionId?: string;
188
+ defaultMaxTurns?: number;
189
+ graceTurns?: number;
190
+ /** Called once after session creation — session delivery mechanism. */
191
+ onSessionCreated?: (session: AgentSession) => void;
192
+ }
193
+ ```
194
+
195
+ Drops: `onToolActivity`, `onTextDelta`, `onTurnEnd`, `onAssistantUsage`, `onCompaction`.
196
+
197
+ ### ResumeOptions after the change
198
+
199
+ ```typescript
200
+ export interface ResumeOptions {
201
+ signal?: AbortSignal;
202
+ }
203
+ ```
204
+
205
+ Drops: `onToolActivity`, `onAssistantUsage`, `onCompaction`.
206
+
207
+ ### Runner simplification
208
+
209
+ The runner's big `session.subscribe()` block in `runAgent()` (currently ~50 lines mixing turn-limit enforcement with callback forwarding) simplifies to turn-limit enforcement only (~15 lines):
210
+
211
+ ```typescript
212
+ const unsubTurns = session.subscribe((event) => {
213
+ if (event.type === "turn_end") {
214
+ turnCount++;
215
+ if (maxTurns != null) {
216
+ if (!softLimitReached && turnCount >= maxTurns) {
217
+ softLimitReached = true;
218
+ session.steer("...");
219
+ } else if (softLimitReached && turnCount >= maxTurns + grace) {
220
+ aborted = true;
221
+ session.abort();
222
+ }
223
+ }
224
+ }
225
+ });
226
+ ```
227
+
228
+ `collectResponseText(session)` and `forwardAbortSignal(session, signal)` remain unchanged — they are the runner's own concerns.
229
+
230
+ `resumeAgent()` drops its conditional event subscription entirely — only `collectResponseText` and `forwardAbortSignal` remain.
231
+
232
+ ### Unsubscription strategy
233
+
234
+ | Observer | Subscribe point | Unsubscribe point |
235
+ | ------------------- | -------------------------- | ---------------------------- |
236
+ | Record (startAgent) | `onSessionCreated` handler | `.then()` and `.catch()` |
237
+ | Record (resume) | Before `runner.resume()` | `finally` block |
238
+ | UI (foreground) | `onSessionCreated` handler | After `spawnAndWait` returns |
239
+ | UI (background) | `onSessionCreated` handler | Session disposal on cleanup |
240
+
241
+ ### createActivityTracker replacement
242
+
243
+ `createActivityTracker()` in `agent-tool.ts` is replaced by:
244
+
245
+ 1. Inline `AgentActivity` state construction (the state object is simple enough).
246
+ 2. In the `onSessionCreated` callback: `subscribeUIObserver(session, state, onStreamUpdate)`.
247
+
248
+ The `callbacks` spread pattern (`...bgCallbacks` / `...fgCallbacks`) disappears entirely.
249
+
250
+ ## Module-Level Changes
251
+
252
+ ### New files
253
+
254
+ 1. `src/record-observer.ts` — `subscribeRecordObserver()` function.
255
+ 2. `test/record-observer.test.ts` — unit tests with mock session.
256
+ 3. `src/ui/ui-observer.ts` — `subscribeUIObserver()` function.
257
+ 4. `test/ui/ui-observer.test.ts` — unit tests with mock session.
258
+
259
+ ### Changed files (source)
260
+
261
+ 1. `src/agent-manager.ts` — Wire record observer in `onSessionCreated`; simplify callback wrappers to pass-through (cycle 3); drop 5 `on*` fields from `SpawnOptions` (cycle 4).
262
+ 2. `src/tools/agent-tool.ts` — Replace `createActivityTracker` with inline state + `subscribeUIObserver`; drop callback spread from spawn calls (cycle 4).
263
+ 3. `src/agent-runner.ts` — Drop 5 `on*` fields from `RunOptions`; drop 3 `on*` fields from `ResumeOptions`; simplify `runAgent()` subscription to turn-limit only; remove conditional subscription from `resumeAgent()` (cycle 5).
264
+
265
+ ### Changed files (tests)
266
+
267
+ 1. `test/agent-manager.test.ts` — Upgrade `mockSession()` to support `subscribe()`; update 3 stat-verification tests to emit events through mock session instead of calling callbacks on `RunOptions` (cycle 3).
268
+ 2. `test/agent-runner.test.ts` — Drop tests for callback forwarding (`onAssistantUsage` wiring, `onCompaction` forwarding); runner tests focus on turn-limit enforcement and session creation (cycle 5).
269
+ 3. `test/agent-runner-extension-tools.test.ts` — Drop callback-related fields from `RunOptions` mock construction (cycle 5).
270
+
271
+ ### Unchanged files
272
+
273
+ - `src/service.ts` — public API unchanged (its `SpawnOptions` is a separate type).
274
+ - `src/agent-record.ts` — public mutable fields used by observer, no changes needed.
275
+ - `src/usage.ts` — `addUsage` helper used by observer, unchanged.
276
+ - `src/ui/agent-widget.ts` — `AgentActivity` interface unchanged; widget reads state as before.
277
+ - `src/session-config.ts`, `src/context.ts`, `src/env.ts` — unrelated.
278
+ - `src/service-adapter.ts` — `AgentManagerLike.spawn` signature unchanged (onSessionCreated is already optional on SpawnOptions).
279
+ - `test/tools/agent-tool.test.ts` — tests use mocked `deps.manager` and don't exercise callback wiring; spawn mock shape doesn't change.
280
+ - `test/service-adapter.test.ts` — tests mock `AgentManagerLike`, unaffected by internal SpawnOptions changes.
281
+
282
+ ## Test Impact Analysis
283
+
284
+ ### New tests enabled by the extraction
285
+
286
+ 1. `subscribeRecordObserver` tested in isolation with a mock session and real `AgentRecord`.
287
+ Previously impossible — record stat updates were interleaved with callback wrapping inside `startAgent()`.
288
+ 2. `subscribeUIObserver` tested in isolation with a mock session and `AgentActivity` state object.
289
+ Previously impossible — UI state updates were buried in `createActivityTracker` closures that required a full spawn flow to exercise.
290
+
291
+ ### Existing tests that simplify
292
+
293
+ 1. `agent-manager.test.ts` stat tests — currently simulate callbacks by having the mock runner call `opts.onAssistantUsage?.(...)`.
294
+ After the change, mock runners call `opts.onSessionCreated?.(session)` and tests emit events through the mock session.
295
+ The pattern is more realistic (events drive state, not callbacks).
296
+ 2. `agent-runner.test.ts` — callback forwarding tests (`onAssistantUsage wiring`, `onCompaction forwarding`) become unnecessary since the runner no longer translates events to callbacks.
297
+ Turn-limit tests remain and simplify (no callback interleaving).
298
+
299
+ ### Existing tests that stay as-is
300
+
301
+ 1. All `agent-manager.test.ts` lifecycle tests (spawn, abort, queue drain, worktree, resume flow) — they verify AgentManager behavior and don't depend on callback wiring.
302
+ Only the mock construction (`mockSession`) gains a `subscribe` method.
303
+ 2. `agent-record.test.ts` — state-machine transitions are unrelated.
304
+ 3. `agent-runner-extension-tools.test.ts` — tool filtering tests are unrelated to callbacks.
305
+ 4. `test/tools/agent-tool.test.ts` — tests mock the manager; spawn/spawnAndWait call shapes don't change from the test's perspective.
306
+
307
+ ## TDD Order
308
+
309
+ ### Phase 1: Extract observers (additive, no breaking changes)
310
+
311
+ #### Cycle 1: Record observer module
312
+
313
+ Test surface: `test/record-observer.test.ts` (new).
314
+
315
+ Tests:
316
+
317
+ - `tool_execution_end` event increments `record.toolUses`.
318
+ - `message_end` (assistant, with usage) accumulates into `record.lifetimeUsage`.
319
+ - `compaction_end` (not aborted) increments `record.compactionCount` and calls `onCompact`.
320
+ - `compaction_end` with `aborted: true` is ignored.
321
+ - Returned function unsubscribes from session.
322
+
323
+ Source changes:
324
+
325
+ - `src/record-observer.ts`: `subscribeRecordObserver()` function.
326
+
327
+ Commit: `feat: add record observer for direct session subscription (#100)`
328
+
329
+ #### Cycle 2: UI observer module
330
+
331
+ Test surface: `test/ui/ui-observer.test.ts` (new).
332
+
333
+ Tests:
334
+
335
+ - `tool_execution_start` adds to `state.activeTools`, calls `onUpdate`.
336
+ - `tool_execution_end` removes from `state.activeTools`, increments `state.toolUses`, calls `onUpdate`.
337
+ - `message_start` resets `state.responseText`.
338
+ - `message_update` (text_delta) appends to `state.responseText`, calls `onUpdate`.
339
+ - `turn_end` increments `state.turnCount`, calls `onUpdate`.
340
+ - `message_end` (assistant, with usage) accumulates into `state.lifetimeUsage`, calls `onUpdate`.
341
+ - Returned function unsubscribes from session.
342
+
343
+ Source changes:
344
+
345
+ - `src/ui/ui-observer.ts`: `subscribeUIObserver()` function.
346
+
347
+ Commit: `feat: add UI observer for direct session subscription (#100)`
348
+
349
+ ### Phase 2: AgentManager uses record observer
350
+
351
+ #### Cycle 3: Wire record observer into startAgent and resume
352
+
353
+ Test surface: `test/agent-manager.test.ts` (updated).
354
+
355
+ This cycle replaces the record-mutation logic in `startAgent()` and `resume()` callback wrappers with `subscribeRecordObserver`.
356
+ The 5 UI callbacks (`onToolActivity` through `onCompaction`) are still accepted on `SpawnOptions` and forwarded to `RunOptions` as pass-through (no wrapping).
357
+ They are removed in cycle 4.
358
+
359
+ Source changes:
360
+
361
+ - `src/agent-manager.ts`:
362
+ - Import `subscribeRecordObserver`.
363
+ - In `startAgent()`: subscribe record observer inside `onSessionCreated` handler; remove `record.toolUses++` from `onToolActivity` wrapper (pass through `options.onToolActivity`); remove `addUsage` from `onAssistantUsage` wrapper (pass through); remove `record.compactionCount++` from `onCompaction` wrapper (pass through, `this.onCompact` moves to observer's `onCompact` option); capture unsubscribe function, call in `.then()` and `.catch()`.
364
+ - In `resume()`: subscribe record observer to `record.session` before calling `runner.resume()`; drop `onToolActivity`, `onAssistantUsage`, `onCompaction` args from `runner.resume()` call; unsubscribe in `finally` block.
365
+
366
+ Test changes:
367
+
368
+ - `test/agent-manager.test.ts`:
369
+ - Upgrade `mockSession()` to support `subscribe()` and provide a test helper `emit()` method.
370
+ - Update the `onAssistantUsage` test: mock runner calls `opts.onSessionCreated?.(session)`, then emits `message_end` events through the mock session.
371
+ - Update the `onCompaction` test: same pattern — emit `compaction_end` events through mock session.
372
+ - Update the `resume()` accumulation test: mock session emits events during `runner.resume()`.
373
+
374
+ Run: full test suite + `pnpm run check`.
375
+
376
+ Commit: `refactor: AgentManager subscribes record observer directly (#100)`
377
+
378
+ ### Phase 3: Agent-tool uses UI observer
379
+
380
+ #### Cycle 4: Replace createActivityTracker with UI observer, drop SpawnOptions callbacks
381
+
382
+ Test surface: `test/tools/agent-tool.test.ts` (verified unchanged), `test/agent-manager.test.ts` (verified compatible).
383
+
384
+ This cycle replaces `createActivityTracker` with inline `AgentActivity` state construction and `subscribeUIObserver` subscription via `onSessionCreated`.
385
+ `SpawnOptions` drops 5 `on*` fields.
386
+
387
+ Source changes:
388
+
389
+ - `src/tools/agent-tool.ts`:
390
+ - Import `subscribeUIObserver` from `../ui/ui-observer.js`.
391
+ - Remove `createActivityTracker` function.
392
+ - Background path: construct `AgentActivity` state inline; pass `onSessionCreated` callback that subscribes UI observer; remove `...bgCallbacks` spread.
393
+ - Foreground path: construct `AgentActivity` state inline; pass `onSessionCreated` callback that subscribes UI observer, registers activity in widget, and captures unsubscribe; remove `...fgCallbacks` spread; call UI unsubscribe after `spawnAndWait` returns.
394
+ - `src/agent-manager.ts`:
395
+ - `SpawnOptions`: remove `onToolActivity`, `onTextDelta`, `onTurnEnd`, `onAssistantUsage`, `onCompaction` fields.
396
+ - `startAgent()`: remove pass-through forwarding of the 5 dropped callbacks to `RunOptions`.
397
+
398
+ Run: full test suite + `pnpm run check`.
399
+
400
+ Commit: `refactor: agent-tool subscribes UI observer, drop SpawnOptions callbacks (#100)`
401
+
402
+ ### Phase 4: Runner drops callback forwarding
403
+
404
+ #### Cycle 5: RunOptions and ResumeOptions drop callback fields, runner simplifies
405
+
406
+ Test surface: `test/agent-runner.test.ts` (updated), `test/agent-runner-extension-tools.test.ts` (updated).
407
+
408
+ Source changes:
409
+
410
+ - `src/agent-runner.ts`:
411
+ - `RunOptions`: remove `onToolActivity`, `onTextDelta`, `onTurnEnd`, `onAssistantUsage`, `onCompaction` fields.
412
+ - `ResumeOptions`: remove `onToolActivity`, `onAssistantUsage`, `onCompaction` fields (keep `signal`).
413
+ - `runAgent()`: simplify the big `session.subscribe()` block to turn-limit enforcement only (turn_end → count, steer, abort).
414
+ Remove message_start/message_update/tool_execution_start/tool_execution_end/message_end/compaction_end handling.
415
+ - `resumeAgent()`: remove the conditional `session.subscribe()` block entirely (no callbacks to forward).
416
+ Keep `collectResponseText` and `forwardAbortSignal`.
417
+ - Remove `ToolActivity` export if no longer needed externally (check consumers).
418
+
419
+ Test changes:
420
+
421
+ - `test/agent-runner.test.ts`:
422
+ - Remove or simplify callback forwarding tests (onAssistantUsage wiring, onCompaction forwarding, tool activity forwarding).
423
+ - Turn-limit tests remain, with simplified setup (no callback expectations).
424
+ - `runAgent()` call sites drop the 5 callback fields.
425
+ - `test/agent-runner-extension-tools.test.ts`:
426
+ - Drop callback-related fields from `runAgent()` call construction.
427
+
428
+ Run: full test suite + `pnpm run check`.
429
+
430
+ Commit: `refactor: RunOptions and ResumeOptions drop callback fields (#100)`
431
+
432
+ ## Risks and Mitigations
433
+
434
+ | Risk | Mitigation |
435
+ | ----------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------- |
436
+ | Double-counting during transition (old callbacks + new observer both update record) | Cycle 3 replaces record mutations atomically — old wrappers become pass-through in the same commit that adds the observer |
437
+ | Mock session complexity increases (need `subscribe` + `emit` helper) | One-time investment; upgrade `mockSession()` once, reused across all tests; more realistic than simulating callbacks |
438
+ | Event ordering between record observer and UI observer | Observers update independent state (record vs AgentActivity); no cross-dependency, ordering irrelevant |
439
+ | `ToolActivity` type used outside this package | Grep before removing; if used, keep the export and add a deprecation note |
440
+ | `createActivityTracker` exported but unused externally | Verified: only used within `agent-tool.ts`; safe to remove |
441
+ | Turn-limit enforcement mixed with callback subscription in runner | Cycle 5 extracts turn-limit into its own subscription cleanly; `collectResponseText` stays separate |
442
+ | Unsubscription missed on error paths | Record observer unsubscribe captured in closure, called in both `.then()` and `.catch()`; resume uses `finally` |
443
+
444
+ ## Open Questions
445
+
446
+ - Whether to extract turn-limit enforcement from the runner into a separate `turn-limiter.ts` module.
447
+ This would improve testability but is orthogonal to callback elimination.
448
+ Deferred — the runner's subscription simplifies enough in cycle 5.
449
+ - Whether `ResumeOptions` should be renamed or inlined since it reduces to `{ signal?: AbortSignal }`.
450
+ Keeping it as a named type is fine for extensibility.
451
+ Deferred — cosmetic, can be done anytime.
452
+ - Whether to introduce a `SessionLike` interface for the mock session pattern used in `record-observer.test.ts` and `ui-observer.test.ts`.
453
+ Both tests need a minimal session with `subscribe()`.
454
+ If the pattern proves reusable, extract it; otherwise keep the inline mock.
@@ -0,0 +1,46 @@
1
+ ---
2
+ issue: 98
3
+ issue_title: "Extract AgentRecord state machine from scattered status transitions"
4
+ ---
5
+
6
+ # Retro: #98 — Extract AgentRecord state machine
7
+
8
+ ## Final Retrospective (2026-05-20T23:45:00Z)
9
+
10
+ ### Session summary
11
+
12
+ Converted `AgentRecord` from a plain interface to a class with 7 encapsulated transition methods, centralizing the scattered status-transition logic from 6 locations in `agent-manager.ts`.
13
+ The prerequisite issue #102 (shared test factory) made the class conversion a 2-file change instead of an 8-file rewrite.
14
+ Released as `pi-subagents-v6.1.0`.
15
+
16
+ ### Observations
17
+
18
+ #### What went well
19
+
20
+ - The "encapsulate last" TDD strategy (cycles 1–5 with public fields, cycle 6 makes them private) let the compiler verify migration completeness before locking down access.
21
+ `get-result-tool.test.ts` and `make-record.test.ts` failures were caught immediately.
22
+ - The worktree-reorder design insight (compute final result including branch text before calling the transition method) avoided the need for an `appendToResult()` method and survived implementation unchanged.
23
+ - The prerequisite issue #102 (shared `createTestRecord` factory) proved essential — the class conversion touched only the factory and `agent-manager.ts`, not 8 individual test files.
24
+
25
+ #### What caused friction (agent side)
26
+
27
+ - `premature-convergence` — The initial plan proposed `MutableAgentRecord` alongside the existing interface, which was the lowest-friction path but the wrong abstraction.
28
+ The user redirected twice ("It should definitely become a class" → "I meant encapsulate") before the design was right.
29
+ Impact: full plan rewrite and an extra prerequisite issue (#102).
30
+ The plan template's "use ask-user for ambiguous design choices" instruction applied (class vs wrapper was genuinely ambiguous), but I didn't invoke it.
31
+
32
+ - `missing-context` — The revised plan stated "No test files need updating (except the shared factory)" but missed `{ ...baseRecord, field }` spread patterns in `test/notification.test.ts`, `test/service-adapter.test.ts`, `test/tools/get-result-tool.test.ts`, and `test/helpers/make-record.test.ts`.
33
+ Spreading a class instance produces a plain object that lacks the class's methods.
34
+ Impact: 4 extra test files needed mechanical fixes in cycles 4 and 6; plan's "Unchanged files" list was wrong.
35
+ The compiler caught all of them, so no behavioral risk, but the plan should have predicted the churn.
36
+
37
+ #### What caused friction (user side)
38
+
39
+ - The issue body was intentionally flexible ("onto AgentRecord (or a thin wrapper)"), which left the wrapper-vs-class decision open.
40
+ The user's preference for a class with encapsulated state became clear only after seeing the initial plan.
41
+ Earlier signal (e.g., labeling the issue with a "class conversion" tag or stating "must encapsulate" in the issue body) would have avoided the plan rewrite.
42
+
43
+ ### Changes made
44
+
45
+ 1. `.pi/skills/testing/SKILL.md` — Added interface→class spread-pattern rule to TDD planning rules.
46
+ 2. `.pi/skills/package-pi-subagents/SKILL.md` — Added `agent-record.ts` to the Core engine module table and updated `types.ts` description.
@@ -0,0 +1,37 @@
1
+ ---
2
+ issue: 99
3
+ issue_title: "Replace live `ctx` capture with ParentSnapshot in AgentManager"
4
+ ---
5
+
6
+ # Retro: #99 — Replace live ctx capture with ParentSnapshot
7
+
8
+ ## Final Retrospective (2026-05-20T20:30:00-04:00)
9
+
10
+ ### Session summary
11
+
12
+ Implemented the `ParentSnapshot` extraction — Step 2 of the AgentManager internal decomposition.
13
+ The user prompted a mid-planning restructure using Kent Beck's "make the change that makes the change easy" principle, which split the work into two clean phases: pi-elimination prep (cycles 1–2), then snapshot introduction (cycles 3–5).
14
+ All 5 cycles landed first-try with no rework, releasing as `pi-subagents-v6.2.0`.
15
+
16
+ ### Observations
17
+
18
+ #### What went well
19
+
20
+ - The two-phase plan structure (prep then payload) kept each cycle focused on one concern.
21
+ Cycle 5 was particularly clean — only 2 files changed because `buildParentSnapshot` was mocked in agent-manager tests.
22
+ - `sed` for mechanical bulk replacements (27 `spawn(mockPi,` → `spawn(mockCtx,`) was the right tool — faster and less error-prone than 27 individual edits.
23
+ - The `pnpm run check` after cycle 4 caught exactly the expected type error in `agent-manager.ts`, confirming the incremental approach was working as designed.
24
+
25
+ #### What caused friction (agent side)
26
+
27
+ - `wrong-abstraction` — Initial plan (before user intervention) interleaved pi-elimination and snapshot concerns across all 5 cycles.
28
+ The user had to invoke Kent Beck's principle to trigger the restructure.
29
+ Impact: one plan rewrite (amend commit), but no implementation rework since it happened before coding started.
30
+
31
+ - `missing-context` — Used `Parameters<typeof createAgentSession>[0]["modelRegistry"]` as a type cast without checking that the SDK's `createAgentSession` parameter type is a union including `undefined`.
32
+ Impact: minimal — caught immediately by `pnpm run check`, fixed with a simple `as any` in the same cycle.
33
+
34
+ #### What caused friction (user side)
35
+
36
+ - The user's "make the change easy" prompt was well-timed — after the plan was written but before implementation.
37
+ No suggestions for improvement here; the intervention was strategic and well-placed.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gotgenes/pi-subagents",
3
- "version": "6.1.0",
3
+ "version": "6.3.0",
4
4
  "exports": {
5
5
  ".": "./src/service.ts"
6
6
  },