@arhen/pi-core-subagent 1.3.66 → 1.3.67
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/README.md +1 -1
- package/package.json +3 -9
- package/src/delivery.ts +744 -0
- package/src/manager.ts +76 -1022
- package/src/outcome.ts +58 -0
- package/src/run-widget.ts +122 -0
- package/src/runtime.ts +136 -0
- package/src/types.ts +9 -0
package/src/manager.ts
CHANGED
|
@@ -2,13 +2,7 @@ import { existsSync, mkdirSync, readdirSync, readFileSync, realpathSync, rmSync
|
|
|
2
2
|
import { rename, rm, writeFile } from "node:fs/promises";
|
|
3
3
|
import { basename, dirname, join, relative, sep } from "node:path";
|
|
4
4
|
import type { ThinkingLevel } from "@earendil-works/pi-agent-core";
|
|
5
|
-
import {
|
|
6
|
-
type Api,
|
|
7
|
-
type AssistantMessage,
|
|
8
|
-
clampThinkingLevel,
|
|
9
|
-
getSupportedThinkingLevels,
|
|
10
|
-
type Model,
|
|
11
|
-
} from "@earendil-works/pi-ai";
|
|
5
|
+
import type { Api, AssistantMessage, Model } from "@earendil-works/pi-ai";
|
|
12
6
|
import {
|
|
13
7
|
type AgentSessionEvent,
|
|
14
8
|
createAgentSession,
|
|
@@ -16,33 +10,39 @@ import {
|
|
|
16
10
|
type ExtensionAPI,
|
|
17
11
|
type ExtensionContext,
|
|
18
12
|
getAgentDir,
|
|
19
|
-
ModelRuntime,
|
|
20
13
|
SessionManager,
|
|
21
14
|
type ToolDefinition,
|
|
22
15
|
} from "@earendil-works/pi-coding-agent";
|
|
23
|
-
import type { TUI } from "@earendil-works/pi-tui";
|
|
24
16
|
import { resolveAgentFile } from "./agentfile.ts";
|
|
25
17
|
import { CHILD_TALK_TOOLS, type ChildHandlers, createChildTools } from "./child.ts";
|
|
26
18
|
import { readSubagentConfig, writeSubagentConfig } from "./config.ts";
|
|
27
|
-
import {
|
|
28
|
-
|
|
29
|
-
describeCall,
|
|
30
|
-
getFirstText,
|
|
31
|
-
isTalking,
|
|
32
|
-
makeAskNotice,
|
|
33
|
-
makeNotice,
|
|
34
|
-
makeTaskArtifactNotice,
|
|
35
|
-
makeTaskNotice,
|
|
36
|
-
SubagentsWidget,
|
|
37
|
-
truncateText,
|
|
38
|
-
} from "./format.ts";
|
|
19
|
+
import { LeaderDelivery } from "./delivery.ts";
|
|
20
|
+
import { activitySnippet, describeCall, getFirstText, truncateText } from "./format.ts";
|
|
39
21
|
import { applyUpstream, resolveNeeds, runWaveScheduler } from "./graph.ts";
|
|
40
22
|
import { createMailbox, type Mailbox } from "./mailbox.ts";
|
|
41
23
|
import { chooseModel, resolveChildModel } from "./models.ts";
|
|
42
|
-
import {
|
|
24
|
+
import { textFingerprint } from "./notification-state.ts";
|
|
25
|
+
import {
|
|
26
|
+
aggregateUsage,
|
|
27
|
+
classifyFailure,
|
|
28
|
+
cloneRun,
|
|
29
|
+
emptyUsage,
|
|
30
|
+
failureError,
|
|
31
|
+
lastAssistantFailure,
|
|
32
|
+
updateUsageFromMessage,
|
|
33
|
+
} from "./outcome.ts";
|
|
34
|
+
import { RunWidget } from "./run-widget.ts";
|
|
35
|
+
import {
|
|
36
|
+
clampResumeThinking,
|
|
37
|
+
codemodeFactories,
|
|
38
|
+
createChildModelRuntime,
|
|
39
|
+
ensureUsableModel,
|
|
40
|
+
validateThinking,
|
|
41
|
+
} from "./runtime.ts";
|
|
43
42
|
import type { SubagentParamsShape, TaskInput } from "./schemas.ts";
|
|
44
43
|
import {
|
|
45
44
|
MAX_TASKS,
|
|
45
|
+
type ParkedMsg,
|
|
46
46
|
type PendingReply,
|
|
47
47
|
type RunDetails,
|
|
48
48
|
type RunMode,
|
|
@@ -51,7 +51,6 @@ import {
|
|
|
51
51
|
type TaskSnapshot,
|
|
52
52
|
type TaskStatus,
|
|
53
53
|
TERMINAL,
|
|
54
|
-
type UsageStats,
|
|
55
54
|
} from "./types.ts";
|
|
56
55
|
import {
|
|
57
56
|
attachWorktree,
|
|
@@ -65,8 +64,9 @@ import {
|
|
|
65
64
|
type Worktree,
|
|
66
65
|
} from "./worktree.ts";
|
|
67
66
|
|
|
68
|
-
|
|
69
|
-
|
|
67
|
+
export type { ParkedMsg };
|
|
68
|
+
/** Legacy callers import these from here; their owners are `models.ts`, `outcome.ts`, `runtime.ts`. */
|
|
69
|
+
export { clampResumeThinking, classifyFailure, cloneRun, ensureUsableModel, resolveChildModel, validateThinking };
|
|
70
70
|
|
|
71
71
|
export const DEFAULT_CONCURRENCY = 3;
|
|
72
72
|
export const MAX_CONCURRENCY = 8;
|
|
@@ -78,51 +78,11 @@ const READONLY_TOOLS = ["read", "grep", "find", "ls", "codemode"];
|
|
|
78
78
|
const WRITE_TOOLS = ["read", "grep", "find", "ls", "bash", "edit", "write", "codemode"];
|
|
79
79
|
const WRITE_CAPABLE = ["bash", "edit", "write"];
|
|
80
80
|
const SAFE_TASK_ID = /^[A-Za-z0-9_-]{1,64}$/;
|
|
81
|
-
const WIDGET_THROTTLE_MS = 150;
|
|
82
|
-
/** A submitted notice missing from the canonical context this long is treated as lost. */
|
|
83
|
-
const RECEIPT_GRACE_MS = 3_000;
|
|
84
|
-
/** Minimum wait before retrying a failed submission, so one flush cannot burn every attempt. */
|
|
85
|
-
const RETRY_BACKOFF_MS = 1_000;
|
|
86
|
-
/** Total submission attempts before a notice is declared lost. */
|
|
87
|
-
const MAX_DELIVERY_ATTEMPTS = 3;
|
|
88
81
|
|
|
89
82
|
function newId(prefix: string): string {
|
|
90
83
|
return `${prefix}_${Date.now().toString(36)}_${Math.random().toString(36).slice(2, 8)}`;
|
|
91
84
|
}
|
|
92
85
|
|
|
93
|
-
type ChildExtensionFactories = ConstructorParameters<typeof DefaultResourceLoader>[0]["extensionFactories"];
|
|
94
|
-
|
|
95
|
-
/**
|
|
96
|
-
* Children run with `noExtensions`, so they get none of the configured extensions — codemode is the
|
|
97
|
-
* one exception, because it is how a child batches tool calls. `createCodemodeExtension()` is the
|
|
98
|
-
* supported factory (pi >= 1.0); hosts that do not export it simply give children no codemode.
|
|
99
|
-
*/
|
|
100
|
-
async function codemodeFactories(): Promise<ChildExtensionFactories> {
|
|
101
|
-
try {
|
|
102
|
-
const host = (await import("@earendil-works/pi-coding-agent")) as unknown as {
|
|
103
|
-
createCodemodeExtension?: () => unknown;
|
|
104
|
-
};
|
|
105
|
-
const factory = host.createCodemodeExtension?.();
|
|
106
|
-
return factory ? ([factory] as ChildExtensionFactories) : [];
|
|
107
|
-
} catch {
|
|
108
|
-
return [];
|
|
109
|
-
}
|
|
110
|
-
}
|
|
111
|
-
function emptyUsage(): UsageStats {
|
|
112
|
-
return { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, cost: 0, turns: 0 };
|
|
113
|
-
}
|
|
114
|
-
function aggregateUsage(tasks: TaskSnapshot[]): UsageStats {
|
|
115
|
-
const total = emptyUsage();
|
|
116
|
-
for (const task of tasks) {
|
|
117
|
-
total.input += task.usage.input;
|
|
118
|
-
total.output += task.usage.output;
|
|
119
|
-
total.cacheRead += task.usage.cacheRead;
|
|
120
|
-
total.cacheWrite += task.usage.cacheWrite;
|
|
121
|
-
total.cost += task.usage.cost;
|
|
122
|
-
total.turns += task.usage.turns;
|
|
123
|
-
}
|
|
124
|
-
return total;
|
|
125
|
-
}
|
|
126
86
|
function safeRealPath(path: string): string {
|
|
127
87
|
try {
|
|
128
88
|
return realpathSync(path);
|
|
@@ -137,150 +97,6 @@ function getParentSessionFile(ctx: ExtensionContext): string | undefined {
|
|
|
137
97
|
return undefined;
|
|
138
98
|
}
|
|
139
99
|
}
|
|
140
|
-
export function classifyFailure(
|
|
141
|
-
stopReason: string | undefined,
|
|
142
|
-
errorMessage?: string,
|
|
143
|
-
): { status: "failed" | "aborted"; message: string } | undefined {
|
|
144
|
-
if (!stopReason || stopReason === "stop" || stopReason === "end") return undefined;
|
|
145
|
-
if (stopReason === "aborted") return { status: "aborted", message: errorMessage || "Subagent was aborted." };
|
|
146
|
-
return { status: "failed", message: errorMessage || `Subagent ended with stopReason "${stopReason}".` };
|
|
147
|
-
}
|
|
148
|
-
function lastAssistantFailure(
|
|
149
|
-
messages: AssistantMessage[] | undefined,
|
|
150
|
-
): { status: "failed" | "aborted"; message: string } | undefined {
|
|
151
|
-
for (const message of [...(messages ?? [])].reverse()) {
|
|
152
|
-
if (message?.role !== "assistant") continue;
|
|
153
|
-
return classifyFailure(message.stopReason, message.errorMessage);
|
|
154
|
-
}
|
|
155
|
-
return undefined;
|
|
156
|
-
}
|
|
157
|
-
function failureError(failure: { status: "failed" | "aborted"; message: string }): Error {
|
|
158
|
-
const error = new Error(failure.message);
|
|
159
|
-
(error as Error & { subagentStatus?: string }).subagentStatus = failure.status;
|
|
160
|
-
return error;
|
|
161
|
-
}
|
|
162
|
-
function updateUsageFromMessage(task: TaskSnapshot, message: AssistantMessage): void {
|
|
163
|
-
if (message?.role !== "assistant") return;
|
|
164
|
-
task.usage.turns += 1;
|
|
165
|
-
const usage = message.usage;
|
|
166
|
-
if (!usage) return;
|
|
167
|
-
task.usage.input += usage.input ?? 0;
|
|
168
|
-
task.usage.output += usage.output ?? 0;
|
|
169
|
-
task.usage.cacheRead += usage.cacheRead ?? 0;
|
|
170
|
-
task.usage.cacheWrite += usage.cacheWrite ?? 0;
|
|
171
|
-
task.usage.cost += usage.cost?.total ?? 0;
|
|
172
|
-
if (message.model && !task.model) task.model = message.model;
|
|
173
|
-
}
|
|
174
|
-
export function cloneRun(run: RunSnapshot): RunSnapshot {
|
|
175
|
-
return JSON.parse(JSON.stringify(run)) as RunSnapshot;
|
|
176
|
-
}
|
|
177
|
-
/**
|
|
178
|
-
* A resumed task keeps its stored thinking level, clamped to what the target model accepts — a
|
|
179
|
-
* resume that swaps model must not fail on an effort the new model does not define.
|
|
180
|
-
*/
|
|
181
|
-
export function clampResumeThinking(
|
|
182
|
-
model: Model<Api> | undefined,
|
|
183
|
-
thinking: ThinkingLevel | undefined,
|
|
184
|
-
): ThinkingLevel | undefined {
|
|
185
|
-
if (!thinking || !model) return thinking;
|
|
186
|
-
return clampThinkingLevel(model, thinking) as ThinkingLevel;
|
|
187
|
-
}
|
|
188
|
-
|
|
189
|
-
const PROBE_THINKING_LEVELS: ThinkingLevel[] = ["low", "minimal", "medium", "high", "xhigh", "max"];
|
|
190
|
-
|
|
191
|
-
/**
|
|
192
|
-
* Thinking level for the usability probe: exactly what the child session will send — the clamped
|
|
193
|
-
* requested level, or the cheapest the model accepts when none was requested. Probing without a
|
|
194
|
-
* level makes adaptive-thinking providers reject the request (9router claude models answer
|
|
195
|
-
* "thinking.type.disabled is not supported"), which used to make every preflight fail and fall
|
|
196
|
-
* back to the session model.
|
|
197
|
-
*/
|
|
198
|
-
function probeThinking(model: Model<Api>, thinking?: string): ThinkingLevel | undefined {
|
|
199
|
-
if (!model.reasoning) return undefined;
|
|
200
|
-
const supported = getSupportedThinkingLevels(model);
|
|
201
|
-
if (thinking && thinking !== "off") return clampThinkingLevel(model, thinking as ThinkingLevel);
|
|
202
|
-
// the child clamps an unsupported "off" up to its cheapest level, so probe that instead
|
|
203
|
-
if (thinking === "off") return supported.includes("off") ? undefined : supported[0];
|
|
204
|
-
return PROBE_THINKING_LEVELS.find((level) => supported.includes(level));
|
|
205
|
-
}
|
|
206
|
-
|
|
207
|
-
async function probeModel(
|
|
208
|
-
ctx: ExtensionContext,
|
|
209
|
-
model: Model<Api>,
|
|
210
|
-
signal: AbortSignal | undefined,
|
|
211
|
-
thinking?: string,
|
|
212
|
-
): Promise<string | undefined> {
|
|
213
|
-
try {
|
|
214
|
-
const reasoningEffort = probeThinking(model, thinking);
|
|
215
|
-
const reply = await ctx.modelRegistry.complete(
|
|
216
|
-
model,
|
|
217
|
-
{ messages: [{ role: "user", content: "ping", timestamp: Date.now() }] },
|
|
218
|
-
{ maxTokens: 16, signal, ...(reasoningEffort ? { reasoningEffort } : {}) },
|
|
219
|
-
);
|
|
220
|
-
return reply.stopReason === "error" ? (reply.errorMessage ?? "provider returned an error") : undefined;
|
|
221
|
-
} catch (err) {
|
|
222
|
-
return err instanceof Error ? err.message : String(err);
|
|
223
|
-
}
|
|
224
|
-
}
|
|
225
|
-
|
|
226
|
-
export async function ensureUsableModel(
|
|
227
|
-
ctx: ExtensionContext,
|
|
228
|
-
model: Model<Api> | undefined,
|
|
229
|
-
signal: AbortSignal | undefined,
|
|
230
|
-
thinking?: string,
|
|
231
|
-
): Promise<{ model: Model<Api> | undefined; note?: string }> {
|
|
232
|
-
const session = ctx.model;
|
|
233
|
-
if (!model || !ctx.modelRegistry) return { model };
|
|
234
|
-
if (session && model.provider === session.provider && model.id === session.id) return { model };
|
|
235
|
-
const error = await probeModel(ctx, model, signal, thinking);
|
|
236
|
-
if (!error) return { model };
|
|
237
|
-
if (model.provider === "opencode-go" && /MissingSessionID|x-opencode-session/i.test(error)) {
|
|
238
|
-
// ponytail: opencode-go rejects stateless probes but accepts AgentSession requests, which add the session header.
|
|
239
|
-
// Upgrade path: remove this exception when modelRegistry.complete can carry AgentSession request transforms.
|
|
240
|
-
return { model, note: `preflight unavailable (${error}); child session will validate the model` };
|
|
241
|
-
}
|
|
242
|
-
if (!session) throw new Error(`Model ${model.provider}/${model.id} is unusable: ${error}`);
|
|
243
|
-
return {
|
|
244
|
-
model: session,
|
|
245
|
-
note: `${model.provider}/${model.id} failed preflight (${error}); using session model ${session.provider}/${session.id}`,
|
|
246
|
-
};
|
|
247
|
-
}
|
|
248
|
-
|
|
249
|
-
async function createChildModelRuntime(ctx: ExtensionContext) {
|
|
250
|
-
const ids = ctx.modelRegistry.getRegisteredProviderIds?.() ?? [];
|
|
251
|
-
if (ids.length === 0) return undefined;
|
|
252
|
-
const agentDir = getAgentDir();
|
|
253
|
-
const runtime = await ModelRuntime.create({
|
|
254
|
-
authPath: join(agentDir, "auth.json"),
|
|
255
|
-
modelsPath: join(agentDir, "models.json"),
|
|
256
|
-
});
|
|
257
|
-
for (const id of ids) {
|
|
258
|
-
const native = ctx.modelRegistry.getRegisteredNativeProvider?.(id);
|
|
259
|
-
if (native) {
|
|
260
|
-
runtime.registerNativeProvider(native);
|
|
261
|
-
continue;
|
|
262
|
-
}
|
|
263
|
-
const config = ctx.modelRegistry.getRegisteredProviderConfig?.(id);
|
|
264
|
-
if (config) runtime.registerProvider(id, config);
|
|
265
|
-
}
|
|
266
|
-
await runtime.refresh({ allowNetwork: false });
|
|
267
|
-
return runtime;
|
|
268
|
-
}
|
|
269
|
-
|
|
270
|
-
export function validateThinking(model: Model<Api> | undefined, level: string | undefined): void {
|
|
271
|
-
if (!level || level === "off") return;
|
|
272
|
-
if (!model) return;
|
|
273
|
-
const map = model.thinkingLevelMap;
|
|
274
|
-
if (map && level in map && map[level as keyof typeof map] === null) {
|
|
275
|
-
const supported = Object.keys(map).filter((k) => map[k as keyof typeof map] !== null);
|
|
276
|
-
throw new Error(
|
|
277
|
-
`Thinking level "${level}" is not supported by ${model.provider}/${model.id}. Supported: ${supported.length ? supported.join(" | ") : 'none — use thinking: "off"'}.`,
|
|
278
|
-
);
|
|
279
|
-
}
|
|
280
|
-
if (!model.reasoning) {
|
|
281
|
-
throw new Error(`Model ${model.provider}/${model.id} does not support thinking. Use thinking: "off".`);
|
|
282
|
-
}
|
|
283
|
-
}
|
|
284
100
|
|
|
285
101
|
interface ResumeInput {
|
|
286
102
|
sessionFile: string;
|
|
@@ -294,43 +110,6 @@ interface ChildEventState {
|
|
|
294
110
|
childEndResolve?: () => void;
|
|
295
111
|
}
|
|
296
112
|
|
|
297
|
-
export interface ParkedMsg {
|
|
298
|
-
kind: "ask" | "notify" | "done";
|
|
299
|
-
taskId: string;
|
|
300
|
-
agent: string;
|
|
301
|
-
text: string;
|
|
302
|
-
final?: boolean;
|
|
303
|
-
}
|
|
304
|
-
|
|
305
|
-
interface LeaderMessageReceipt {
|
|
306
|
-
runId: string;
|
|
307
|
-
taskIds: string[];
|
|
308
|
-
kind: "update" | "final" | "terminal";
|
|
309
|
-
deliverAs: "steer" | "followUp";
|
|
310
|
-
delivered: boolean;
|
|
311
|
-
submitted: boolean;
|
|
312
|
-
lost: boolean;
|
|
313
|
-
submittedAt: number;
|
|
314
|
-
attempts: number;
|
|
315
|
-
/** Agent runs started when this receipt was submitted; a later run is needed to prove loss. */
|
|
316
|
-
submittedRunCount: number;
|
|
317
|
-
}
|
|
318
|
-
|
|
319
|
-
interface OutboxEntry {
|
|
320
|
-
runId: string;
|
|
321
|
-
taskIds: string[];
|
|
322
|
-
kind: "update" | "final" | "terminal";
|
|
323
|
-
deliverAs: "steer" | "followUp";
|
|
324
|
-
attempts?: number;
|
|
325
|
-
attemptedAt?: number;
|
|
326
|
-
}
|
|
327
|
-
|
|
328
|
-
interface TerminalNotice {
|
|
329
|
-
body: string;
|
|
330
|
-
fingerprint: string;
|
|
331
|
-
delivery: "pending" | "received" | "lost";
|
|
332
|
-
}
|
|
333
|
-
|
|
334
113
|
export class SubagentManager {
|
|
335
114
|
private runs = new Map<string, RunSnapshot>();
|
|
336
115
|
private settlers = new Map<string, true>();
|
|
@@ -343,31 +122,29 @@ export class SubagentManager {
|
|
|
343
122
|
private mailboxes: Mailbox = createMailbox();
|
|
344
123
|
private liveWorktrees = new Map<string, Worktree>();
|
|
345
124
|
private runControllers = new Map<string, AbortController>();
|
|
346
|
-
private
|
|
347
|
-
private
|
|
125
|
+
private readonly widget = new RunWidget();
|
|
126
|
+
private readonly delivery: LeaderDelivery;
|
|
348
127
|
private eventSeq = 0;
|
|
349
128
|
private persistSeq = 0;
|
|
350
129
|
private persistedSeq = 0;
|
|
351
130
|
private persistChain: Promise<unknown> = Promise.resolve();
|
|
352
131
|
private readonly instanceNonce = Math.random().toString(36).slice(2, 8);
|
|
353
132
|
private cleared = false;
|
|
354
|
-
private leaderMessages = new Map<string, LeaderMessageReceipt>();
|
|
355
|
-
private outbox = new Map<string, OutboxEntry>();
|
|
356
|
-
private parkedBodies = new Map<string, Set<string>>();
|
|
357
|
-
private agentRunCount = 0;
|
|
358
|
-
private terminalNotices = new Map<string, TerminalNotice>();
|
|
359
|
-
private awaitedTaskNotices = new Map<string, string>();
|
|
360
|
-
private pendingTaskNotices = new Map<string, { run: RunSnapshot; task: TaskSnapshot }>();
|
|
361
|
-
private pendingRunNotices = new Map<string, RunSnapshot>();
|
|
362
|
-
private notificationRetries = new Set<string>();
|
|
363
|
-
private notificationFlushTimer: ReturnType<typeof setTimeout> | undefined;
|
|
364
|
-
private notificationContext: ExtensionContext | undefined;
|
|
365
133
|
|
|
366
134
|
private autoLimit = false;
|
|
367
135
|
|
|
368
136
|
turnActivity = false;
|
|
369
137
|
|
|
370
138
|
constructor(private readonly pi: ExtensionAPI) {
|
|
139
|
+
this.delivery = new LeaderDelivery({
|
|
140
|
+
pi,
|
|
141
|
+
isCleared: () => this.cleared,
|
|
142
|
+
findRun: (runId) => this.runs.get(runId),
|
|
143
|
+
isLive: (key) => this.liveChildren.has(key),
|
|
144
|
+
persist: (ctx) => this.persist(ctx),
|
|
145
|
+
emit: (type, payload) => this.emit(type, payload),
|
|
146
|
+
collectParked: (runId, msg) => this.collectParked(runId, msg),
|
|
147
|
+
});
|
|
371
148
|
try {
|
|
372
149
|
const cfg = readSubagentConfig();
|
|
373
150
|
if (typeof cfg.autoLimit === "boolean") this.autoLimit = cfg.autoLimit;
|
|
@@ -392,13 +169,24 @@ export class SubagentManager {
|
|
|
392
169
|
}
|
|
393
170
|
|
|
394
171
|
clearWidget(ctx?: ExtensionContext): void {
|
|
395
|
-
this.
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
172
|
+
this.widget.clear(ctx);
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/** An agent run started; a receipt can only be declared lost after one full later run. */
|
|
176
|
+
noteAgentStart(): void {
|
|
177
|
+
this.delivery.noteAgentStart();
|
|
178
|
+
}
|
|
179
|
+
markAwaitCoverage(run: RunSnapshot, coveredTaskIds?: ReadonlySet<string>): void {
|
|
180
|
+
this.delivery.markAwaitCoverage(run, coveredTaskIds);
|
|
181
|
+
}
|
|
182
|
+
confirmLeaderMessages(ctx: ExtensionContext): void {
|
|
183
|
+
this.delivery.confirmLeaderMessages(ctx);
|
|
184
|
+
}
|
|
185
|
+
flushPendingNotifications(ctx: ExtensionContext): void {
|
|
186
|
+
this.delivery.flushPendingNotifications(ctx);
|
|
187
|
+
}
|
|
188
|
+
scheduleNotificationFlush(ctx: ExtensionContext, delayMs?: number): void {
|
|
189
|
+
this.delivery.scheduleNotificationFlush(ctx, delayMs);
|
|
402
190
|
}
|
|
403
191
|
|
|
404
192
|
listRuns(): RunSnapshot[] {
|
|
@@ -431,17 +219,7 @@ export class SubagentManager {
|
|
|
431
219
|
pending.resolve("(session ended — stop work immediately)");
|
|
432
220
|
}
|
|
433
221
|
this.parked.clear();
|
|
434
|
-
this.
|
|
435
|
-
this.outbox.clear();
|
|
436
|
-
this.parkedBodies.clear();
|
|
437
|
-
this.terminalNotices.clear();
|
|
438
|
-
this.awaitedTaskNotices.clear();
|
|
439
|
-
this.pendingTaskNotices.clear();
|
|
440
|
-
this.pendingRunNotices.clear();
|
|
441
|
-
this.notificationRetries.clear();
|
|
442
|
-
if (this.notificationFlushTimer) clearTimeout(this.notificationFlushTimer);
|
|
443
|
-
this.notificationFlushTimer = undefined;
|
|
444
|
-
this.notificationContext = undefined;
|
|
222
|
+
this.delivery.reset();
|
|
445
223
|
|
|
446
224
|
this.liveWorktrees.clear();
|
|
447
225
|
this.runs.clear();
|
|
@@ -451,14 +229,7 @@ export class SubagentManager {
|
|
|
451
229
|
this.runControllers.clear();
|
|
452
230
|
this.cleared = true;
|
|
453
231
|
this.mailboxes = createMailbox();
|
|
454
|
-
this.
|
|
455
|
-
if (this.pulseTimer) {
|
|
456
|
-
clearTimeout(this.pulseTimer);
|
|
457
|
-
this.pulseTimer = null;
|
|
458
|
-
}
|
|
459
|
-
for (const t of this.widgetTimers.values()) clearTimeout(t);
|
|
460
|
-
this.widgetTimers.clear();
|
|
461
|
-
this.widgetRuns = [];
|
|
232
|
+
this.widget.dispose();
|
|
462
233
|
}
|
|
463
234
|
|
|
464
235
|
async restoreFromSidecar(ctx: ExtensionContext): Promise<void> {
|
|
@@ -510,7 +281,7 @@ export class SubagentManager {
|
|
|
510
281
|
}
|
|
511
282
|
if (added > 0) {
|
|
512
283
|
this.emit("subagent:runs-restored", { count: added });
|
|
513
|
-
this.
|
|
284
|
+
this.widget.schedule(this.listRuns()[0], ctx);
|
|
514
285
|
}
|
|
515
286
|
}
|
|
516
287
|
private persist(ctx: ExtensionContext): void {
|
|
@@ -541,698 +312,6 @@ export class SubagentManager {
|
|
|
541
312
|
this.pi.events.emit(type, { type, timestamp: Date.now(), ...payload });
|
|
542
313
|
}
|
|
543
314
|
|
|
544
|
-
private finalReportMatches(task: TaskSnapshot): boolean {
|
|
545
|
-
const report = task.finalParentReport;
|
|
546
|
-
if (!report || report.toolCalls !== task.toolCalls) return false;
|
|
547
|
-
const fingerprint = task.finalTextFingerprint ?? textFingerprint(task.finalText ?? "");
|
|
548
|
-
return report.fingerprint === fingerprint;
|
|
549
|
-
}
|
|
550
|
-
|
|
551
|
-
private hasArtifactDelta(task: TaskSnapshot): boolean {
|
|
552
|
-
return artifactFingerprint(task) !== task.finalParentReport?.artifacts;
|
|
553
|
-
}
|
|
554
|
-
|
|
555
|
-
private leaderMessageText(content: unknown): string {
|
|
556
|
-
return typeof content === "string"
|
|
557
|
-
? content
|
|
558
|
-
: Array.isArray(content)
|
|
559
|
-
? content
|
|
560
|
-
.filter((part) => part?.type === "text")
|
|
561
|
-
.map((part) => part.text ?? "")
|
|
562
|
-
.join("\n")
|
|
563
|
-
: "";
|
|
564
|
-
}
|
|
565
|
-
|
|
566
|
-
/**
|
|
567
|
-
* One pass over the finalized leader projection. Returns undefined when the pinned pi build
|
|
568
|
-
* cannot project (never treat that as loss, or every receipt would be replayed).
|
|
569
|
-
*/
|
|
570
|
-
private projectionBody(ctx: ExtensionContext): string | undefined {
|
|
571
|
-
try {
|
|
572
|
-
const manager = ctx.sessionManager as unknown as {
|
|
573
|
-
buildSessionProjection?: () => { messages: readonly { role?: string; content?: unknown }[] };
|
|
574
|
-
};
|
|
575
|
-
if (typeof manager.buildSessionProjection !== "function") return undefined;
|
|
576
|
-
const messages = manager.buildSessionProjection().messages;
|
|
577
|
-
const parts: string[] = [];
|
|
578
|
-
for (const message of messages) {
|
|
579
|
-
if (message.role !== "user" && message.role !== "custom") continue;
|
|
580
|
-
parts.push(this.leaderMessageText(message.content));
|
|
581
|
-
}
|
|
582
|
-
return parts.join("\n");
|
|
583
|
-
} catch {
|
|
584
|
-
return undefined;
|
|
585
|
-
}
|
|
586
|
-
}
|
|
587
|
-
|
|
588
|
-
/** An agent run started; a receipt can only be declared lost after one full later run. */
|
|
589
|
-
noteAgentStart(): void {
|
|
590
|
-
this.agentRunCount += 1;
|
|
591
|
-
}
|
|
592
|
-
|
|
593
|
-
/** True when a terminal await already rendered this task's outcome. */
|
|
594
|
-
private coveredByAwait(runId: string, taskId: string, fingerprint: string): boolean {
|
|
595
|
-
return this.awaitedTaskNotices.get(`${runId}:${taskId}`) === fingerprint;
|
|
596
|
-
}
|
|
597
|
-
|
|
598
|
-
/** Record what a terminal await actually rendered, so held notices are dropped instead of echoed. */
|
|
599
|
-
markAwaitCoverage(run: RunSnapshot, coveredTaskIds?: ReadonlySet<string>): void {
|
|
600
|
-
for (const task of run.tasks) {
|
|
601
|
-
if (!TERMINAL.includes(task.status)) continue;
|
|
602
|
-
if (coveredTaskIds && !coveredTaskIds.has(task.id)) continue;
|
|
603
|
-
const scope = `${run.id}:${task.id}`;
|
|
604
|
-
this.awaitedTaskNotices.set(scope, outcomeFingerprint(task));
|
|
605
|
-
this.pendingTaskNotices.delete(scope);
|
|
606
|
-
}
|
|
607
|
-
for (const [body, entry] of [...this.outbox]) {
|
|
608
|
-
if (entry.runId !== run.id) continue;
|
|
609
|
-
if (!this.outboxCovered(entry)) continue;
|
|
610
|
-
const task = run.tasks.find((t) => t.id === entry.taskIds[0]);
|
|
611
|
-
// An update is not part of the rendered summary; only terminal/aggregate entries are safe to drop.
|
|
612
|
-
if (entry.kind === "update") continue;
|
|
613
|
-
if (entry.kind === "final" && (!task || !this.finalReportMatches(task))) continue;
|
|
614
|
-
this.dropOutbox(body, entry, "awaited");
|
|
615
|
-
}
|
|
616
|
-
}
|
|
617
|
-
|
|
618
|
-
private outboxCovered(entry: OutboxEntry): boolean {
|
|
619
|
-
const run = this.runs.get(entry.runId);
|
|
620
|
-
if (!run) return false;
|
|
621
|
-
return entry.taskIds.every((taskId) => {
|
|
622
|
-
const task = run.tasks.find((t) => t.id === taskId);
|
|
623
|
-
if (!task || !TERMINAL.includes(task.status)) return false;
|
|
624
|
-
return this.coveredByAwait(entry.runId, taskId, outcomeFingerprint(task));
|
|
625
|
-
});
|
|
626
|
-
}
|
|
627
|
-
|
|
628
|
-
private dropOutbox(body: string, entry: OutboxEntry, delivery: string): void {
|
|
629
|
-
this.outbox.delete(body);
|
|
630
|
-
const task = this.runs.get(entry.runId)?.tasks.find((t) => t.id === entry.taskIds[0]);
|
|
631
|
-
if (entry.kind === "final" && task?.finalParentReport?.body === body) task.finalParentReport.delivery = "awaited";
|
|
632
|
-
if (entry.kind === "terminal") {
|
|
633
|
-
for (const taskId of entry.taskIds) {
|
|
634
|
-
const notice = this.terminalNotices.get(`${entry.runId}:${taskId}`);
|
|
635
|
-
if (notice?.body === body) notice.delivery = "received";
|
|
636
|
-
}
|
|
637
|
-
}
|
|
638
|
-
this.emit("subagent:notification", {
|
|
639
|
-
runId: entry.runId,
|
|
640
|
-
taskId: entry.taskIds[0],
|
|
641
|
-
kind: "completed",
|
|
642
|
-
body: "",
|
|
643
|
-
suppressed: true,
|
|
644
|
-
delivery,
|
|
645
|
-
});
|
|
646
|
-
}
|
|
647
|
-
|
|
648
|
-
private enqueueNotification(body: string, entry: OutboxEntry, ctx?: ExtensionContext): void {
|
|
649
|
-
this.outbox.set(body, entry);
|
|
650
|
-
const flushContext = ctx ?? this.notificationContext;
|
|
651
|
-
if (flushContext) this.scheduleNotificationFlush(flushContext);
|
|
652
|
-
}
|
|
653
|
-
|
|
654
|
-
private noteTerminal(runId: string, taskId: string, body: string, fingerprint: string): void {
|
|
655
|
-
this.terminalNotices.set(`${runId}:${taskId}`, { body, fingerprint, delivery: "pending" });
|
|
656
|
-
}
|
|
657
|
-
|
|
658
|
-
private submitLeaderReport(
|
|
659
|
-
run: RunSnapshot,
|
|
660
|
-
task: TaskSnapshot,
|
|
661
|
-
message: string,
|
|
662
|
-
level: "info" | "warning" | "error",
|
|
663
|
-
final: boolean,
|
|
664
|
-
ctx: ExtensionContext,
|
|
665
|
-
): boolean {
|
|
666
|
-
if (this.cleared) return false;
|
|
667
|
-
this.notificationContext = ctx;
|
|
668
|
-
const body = `[Subagent ${task.agent} (${task.id}, ${run.id})]${final ? " Final result:" : ""}${level === "info" ? "" : ` [${level}]`} ${message}`;
|
|
669
|
-
const previous = this.leaderMessages.get(body);
|
|
670
|
-
if ((previous && !previous.lost) || this.outbox.has(body)) {
|
|
671
|
-
if (final) {
|
|
672
|
-
task.finalParentReport = {
|
|
673
|
-
body,
|
|
674
|
-
message,
|
|
675
|
-
toolCalls: task.toolCalls,
|
|
676
|
-
fingerprint: textFingerprint(message),
|
|
677
|
-
artifacts: artifactFingerprint(task),
|
|
678
|
-
delivery: previous?.delivered ? "message" : "pending",
|
|
679
|
-
};
|
|
680
|
-
}
|
|
681
|
-
return true;
|
|
682
|
-
}
|
|
683
|
-
if (previous?.lost) this.leaderMessages.delete(body);
|
|
684
|
-
const report: TaskSnapshot["finalParentReport"] = final
|
|
685
|
-
? {
|
|
686
|
-
body,
|
|
687
|
-
message,
|
|
688
|
-
toolCalls: task.toolCalls,
|
|
689
|
-
fingerprint: textFingerprint(message),
|
|
690
|
-
artifacts: artifactFingerprint(task),
|
|
691
|
-
delivery: "pending",
|
|
692
|
-
}
|
|
693
|
-
: undefined;
|
|
694
|
-
if (report) task.finalParentReport = report;
|
|
695
|
-
this.emit("subagent:intercom", { runId: run.id, taskId: task.id, kind: "notify", level, message, final });
|
|
696
|
-
if (this.collectParked(run.id, { kind: "notify", taskId: task.id, agent: task.agent, text: message, final })) {
|
|
697
|
-
let parked = this.parkedBodies.get(run.id);
|
|
698
|
-
if (!parked) {
|
|
699
|
-
parked = new Set();
|
|
700
|
-
this.parkedBodies.set(run.id, parked);
|
|
701
|
-
}
|
|
702
|
-
if (parked.has(body)) return true;
|
|
703
|
-
parked.add(body);
|
|
704
|
-
task.notifiedParent = true;
|
|
705
|
-
if (report) report.delivery = "parked";
|
|
706
|
-
return true;
|
|
707
|
-
}
|
|
708
|
-
this.enqueueNotification(body, {
|
|
709
|
-
runId: run.id,
|
|
710
|
-
taskIds: [task.id],
|
|
711
|
-
kind: final ? "final" : "update",
|
|
712
|
-
deliverAs: "followUp",
|
|
713
|
-
});
|
|
714
|
-
return true;
|
|
715
|
-
}
|
|
716
|
-
|
|
717
|
-
private hasPendingSuccessMessages(run: RunSnapshot): boolean {
|
|
718
|
-
return run.tasks.some((task) => {
|
|
719
|
-
const notice = this.terminalNotices.get(`${run.id}:${task.id}`);
|
|
720
|
-
if (notice?.delivery !== "pending") return false;
|
|
721
|
-
const receipt = this.leaderMessages.get(notice.body);
|
|
722
|
-
return receipt ? !receipt.delivered && !receipt.lost : this.outbox.has(notice.body);
|
|
723
|
-
});
|
|
724
|
-
}
|
|
725
|
-
|
|
726
|
-
confirmLeaderMessages(ctx: ExtensionContext): void {
|
|
727
|
-
if (this.cleared) return;
|
|
728
|
-
const projection = this.projectionBody(ctx);
|
|
729
|
-
let changed = false;
|
|
730
|
-
for (const [body, receipt] of [...this.leaderMessages]) {
|
|
731
|
-
if (receipt.delivered || receipt.lost || projection === undefined || !projection.includes(body)) continue;
|
|
732
|
-
receipt.delivered = true;
|
|
733
|
-
changed = true;
|
|
734
|
-
const run = this.runs.get(receipt.runId);
|
|
735
|
-
for (const taskId of receipt.taskIds) {
|
|
736
|
-
const task = run?.tasks.find((t) => t.id === taskId);
|
|
737
|
-
if (!task) continue;
|
|
738
|
-
task.notifiedParent = true;
|
|
739
|
-
if (task.finalParentReport?.body === body) task.finalParentReport.delivery = "message";
|
|
740
|
-
const key = `${receipt.runId}:${taskId}`;
|
|
741
|
-
const notice = this.terminalNotices.get(key);
|
|
742
|
-
if (notice?.body === body) notice.delivery = "received";
|
|
743
|
-
const pending = this.pendingTaskNotices.get(key);
|
|
744
|
-
if (pending && this.finalReportMatches(task)) {
|
|
745
|
-
this.pendingTaskNotices.delete(key);
|
|
746
|
-
if (this.hasArtifactDelta(task)) this.notifyTask(pending.run, task, "completed");
|
|
747
|
-
}
|
|
748
|
-
}
|
|
749
|
-
if (run && !this.hasPendingSuccessMessages(run)) this.pendingRunNotices.delete(run.id);
|
|
750
|
-
}
|
|
751
|
-
if (changed) this.persist(ctx);
|
|
752
|
-
for (const [body, receipt] of this.leaderMessages) {
|
|
753
|
-
if (!receipt.delivered && !receipt.lost) continue;
|
|
754
|
-
const run = this.runs.get(receipt.runId);
|
|
755
|
-
const tasks = run?.tasks.filter((task) => receipt.taskIds.includes(task.id)) ?? [];
|
|
756
|
-
if (
|
|
757
|
-
tasks.length > 0 &&
|
|
758
|
-
tasks.every((task) => TERMINAL.includes(task.status) && !this.liveChildren.has(`${receipt.runId}:${task.id}`))
|
|
759
|
-
)
|
|
760
|
-
this.leaderMessages.delete(body);
|
|
761
|
-
}
|
|
762
|
-
}
|
|
763
|
-
|
|
764
|
-
/**
|
|
765
|
-
* A submitted receipt that never appears in the canonical context after a grace period is lost.
|
|
766
|
-
* The grace period keeps the check from racing Pi's input/auth/start hooks, which run before the
|
|
767
|
-
* message is appended to the session and can be slow while the session still reports idle.
|
|
768
|
-
*/
|
|
769
|
-
private markLostReceipts(ctx: ExtensionContext, existing: ReadonlySet<string>): void {
|
|
770
|
-
const now = Date.now();
|
|
771
|
-
const projection = this.projectionBody(ctx);
|
|
772
|
-
if (projection === undefined) return;
|
|
773
|
-
for (const [body, receipt] of [...this.leaderMessages]) {
|
|
774
|
-
if (receipt.delivered || receipt.lost || !existing.has(body)) continue;
|
|
775
|
-
if (this.outbox.has(body)) continue;
|
|
776
|
-
if (now - receipt.submittedAt < RECEIPT_GRACE_MS) continue;
|
|
777
|
-
// A later agent run proves the submitted prompt had its turn; a message that never runs
|
|
778
|
-
// at all is caught by the longer bound. While a prompt is still queued or in preflight
|
|
779
|
-
// (deferred settle, slow input/auth hooks) the session can report idle without the
|
|
780
|
-
// message being appended yet, so the short window alone must not declare loss.
|
|
781
|
-
const laterRun = this.agentRunCount > receipt.submittedRunCount;
|
|
782
|
-
const longWait = now - receipt.submittedAt >= RECEIPT_GRACE_MS * 2;
|
|
783
|
-
if (!laterRun && !longWait) continue;
|
|
784
|
-
if (projection.includes(body)) continue;
|
|
785
|
-
if (receipt.attempts < MAX_DELIVERY_ATTEMPTS) {
|
|
786
|
-
this.leaderMessages.delete(body);
|
|
787
|
-
this.outbox.set(body, {
|
|
788
|
-
runId: receipt.runId,
|
|
789
|
-
taskIds: receipt.taskIds,
|
|
790
|
-
kind: receipt.kind,
|
|
791
|
-
deliverAs: receipt.deliverAs,
|
|
792
|
-
attempts: receipt.attempts,
|
|
793
|
-
attemptedAt: now,
|
|
794
|
-
});
|
|
795
|
-
continue;
|
|
796
|
-
}
|
|
797
|
-
receipt.lost = true;
|
|
798
|
-
for (const taskId of receipt.taskIds) {
|
|
799
|
-
const key = `${receipt.runId}:${taskId}`;
|
|
800
|
-
const notice = this.terminalNotices.get(key);
|
|
801
|
-
if (notice?.body === body) notice.delivery = "lost";
|
|
802
|
-
const task = this.runs.get(receipt.runId)?.tasks.find((t) => t.id === taskId);
|
|
803
|
-
if (task?.finalParentReport?.body === body) task.finalParentReport.delivery = "lost";
|
|
804
|
-
}
|
|
805
|
-
this.emit("subagent:notification", {
|
|
806
|
-
runId: receipt.runId,
|
|
807
|
-
taskId: receipt.taskIds[0],
|
|
808
|
-
kind: "completed",
|
|
809
|
-
body,
|
|
810
|
-
delivery: "lost",
|
|
811
|
-
});
|
|
812
|
-
// A lost final report must still reach the leader: per-task hold or a direct full fallback.
|
|
813
|
-
if (receipt.kind === "final") {
|
|
814
|
-
const run = this.runs.get(receipt.runId);
|
|
815
|
-
for (const taskId of receipt.taskIds) {
|
|
816
|
-
if (this.pendingTaskNotices.has(`${receipt.runId}:${taskId}`)) continue;
|
|
817
|
-
const task = run?.tasks.find((t) => t.id === taskId);
|
|
818
|
-
if (run && task) this.notifyTask(run, task, "completed");
|
|
819
|
-
}
|
|
820
|
-
}
|
|
821
|
-
}
|
|
822
|
-
}
|
|
823
|
-
|
|
824
|
-
private flushOutbox(): void {
|
|
825
|
-
for (const [body, entry] of [...this.outbox]) {
|
|
826
|
-
if (this.outboxCovered(entry)) {
|
|
827
|
-
this.dropOutbox(body, entry, "awaited");
|
|
828
|
-
continue;
|
|
829
|
-
}
|
|
830
|
-
if (entry.attemptedAt && Date.now() - entry.attemptedAt < RETRY_BACKOFF_MS) continue;
|
|
831
|
-
this.outbox.delete(body);
|
|
832
|
-
try {
|
|
833
|
-
this.pi.sendUserMessage(body, { deliverAs: entry.deliverAs });
|
|
834
|
-
this.leaderMessages.set(body, {
|
|
835
|
-
runId: entry.runId,
|
|
836
|
-
taskIds: entry.taskIds,
|
|
837
|
-
kind: entry.kind,
|
|
838
|
-
deliverAs: entry.deliverAs,
|
|
839
|
-
delivered: false,
|
|
840
|
-
submitted: true,
|
|
841
|
-
lost: false,
|
|
842
|
-
submittedAt: Date.now(),
|
|
843
|
-
attempts: (entry.attempts ?? 0) + 1,
|
|
844
|
-
submittedRunCount: this.agentRunCount,
|
|
845
|
-
});
|
|
846
|
-
if (entry.kind === "terminal") {
|
|
847
|
-
for (const taskId of entry.taskIds) {
|
|
848
|
-
const notice = this.terminalNotices.get(`${entry.runId}:${taskId}`);
|
|
849
|
-
if (notice?.body === body) notice.delivery = "pending";
|
|
850
|
-
}
|
|
851
|
-
}
|
|
852
|
-
this.emit("subagent:notification", {
|
|
853
|
-
runId: entry.runId,
|
|
854
|
-
taskId: entry.taskIds[0],
|
|
855
|
-
kind: "completed",
|
|
856
|
-
body,
|
|
857
|
-
delivery: "submitted",
|
|
858
|
-
});
|
|
859
|
-
} catch {
|
|
860
|
-
const attempts = (entry.attempts ?? 0) + 1;
|
|
861
|
-
if (attempts < MAX_DELIVERY_ATTEMPTS) {
|
|
862
|
-
this.outbox.set(body, { ...entry, attempts, attemptedAt: Date.now() });
|
|
863
|
-
continue;
|
|
864
|
-
}
|
|
865
|
-
this.emit("subagent:notification", {
|
|
866
|
-
runId: entry.runId,
|
|
867
|
-
taskId: entry.taskIds[0],
|
|
868
|
-
kind: "completed",
|
|
869
|
-
body,
|
|
870
|
-
delivery: "failed",
|
|
871
|
-
});
|
|
872
|
-
}
|
|
873
|
-
}
|
|
874
|
-
}
|
|
875
|
-
|
|
876
|
-
flushPendingNotifications(ctx: ExtensionContext): void {
|
|
877
|
-
const existing = new Set([...this.leaderMessages.keys(), ...this.outbox.keys()]);
|
|
878
|
-
this.confirmLeaderMessages(ctx);
|
|
879
|
-
if (this.cleared) return;
|
|
880
|
-
let idle = false;
|
|
881
|
-
try {
|
|
882
|
-
idle = ctx.isIdle() && !ctx.hasPendingMessages();
|
|
883
|
-
} catch {
|
|
884
|
-
idle = false;
|
|
885
|
-
}
|
|
886
|
-
if (idle) this.markLostReceipts(ctx, existing);
|
|
887
|
-
const outstandingReceipts = () =>
|
|
888
|
-
[...this.leaderMessages.values()].some((receipt) => !receipt.delivered && !receipt.lost);
|
|
889
|
-
if (!this.pendingTaskNotices.size && !this.pendingRunNotices.size && !this.outbox.size && !outstandingReceipts())
|
|
890
|
-
return;
|
|
891
|
-
if (idle) {
|
|
892
|
-
this.flushOutbox();
|
|
893
|
-
for (const [key, pending] of [...this.pendingTaskNotices]) {
|
|
894
|
-
const report = pending.task.finalParentReport;
|
|
895
|
-
if (report?.delivery === "lost") {
|
|
896
|
-
this.pendingTaskNotices.delete(key);
|
|
897
|
-
pending.task.finalParentReport = undefined;
|
|
898
|
-
this.notifyTask(pending.run, pending.task, "completed");
|
|
899
|
-
continue;
|
|
900
|
-
}
|
|
901
|
-
if (report?.delivery === "pending") {
|
|
902
|
-
const outstanding = this.leaderMessages.has(report.body) || this.outbox.has(report.body);
|
|
903
|
-
if (outstanding) continue;
|
|
904
|
-
this.pendingTaskNotices.delete(key);
|
|
905
|
-
pending.task.finalParentReport = undefined;
|
|
906
|
-
this.notifyTask(pending.run, pending.task, "completed");
|
|
907
|
-
continue;
|
|
908
|
-
}
|
|
909
|
-
this.pendingTaskNotices.delete(key);
|
|
910
|
-
if (report && this.finalReportMatches(pending.task) && this.hasArtifactDelta(pending.task))
|
|
911
|
-
this.notifyTask(pending.run, pending.task, "completed");
|
|
912
|
-
}
|
|
913
|
-
for (const [key, run] of [...this.pendingRunNotices]) {
|
|
914
|
-
const notices = run.tasks
|
|
915
|
-
.map((task) => this.terminalNotices.get(`${run.id}:${task.id}`))
|
|
916
|
-
.filter((notice): notice is TerminalNotice => Boolean(notice));
|
|
917
|
-
const waiting = notices.some((notice) => {
|
|
918
|
-
const receipt = this.leaderMessages.get(notice.body);
|
|
919
|
-
return notice.delivery === "pending" && (!receipt || (!receipt.delivered && !receipt.lost));
|
|
920
|
-
});
|
|
921
|
-
if (waiting) continue;
|
|
922
|
-
this.pendingRunNotices.delete(key);
|
|
923
|
-
const lost = notices.some((notice) => notice.delivery === "lost");
|
|
924
|
-
if (!lost || this.notificationRetries.has(run.id)) continue;
|
|
925
|
-
this.notificationRetries.add(run.id);
|
|
926
|
-
this.notifyParent(run, run.status === "aborted" ? "aborted" : run.status === "failed" ? "failed" : "completed");
|
|
927
|
-
}
|
|
928
|
-
this.flushOutbox();
|
|
929
|
-
}
|
|
930
|
-
// Anything still unresolved (including a retry backoff) must be revisited without unrelated activity.
|
|
931
|
-
this.scheduleNotificationFlush(ctx, RETRY_BACKOFF_MS);
|
|
932
|
-
}
|
|
933
|
-
|
|
934
|
-
scheduleNotificationFlush(ctx: ExtensionContext, delayMs = 100): void {
|
|
935
|
-
if (
|
|
936
|
-
this.notificationFlushTimer ||
|
|
937
|
-
this.cleared ||
|
|
938
|
-
(!this.pendingTaskNotices.size &&
|
|
939
|
-
!this.pendingRunNotices.size &&
|
|
940
|
-
!this.outbox.size &&
|
|
941
|
-
![...this.leaderMessages.values()].some((receipt) => !receipt.delivered && !receipt.lost))
|
|
942
|
-
)
|
|
943
|
-
return;
|
|
944
|
-
this.notificationFlushTimer = setTimeout(() => {
|
|
945
|
-
this.notificationFlushTimer = undefined;
|
|
946
|
-
this.flushPendingNotifications(ctx);
|
|
947
|
-
}, delayMs);
|
|
948
|
-
this.notificationFlushTimer.unref?.();
|
|
949
|
-
}
|
|
950
|
-
|
|
951
|
-
private notifyTask(run: RunSnapshot, task: TaskSnapshot, kind: "completed" | "failed" | "aborted"): void {
|
|
952
|
-
const key = `${run.id}:${task.id}`;
|
|
953
|
-
const outcome = outcomeFingerprint(task);
|
|
954
|
-
const final = kind === "completed" && this.finalReportMatches(task);
|
|
955
|
-
const report = task.finalParentReport;
|
|
956
|
-
if (final && report?.delivery === "pending") {
|
|
957
|
-
this.pendingTaskNotices.set(key, { run, task });
|
|
958
|
-
if (this.notificationContext) this.scheduleNotificationFlush(this.notificationContext);
|
|
959
|
-
this.emit("subagent:notification", {
|
|
960
|
-
runId: run.id,
|
|
961
|
-
taskId: task.id,
|
|
962
|
-
kind,
|
|
963
|
-
body: "",
|
|
964
|
-
suppressed: true,
|
|
965
|
-
delivery: "pending",
|
|
966
|
-
});
|
|
967
|
-
return;
|
|
968
|
-
}
|
|
969
|
-
if (
|
|
970
|
-
final &&
|
|
971
|
-
report &&
|
|
972
|
-
report.delivery !== "lost" &&
|
|
973
|
-
report.delivery !== "pending" &&
|
|
974
|
-
!this.hasArtifactDelta(task)
|
|
975
|
-
) {
|
|
976
|
-
this.emit("subagent:notification", {
|
|
977
|
-
runId: run.id,
|
|
978
|
-
taskId: task.id,
|
|
979
|
-
kind,
|
|
980
|
-
body: "",
|
|
981
|
-
suppressed: true,
|
|
982
|
-
delivery: report.delivery,
|
|
983
|
-
});
|
|
984
|
-
return;
|
|
985
|
-
}
|
|
986
|
-
const covered = this.terminalNotices.get(key);
|
|
987
|
-
if (covered?.fingerprint === outcome && covered.delivery !== "lost") {
|
|
988
|
-
this.emit("subagent:notification", {
|
|
989
|
-
runId: run.id,
|
|
990
|
-
taskId: task.id,
|
|
991
|
-
kind,
|
|
992
|
-
body: "",
|
|
993
|
-
suppressed: true,
|
|
994
|
-
delivery: covered.delivery,
|
|
995
|
-
});
|
|
996
|
-
return;
|
|
997
|
-
}
|
|
998
|
-
if (this.coveredByAwait(run.id, task.id, outcome)) {
|
|
999
|
-
this.emit("subagent:notification", {
|
|
1000
|
-
runId: run.id,
|
|
1001
|
-
taskId: task.id,
|
|
1002
|
-
kind,
|
|
1003
|
-
body: "",
|
|
1004
|
-
suppressed: true,
|
|
1005
|
-
delivery: "awaited",
|
|
1006
|
-
});
|
|
1007
|
-
return;
|
|
1008
|
-
}
|
|
1009
|
-
const artifactOnly = final && report?.delivery !== "lost";
|
|
1010
|
-
const body = artifactOnly ? makeTaskArtifactNotice(run, task) : makeTaskNotice(run, task, kind);
|
|
1011
|
-
if (this.collectParked(run.id, { kind: "done", taskId: task.id, agent: task.agent, text: body })) {
|
|
1012
|
-
this.noteTerminal(run.id, task.id, body, outcome);
|
|
1013
|
-
this.terminalNotices.get(key)!.delivery = "received";
|
|
1014
|
-
this.emit("subagent:notification", { runId: run.id, taskId: task.id, kind, body, delivery: "parked" });
|
|
1015
|
-
return;
|
|
1016
|
-
}
|
|
1017
|
-
if (kind === "failed") {
|
|
1018
|
-
this.noteTerminal(run.id, task.id, body, outcome);
|
|
1019
|
-
try {
|
|
1020
|
-
this.pi.sendUserMessage(body, { deliverAs: "steer" });
|
|
1021
|
-
this.leaderMessages.set(body, {
|
|
1022
|
-
runId: run.id,
|
|
1023
|
-
taskIds: [task.id],
|
|
1024
|
-
kind: "terminal",
|
|
1025
|
-
deliverAs: "steer",
|
|
1026
|
-
delivered: false,
|
|
1027
|
-
submitted: true,
|
|
1028
|
-
lost: false,
|
|
1029
|
-
submittedAt: Date.now(),
|
|
1030
|
-
attempts: 0,
|
|
1031
|
-
submittedRunCount: this.agentRunCount,
|
|
1032
|
-
});
|
|
1033
|
-
this.emit("subagent:notification", { runId: run.id, taskId: task.id, kind, body, delivery: "submitted" });
|
|
1034
|
-
if (this.notificationContext) this.scheduleNotificationFlush(this.notificationContext, RECEIPT_GRACE_MS);
|
|
1035
|
-
} catch {
|
|
1036
|
-
this.terminalNotices.get(key)!.delivery = "lost";
|
|
1037
|
-
this.emit("subagent:notification", { runId: run.id, taskId: task.id, kind, body, delivery: "failed" });
|
|
1038
|
-
}
|
|
1039
|
-
return;
|
|
1040
|
-
}
|
|
1041
|
-
this.noteTerminal(run.id, task.id, body, outcome);
|
|
1042
|
-
this.enqueueNotification(body, {
|
|
1043
|
-
runId: run.id,
|
|
1044
|
-
taskIds: [task.id],
|
|
1045
|
-
kind: "terminal",
|
|
1046
|
-
deliverAs: "followUp",
|
|
1047
|
-
});
|
|
1048
|
-
this.emit("subagent:notification", { runId: run.id, taskId: task.id, kind, body, delivery: "held" });
|
|
1049
|
-
}
|
|
1050
|
-
|
|
1051
|
-
private notifyParent(
|
|
1052
|
-
run: RunSnapshot,
|
|
1053
|
-
kind: "completed" | "failed" | "aborted" | "asked",
|
|
1054
|
-
extra?: { taskId?: string; agent?: string; question?: string; urgent?: boolean },
|
|
1055
|
-
): void {
|
|
1056
|
-
if (kind !== "asked" && run.completionAwaited) return;
|
|
1057
|
-
let tasks = run.tasks;
|
|
1058
|
-
if (kind !== "asked") {
|
|
1059
|
-
tasks = tasks.filter((task) => {
|
|
1060
|
-
const key = `${run.id}:${task.id}`;
|
|
1061
|
-
const outcome = outcomeFingerprint(task);
|
|
1062
|
-
const covered = this.terminalNotices.get(key);
|
|
1063
|
-
if (covered?.fingerprint === outcome && covered.delivery !== "lost") return false;
|
|
1064
|
-
if (this.pendingTaskNotices.has(key)) return false;
|
|
1065
|
-
if (this.coveredByAwait(run.id, task.id, outcome)) return false;
|
|
1066
|
-
if (
|
|
1067
|
-
kind === "completed" &&
|
|
1068
|
-
task.finalParentReport?.delivery !== "lost" &&
|
|
1069
|
-
this.finalReportMatches(task) &&
|
|
1070
|
-
!this.hasArtifactDelta(task)
|
|
1071
|
-
)
|
|
1072
|
-
return false;
|
|
1073
|
-
return true;
|
|
1074
|
-
});
|
|
1075
|
-
if (tasks.length === 0) {
|
|
1076
|
-
if (!this.notificationRetries.has(run.id) && this.hasPendingSuccessMessages(run)) {
|
|
1077
|
-
this.pendingRunNotices.set(run.id, run);
|
|
1078
|
-
if (this.notificationContext) this.scheduleNotificationFlush(this.notificationContext);
|
|
1079
|
-
}
|
|
1080
|
-
this.emit("subagent:notification", { runId: run.id, kind, body: "", suppressed: true });
|
|
1081
|
-
return;
|
|
1082
|
-
}
|
|
1083
|
-
// Aggregate-only mode has no per-task hold: keep a run-level watch so a pending report
|
|
1084
|
-
// that later goes lost still produces a fallback.
|
|
1085
|
-
if (
|
|
1086
|
-
!this.notificationRetries.has(run.id) &&
|
|
1087
|
-
!this.pendingRunNotices.has(run.id) &&
|
|
1088
|
-
run.tasks.some((task) => task.finalParentReport?.delivery === "pending")
|
|
1089
|
-
) {
|
|
1090
|
-
this.pendingRunNotices.set(run.id, run);
|
|
1091
|
-
if (this.notificationContext) this.scheduleNotificationFlush(this.notificationContext);
|
|
1092
|
-
}
|
|
1093
|
-
}
|
|
1094
|
-
if (kind === "asked") {
|
|
1095
|
-
const body = makeAskNotice(run, extra ?? {});
|
|
1096
|
-
try {
|
|
1097
|
-
this.pi.sendUserMessage(body, { deliverAs: "steer" });
|
|
1098
|
-
this.emit("subagent:notification", { runId: run.id, taskId: extra?.taskId, kind, body, delivery: "submitted" });
|
|
1099
|
-
} catch {
|
|
1100
|
-
this.emit("subagent:notification", { runId: run.id, taskId: extra?.taskId, kind, body, delivery: "failed" });
|
|
1101
|
-
}
|
|
1102
|
-
return;
|
|
1103
|
-
}
|
|
1104
|
-
const body = makeNotice(run, kind, tasks);
|
|
1105
|
-
const deliverAs = kind === "failed" ? "steer" : "followUp";
|
|
1106
|
-
for (const task of tasks) this.noteTerminal(run.id, task.id, body, outcomeFingerprint(task));
|
|
1107
|
-
if (deliverAs === "steer") {
|
|
1108
|
-
try {
|
|
1109
|
-
this.pi.sendUserMessage(body, { deliverAs });
|
|
1110
|
-
this.leaderMessages.set(body, {
|
|
1111
|
-
runId: run.id,
|
|
1112
|
-
taskIds: tasks.map((t) => t.id),
|
|
1113
|
-
kind: "terminal",
|
|
1114
|
-
deliverAs,
|
|
1115
|
-
delivered: false,
|
|
1116
|
-
submitted: true,
|
|
1117
|
-
lost: false,
|
|
1118
|
-
submittedAt: Date.now(),
|
|
1119
|
-
attempts: 0,
|
|
1120
|
-
submittedRunCount: this.agentRunCount,
|
|
1121
|
-
});
|
|
1122
|
-
if (this.notificationContext) this.scheduleNotificationFlush(this.notificationContext, RECEIPT_GRACE_MS);
|
|
1123
|
-
} catch {
|
|
1124
|
-
for (const task of tasks) {
|
|
1125
|
-
const notice = this.terminalNotices.get(`${run.id}:${task.id}`);
|
|
1126
|
-
if (notice?.body === body) notice.delivery = "lost";
|
|
1127
|
-
}
|
|
1128
|
-
}
|
|
1129
|
-
this.emit("subagent:notification", { runId: run.id, kind, body, delivery: "submitted" });
|
|
1130
|
-
this.holdRunFallback(run, tasks);
|
|
1131
|
-
return;
|
|
1132
|
-
}
|
|
1133
|
-
this.enqueueNotification(body, {
|
|
1134
|
-
runId: run.id,
|
|
1135
|
-
taskIds: tasks.map((t) => t.id),
|
|
1136
|
-
kind: "terminal",
|
|
1137
|
-
deliverAs,
|
|
1138
|
-
});
|
|
1139
|
-
this.emit("subagent:notification", { runId: run.id, kind, body, delivery: "held" });
|
|
1140
|
-
this.holdRunFallback(run, tasks);
|
|
1141
|
-
}
|
|
1142
|
-
|
|
1143
|
-
/** Keep one bounded retry for aggregates whose per-task receipts never arrived. */
|
|
1144
|
-
private holdRunFallback(run: RunSnapshot, tasks: TaskSnapshot[]): void {
|
|
1145
|
-
if (this.notificationRetries.has(run.id)) return;
|
|
1146
|
-
const covered = tasks.filter((task) => {
|
|
1147
|
-
const notice = this.terminalNotices.get(`${run.id}:${task.id}`);
|
|
1148
|
-
return Boolean(notice && notice.body !== "");
|
|
1149
|
-
});
|
|
1150
|
-
if (covered.length === 0) return;
|
|
1151
|
-
this.pendingRunNotices.set(run.id, run);
|
|
1152
|
-
if (this.notificationContext) this.scheduleNotificationFlush(this.notificationContext);
|
|
1153
|
-
}
|
|
1154
|
-
|
|
1155
|
-
private widgetTui: TUI | null = null;
|
|
1156
|
-
|
|
1157
|
-
/** Settled runs are history (subagent_status/result still reach them) — the widget shows live work only. */
|
|
1158
|
-
private pruneSettledWidget(): void {
|
|
1159
|
-
this.widgetRuns = this.widgetRuns.filter((r) => !TERMINAL.includes(r.status));
|
|
1160
|
-
}
|
|
1161
|
-
private upsertWidgetRun(run: RunSnapshot | undefined): void {
|
|
1162
|
-
this.pruneSettledWidget();
|
|
1163
|
-
if (!run || TERMINAL.includes(run.status)) return;
|
|
1164
|
-
const idx = this.widgetRuns.findIndex((r) => r.id === run.id);
|
|
1165
|
-
if (idx >= 0) this.widgetRuns[idx] = run;
|
|
1166
|
-
else this.widgetRuns.push(run);
|
|
1167
|
-
}
|
|
1168
|
-
private scheduleWidget(run: RunSnapshot | undefined, ctx?: ExtensionContext): void {
|
|
1169
|
-
this.upsertWidgetRun(run);
|
|
1170
|
-
if (!run || this.widgetTimers.has(run.id)) return;
|
|
1171
|
-
this.widgetTimers.set(
|
|
1172
|
-
run.id,
|
|
1173
|
-
setTimeout(() => {
|
|
1174
|
-
this.widgetTimers.delete(run.id);
|
|
1175
|
-
if (ctx?.hasUI) {
|
|
1176
|
-
this.ensureWidget(ctx);
|
|
1177
|
-
this.widgetTui?.requestRender();
|
|
1178
|
-
}
|
|
1179
|
-
this.maybePulse(ctx);
|
|
1180
|
-
}, WIDGET_THROTTLE_MS),
|
|
1181
|
-
);
|
|
1182
|
-
}
|
|
1183
|
-
|
|
1184
|
-
private pulseTimer: ReturnType<typeof setTimeout> | null = null;
|
|
1185
|
-
private maybePulse(ctx?: ExtensionContext): void {
|
|
1186
|
-
if (this.pulseTimer || !this.widgetTui) return;
|
|
1187
|
-
const talking = this.widgetRuns.some((r) => r.tasks.some(isTalking));
|
|
1188
|
-
if (!talking) return;
|
|
1189
|
-
this.pulseTimer = setTimeout(() => {
|
|
1190
|
-
this.pulseTimer = null;
|
|
1191
|
-
this.widgetTui?.requestRender();
|
|
1192
|
-
this.maybePulse(ctx);
|
|
1193
|
-
}, 700);
|
|
1194
|
-
}
|
|
1195
|
-
private flushWidget(run: RunSnapshot | undefined, ctx?: ExtensionContext, onUpdate?: (partial: any) => void): void {
|
|
1196
|
-
if (run) {
|
|
1197
|
-
const t = this.widgetTimers.get(run.id);
|
|
1198
|
-
if (t) {
|
|
1199
|
-
clearTimeout(t);
|
|
1200
|
-
this.widgetTimers.delete(run.id);
|
|
1201
|
-
}
|
|
1202
|
-
}
|
|
1203
|
-
this.pruneSettledWidget();
|
|
1204
|
-
if (this.widgetRuns.length === 0) {
|
|
1205
|
-
if (this.widgetTui) this.clearWidget(ctx);
|
|
1206
|
-
} else {
|
|
1207
|
-
if (ctx?.hasUI) {
|
|
1208
|
-
this.ensureWidget(ctx);
|
|
1209
|
-
this.widgetTui?.requestRender();
|
|
1210
|
-
}
|
|
1211
|
-
this.maybePulse(ctx);
|
|
1212
|
-
}
|
|
1213
|
-
if (!run) return;
|
|
1214
|
-
|
|
1215
|
-
onUpdate?.({
|
|
1216
|
-
content: [
|
|
1217
|
-
{
|
|
1218
|
-
type: "text",
|
|
1219
|
-
text: `${run.tasks.filter((t) => TERMINAL.includes(t.status)).length}/${run.tasks.length} done · ${run.status}`,
|
|
1220
|
-
},
|
|
1221
|
-
],
|
|
1222
|
-
});
|
|
1223
|
-
}
|
|
1224
|
-
private ensureWidget(ctx: ExtensionContext): void {
|
|
1225
|
-
if (this.widgetTui !== null || !ctx.hasUI) return;
|
|
1226
|
-
ctx.ui.setWidget(
|
|
1227
|
-
"subagents",
|
|
1228
|
-
(tui, theme) => {
|
|
1229
|
-
this.widgetTui = tui;
|
|
1230
|
-
return new SubagentsWidget(() => [...this.widgetRuns], theme);
|
|
1231
|
-
},
|
|
1232
|
-
{ placement: "aboveEditor" },
|
|
1233
|
-
);
|
|
1234
|
-
}
|
|
1235
|
-
|
|
1236
315
|
private updateRun(run: RunSnapshot, ctx?: ExtensionContext, _onUpdate?: (partial: any) => void): void {
|
|
1237
316
|
run.aggregateUsage = aggregateUsage(run.tasks);
|
|
1238
317
|
this.runs.set(run.id, run);
|
|
@@ -1241,7 +320,7 @@ export class SubagentManager {
|
|
|
1241
320
|
status: run.status,
|
|
1242
321
|
live: run.tasks.filter((t) => !TERMINAL.includes(t.status)).length,
|
|
1243
322
|
});
|
|
1244
|
-
this.
|
|
323
|
+
this.widget.schedule(run, ctx);
|
|
1245
324
|
}
|
|
1246
325
|
private updateTask(
|
|
1247
326
|
run: RunSnapshot,
|
|
@@ -1256,7 +335,7 @@ export class SubagentManager {
|
|
|
1256
335
|
}
|
|
1257
336
|
|
|
1258
337
|
private makeChildHandlers(run: RunSnapshot, task: TaskSnapshot, ctx: ExtensionContext): ChildHandlers {
|
|
1259
|
-
this.notificationContext = ctx;
|
|
338
|
+
this.delivery.notificationContext = ctx;
|
|
1260
339
|
return {
|
|
1261
340
|
onAskParent: async (_taskId, question, urgent) => {
|
|
1262
341
|
if (TERMINAL.includes(task.status)) {
|
|
@@ -1265,7 +344,7 @@ export class SubagentManager {
|
|
|
1265
344
|
this.updateTask(run, task, { status: "awaiting_parent" }, ctx);
|
|
1266
345
|
|
|
1267
346
|
if (!this.collectParked(run.id, { kind: "ask", taskId: task.id, agent: task.agent, text: question })) {
|
|
1268
|
-
this.notifyParent(run, "asked", { taskId: task.id, agent: task.agent, question, urgent });
|
|
347
|
+
this.delivery.notifyParent(run, "asked", { taskId: task.id, agent: task.agent, question, urgent });
|
|
1269
348
|
}
|
|
1270
349
|
|
|
1271
350
|
const reply = await this.awaitParentReply(run.id, task.id, PARENT_REPLY_TIMEOUT_MS);
|
|
@@ -1277,9 +356,9 @@ export class SubagentManager {
|
|
|
1277
356
|
return reply;
|
|
1278
357
|
},
|
|
1279
358
|
onNotifyParent: (_taskId, message, level, final = false) =>
|
|
1280
|
-
this.submitLeaderReport(run, task, message, level, final, ctx),
|
|
359
|
+
this.delivery.submitLeaderReport(run, task, message, level, final, ctx),
|
|
1281
360
|
onSendMessage: (_taskId, to, text, final = false) => {
|
|
1282
|
-
if (to === "leader") return this.submitLeaderReport(run, task, text, "info", final, ctx);
|
|
361
|
+
if (to === "leader") return this.delivery.submitLeaderReport(run, task, text, "info", final, ctx);
|
|
1283
362
|
|
|
1284
363
|
return this.mailboxes.send(`${run.id}:${task.id}`, `${run.id}:${to}`, text);
|
|
1285
364
|
},
|
|
@@ -1349,7 +428,7 @@ export class SubagentManager {
|
|
|
1349
428
|
onUpdate,
|
|
1350
429
|
);
|
|
1351
430
|
} else if (event.type === "tool_execution_end") {
|
|
1352
|
-
this.
|
|
431
|
+
this.widget.schedule(run, ctx);
|
|
1353
432
|
} else if (event.type === "message_end") {
|
|
1354
433
|
const message = event.message as AssistantMessage;
|
|
1355
434
|
if (message?.role === "assistant") {
|
|
@@ -1907,7 +986,7 @@ export class SubagentManager {
|
|
|
1907
986
|
);
|
|
1908
987
|
if (task.status === "completed") outputs.set(task.id, task.finalText ?? "");
|
|
1909
988
|
if (run.notifyPerTask && TERMINAL.includes(task.status)) {
|
|
1910
|
-
this.notifyTask(run, task, task.status as "completed" | "failed" | "aborted");
|
|
989
|
+
this.delivery.notifyTask(run, task, task.status as "completed" | "failed" | "aborted");
|
|
1911
990
|
}
|
|
1912
991
|
},
|
|
1913
992
|
);
|
|
@@ -1945,10 +1024,10 @@ export class SubagentManager {
|
|
|
1945
1024
|
const aborted = run.tasks.some((t) => t.status === "aborted") || Boolean(signal?.aborted);
|
|
1946
1025
|
run.status = aborted ? "aborted" : failed ? "failed" : "completed";
|
|
1947
1026
|
run.endedAt = Date.now();
|
|
1948
|
-
this.
|
|
1027
|
+
this.widget.flush(run, ctx, onUpdate);
|
|
1949
1028
|
|
|
1950
1029
|
const live = this.listRuns().find((r) => !TERMINAL.includes(r.status));
|
|
1951
|
-
if (live) this.
|
|
1030
|
+
if (live) this.widget.schedule(live, ctx);
|
|
1952
1031
|
|
|
1953
1032
|
if (this.settlers.has(run.id)) {
|
|
1954
1033
|
this.emit("subagent:run-completed", {
|
|
@@ -1986,7 +1065,7 @@ export class SubagentManager {
|
|
|
1986
1065
|
const { run, inputs } = this.createRun(params, ctx);
|
|
1987
1066
|
void this.executeTasks(run, inputs, ctx, undefined, undefined)
|
|
1988
1067
|
.then(() => {
|
|
1989
|
-
this.notifyParent(
|
|
1068
|
+
this.delivery.notifyParent(
|
|
1990
1069
|
run,
|
|
1991
1070
|
run.status === "completed" ? "completed" : run.status === "aborted" ? "aborted" : "failed",
|
|
1992
1071
|
);
|
|
@@ -2005,7 +1084,7 @@ export class SubagentManager {
|
|
|
2005
1084
|
this.runControllers.delete(run.id);
|
|
2006
1085
|
for (const task of run.tasks) this.mailboxes.close(`${run.id}:${task.id}`);
|
|
2007
1086
|
this.emit("subagent:run-completed", { runId: run.id, status: "failed", run: cloneRun(run) });
|
|
2008
|
-
this.notifyParent(run, "failed");
|
|
1087
|
+
this.delivery.notifyParent(run, "failed");
|
|
2009
1088
|
this.persist(ctx);
|
|
2010
1089
|
});
|
|
2011
1090
|
return { run: cloneRun(run) };
|
|
@@ -2083,35 +1162,7 @@ export class SubagentManager {
|
|
|
2083
1162
|
changedFiles: undefined,
|
|
2084
1163
|
worktreeError: undefined,
|
|
2085
1164
|
});
|
|
2086
|
-
|
|
2087
|
-
this.terminalNotices.delete(scope);
|
|
2088
|
-
this.pendingTaskNotices.delete(scope);
|
|
2089
|
-
this.pendingRunNotices.delete(run.id);
|
|
2090
|
-
this.notificationRetries.delete(run.id);
|
|
2091
|
-
for (const [body, receipt] of this.leaderMessages) {
|
|
2092
|
-
if (receipt.runId !== run.id || !receipt.taskIds.includes(task.id)) continue;
|
|
2093
|
-
if (receipt.taskIds.length === 1) {
|
|
2094
|
-
this.leaderMessages.delete(body);
|
|
2095
|
-
continue;
|
|
2096
|
-
}
|
|
2097
|
-
// A shared aggregate stays for its remaining tasks; the resumed task's slice is published
|
|
2098
|
-
// again by the run's next settle.
|
|
2099
|
-
receipt.taskIds = receipt.taskIds.filter((id) => id !== task.id);
|
|
2100
|
-
}
|
|
2101
|
-
for (const [body, entry] of this.outbox) {
|
|
2102
|
-
if (entry.runId !== run.id || !entry.taskIds.includes(task.id)) continue;
|
|
2103
|
-
if (entry.taskIds.length === 1) {
|
|
2104
|
-
this.outbox.delete(body);
|
|
2105
|
-
continue;
|
|
2106
|
-
}
|
|
2107
|
-
this.outbox.delete(body);
|
|
2108
|
-
for (const otherId of entry.taskIds) {
|
|
2109
|
-
if (otherId === task.id) continue;
|
|
2110
|
-
const notice = this.terminalNotices.get(`${run.id}:${otherId}`);
|
|
2111
|
-
if (notice) notice.delivery = "lost";
|
|
2112
|
-
}
|
|
2113
|
-
}
|
|
2114
|
-
this.awaitedTaskNotices.delete(scope);
|
|
1165
|
+
this.delivery.forgetTask(run, task);
|
|
2115
1166
|
run.status = "running";
|
|
2116
1167
|
run.endedAt = undefined;
|
|
2117
1168
|
run.awaited = false;
|
|
@@ -2134,7 +1185,7 @@ export class SubagentManager {
|
|
|
2134
1185
|
}
|
|
2135
1186
|
})
|
|
2136
1187
|
.then(() => {
|
|
2137
|
-
if (run.notifyPerTask) this.notifyTask(run, task, task.status as "completed" | "failed" | "aborted");
|
|
1188
|
+
if (run.notifyPerTask) this.delivery.notifyTask(run, task, task.status as "completed" | "failed" | "aborted");
|
|
2138
1189
|
this.finishRunIfSettled(run, ctx);
|
|
2139
1190
|
});
|
|
2140
1191
|
return { ok: true, task, note: thinkingNote };
|
|
@@ -2146,7 +1197,7 @@ export class SubagentManager {
|
|
|
2146
1197
|
const aborted = run.tasks.some((t) => t.status === "aborted");
|
|
2147
1198
|
run.status = aborted ? "aborted" : failed ? "failed" : "completed";
|
|
2148
1199
|
run.endedAt = Date.now();
|
|
2149
|
-
this.
|
|
1200
|
+
this.widget.flush(run, ctx);
|
|
2150
1201
|
this.emit("subagent:run-completed", {
|
|
2151
1202
|
runId: run.id,
|
|
2152
1203
|
status: run.status,
|
|
@@ -2157,7 +1208,10 @@ export class SubagentManager {
|
|
|
2157
1208
|
this.runControllers.delete(run.id);
|
|
2158
1209
|
for (const task of run.tasks) this.mailboxes.close(`${run.id}:${task.id}`);
|
|
2159
1210
|
this.persist(ctx);
|
|
2160
|
-
this.notifyParent(
|
|
1211
|
+
this.delivery.notifyParent(
|
|
1212
|
+
run,
|
|
1213
|
+
run.status === "completed" ? "completed" : run.status === "aborted" ? "aborted" : "failed",
|
|
1214
|
+
);
|
|
2161
1215
|
}
|
|
2162
1216
|
|
|
2163
1217
|
steerTask(runId: string, taskId: string | undefined, message: string): boolean {
|
|
@@ -2181,7 +1235,7 @@ export class SubagentManager {
|
|
|
2181
1235
|
this.pendingReplies.delete(`${runId}:${taskId}`);
|
|
2182
1236
|
this.liveChildren.get(`${runId}:${taskId}`)?.abort();
|
|
2183
1237
|
this.mailboxes.close(`${runId}:${taskId}`);
|
|
2184
|
-
if (ctx) this.
|
|
1238
|
+
if (ctx) this.widget.flush(run, ctx);
|
|
2185
1239
|
this.emit("subagent:task-aborted", { runId, taskId });
|
|
2186
1240
|
return true;
|
|
2187
1241
|
}
|