pi-subagents 0.35.0 → 0.36.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.
Files changed (76) hide show
  1. package/CHANGELOG.md +59 -0
  2. package/README.md +132 -24
  3. package/agents/advisor.md +73 -0
  4. package/package.json +8 -12
  5. package/skills/pi-subagents/SKILL.md +22 -9
  6. package/src/agents/agents.ts +22 -5
  7. package/src/api/delegation.ts +125 -0
  8. package/src/extension/config.ts +7 -1
  9. package/src/extension/index.ts +50 -38
  10. package/src/extension/rpc.ts +27 -2
  11. package/src/extension/schemas.ts +22 -2
  12. package/src/extension/tool-description.ts +2 -2
  13. package/src/intercom/intercom-bridge.ts +1 -1
  14. package/src/intercom/native-supervisor-channel.ts +45 -6
  15. package/src/intercom/result-intercom.ts +7 -0
  16. package/src/runs/background/async-execution.ts +34 -4
  17. package/src/runs/background/async-job-tracker.ts +4 -0
  18. package/src/runs/background/async-resume.ts +27 -5
  19. package/src/runs/background/async-status.ts +76 -3
  20. package/src/runs/background/chain-append.ts +2 -0
  21. package/src/runs/background/completion-batcher.ts +6 -4
  22. package/src/runs/background/completion-dedupe.ts +2 -11
  23. package/src/runs/background/fleet-view.ts +9 -4
  24. package/src/runs/background/notify.ts +132 -120
  25. package/src/runs/background/result-watcher.ts +138 -78
  26. package/src/runs/background/run-status.ts +3 -1
  27. package/src/runs/background/subagent-runner.ts +225 -43
  28. package/src/runs/background/subagent-wait.ts +130 -4
  29. package/src/runs/background/wait-tool.ts +2 -2
  30. package/src/runs/foreground/chain-execution.ts +176 -111
  31. package/src/runs/foreground/execution.ts +90 -36
  32. package/src/runs/foreground/foreground-control.ts +90 -0
  33. package/src/runs/foreground/subagent-executor.ts +394 -163
  34. package/src/runs/shared/acceptance.ts +55 -13
  35. package/src/runs/shared/agent-contract.ts +38 -0
  36. package/src/runs/shared/child-protocol.ts +1 -1
  37. package/src/runs/shared/completion-guard.ts +36 -5
  38. package/src/runs/shared/context-mode.ts +44 -0
  39. package/src/runs/shared/dynamic-fanout.ts +4 -4
  40. package/src/runs/shared/long-running-guard.ts +4 -0
  41. package/src/runs/shared/nested-events.ts +27 -2
  42. package/src/runs/shared/parallel-handoff.ts +154 -0
  43. package/src/runs/shared/parallel-utils.ts +6 -0
  44. package/src/runs/shared/pi-args.ts +23 -14
  45. package/src/runs/shared/run-history.ts +90 -5
  46. package/src/runs/shared/structured-output.ts +112 -7
  47. package/src/runs/shared/subagent-control.ts +4 -0
  48. package/src/runs/shared/subagent-prompt-runtime.ts +17 -18
  49. package/src/runs/shared/task-intent.ts +10 -5
  50. package/src/runs/shared/tool-availability.ts +3 -1
  51. package/src/runs/shared/tool-budget.ts +11 -5
  52. package/src/runs/shared/turn-budget.ts +2 -1
  53. package/src/runs/shared/worktree.ts +63 -14
  54. package/src/shared/accessible-dir.ts +25 -0
  55. package/src/shared/artifacts.ts +37 -7
  56. package/src/shared/atomic-json.ts +14 -42
  57. package/src/shared/child-transcript.ts +52 -0
  58. package/src/shared/file-system-retry.ts +47 -0
  59. package/src/shared/settings.ts +9 -1
  60. package/src/shared/types.ts +211 -22
  61. package/src/slash/delegation-adapters.ts +152 -5
  62. package/src/slash/delegation-json.ts +108 -0
  63. package/src/slash/delegation-request.ts +182 -36
  64. package/src/slash/prompt-template-bridge.ts +222 -37
  65. package/src/slash/selector.ts +147 -0
  66. package/src/slash/slash-commands.ts +14 -5
  67. package/src/slash/slash-live-state.ts +2 -2
  68. package/src/slash/subagents-admin.ts +42 -42
  69. package/src/tui/fleet-status.ts +362 -0
  70. package/src/tui/fleet-transcript.ts +472 -0
  71. package/src/tui/fleet.ts +318 -59
  72. package/src/tui/render.ts +25 -15
  73. package/src/watchdog/change-signature.ts +105 -12
  74. package/src/watchdog/review.ts +7 -2
  75. package/src/watchdog/runtime.ts +5 -3
  76. package/src/slash/subagents-editor.ts +0 -86
@@ -16,6 +16,7 @@ import {
16
16
  } from "../../shared/types.ts";
17
17
  import { readStatus } from "../../shared/utils.ts";
18
18
  import { formatNestedRunStatusLines } from "../shared/nested-render.ts";
19
+ import { contextModeLabel, summarizeContextModes } from "../shared/context-mode.ts";
19
20
  import { formatAsyncRunOutputPath, formatAsyncRunProgressLabel, listAsyncRuns, type AsyncRunSummary } from "./async-status.ts";
20
21
 
21
22
  const DEFAULT_TRANSCRIPT_LINES = 80;
@@ -277,15 +278,17 @@ function formatAsyncFleetLines(runs: AsyncRunSummary[]): string[] {
277
278
  const activity = formatActivityFacts(run);
278
279
  const cwd = run.cwd ? shortenPath(run.cwd) : shortenPath(run.asyncDir);
279
280
  const pending = run.pendingAppends ? ` | ${run.pendingAppends} pending append${run.pendingAppends === 1 ? "" : "s"}` : "";
280
- lines.push(`- ${run.id} | ${run.state}${activity ? ` | ${activity}` : ""} | ${run.mode} | ${progress}${pending} | ${cwd}`);
281
+ const runContext = contextModeLabel(run.context);
282
+ lines.push(`- ${run.id} | ${run.state}${activity ? ` | ${activity}` : ""} | ${run.mode}${runContext ? ` ${runContext}` : ""} | ${progress}${pending} | ${cwd}`);
281
283
  lines.push(` status: subagent({ action: "status", id: "${run.id}" })`);
282
284
  lines.push(` transcript: subagent({ action: "status", id: "${run.id}", view: "transcript" })`);
283
285
  for (const step of run.steps) {
284
286
  const display = step.label ? `${step.label} (${step.agent})` : step.agent;
287
+ const stepContext = contextModeLabel(step.context);
285
288
  const phase = step.phase ? `[${step.phase}] ` : "";
286
289
  const stepActivity = formatActivityFacts(step);
287
290
  const modelThinking = formatModelThinking(step.model, step.thinking);
288
- const parts = [`${step.index}. ${phase}${display}`, step.status, stepActivity, modelThinking].filter(Boolean);
291
+ const parts = [`${step.index}. ${phase}${display}${stepContext ? ` ${stepContext}` : ""}`, step.status, stepActivity, modelThinking].filter(Boolean);
289
292
  lines.push(` ${parts.join(" | ")}`);
290
293
  const output = path.join(run.asyncDir, `output-${step.index}.log`);
291
294
  if (fs.existsSync(output)) lines.push(` output: ${shortenPath(output)}`);
@@ -385,8 +388,9 @@ function selectTranscriptStep(status: AsyncStatus, options: TranscriptOptions):
385
388
  function stepStateLine(mode: SubagentRunMode, index: number | undefined, step: AsyncJobStep | undefined): string | undefined {
386
389
  if (index === undefined || !step) return undefined;
387
390
  const modelThinking = formatModelThinking(step.model, step.thinking);
391
+ const context = contextModeLabel(step.context);
388
392
  const parts = [
389
- `${mode === "parallel" ? "Agent" : "Step"}: ${index} (${step.agent})`,
393
+ `${mode === "parallel" ? "Agent" : "Step"}: ${index} (${step.agent})${context ? ` ${context}` : ""}`,
390
394
  step.status,
391
395
  formatActivityFacts(step),
392
396
  modelThinking,
@@ -428,10 +432,11 @@ export function formatAsyncRunTranscript(status: AsyncStatus, asyncDir: string,
428
432
  const sessionFile = selected.index !== undefined ? selected.step?.sessionFile : status.sessionFile;
429
433
  const eventsPath = path.join(asyncDir, "events.jsonl");
430
434
 
435
+ const context = contextModeLabel(status.context ?? summarizeContextModes((status.steps ?? []).map((step) => step.context)));
431
436
  const lines = [
432
437
  `Run: ${status.runId}`,
433
438
  `State: ${status.state}`,
434
- `Mode: ${status.mode}`,
439
+ `Mode: ${status.mode}${context ? ` ${context}` : ""}`,
435
440
  stepStateLine(status.mode, selected.index, selected.step),
436
441
  selected.hint,
437
442
  ].filter((line): line is string => Boolean(line));
@@ -1,28 +1,20 @@
1
1
  /**
2
- * Subagent completion notifications.
2
+ * Completion notification delivery.
3
3
  *
4
- * Successful (completed) async results are held briefly and emitted as a
5
- * single grouped message when sibling jobs finish within a short window (see
6
- * `completion-batcher.ts`). Failed and paused results bypass grouping and fire
7
- * immediately, flushing any held successes first, so failure and attention
8
- * signals are never delayed.
4
+ * Async result files call this notifier directly and are deleted only after
5
+ * `sendMessage()` accepts the notification. The event bus remains an
6
+ * observation channel, not a delivery acknowledgement.
9
7
  */
10
8
 
11
9
  import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
12
- import { buildCompletionKey, getGlobalSeenMap, markSeenWithTtl } from "./completion-dedupe.ts";
10
+ import { buildCompletionKey, markSeenWithTtl } from "./completion-dedupe.ts";
13
11
  import {
14
12
  type CompletionBatchConfig,
15
13
  type CompletionBatcher,
16
14
  createCompletionBatcher,
17
15
  resolveCompletionBatchConfig,
18
16
  } from "./completion-batcher.ts";
19
- import { SUBAGENT_ASYNC_COMPLETE_EVENT, SUBAGENT_FOREGROUND_COMPLETE_EVENT, type SubagentState } from "../../shared/types.ts";
20
-
21
- interface ChainStepResult {
22
- agent: string;
23
- output: string;
24
- success: boolean;
25
- }
17
+ import { SUBAGENT_ASYNC_COMPLETE_EVENT, SUBAGENT_FOREGROUND_COMPLETE_EVENT, type ParallelHandoffReference, type SubagentState } from "../../shared/types.ts";
26
18
 
27
19
  export interface SubagentNotifyDetails {
28
20
  agent: string;
@@ -33,27 +25,30 @@ export interface SubagentNotifyDetails {
33
25
  durationMs?: number;
34
26
  sessionLabel?: string;
35
27
  sessionValue?: string;
28
+ handoffPath?: string;
36
29
  }
37
30
 
38
- interface SubagentResult {
39
- id: string | null;
31
+ export interface CompletionNotification {
32
+ [key: string]: unknown;
33
+ id?: string | null;
40
34
  source?: "async" | "foreground";
41
- agent: string | null;
42
- success: boolean;
43
- summary: string;
35
+ agent?: string | null;
36
+ success?: boolean;
37
+ summary?: string;
44
38
  exitCode?: number;
45
39
  state?: string;
46
- timestamp: number;
40
+ timestamp?: number;
47
41
  durationMs?: number;
48
42
  cwd?: string;
49
43
  sessionFile?: string;
50
44
  shareUrl?: string;
51
45
  gistUrl?: string;
52
46
  shareError?: string;
53
- results?: ChainStepResult[];
54
47
  taskIndex?: number;
55
48
  totalTasks?: number;
56
49
  sessionId?: string | null;
50
+ triggerTurn?: boolean;
51
+ parallelHandoff?: ParallelHandoffReference;
57
52
  }
58
53
 
59
54
  interface NotifyTimerApi {
@@ -67,6 +62,11 @@ export interface RegisterSubagentNotifyOptions {
67
62
  now?: () => number;
68
63
  }
69
64
 
65
+ export interface CompletionNotifier {
66
+ deliver(result: CompletionNotification): Promise<boolean>;
67
+ dispose(): void;
68
+ }
69
+
70
70
  function formatSessionLine(details: SubagentNotifyDetails): string | undefined {
71
71
  if (!details.sessionValue) return undefined;
72
72
  return details.sessionLabel ? `${details.sessionLabel}: ${details.sessionValue}` : details.sessionValue;
@@ -79,6 +79,8 @@ export function formatSingleCompletion(details: SubagentNotifyDetails): string {
79
79
  `${taskKind} ${details.status}: **${details.agent}**${details.taskInfo ?? ""}`,
80
80
  "",
81
81
  details.resultPreview.trim() ? details.resultPreview : "(no output)",
82
+ details.handoffPath ? "" : undefined,
83
+ details.handoffPath ? `Parallel handoff: ${details.handoffPath}` : undefined,
82
84
  sessionLine ? "" : undefined,
83
85
  sessionLine,
84
86
  ]
@@ -99,7 +101,12 @@ export function parseSubagentNotifyContent(content: string): SubagentNotifyDetai
99
101
  }
100
102
  }
101
103
  const sessionLine = sessionIndex >= 0 ? body[sessionIndex] : undefined;
102
- const resultPreview = (sessionIndex >= 0 ? body.slice(0, sessionIndex) : body).join("\n").trim() || "(no output)";
104
+ const handoffIndex = body.findIndex((line) => line.startsWith("Parallel handoff: "));
105
+ const metadataIndexes = [sessionIndex, handoffIndex].filter((index) => index >= 0);
106
+ const firstMetadataIndex = metadataIndexes.length ? Math.min(...metadataIndexes) : body.length;
107
+ const resultEnd = firstMetadataIndex > 0 && body[firstMetadataIndex - 1]?.trim() === "" ? firstMetadataIndex - 1 : firstMetadataIndex;
108
+ const resultPreview = body.slice(0, resultEnd).join("\n").trim() || "(no output)";
109
+ const handoffPath = handoffIndex >= 0 ? body[handoffIndex]!.slice("Parallel handoff: ".length).trim() : undefined;
103
110
  let sessionLabel: string | undefined;
104
111
  let sessionValue: string | undefined;
105
112
  if (sessionLine) {
@@ -113,6 +120,7 @@ export function parseSubagentNotifyContent(content: string): SubagentNotifyDetai
113
120
  ...(match[1] === "Detached foreground task" ? { source: "foreground" as const } : {}),
114
121
  ...(match[4] ? { taskInfo: match[4] } : {}),
115
122
  resultPreview,
123
+ ...(handoffPath ? { handoffPath } : {}),
116
124
  ...(sessionLabel && sessionValue ? { sessionLabel, sessionValue } : {}),
117
125
  };
118
126
  }
@@ -126,35 +134,47 @@ export function formatGroupedCompletion(details: SubagentNotifyDetails[]): strin
126
134
  const sessionLine = formatSessionLine(detail);
127
135
  blocks.push(`${index + 1}. ${detail.agent}${detail.taskInfo ?? ""}`);
128
136
  blocks.push(detail.resultPreview.trim() ? detail.resultPreview : "(no output)");
137
+ if (detail.handoffPath) blocks.push(`Parallel handoff: ${detail.handoffPath}`);
129
138
  if (sessionLine) blocks.push(sessionLine);
130
139
  blocks.push("");
131
140
  }
132
141
  return blocks.join("\n").trimEnd();
133
142
  }
134
143
 
135
- function sendCompletion(pi: Pick<ExtensionAPI, "sendMessage">, details: SubagentNotifyDetails[]): void {
136
- if (details.length === 0) return;
137
- const content = details.length === 1
138
- ? formatSingleCompletion(details[0]!)
139
- : formatGroupedCompletion(details);
140
- pi.sendMessage(
141
- {
142
- customType: "subagent-notify",
143
- content,
144
- display: true,
145
- },
146
- { triggerTurn: true },
147
- );
144
+ interface PendingCompletion {
145
+ key: string;
146
+ details: SubagentNotifyDetails;
147
+ triggerTurn: boolean;
148
+ resolve(accepted: boolean): void;
149
+ }
150
+
151
+ function sendCompletion(pi: Pick<ExtensionAPI, "sendMessage">, items: PendingCompletion[]): boolean {
152
+ if (items.length === 0) return true;
153
+ const details = items.map((item) => item.details);
154
+ const content = details.length === 1 ? formatSingleCompletion(details[0]!) : formatGroupedCompletion(details);
155
+ try {
156
+ pi.sendMessage(
157
+ {
158
+ customType: "subagent-notify",
159
+ content,
160
+ display: true,
161
+ },
162
+ { triggerTurn: items.some((item) => item.triggerTurn) },
163
+ );
164
+ return true;
165
+ } catch {
166
+ return false;
167
+ }
148
168
  }
149
169
 
150
- function completionBatchKey(result: SubagentResult): string {
170
+ function completionBatchKey(result: CompletionNotification): string {
151
171
  const sessionId = typeof result.sessionId === "string" ? result.sessionId.trim() : "";
152
172
  if (sessionId) return `session:${sessionId}`;
153
173
  const cwd = typeof result.cwd === "string" ? result.cwd.trim() : "";
154
174
  return cwd ? `cwd:${cwd}` : "unknown";
155
175
  }
156
176
 
157
- export function buildCompletionDetails(result: SubagentResult): SubagentNotifyDetails {
177
+ export function buildCompletionDetails(result: CompletionNotification): SubagentNotifyDetails {
158
178
  const agent = result.agent ?? "unknown";
159
179
  const summary = typeof result.summary === "string" ? result.summary : "";
160
180
  const paused = !result.success && (
@@ -163,12 +183,15 @@ export function buildCompletionDetails(result: SubagentResult): SubagentNotifyDe
163
183
  || summary.startsWith("Paused after interrupt.")
164
184
  );
165
185
  const status = paused ? "paused" : result.success ? "completed" : "failed";
166
-
167
186
  const taskInfo =
168
187
  result.taskIndex !== undefined && result.totalTasks !== undefined
169
188
  ? ` (${result.taskIndex + 1}/${result.totalTasks})`
170
189
  : undefined;
171
190
 
191
+ const parallelHandoff = result.parallelHandoff && typeof result.parallelHandoff === "object"
192
+ ? result.parallelHandoff as { path?: unknown }
193
+ : undefined;
194
+ const handoffPath = typeof parallelHandoff?.path === "string" ? parallelHandoff.path : undefined;
172
195
  const session =
173
196
  result.shareUrl
174
197
  ? { label: "Session", value: result.shareUrl }
@@ -177,7 +200,6 @@ export function buildCompletionDetails(result: SubagentResult): SubagentNotifyDe
177
200
  : result.sessionFile
178
201
  ? { label: "Session file", value: result.sessionFile }
179
202
  : undefined;
180
-
181
203
  return {
182
204
  agent,
183
205
  status,
@@ -185,6 +207,7 @@ export function buildCompletionDetails(result: SubagentResult): SubagentNotifyDe
185
207
  ...(taskInfo ? { taskInfo } : {}),
186
208
  resultPreview: summary,
187
209
  ...(typeof result.durationMs === "number" ? { durationMs: result.durationMs } : {}),
210
+ ...(handoffPath ? { handoffPath } : {}),
188
211
  ...(session ? { sessionLabel: session.label, sessionValue: session.value } : {}),
189
212
  };
190
213
  }
@@ -193,102 +216,91 @@ export default function registerSubagentNotify(
193
216
  pi: ExtensionAPI,
194
217
  state: Pick<SubagentState, "currentSessionId">,
195
218
  options: RegisterSubagentNotifyOptions = {},
196
- ): () => void {
197
- const unsubscribeStoreKey = "__pi_subagents_notify_unsubscribe__";
198
- const batcherStoreKey = "__pi_subagents_notify_batcher__";
199
- const globalStore = globalThis as Record<string, unknown>;
200
- const previousUnsubscribe = globalStore[unsubscribeStoreKey];
201
- if (typeof previousUnsubscribe === "function") {
202
- try {
203
- previousUnsubscribe();
204
- } catch {
205
- // Best effort cleanup for stale handlers from an older reload.
206
- }
207
- }
208
- const previousBatcher = globalStore[batcherStoreKey];
209
- if (previousBatcher && typeof (previousBatcher as { dispose?: () => void }).dispose === "function") {
210
- try {
211
- (previousBatcher as { dispose: () => void }).dispose();
212
- } catch {
213
- // Best effort cleanup for a stale batcher from an older reload.
214
- }
215
- }
216
-
217
- const seen = getGlobalSeenMap("__pi_subagents_notify_seen__");
219
+ ): CompletionNotifier {
220
+ const seen = new Map<string, number>();
221
+ const pending = new Map<string, Promise<boolean>>();
218
222
  const ttlMs = 10 * 60 * 1000;
219
- const nowFn = options.now ?? Date.now;
223
+ const now = options.now ?? Date.now;
220
224
  const batchConfig = resolveCompletionBatchConfig(options.batchConfig);
221
- const batchers = new Map<string, CompletionBatcher<SubagentNotifyDetails>>();
225
+ const batchers = new Map<string, CompletionBatcher<PendingCompletion>>();
222
226
  let disposed = false;
223
227
 
224
- const handleComplete = (data: unknown) => {
225
- if (disposed) return;
226
- const result = data as SubagentResult;
227
- if (typeof result.sessionId !== "string" || result.sessionId !== state.currentSessionId) return;
228
- const now = nowFn();
229
- const key = buildCompletionKey(result, "notify");
230
- if (markSeenWithTtl(seen, key, now, ttlMs)) return;
231
-
232
- const details = buildCompletionDetails(result);
233
- if (result.source === "foreground") {
234
- sendCompletion(pi, [details]);
235
- return;
228
+ const settle = (items: PendingCompletion[], accepted: boolean) => {
229
+ for (const item of items) {
230
+ pending.delete(item.key);
231
+ if (accepted) markSeenWithTtl(seen, item.key, now(), ttlMs);
232
+ item.resolve(accepted);
236
233
  }
237
- const batchKey = completionBatchKey(result);
238
- let batcher = batchers.get(batchKey);
234
+ };
235
+ const emit = (items: PendingCompletion[]) => settle(items, sendCompletion(pi, items));
236
+ const getBatcher = (result: CompletionNotification) => {
237
+ const key = completionBatchKey(result);
238
+ let batcher = batchers.get(key);
239
239
  if (!batcher) {
240
- batcher = createCompletionBatcher<SubagentNotifyDetails>({
240
+ batcher = createCompletionBatcher<PendingCompletion>({
241
241
  config: batchConfig,
242
- emit: (items) => sendCompletion(pi, items),
242
+ emit,
243
243
  ...(options.timers ? { timers: options.timers } : {}),
244
- now: nowFn,
244
+ now,
245
245
  });
246
- batchers.set(batchKey, batcher);
246
+ batchers.set(key, batcher);
247
247
  }
248
+ return batcher;
249
+ };
250
+
251
+ const deliver = (result: CompletionNotification): Promise<boolean> => {
252
+ if (disposed || typeof result.sessionId !== "string" || result.sessionId !== state.currentSessionId) return Promise.resolve(false);
253
+ const key = buildCompletionKey(result, "notify");
254
+ const seenAt = seen.get(key);
255
+ if (seenAt !== undefined && now() - seenAt <= ttlMs) return Promise.resolve(true);
256
+ if (seenAt !== undefined) seen.delete(key);
257
+ const inFlight = pending.get(key);
258
+ if (inFlight) return inFlight;
259
+ const details = buildCompletionDetails(result);
260
+ let resolve!: (accepted: boolean) => void;
261
+ const completion = new Promise<boolean>((settleCompletion) => { resolve = settleCompletion; });
262
+ pending.set(key, completion);
263
+ const item: PendingCompletion = {
264
+ key,
265
+ details,
266
+ triggerTurn: result.triggerTurn !== false,
267
+ resolve,
268
+ };
269
+ if (details.source === "foreground") {
270
+ emit([item]);
271
+ return completion;
272
+ }
273
+ const batcher = getBatcher(result);
248
274
  if (details.status !== "completed") {
249
- // Failures and paused runs bypass grouping. Flush any held
250
- // successes for the same owner first so they are not stranded
251
- // behind this signal, then emit the non-completion result immediately.
252
275
  batcher.flush();
253
- sendCompletion(pi, [details]);
254
- return;
276
+ emit([item]);
277
+ return completion;
255
278
  }
256
- batcher.push(details);
279
+ batcher.push(item);
280
+ return completion;
257
281
  };
258
282
 
259
- const unsubscribers = [
260
- pi.events.on(SUBAGENT_ASYNC_COMPLETE_EVENT, handleComplete),
261
- pi.events.on(SUBAGENT_FOREGROUND_COMPLETE_EVENT, handleComplete),
262
- ].filter((unsubscribe): unsubscribe is () => void => typeof unsubscribe === "function");
263
- const batcherRegistration = { dispose };
283
+ const unsubscribeAsync = pi.events.on(SUBAGENT_ASYNC_COMPLETE_EVENT, (data) => {
284
+ void deliver(data as CompletionNotification);
285
+ });
286
+ const unsubscribeForeground = pi.events.on(SUBAGENT_FOREGROUND_COMPLETE_EVENT, (data) => {
287
+ void deliver(data as CompletionNotification);
288
+ });
264
289
 
265
- function dispose(): void {
266
- if (disposed) return;
267
- disposed = true;
268
- for (const batcher of batchers.values()) {
269
- try {
270
- batcher.dispose();
271
- } catch {
272
- // Best effort cleanup must continue through every owned batcher.
273
- }
274
- }
275
- batchers.clear();
276
- for (const unsubscribe of unsubscribers) {
277
- try {
278
- unsubscribe();
279
- } catch {
280
- // Best effort cleanup must continue through every owned handler.
290
+ return {
291
+ deliver,
292
+ dispose() {
293
+ if (disposed) return;
294
+ disposed = true;
295
+ for (const batcher of batchers.values()) settle(batcher.dispose(), false);
296
+ batchers.clear();
297
+ for (const unsubscribe of [unsubscribeAsync, unsubscribeForeground]) {
298
+ try {
299
+ unsubscribe?.();
300
+ } catch {
301
+ // The runtime is already shutting down; pending records stay on disk.
302
+ }
281
303
  }
282
- }
283
- if (globalStore[unsubscribeStoreKey] === dispose) {
284
- delete globalStore[unsubscribeStoreKey];
285
- }
286
- if (globalStore[batcherStoreKey] === batcherRegistration) {
287
- delete globalStore[batcherStoreKey];
288
- }
289
- }
290
-
291
- globalStore[unsubscribeStoreKey] = dispose;
292
- globalStore[batcherStoreKey] = batcherRegistration;
293
- return dispose;
304
+ },
305
+ };
294
306
  }