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.
- package/CHANGELOG.md +59 -0
- package/README.md +132 -24
- package/agents/advisor.md +73 -0
- package/package.json +8 -12
- package/skills/pi-subagents/SKILL.md +22 -9
- package/src/agents/agents.ts +22 -5
- package/src/api/delegation.ts +125 -0
- package/src/extension/config.ts +7 -1
- package/src/extension/index.ts +50 -38
- package/src/extension/rpc.ts +27 -2
- package/src/extension/schemas.ts +22 -2
- package/src/extension/tool-description.ts +2 -2
- package/src/intercom/intercom-bridge.ts +1 -1
- package/src/intercom/native-supervisor-channel.ts +45 -6
- package/src/intercom/result-intercom.ts +7 -0
- package/src/runs/background/async-execution.ts +34 -4
- package/src/runs/background/async-job-tracker.ts +4 -0
- package/src/runs/background/async-resume.ts +27 -5
- package/src/runs/background/async-status.ts +76 -3
- package/src/runs/background/chain-append.ts +2 -0
- package/src/runs/background/completion-batcher.ts +6 -4
- package/src/runs/background/completion-dedupe.ts +2 -11
- package/src/runs/background/fleet-view.ts +9 -4
- package/src/runs/background/notify.ts +132 -120
- package/src/runs/background/result-watcher.ts +138 -78
- package/src/runs/background/run-status.ts +3 -1
- package/src/runs/background/subagent-runner.ts +225 -43
- package/src/runs/background/subagent-wait.ts +130 -4
- package/src/runs/background/wait-tool.ts +2 -2
- package/src/runs/foreground/chain-execution.ts +176 -111
- package/src/runs/foreground/execution.ts +90 -36
- package/src/runs/foreground/foreground-control.ts +90 -0
- package/src/runs/foreground/subagent-executor.ts +394 -163
- package/src/runs/shared/acceptance.ts +55 -13
- package/src/runs/shared/agent-contract.ts +38 -0
- package/src/runs/shared/child-protocol.ts +1 -1
- package/src/runs/shared/completion-guard.ts +36 -5
- package/src/runs/shared/context-mode.ts +44 -0
- package/src/runs/shared/dynamic-fanout.ts +4 -4
- package/src/runs/shared/long-running-guard.ts +4 -0
- package/src/runs/shared/nested-events.ts +27 -2
- package/src/runs/shared/parallel-handoff.ts +154 -0
- package/src/runs/shared/parallel-utils.ts +6 -0
- package/src/runs/shared/pi-args.ts +23 -14
- package/src/runs/shared/run-history.ts +90 -5
- package/src/runs/shared/structured-output.ts +112 -7
- package/src/runs/shared/subagent-control.ts +4 -0
- package/src/runs/shared/subagent-prompt-runtime.ts +17 -18
- package/src/runs/shared/task-intent.ts +10 -5
- package/src/runs/shared/tool-availability.ts +3 -1
- package/src/runs/shared/tool-budget.ts +11 -5
- package/src/runs/shared/turn-budget.ts +2 -1
- package/src/runs/shared/worktree.ts +63 -14
- package/src/shared/accessible-dir.ts +25 -0
- package/src/shared/artifacts.ts +37 -7
- package/src/shared/atomic-json.ts +14 -42
- package/src/shared/child-transcript.ts +52 -0
- package/src/shared/file-system-retry.ts +47 -0
- package/src/shared/settings.ts +9 -1
- package/src/shared/types.ts +211 -22
- package/src/slash/delegation-adapters.ts +152 -5
- package/src/slash/delegation-json.ts +108 -0
- package/src/slash/delegation-request.ts +182 -36
- package/src/slash/prompt-template-bridge.ts +222 -37
- package/src/slash/selector.ts +147 -0
- package/src/slash/slash-commands.ts +14 -5
- package/src/slash/slash-live-state.ts +2 -2
- package/src/slash/subagents-admin.ts +42 -42
- package/src/tui/fleet-status.ts +362 -0
- package/src/tui/fleet-transcript.ts +472 -0
- package/src/tui/fleet.ts +318 -59
- package/src/tui/render.ts +25 -15
- package/src/watchdog/change-signature.ts +105 -12
- package/src/watchdog/review.ts +7 -2
- package/src/watchdog/runtime.ts +5 -3
- 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
|
-
|
|
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
|
-
*
|
|
2
|
+
* Completion notification delivery.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
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,
|
|
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
|
|
39
|
-
|
|
31
|
+
export interface CompletionNotification {
|
|
32
|
+
[key: string]: unknown;
|
|
33
|
+
id?: string | null;
|
|
40
34
|
source?: "async" | "foreground";
|
|
41
|
-
agent
|
|
42
|
-
success
|
|
43
|
-
summary
|
|
35
|
+
agent?: string | null;
|
|
36
|
+
success?: boolean;
|
|
37
|
+
summary?: string;
|
|
44
38
|
exitCode?: number;
|
|
45
39
|
state?: string;
|
|
46
|
-
timestamp
|
|
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
|
|
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
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
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:
|
|
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:
|
|
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
|
-
):
|
|
197
|
-
const
|
|
198
|
-
const
|
|
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
|
|
223
|
+
const now = options.now ?? Date.now;
|
|
220
224
|
const batchConfig = resolveCompletionBatchConfig(options.batchConfig);
|
|
221
|
-
const batchers = new Map<string, CompletionBatcher<
|
|
225
|
+
const batchers = new Map<string, CompletionBatcher<PendingCompletion>>();
|
|
222
226
|
let disposed = false;
|
|
223
227
|
|
|
224
|
-
const
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
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
|
-
|
|
238
|
-
|
|
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<
|
|
240
|
+
batcher = createCompletionBatcher<PendingCompletion>({
|
|
241
241
|
config: batchConfig,
|
|
242
|
-
emit
|
|
242
|
+
emit,
|
|
243
243
|
...(options.timers ? { timers: options.timers } : {}),
|
|
244
|
-
now
|
|
244
|
+
now,
|
|
245
245
|
});
|
|
246
|
-
batchers.set(
|
|
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
|
-
|
|
254
|
-
return;
|
|
276
|
+
emit([item]);
|
|
277
|
+
return completion;
|
|
255
278
|
}
|
|
256
|
-
batcher.push(
|
|
279
|
+
batcher.push(item);
|
|
280
|
+
return completion;
|
|
257
281
|
};
|
|
258
282
|
|
|
259
|
-
const
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
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
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
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
|
-
|
|
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
|
}
|