@gotgenes/pi-subagents 6.6.0 → 6.8.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,40 @@
1
+ ---
2
+ issue: 118
3
+ issue_title: "refactor(pi-subagents): SettingsManager apply methods — eliminate cross-collaborator orchestration"
4
+ ---
5
+
6
+ # Retro: #118 — SettingsManager apply methods
7
+
8
+ ## Final Retrospective (2026-05-21T21:00:00Z)
9
+
10
+ ### Session summary
11
+
12
+ Planned and implemented 3 `apply*` methods on `SettingsManager` (`applyMaxConcurrent`, `applyDefaultMaxTurns`, `applyGraceTurns`) across 5 TDD cycles plus doc updates, released as `pi-subagents-v6.6.0`.
13
+ Each method owns the full consequence chain (normalize → set → callback → persist → emit → return toast), eliminating the LoD/Tell-Don't-Ask violation in `showSettings` that was identified during the #109 retro.
14
+ `notifyConcurrencyChanged` was removed from `AgentMenuManager`; the menu no longer coordinates between settings and the agent manager.
15
+
16
+ ### Observations
17
+
18
+ #### What went well
19
+
20
+ - **Retro-driven improvement validated.**
21
+ Issue #118 was filed during the #109 retro as a LoD/Tell-Don't-Ask follow-up, and the plan-issue prompt's consumer call-site sketch heuristic (added in #109's retro) was already in the plan template.
22
+ The plan for #118 included concrete before/after call-site sketches that made the design unambiguous — no `ask-user` decision needed.
23
+ - **Interface-then-wiring TDD order worked cleanly.**
24
+ The #109 retro noted that interface changes propagate to `index.ts` immediately, forcing unplanned bridge edits.
25
+ This time the plan accounted for it: Cycle 4 committed only menu files (leaving a known `index.ts` type error), and Cycle 5 fixed the wiring in a separate commit.
26
+ The intermediate type error was contained and expected.
27
+ - **`defaultMaxTurns` branch consolidation.**
28
+ During Cycle 4, the separate `n === 0` and `n >= 1` branches in `showSettings` were consolidated to a single `n >= 0` check, since `applyDefaultMaxTurns` handles the 0→unlimited mapping internally.
29
+ This was a minor but correct simplification that emerged naturally from the Tell-Don't-Ask refactor.
30
+
31
+ #### What caused friction (agent side)
32
+
33
+ - No material friction.
34
+ All 5 TDD cycles completed without rework, failed edits, or unexpected test failures.
35
+ The plan was tight and the issue's "Proposed change" section was unambiguous.
36
+
37
+ #### What caused friction (user side)
38
+
39
+ - No material friction observed.
40
+ The session ran end-to-end (plan → implement → ship → release) without user intervention.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gotgenes/pi-subagents",
3
- "version": "6.6.0",
3
+ "version": "6.8.0",
4
4
  "exports": {
5
5
  ".": "./src/service.ts"
6
6
  },
@@ -13,11 +13,13 @@ import { AgentRecord } from "./agent-record.js";
13
13
  import type { AgentRunner } from "./agent-runner.js";
14
14
  import { AgentTypeRegistry } from "./agent-types.js";
15
15
  import { debugLog } from "./debug.js";
16
+ import type { ExecutionState } from "./execution-state.js";
16
17
  import { buildParentSnapshot } from "./parent-snapshot.js";
17
18
  import { subscribeRecordObserver } from "./record-observer.js";
18
19
  import type { RunConfig } from "./runtime.js";
19
20
  import type { AgentInvocation, IsolationMode, ParentSnapshot, ShellExec, SubagentType, ThinkingLevel } from "./types.js";
20
21
  import type { WorktreeManager } from "./worktree.js";
22
+ import { WorktreeState } from "./worktree-state.js";
21
23
 
22
24
  export type OnAgentComplete = (record: AgentRecord) => void;
23
25
  export type OnAgentStart = (record: AgentRecord) => void;
@@ -92,6 +94,8 @@ export class AgentManager {
92
94
  private queue: { id: string; args: SpawnArgs }[] = [];
93
95
  /** Number of currently running background agents. */
94
96
  private runningBackground = 0;
97
+ /** Steers buffered for agents whose session hasn’t been created yet. */
98
+ private pendingSteers = new Map<string, string[]>();
95
99
 
96
100
  constructor(options: AgentManagerOptions) {
97
101
  this.runner = options.runner;
@@ -116,6 +120,19 @@ export class AgentManager {
116
120
  this.drainQueue();
117
121
  }
118
122
 
123
+ /**
124
+ * Buffer a steer message for an agent whose session isn’t ready yet.
125
+ * Returns false if the agent id is not tracked (already cleaned up or unknown).
126
+ * Called by steer-tool and service-adapter when record.execution is undefined.
127
+ */
128
+ queueSteer(id: string, message: string): boolean {
129
+ if (!this.agents.has(id)) return false;
130
+ const steers = this.pendingSteers.get(id) ?? [];
131
+ steers.push(message);
132
+ this.pendingSteers.set(id, steers);
133
+ return true;
134
+ }
135
+
119
136
  /**
120
137
  * Spawn an agent and return its ID immediately (for background use).
121
138
  * If the concurrency limit is reached, the agent is queued.
@@ -173,7 +190,7 @@ export class AgentManager {
173
190
  'Initialize git and commit at least once, or omit `isolation`.',
174
191
  );
175
192
  }
176
- record.worktree = wt;
193
+ record.worktreeState = new WorktreeState(wt);
177
194
  worktreeCwd = wt.path;
178
195
  }
179
196
 
@@ -207,17 +224,18 @@ export class AgentManager {
207
224
  signal: record.abortController!.signal,
208
225
  registry: this.registry,
209
226
  onSessionCreated: (session) => {
210
- record.session = session;
211
227
  // Capture the session file path early so it's available for display
212
228
  // before the run completes (e.g. in background agent status messages).
213
- const file = session.sessionManager?.getSessionFile?.();
214
- if (file) record.outputFile = file;
229
+ const outputFile = session.sessionManager?.getSessionFile?.() ?? undefined;
230
+ // Set the execution-state collaborator — born complete at session creation.
231
+ record.execution = { session, outputFile };
215
232
  // Flush any steers that arrived before the session was ready
216
- if (record.pendingSteers?.length) {
217
- for (const msg of record.pendingSteers) {
233
+ const buffered = this.pendingSteers.get(id);
234
+ if (buffered?.length) {
235
+ for (const msg of buffered) {
218
236
  session.steer(msg).catch(() => {});
219
237
  }
220
- record.pendingSteers = undefined;
238
+ this.pendingSteers.delete(id);
221
239
  }
222
240
  // Subscribe record observer for stats accumulation
223
241
  unsubRecordObserver = subscribeRecordObserver(session, record, {
@@ -232,9 +250,9 @@ export class AgentManager {
232
250
 
233
251
  // Clean up worktree before transition so the final result includes branch text
234
252
  let finalResult = responseText;
235
- if (record.worktree) {
236
- const wtResult = this.worktrees.cleanup(record.worktree, options.description);
237
- record.worktreeResult = wtResult;
253
+ if (record.worktreeState) {
254
+ const wtResult = this.worktrees.cleanup(record.worktreeState, options.description);
255
+ record.worktreeState.recordCleanup(wtResult);
238
256
  if (wtResult.hasChanges && wtResult.branch) {
239
257
  finalResult += `\n\n---\nChanges saved to branch \`${wtResult.branch}\`. Merge with: \`git merge ${wtResult.branch}\``;
240
258
  }
@@ -245,8 +263,8 @@ export class AgentManager {
245
263
  else if (steered) record.markSteered(finalResult);
246
264
  else record.markCompleted(finalResult);
247
265
 
248
- record.session = session;
249
- if (sessionFile) record.outputFile = sessionFile;
266
+ // Update execution collaborator with final session/outputFile from runner
267
+ record.execution = { session, outputFile: sessionFile ?? record.execution?.outputFile };
250
268
 
251
269
  if (options.isBackground) {
252
270
  this.runningBackground--;
@@ -262,10 +280,11 @@ export class AgentManager {
262
280
  detach();
263
281
 
264
282
  // Best-effort worktree cleanup on error
265
- if (record.worktree) {
283
+ if (record.worktreeState) {
266
284
  try {
267
- const wtResult = this.worktrees.cleanup(record.worktree, options.description);
268
- record.worktreeResult = wtResult;
285
+ const wtResult = this.worktrees.cleanup(record.worktreeState, options.description);
286
+ record.worktreeState.recordCleanup(wtResult);
287
+
269
288
  } catch (err) { debugLog("cleanupWorktree on agent error", err); }
270
289
  }
271
290
 
@@ -322,16 +341,17 @@ export class AgentManager {
322
341
  signal?: AbortSignal,
323
342
  ): Promise<AgentRecord | undefined> {
324
343
  const record = this.agents.get(id);
325
- if (!record?.session) return undefined;
344
+ const session = record?.execution?.session;
345
+ if (!session) return undefined;
326
346
 
327
347
  record.resetForResume(Date.now());
328
348
 
329
- const unsubResume = subscribeRecordObserver(record.session, record, {
349
+ const unsubResume = subscribeRecordObserver(session, record, {
330
350
  onCompact: (r, info) => this.onCompact?.(r, info),
331
351
  });
332
352
 
333
353
  try {
334
- const responseText = await this.runner.resume(record.session, prompt, {
354
+ const responseText = await this.runner.resume(session, prompt, {
335
355
  signal,
336
356
  });
337
357
  record.markCompleted(responseText);
@@ -373,9 +393,9 @@ export class AgentManager {
373
393
 
374
394
  /** Dispose a record's session and remove it from the map. */
375
395
  private removeRecord(id: string, record: AgentRecord): void {
376
- record.session?.dispose?.();
377
- record.session = undefined;
396
+ record.execution?.session?.dispose?.();
378
397
  this.agents.delete(id);
398
+ this.pendingSteers.delete(id);
379
399
  }
380
400
 
381
401
  private cleanup() {
@@ -448,7 +468,7 @@ export class AgentManager {
448
468
  // Clear queue
449
469
  this.queue = [];
450
470
  for (const record of this.agents.values()) {
451
- record.session?.dispose();
471
+ record.execution?.session?.dispose();
452
472
  }
453
473
  this.agents.clear();
454
474
  // Prune any orphaned git worktrees (crash recovery)
@@ -5,12 +5,19 @@
5
5
  * by the class and exposed via transition methods. External code reads these
6
6
  * fields through public properties but cannot write them directly.
7
7
  *
8
- * Non-transition state (session, toolUses, lifetimeUsage, etc.) remains public.
8
+ * Stats (toolUses, lifetimeUsage, compactionCount) are owned by the class and
9
+ * accumulated via mutation methods (incrementToolUses, addUsage, incrementCompactions).
10
+ *
11
+ * Phase-specific collaborators (execution, worktreeState, notification) are attached
12
+ * after construction as lifecycle information becomes available.
9
13
  */
10
14
 
11
- import type { AgentSession } from "@earendil-works/pi-coding-agent";
15
+ import type { ExecutionState } from "./execution-state.js";
16
+ import type { NotificationState } from "./notification-state.js";
12
17
  import type { AgentInvocation, SubagentType } from "./types.js";
13
18
  import type { LifetimeUsage } from "./usage.js";
19
+ import { addUsage } from "./usage.js";
20
+ import type { WorktreeState } from "./worktree-state.js";
14
21
 
15
22
  export type AgentRecordStatus =
16
23
  | "queued"
@@ -30,19 +37,9 @@ export interface AgentRecordInit {
30
37
  completedAt?: number;
31
38
  result?: string;
32
39
  error?: string;
33
- toolUses?: number;
34
- lifetimeUsage?: LifetimeUsage;
35
- compactionCount?: number;
36
40
  abortController?: AbortController;
37
41
  invocation?: AgentInvocation;
38
- session?: AgentSession;
39
42
  promise?: Promise<string>;
40
- resultConsumed?: boolean;
41
- pendingSteers?: string[];
42
- worktree?: { path: string; branch: string };
43
- worktreeResult?: { hasChanges: boolean; branch?: string };
44
- toolCallId?: string;
45
- outputFile?: string;
46
43
  }
47
44
 
48
45
  export class AgentRecord {
@@ -68,19 +65,25 @@ export class AgentRecord {
68
65
  private _completedAt?: number;
69
66
  get completedAt(): number | undefined { return this._completedAt; }
70
67
 
71
- // Non-transition mutable state
72
- toolUses: number;
73
- lifetimeUsage: LifetimeUsage;
74
- compactionCount: number;
75
- session?: AgentSession;
76
- abortController?: AbortController;
68
+ // Stats accumulated via mutation methods, readable via getters
69
+ private _toolUses: number;
70
+ get toolUses(): number { return this._toolUses; }
71
+
72
+ private _lifetimeUsage: LifetimeUsage;
73
+ get lifetimeUsage(): Readonly<LifetimeUsage> { return this._lifetimeUsage; }
74
+
75
+ private _compactionCount: number;
76
+ get compactionCount(): number { return this._compactionCount; }
77
+
78
+ /** AbortController for cancelling this agent. Set at construction; used only by AgentManager. */
79
+ readonly abortController?: AbortController;
80
+ /** Promise for the full agent run (including post-processing). Set once by AgentManager. */
77
81
  promise?: Promise<string>;
78
- resultConsumed?: boolean;
79
- pendingSteers?: string[];
80
- worktree?: { path: string; branch: string };
81
- worktreeResult?: { hasChanges: boolean; branch?: string };
82
- toolCallId?: string;
83
- outputFile?: string;
82
+
83
+ // Phase-specific collaborators — each born complete when their info becomes available
84
+ execution?: ExecutionState;
85
+ worktreeState?: WorktreeState;
86
+ notification?: NotificationState;
84
87
 
85
88
  constructor(init: AgentRecordInit) {
86
89
  this.id = init.id;
@@ -94,18 +97,26 @@ export class AgentRecord {
94
97
  this._startedAt = init.startedAt ?? Date.now();
95
98
  this._completedAt = init.completedAt;
96
99
 
97
- this.toolUses = init.toolUses ?? 0;
98
- this.lifetimeUsage = init.lifetimeUsage ?? { input: 0, output: 0, cacheWrite: 0 };
99
- this.compactionCount = init.compactionCount ?? 0;
100
+ this._toolUses = 0;
101
+ this._lifetimeUsage = { input: 0, output: 0, cacheWrite: 0 };
102
+ this._compactionCount = 0;
100
103
  this.abortController = init.abortController;
101
- this.session = init.session;
102
104
  this.promise = init.promise;
103
- this.resultConsumed = init.resultConsumed;
104
- this.pendingSteers = init.pendingSteers;
105
- this.worktree = init.worktree;
106
- this.worktreeResult = init.worktreeResult;
107
- this.toolCallId = init.toolCallId;
108
- this.outputFile = init.outputFile;
105
+ }
106
+
107
+ /** Increment tool use count. Called by record-observer on tool_execution_end. */
108
+ incrementToolUses(): void {
109
+ this._toolUses++;
110
+ }
111
+
112
+ /** Accumulate a usage delta into lifetimeUsage. Called by record-observer on message_end. */
113
+ addUsage(delta: { input: number; output: number; cacheWrite: number }): void {
114
+ addUsage(this._lifetimeUsage, delta);
115
+ }
116
+
117
+ /** Increment compaction count. Called by record-observer on compaction_end. */
118
+ incrementCompactions(): void {
119
+ this._compactionCount++;
109
120
  }
110
121
 
111
122
  /** Transition to running state. Sets status and startedAt. */
@@ -0,0 +1,17 @@
1
+ /**
2
+ * execution-state.ts — ExecutionState: execution-phase state for a running agent.
3
+ *
4
+ * Constructed and attached to AgentRecord when onSessionCreated fires inside startAgent().
5
+ * Contains the session and output file — the two fields that become known once the
6
+ * runner creates the session. promise stays as a separate AgentRecord field because
7
+ * it is set at a different moment (after runner.run() returns).
8
+ */
9
+
10
+ import type { AgentSession } from "@earendil-works/pi-coding-agent";
11
+
12
+ export interface ExecutionState {
13
+ /** The active agent session — available from the moment the session is created. */
14
+ readonly session: AgentSession;
15
+ /** Path to the agent's session JSONL file, or undefined if not yet available. */
16
+ readonly outputFile: string | undefined;
17
+ }
package/src/index.ts CHANGED
@@ -88,7 +88,7 @@ export default function (pi: ExtensionAPI) {
88
88
  });
89
89
 
90
90
  // Skip notification if result was already consumed via get_subagent_result
91
- if (record.resultConsumed) {
91
+ if (record.notification?.resultConsumed) {
92
92
  notifications.cleanupCompleted(record.id);
93
93
  return;
94
94
  }
@@ -215,6 +215,7 @@ export default function (pi: ExtensionAPI) {
215
215
  getRecord: (id) => manager.getRecord(id),
216
216
  emitEvent: (name, data) => pi.events.emit(name, data),
217
217
  steerAgent: (session, message) => steerAgent(session, message),
218
+ queueSteer: (id, message) => manager.queueSteer(id, message),
218
219
  })));
219
220
 
220
221
  // ---- /agents interactive menu ----
@@ -0,0 +1,27 @@
1
+ /**
2
+ * notification-state.ts — NotificationState: notification-scoped tracking per background agent.
3
+ *
4
+ * Constructed once when agent-tool assigns the tool call ID (background agents only).
5
+ * Foreground agents never get a NotificationState — record.notification stays undefined.
6
+ */
7
+
8
+ export class NotificationState {
9
+ /** The tool call ID that spawned this background agent. Used in task-notification XML. */
10
+ readonly toolCallId: string;
11
+
12
+ private _resultConsumed = false;
13
+
14
+ constructor(toolCallId: string) {
15
+ this.toolCallId = toolCallId;
16
+ }
17
+
18
+ /** Whether the parent agent has already consumed this result (suppresses duplicate notifications). */
19
+ get resultConsumed(): boolean {
20
+ return this._resultConsumed;
21
+ }
22
+
23
+ /** Mark the result as consumed — suppresses the completion notification. */
24
+ markConsumed(): void {
25
+ this._resultConsumed = true;
26
+ }
27
+ }
@@ -1,6 +1,6 @@
1
1
  import { debugLog } from "./debug.js";
2
2
  import type { AgentRecord, NotificationDetails } from "./types.js";
3
- import type { AgentActivity } from "./ui/agent-widget.js";
3
+ import type { AgentActivityTracker } from "./ui/agent-activity-tracker.js";
4
4
  import { getLifetimeTotal, getSessionContextPercent } from "./usage.js";
5
5
 
6
6
  // ---- Pure helpers (exported for unit testing) ----
@@ -31,7 +31,7 @@ export function formatTaskNotification(record: AgentRecord, resultMaxLen: number
31
31
  const status = getStatusLabel(record.status, record.error);
32
32
  const durationMs = record.completedAt ? record.completedAt - record.startedAt : 0;
33
33
  const totalTokens = getLifetimeTotal(record.lifetimeUsage);
34
- const contextPercent = getSessionContextPercent(record.session);
34
+ const contextPercent = getSessionContextPercent(record.execution?.session);
35
35
  const ctxXml = contextPercent !== null ? `<context_percent>${Math.round(contextPercent)}</context_percent>` : "";
36
36
  const compactXml = record.compactionCount ? `<compactions>${record.compactionCount}</compactions>` : "";
37
37
 
@@ -41,11 +41,13 @@ export function formatTaskNotification(record: AgentRecord, resultMaxLen: number
41
41
  : record.result
42
42
  : "No output.";
43
43
 
44
+ const toolCallId = record.notification?.toolCallId;
45
+ const outputFile = record.execution?.outputFile;
44
46
  return [
45
47
  "<task-notification>",
46
48
  `<task-id>${record.id}</task-id>`,
47
- record.toolCallId ? `<tool-use-id>${escapeXml(record.toolCallId)}</tool-use-id>` : null,
48
- record.outputFile ? `<output-file>${escapeXml(record.outputFile)}</output-file>` : null,
49
+ toolCallId ? `<tool-use-id>${escapeXml(toolCallId)}</tool-use-id>` : null,
50
+ outputFile ? `<output-file>${escapeXml(outputFile)}</output-file>` : null,
49
51
  `<status>${escapeXml(status)}</status>`,
50
52
  `<summary>Agent "${escapeXml(record.description)}" ${record.status}</summary>`,
51
53
  `<result>${escapeXml(resultPreview)}</result>`,
@@ -60,7 +62,7 @@ export function formatTaskNotification(record: AgentRecord, resultMaxLen: number
60
62
  export function buildNotificationDetails(
61
63
  record: AgentRecord,
62
64
  resultMaxLen: number,
63
- activity?: AgentActivity,
65
+ activity?: AgentActivityTracker,
64
66
  ): NotificationDetails {
65
67
  const totalTokens = getLifetimeTotal(record.lifetimeUsage);
66
68
 
@@ -73,7 +75,7 @@ export function buildNotificationDetails(
73
75
  maxTurns: activity?.maxTurns,
74
76
  totalTokens,
75
77
  durationMs: record.completedAt ? record.completedAt - record.startedAt : 0,
76
- outputFile: record.outputFile,
78
+ outputFile: record.execution?.outputFile,
77
79
  error: record.error,
78
80
  resultPreview: record.result
79
81
  ? record.result.length > resultMaxLen
@@ -113,7 +115,7 @@ export interface NotificationDeps {
113
115
  msg: { customType: string; content: string; display: boolean; details?: unknown },
114
116
  opts?: { triggerTurn?: boolean; deliverAs?: "steer" | "followUp" | "nextTurn" },
115
117
  ) => void;
116
- agentActivity: Map<string, AgentActivity>;
118
+ agentActivity: Map<string, AgentActivityTracker>;
117
119
  markFinished: (id: string) => void;
118
120
  updateWidget: () => void;
119
121
  }
@@ -154,10 +156,11 @@ export function createNotificationSystem(deps: NotificationDeps): NotificationSy
154
156
  }
155
157
 
156
158
  function emitIndividualNudge(record: AgentRecord) {
157
- if (record.resultConsumed) return;
159
+ if (record.notification?.resultConsumed) return;
158
160
 
159
161
  const notification = formatTaskNotification(record, 500);
160
- const footer = record.outputFile ? `\nFull transcript available at: ${record.outputFile}` : "";
162
+ const outputFile = record.execution?.outputFile;
163
+ const footer = outputFile ? `\nFull transcript available at: ${outputFile}` : "";
161
164
 
162
165
  deps.sendMessage(
163
166
  {
@@ -7,7 +7,6 @@
7
7
 
8
8
  import type { CompactionInfo } from "./agent-manager.js";
9
9
  import type { AgentRecord } from "./agent-record.js";
10
- import { addUsage } from "./usage.js";
11
10
 
12
11
  /** Narrow session interface — only the subscribe method needed by the observer. */
13
12
  interface SubscribableSession {
@@ -22,9 +21,9 @@ export interface RecordObserverOptions {
22
21
  * Subscribe to session events and accumulate stats on the agent record.
23
22
  *
24
23
  * Handles:
25
- * - `tool_execution_end` → `record.toolUses++`
26
- * - `message_end` (assistant, with usage) → `addUsage(record.lifetimeUsage, …)`
27
- * - `compaction_end` (not aborted) → `record.compactionCount++`, call `onCompact`
24
+ * - `tool_execution_end` → `record.incrementToolUses()`
25
+ * - `message_end` (assistant, with usage) → `record.addUsage(…)`
26
+ * - `compaction_end` (not aborted) → `record.incrementCompactions()`, call `onCompact`
28
27
  *
29
28
  * @returns An unsubscribe function.
30
29
  */
@@ -35,13 +34,13 @@ export function subscribeRecordObserver(
35
34
  ): () => void {
36
35
  return session.subscribe((event: any) => {
37
36
  if (event.type === "tool_execution_end") {
38
- record.toolUses++;
37
+ record.incrementToolUses();
39
38
  }
40
39
 
41
40
  if (event.type === "message_end" && event.message?.role === "assistant") {
42
41
  const u = event.message.usage;
43
42
  if (u) {
44
- addUsage(record.lifetimeUsage, {
43
+ record.addUsage({
45
44
  input: u.input ?? 0,
46
45
  output: u.output ?? 0,
47
46
  cacheWrite: u.cacheWrite ?? 0,
@@ -50,7 +49,7 @@ export function subscribeRecordObserver(
50
49
  }
51
50
 
52
51
  if (event.type === "compaction_end" && !event.aborted && event.result) {
53
- record.compactionCount++;
52
+ record.incrementCompactions();
54
53
  options?.onCompact?.(record, {
55
54
  reason: event.reason,
56
55
  tokensBefore: event.result.tokensBefore,
package/src/runtime.ts CHANGED
@@ -6,7 +6,8 @@
6
6
  * Follows the same pattern as pi-permission-system's ExtensionRuntime.
7
7
  */
8
8
 
9
- import type { AgentActivity, AgentWidget, UICtx } from "./ui/agent-widget.js";
9
+ import type { AgentActivityTracker } from "./ui/agent-activity-tracker.js";
10
+ import type { AgentWidget, UICtx } from "./ui/agent-widget.js";
10
11
 
11
12
  /**
12
13
  * Narrow config subset read by AgentManager when constructing RunOptions.
@@ -31,7 +32,7 @@ export class SubagentRuntime {
31
32
  * Per-agent live activity state shared across the notification system,
32
33
  * widget, and tool handlers. The Map itself is never replaced.
33
34
  */
34
- readonly agentActivity: Map<string, AgentActivity> = new Map();
35
+ readonly agentActivity: Map<string, AgentActivityTracker> = new Map();
35
36
  /**
36
37
  * Persistent widget reference. Null until constructed after AgentManager.
37
38
  * Delegation methods use optional chaining so callers never need `widget!`.
@@ -17,6 +17,7 @@ export interface AgentManagerLike {
17
17
  abort(id: string): boolean;
18
18
  waitForAll(): Promise<void>;
19
19
  hasRunning(): boolean;
20
+ queueSteer(id: string, message: string): boolean;
20
21
  }
21
22
 
22
23
  /** Dependencies injected into the adapter factory. */
@@ -85,13 +86,12 @@ export function createSubagentsService(deps: AdapterDeps): SubagentsService {
85
86
  if (!record || record.status !== "running") {
86
87
  return false;
87
88
  }
88
- if (!record.session) {
89
- // Session not ready yet — queue for delivery once initialized
90
- if (!record.pendingSteers) record.pendingSteers = [];
91
- record.pendingSteers.push(message);
92
- return true;
89
+ const session = record.execution?.session;
90
+ if (!session) {
91
+ // Session not ready yet — queue via manager for delivery once initialized
92
+ return manager.queueSteer(id, message);
93
93
  }
94
- await record.session.steer(message);
94
+ await session.steer(message);
95
95
  return true;
96
96
  },
97
97
 
@@ -124,7 +124,8 @@ export function toSubagentRecord(record: AgentRecord): SubagentRecord {
124
124
  if (record.result !== undefined) out.result = record.result;
125
125
  if (record.error !== undefined) out.error = record.error;
126
126
  if (record.completedAt !== undefined) out.completedAt = record.completedAt;
127
- if (record.worktreeResult !== undefined) out.worktreeResult = record.worktreeResult;
127
+ const worktreeResult = record.worktreeState?.cleanupResult;
128
+ if (worktreeResult !== undefined) out.worktreeResult = worktreeResult;
128
129
 
129
130
  return out;
130
131
  }