@ferris1225/pi-subagents 4.1.1 → 4.1.3
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 +461 -381
- package/agents/cleaner.md +16 -6
- package/agents/documenter.md +46 -0
- package/agents/explorer.md +15 -11
- package/agents/reviewer.md +8 -4
- package/agents/worker.md +9 -4
- package/package.json +55 -55
- package/src/agents.ts +53 -0
- package/src/announcements.ts +18 -1
- package/src/completion.ts +160 -160
- package/src/config.ts +43 -13
- package/src/dispatch.ts +233 -303
- package/src/fixloop.ts +259 -62
- package/src/index.ts +3 -3
- package/src/models.ts +189 -189
- package/src/monitor.ts +101 -22
- package/src/prompt.ts +47 -12
- package/src/recovery.ts +145 -145
- package/src/rpc-run.ts +29 -10
- package/src/runtime.ts +13 -7
- package/src/session-fork.ts +80 -80
- package/src/setup.ts +162 -130
- package/src/spawn.ts +53 -13
- package/src/thread-lifecycle.ts +240 -54
- package/src/tools.ts +65 -37
- package/src/widget.ts +68 -22
- package/src/worktree.ts +27 -4
package/src/thread-lifecycle.ts
CHANGED
|
@@ -1,16 +1,22 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Stable logical-thread generation lifecycle for background sub-agents.
|
|
3
3
|
*
|
|
4
|
-
* Dispatch owns
|
|
5
|
-
*
|
|
6
|
-
*
|
|
4
|
+
* Dispatch owns workflow policy and internal role briefs; this module owns one
|
|
5
|
+
* stable parent generation end to end: managed-repository lane use,
|
|
6
|
+
* worktree setup/finalization after downstream review, queue/process ownership,
|
|
7
|
+
* retained-session resume/fork, and guarded one-time terminal publication.
|
|
7
8
|
*/
|
|
8
9
|
|
|
9
10
|
import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
10
11
|
import { existsSync } from "node:fs";
|
|
11
12
|
import { rm } from "node:fs/promises";
|
|
12
13
|
import { resolve } from "node:path";
|
|
13
|
-
import {
|
|
14
|
+
import {
|
|
15
|
+
discoverAgents,
|
|
16
|
+
isWriteCapableAgent,
|
|
17
|
+
resolveAgentTools,
|
|
18
|
+
type AgentConfig,
|
|
19
|
+
} from "./agents.ts";
|
|
14
20
|
import { completionTriggersTurn, type CompletionMessageItem } from "./completion.ts";
|
|
15
21
|
import {
|
|
16
22
|
DEFAULT_THINKING_LEVEL,
|
|
@@ -25,7 +31,15 @@ import {
|
|
|
25
31
|
modelLevelTakeoverNote,
|
|
26
32
|
queuedResult,
|
|
27
33
|
} from "./format.ts";
|
|
28
|
-
import {
|
|
34
|
+
import {
|
|
35
|
+
canStartManagedWorkflow,
|
|
36
|
+
formatChainSummary,
|
|
37
|
+
formatManagedWorkflowSummary,
|
|
38
|
+
getManagedWorkflowPlan,
|
|
39
|
+
workflowAgentAvailability,
|
|
40
|
+
type ManagedWorkflowOutcome,
|
|
41
|
+
type ManagedWorkflowPlan,
|
|
42
|
+
} from "./fixloop.ts";
|
|
29
43
|
import {
|
|
30
44
|
availableModelsInScope,
|
|
31
45
|
currentModelRef,
|
|
@@ -34,15 +48,17 @@ import {
|
|
|
34
48
|
resolveAgentModelRoute,
|
|
35
49
|
resolveThinkingLevel,
|
|
36
50
|
} from "./models.ts";
|
|
37
|
-
import { monitor } from "./monitor.ts";
|
|
51
|
+
import { monitor, sumUsage, type ContinuationKind } from "./monitor.ts";
|
|
38
52
|
import { persistRecoveryRecords, recoveryRecordFromFinalization } from "./recovery.ts";
|
|
39
53
|
import type { SubagentRuntime, SubagentThread, ThreadState } from "./runtime.ts";
|
|
40
54
|
import { forkRetainedSession } from "./session-fork.ts";
|
|
41
55
|
import {
|
|
42
56
|
buildResumePrompt,
|
|
57
|
+
getResultOutput,
|
|
43
58
|
RpcRunControl,
|
|
44
59
|
isFailedResult,
|
|
45
60
|
isModelLevelFailure,
|
|
61
|
+
reviewVerdict,
|
|
46
62
|
runSingleAgentWithMainFallback,
|
|
47
63
|
type SingleResult,
|
|
48
64
|
type SubagentDetails,
|
|
@@ -61,7 +77,7 @@ export const FORK_CONTINUATION_PROMPT =
|
|
|
61
77
|
const WORKTREE_ISOLATION_INSTRUCTIONS =
|
|
62
78
|
"You are running in a temporary detached Git worktree. Work only in the current cwd; do not create another worktree or manually copy/apply changes to the original checkout. The parent dispatcher will integrate your tracked, deleted, and untracked changes when this thread finally settles.";
|
|
63
79
|
|
|
64
|
-
function withWorktreeSystemPrompt(agent: AgentConfig): AgentConfig {
|
|
80
|
+
export function withWorktreeSystemPrompt(agent: AgentConfig): AgentConfig {
|
|
65
81
|
return {
|
|
66
82
|
...agent,
|
|
67
83
|
systemPrompt: `${agent.systemPrompt.trimEnd()}\n\n${WORKTREE_ISOLATION_INSTRUCTIONS}`.trim(),
|
|
@@ -69,10 +85,7 @@ function withWorktreeSystemPrompt(agent: AgentConfig): AgentConfig {
|
|
|
69
85
|
}
|
|
70
86
|
|
|
71
87
|
export function isWorktreeCapableAgent(agent: AgentConfig): boolean {
|
|
72
|
-
|
|
73
|
-
if (agent.name === "worker") return true;
|
|
74
|
-
if (!agent.tools) return true;
|
|
75
|
-
return agent.tools.includes("edit") || agent.tools.includes("write");
|
|
88
|
+
return isWriteCapableAgent(agent);
|
|
76
89
|
}
|
|
77
90
|
|
|
78
91
|
interface DispatchEnvironment {
|
|
@@ -116,6 +129,23 @@ export function resolveDispatchModelRoute(
|
|
|
116
129
|
};
|
|
117
130
|
}
|
|
118
131
|
|
|
132
|
+
export interface ManagedWorkflowRequest extends DispatchEnvironment {
|
|
133
|
+
plan: ManagedWorkflowPlan;
|
|
134
|
+
initialResult: SingleResult;
|
|
135
|
+
groupId: string;
|
|
136
|
+
parentRunId: number;
|
|
137
|
+
executionCwd: string;
|
|
138
|
+
projectCwd: string;
|
|
139
|
+
isolation: IsolationMode;
|
|
140
|
+
signal: AbortSignal;
|
|
141
|
+
rememberLatest: (result: SingleResult) => void;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
interface ManagedRepositoryLaneRunner {
|
|
145
|
+
<T>(cwd: string, task: () => Promise<T>): Promise<T>;
|
|
146
|
+
<T>(cwd: string, task: () => Promise<T>, signal: AbortSignal): Promise<T | undefined>;
|
|
147
|
+
}
|
|
148
|
+
|
|
119
149
|
interface BackgroundDispatcherOptions extends DispatchEnvironment {
|
|
120
150
|
runtime: SubagentRuntime;
|
|
121
151
|
finishRun: (
|
|
@@ -131,12 +161,8 @@ interface BackgroundDispatcherOptions extends DispatchEnvironment {
|
|
|
131
161
|
mode: "single" | "parallel",
|
|
132
162
|
background?: boolean,
|
|
133
163
|
) => (results: SingleResult[]) => SubagentDetails;
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
parentGroupId: string,
|
|
137
|
-
parentRunId: number,
|
|
138
|
-
executionCwd: string,
|
|
139
|
-
) => void;
|
|
164
|
+
runManagedWorkflow: (request: ManagedWorkflowRequest) => Promise<ManagedWorkflowOutcome>;
|
|
165
|
+
runInManagedRepositoryLane: ManagedRepositoryLaneRunner;
|
|
140
166
|
}
|
|
141
167
|
|
|
142
168
|
type BackgroundStarter = (
|
|
@@ -155,7 +181,8 @@ export function createBackgroundDispatcher(options: BackgroundDispatcherOptions)
|
|
|
155
181
|
finishRun,
|
|
156
182
|
makeLiveHandler,
|
|
157
183
|
makeDetails,
|
|
158
|
-
|
|
184
|
+
runManagedWorkflow,
|
|
185
|
+
runInManagedRepositoryLane,
|
|
159
186
|
} = options;
|
|
160
187
|
interface SessionSeed {
|
|
161
188
|
sessionId?: string;
|
|
@@ -163,6 +190,7 @@ export function createBackgroundDispatcher(options: BackgroundDispatcherOptions)
|
|
|
163
190
|
prompt?: string;
|
|
164
191
|
worktree?: WorktreeIsolation;
|
|
165
192
|
forkedFromRunId?: number;
|
|
193
|
+
continuationKind?: ContinuationKind;
|
|
166
194
|
}
|
|
167
195
|
|
|
168
196
|
interface ResumeReservation {
|
|
@@ -203,7 +231,7 @@ export function createBackgroundDispatcher(options: BackgroundDispatcherOptions)
|
|
|
203
231
|
cwd: string | undefined,
|
|
204
232
|
isolation: IsolationMode = "shared",
|
|
205
233
|
existingThread?: SubagentThread,
|
|
206
|
-
|
|
234
|
+
appendedObjectiveOnResume = false,
|
|
207
235
|
environment?: DispatchEnvironment,
|
|
208
236
|
seed?: SessionSeed,
|
|
209
237
|
resumeReservation?: ResumeReservation,
|
|
@@ -217,11 +245,14 @@ export function createBackgroundDispatcher(options: BackgroundDispatcherOptions)
|
|
|
217
245
|
const runCtx = environment?.ctx ?? ctx;
|
|
218
246
|
const runConfig = environment?.config ?? config;
|
|
219
247
|
const runAgents = environment?.agents ?? agents;
|
|
220
|
-
const
|
|
221
|
-
if (!
|
|
248
|
+
const discoveredAgent = runAgents.find((candidate) => candidate.name === agentName);
|
|
249
|
+
if (!discoveredAgent) return failedStartResult(agentName, task, `Unknown agent: "${agentName}".`);
|
|
250
|
+
const resolveLiveAgentTools = (candidate: AgentConfig): AgentConfig =>
|
|
251
|
+
resolveAgentTools({ ...candidate, tools: discoveredAgent.tools }, runtime.getActiveTools());
|
|
252
|
+
const agent = resolveLiveAgentTools(discoveredAgent);
|
|
222
253
|
if (isolation === "worktree" && !isWorktreeCapableAgent(agent)) {
|
|
223
254
|
return {
|
|
224
|
-
...failedStartResult(agentName, task, `Agent "${agentName}" is read-only; worktree isolation is available only to write-capable agents such as worker or
|
|
255
|
+
...failedStartResult(agentName, task, `Agent "${agentName}" is read-only; worktree isolation is available only to write-capable agents such as worker, cleaner, or documenter.`),
|
|
225
256
|
isolation,
|
|
226
257
|
};
|
|
227
258
|
}
|
|
@@ -266,6 +297,7 @@ export function createBackgroundDispatcher(options: BackgroundDispatcherOptions)
|
|
|
266
297
|
const runId = existingThread?.id ?? monitor.addRun(agent.name, task, route.agent.model, thinkingLevel, {
|
|
267
298
|
isolation,
|
|
268
299
|
...(seed?.forkedFromRunId !== undefined ? { forkedFromRunId: seed.forkedFromRunId } : {}),
|
|
300
|
+
...(seed?.continuationKind ? { continuationKind: seed.continuationKind } : {}),
|
|
269
301
|
});
|
|
270
302
|
const generation = (existingThread?.generation ?? 0) + 1;
|
|
271
303
|
const pending: SingleResult = {
|
|
@@ -280,7 +312,10 @@ export function createBackgroundDispatcher(options: BackgroundDispatcherOptions)
|
|
|
280
312
|
...(seed?.forkedFromRunId !== undefined ? { forkedFromRunId: seed.forkedFromRunId } : {}),
|
|
281
313
|
};
|
|
282
314
|
if (existingThread) {
|
|
283
|
-
monitor.restartRun(runId, agent.name, task, route.agent.model, thinkingLevel, isolation
|
|
315
|
+
monitor.restartRun(runId, agent.name, task, route.agent.model, thinkingLevel, isolation, {
|
|
316
|
+
elapsedMs: existingThread.elapsedMs,
|
|
317
|
+
continuationKind: appendedObjectiveOnResume ? "resume-appended" : "resume-retained",
|
|
318
|
+
});
|
|
284
319
|
runtime.settledRuns.delete(runId);
|
|
285
320
|
}
|
|
286
321
|
|
|
@@ -345,6 +380,7 @@ export function createBackgroundDispatcher(options: BackgroundDispatcherOptions)
|
|
|
345
380
|
control,
|
|
346
381
|
generationCompletion: Promise.resolve(),
|
|
347
382
|
lifecycleVersion: 0,
|
|
383
|
+
elapsedMs: 0,
|
|
348
384
|
sessionId: seed?.sessionId,
|
|
349
385
|
sessionDir: seed?.sessionDir,
|
|
350
386
|
forkedFromRunId: seed?.forkedFromRunId,
|
|
@@ -365,13 +401,22 @@ export function createBackgroundDispatcher(options: BackgroundDispatcherOptions)
|
|
|
365
401
|
"error",
|
|
366
402
|
);
|
|
367
403
|
};
|
|
404
|
+
const generationWorktree = worktree;
|
|
405
|
+
let generationFinalization: Promise<WorktreeFinalization> | undefined;
|
|
368
406
|
thread.finalizeIsolation = async (
|
|
369
407
|
expectedGeneration: number,
|
|
370
408
|
result?: SingleResult,
|
|
371
409
|
): Promise<WorktreeFinalization | undefined> => {
|
|
372
|
-
if (thread.isolation !== "worktree" || !
|
|
373
|
-
if (thread.generation !== expectedGeneration) return undefined;
|
|
374
|
-
|
|
410
|
+
if (thread.isolation !== "worktree" || !generationWorktree) return undefined;
|
|
411
|
+
if (thread.generation !== expectedGeneration || thread.worktree !== generationWorktree) return undefined;
|
|
412
|
+
// All normal, destructive-stop, and shutdown owners converge here. Cache
|
|
413
|
+
// the lane-protected apply itself so superseding lifecycle paths can project
|
|
414
|
+
// the same finalization onto their own result without acquiring twice.
|
|
415
|
+
generationFinalization ??= runInManagedRepositoryLane(
|
|
416
|
+
generationWorktree.originalRoot,
|
|
417
|
+
() => generationWorktree.finalize(),
|
|
418
|
+
);
|
|
419
|
+
const finalization = await generationFinalization;
|
|
375
420
|
monitor.setIsolation(runId, "worktree", finalization.status);
|
|
376
421
|
if (result) {
|
|
377
422
|
result.runId = runId;
|
|
@@ -473,6 +518,10 @@ export function createBackgroundDispatcher(options: BackgroundDispatcherOptions)
|
|
|
473
518
|
});
|
|
474
519
|
};
|
|
475
520
|
|
|
521
|
+
const persistElapsedTime = (): void => {
|
|
522
|
+
thread.elapsedMs = monitor.getElapsedMs(runId) ?? thread.elapsedMs;
|
|
523
|
+
};
|
|
524
|
+
|
|
476
525
|
thread.park = async (): Promise<"queued" | "active"> => {
|
|
477
526
|
if (thread.retired) throw new Error(`Run #${runId} was retired by subagent_stop.`);
|
|
478
527
|
if (thread.lifecycleOperation) throw new Error(`Run #${runId} is already handling ${thread.lifecycleOperation}.`);
|
|
@@ -498,8 +547,8 @@ export function createBackgroundDispatcher(options: BackgroundDispatcherOptions)
|
|
|
498
547
|
runtime.backgroundQueue.cancel(controller);
|
|
499
548
|
} else {
|
|
500
549
|
await thread.control.park();
|
|
501
|
-
//
|
|
502
|
-
//
|
|
550
|
+
// A managed downstream child does not attach to the top-level RPC
|
|
551
|
+
// control after that child settles, so cancel its queue owner explicitly.
|
|
503
552
|
if (phase === "settled") runtime.backgroundQueue.cancel(controller);
|
|
504
553
|
}
|
|
505
554
|
await completion;
|
|
@@ -513,7 +562,14 @@ export function createBackgroundDispatcher(options: BackgroundDispatcherOptions)
|
|
|
513
562
|
thread.state = "parked";
|
|
514
563
|
thread.queueController = undefined;
|
|
515
564
|
runtime.runControllers.delete(runId);
|
|
565
|
+
const parkedRun = monitor.findRun(runId);
|
|
566
|
+
if (parkedRun?.managedWorkflow && parkedRun.task !== thread.task) {
|
|
567
|
+
// The active child row previously showed this stage objective. Once it
|
|
568
|
+
// disappears, keep the parked parent aligned with what resume retains.
|
|
569
|
+
monitor.setTask(runId, thread.task);
|
|
570
|
+
}
|
|
516
571
|
monitor.setStatus(runId, "parked");
|
|
572
|
+
persistElapsedTime();
|
|
517
573
|
return queued ? "queued" : "active";
|
|
518
574
|
} finally {
|
|
519
575
|
if (thread.lifecycleVersion === version && thread.lifecycleOperation === "park") {
|
|
@@ -791,6 +847,7 @@ export function createBackgroundDispatcher(options: BackgroundDispatcherOptions)
|
|
|
791
847
|
prompt: forkObjective ?? FORK_CONTINUATION_PROMPT,
|
|
792
848
|
worktree: childWorktree,
|
|
793
849
|
forkedFromRunId: runId,
|
|
850
|
+
continuationKind: forkObjective ? "fork-appended" : "fork-retained",
|
|
794
851
|
},
|
|
795
852
|
);
|
|
796
853
|
if (child.exitCode !== -1 || child.runId === undefined) {
|
|
@@ -835,8 +892,10 @@ export function createBackgroundDispatcher(options: BackgroundDispatcherOptions)
|
|
|
835
892
|
};
|
|
836
893
|
|
|
837
894
|
const onLive = makeLiveHandler(runId, generation);
|
|
838
|
-
const
|
|
839
|
-
|
|
895
|
+
const workflowAvailability = workflowAgentAvailability(runAgents);
|
|
896
|
+
const reserveManagedLane =
|
|
897
|
+
isolation === "shared" && canStartManagedWorkflow(agent, workflowAvailability);
|
|
898
|
+
const runGeneration = async (backgroundSignal: AbortSignal): Promise<void> => {
|
|
840
899
|
if (runtime.threads.get(runId)?.generation !== generation) return;
|
|
841
900
|
let result: SingleResult;
|
|
842
901
|
try {
|
|
@@ -844,6 +903,7 @@ export function createBackgroundDispatcher(options: BackgroundDispatcherOptions)
|
|
|
844
903
|
{
|
|
845
904
|
defaultCwd: executionCwd,
|
|
846
905
|
agent: route.agent,
|
|
906
|
+
resolveAgentForAttempt: resolveLiveAgentTools,
|
|
847
907
|
agentName,
|
|
848
908
|
task,
|
|
849
909
|
cwd: executionCwd,
|
|
@@ -858,7 +918,7 @@ export function createBackgroundDispatcher(options: BackgroundDispatcherOptions)
|
|
|
858
918
|
? {
|
|
859
919
|
sessionId: priorSessionId,
|
|
860
920
|
sessionDir: priorSessionDir,
|
|
861
|
-
stdinText: seed?.prompt ?? (
|
|
921
|
+
stdinText: seed?.prompt ?? (appendedObjectiveOnResume
|
|
862
922
|
? task
|
|
863
923
|
: buildResumePrompt(priorTask ?? task, "the retained thread was resumed")),
|
|
864
924
|
}
|
|
@@ -887,8 +947,6 @@ export function createBackgroundDispatcher(options: BackgroundDispatcherOptions)
|
|
|
887
947
|
result.isolation = isolation;
|
|
888
948
|
result.forkedFromRunId = thread.forkedFromRunId;
|
|
889
949
|
result.forkChildRunIds = [...thread.forkChildRunIds];
|
|
890
|
-
thread.queueController = undefined;
|
|
891
|
-
runtime.runControllers.delete(runId);
|
|
892
950
|
thread.task = result.task;
|
|
893
951
|
thread.sessionId = result.sessionId;
|
|
894
952
|
thread.sessionDir = result.sessionDir;
|
|
@@ -897,6 +955,11 @@ export function createBackgroundDispatcher(options: BackgroundDispatcherOptions)
|
|
|
897
955
|
monitor.setModel(runId, result.model, result.modelFallbackFrom);
|
|
898
956
|
monitor.setThinking(runId, result.thinking);
|
|
899
957
|
|
|
958
|
+
const lifecycleInterrupted = (): boolean =>
|
|
959
|
+
thread.lifecycleOperation === "park" ||
|
|
960
|
+
thread.lifecycleOperation === "stop" ||
|
|
961
|
+
thread.state === "parked" ||
|
|
962
|
+
thread.state === "stopped";
|
|
900
963
|
// Destructive stop owns publication once it has synchronously claimed
|
|
901
964
|
// the lifecycle. Leave the partial result/session on the thread; the
|
|
902
965
|
// stop path waits for this queue task, finalizes isolation, and emits
|
|
@@ -909,17 +972,71 @@ export function createBackgroundDispatcher(options: BackgroundDispatcherOptions)
|
|
|
909
972
|
runtime.settledRuns.delete(runId);
|
|
910
973
|
return;
|
|
911
974
|
}
|
|
975
|
+
// A park/shutdown can win in the microtask gap after the top-level RPC
|
|
976
|
+
// settles. Do not launch an obsolete documenter/reviewer or replace the
|
|
977
|
+
// stable top-level session with an aborted downstream attempt.
|
|
978
|
+
if (backgroundSignal.aborted || lifecycleInterrupted() || !runtime.sessionActive) return;
|
|
912
979
|
|
|
913
980
|
if (thread.retireOnSettle) runtime.retireThreadSession(thread);
|
|
914
|
-
|
|
915
|
-
|
|
981
|
+
let workflowOutcome: ManagedWorkflowOutcome | undefined;
|
|
982
|
+
const workflowPlan = getManagedWorkflowPlan(result, runConfig, workflowAvailability);
|
|
983
|
+
if (workflowPlan && runtime.sessionActive) {
|
|
916
984
|
thread.state = "running";
|
|
917
|
-
// The
|
|
918
|
-
//
|
|
985
|
+
// The stable parent row now represents workflow ownership, not whichever
|
|
986
|
+
// model stage ran most recently. Internal rows own their exact role/model/
|
|
987
|
+
// thinking/timing telemetry and remain independently queryable.
|
|
988
|
+
monitor.setManagedWorkflow(runId, true);
|
|
919
989
|
monitor.setStatus(runId, "running");
|
|
920
|
-
monitor.setActivity(
|
|
921
|
-
|
|
922
|
-
|
|
990
|
+
monitor.setActivity(
|
|
991
|
+
runId,
|
|
992
|
+
workflowPlan.kind === "auto-fix" ? "auto-fix chain running" : "managed workflow running",
|
|
993
|
+
);
|
|
994
|
+
workflowOutcome = await runManagedWorkflow({
|
|
995
|
+
plan: workflowPlan,
|
|
996
|
+
initialResult: result,
|
|
997
|
+
groupId: `workflow-${runId}`,
|
|
998
|
+
parentRunId: runId,
|
|
999
|
+
executionCwd: thread.executionCwd,
|
|
1000
|
+
projectCwd: originalCwd,
|
|
1001
|
+
isolation,
|
|
1002
|
+
signal: backgroundSignal,
|
|
1003
|
+
ctx: runCtx,
|
|
1004
|
+
config: runConfig,
|
|
1005
|
+
agents: runAgents,
|
|
1006
|
+
rememberLatest: (latest) => {
|
|
1007
|
+
if (runtime.threads.get(runId) !== thread || thread.generation !== generation) return;
|
|
1008
|
+
thread.lastResult = latest;
|
|
1009
|
+
// Retained control follows the newest child session, but the live parent
|
|
1010
|
+
// row keeps the original top-level role/model/usage. The active internal
|
|
1011
|
+
// row already owns the current stage's role and telemetry.
|
|
1012
|
+
thread.agentName = latest.agent;
|
|
1013
|
+
thread.task = latest.task;
|
|
1014
|
+
thread.sessionId = latest.sessionId;
|
|
1015
|
+
thread.sessionDir = latest.sessionDir;
|
|
1016
|
+
runtime.retainSession(latest);
|
|
1017
|
+
},
|
|
1018
|
+
});
|
|
1019
|
+
|
|
1020
|
+
// Park/stop/shutdown owns this generation once it cancels the queue
|
|
1021
|
+
// signal. The newest internal partial is already on thread.lastResult;
|
|
1022
|
+
// never replace it with the old top-level result or publish stale output.
|
|
1023
|
+
if (backgroundSignal.aborted || lifecycleInterrupted() || !runtime.sessionActive) return;
|
|
1024
|
+
|
|
1025
|
+
const finalStep = workflowOutcome.steps[workflowOutcome.steps.length - 1]!;
|
|
1026
|
+
result = {
|
|
1027
|
+
...finalStep.result,
|
|
1028
|
+
runId,
|
|
1029
|
+
projectCwd: originalCwd,
|
|
1030
|
+
isolation,
|
|
1031
|
+
forkedFromRunId: thread.forkedFromRunId,
|
|
1032
|
+
forkChildRunIds: [...thread.forkChildRunIds],
|
|
1033
|
+
};
|
|
1034
|
+
thread.lastResult = result;
|
|
1035
|
+
thread.agentName = result.agent;
|
|
1036
|
+
thread.task = result.task;
|
|
1037
|
+
thread.sessionId = result.sessionId;
|
|
1038
|
+
thread.sessionDir = result.sessionDir;
|
|
1039
|
+
runtime.retainSession(result);
|
|
923
1040
|
}
|
|
924
1041
|
// Claim terminal settlement synchronously before the first slow await.
|
|
925
1042
|
// Park therefore either wins while RPC is still active, or is rejected
|
|
@@ -934,24 +1051,64 @@ export function createBackgroundDispatcher(options: BackgroundDispatcherOptions)
|
|
|
934
1051
|
thread.lifecycleOperation === "settle" &&
|
|
935
1052
|
!thread.retired;
|
|
936
1053
|
try {
|
|
937
|
-
//
|
|
938
|
-
//
|
|
939
|
-
//
|
|
940
|
-
// the same worktree early.
|
|
1054
|
+
// For isolated writers this is deliberately after the managed documenter
|
|
1055
|
+
// and reviewer stages: every child sees the same worktree, then one
|
|
1056
|
+
// lifecycle owner integrates the complete writer+docs state exactly once.
|
|
941
1057
|
await thread.finalizeIsolation(generation, result);
|
|
942
1058
|
if (!ownsSettlement()) return;
|
|
1059
|
+
if (workflowOutcome && isolation === "worktree") {
|
|
1060
|
+
for (const step of workflowOutcome.steps) {
|
|
1061
|
+
step.result.integrationStatus = result.integrationStatus;
|
|
1062
|
+
step.result.integrationApplied = result.integrationApplied;
|
|
1063
|
+
step.result.integrationError = result.integrationError;
|
|
1064
|
+
step.result.integrationWorktreePath = result.integrationWorktreePath;
|
|
1065
|
+
step.result.integrationPatchPath = result.integrationPatchPath;
|
|
1066
|
+
}
|
|
1067
|
+
}
|
|
943
1068
|
|
|
944
1069
|
const failed = isFailedResult(result);
|
|
945
1070
|
thread.state = failed ? "failed" : "completed";
|
|
946
1071
|
// Stamp the terminal monitor state before projecting it. This gives every
|
|
947
1072
|
// path a fixed endedAt even when the row is removed immediately.
|
|
948
1073
|
monitor.setStatus(runId, failed ? "failed" : "done");
|
|
1074
|
+
persistElapsedTime();
|
|
949
1075
|
if (!runtime.sessionActive || !ownsSettlement()) return;
|
|
950
1076
|
|
|
951
1077
|
const modelLevel = failed && isModelLevelFailure(result);
|
|
952
1078
|
const dispatchFailed = result.dispatchFailed === true;
|
|
953
|
-
|
|
1079
|
+
const ownedController = thread.queueController;
|
|
1080
|
+
if (runtime.runControllers.get(runId) === ownedController) runtime.runControllers.delete(runId);
|
|
1081
|
+
thread.queueController = undefined;
|
|
1082
|
+
finishRun(
|
|
1083
|
+
runId,
|
|
1084
|
+
failed ? "failed" : "done",
|
|
1085
|
+
workflowOutcome || modelLevel || dispatchFailed ? { silent: true } : undefined,
|
|
1086
|
+
);
|
|
954
1087
|
runtime.registerRunResult(runId, result);
|
|
1088
|
+
|
|
1089
|
+
if (workflowOutcome) {
|
|
1090
|
+
const lastStep = workflowOutcome.steps[workflowOutcome.steps.length - 1]!;
|
|
1091
|
+
let block = workflowOutcome.kind === "auto-fix"
|
|
1092
|
+
? formatChainSummary(workflowOutcome.steps, result)
|
|
1093
|
+
: formatManagedWorkflowSummary(workflowOutcome.steps, result);
|
|
1094
|
+
const finalVerdict = lastStep.result.agent === "reviewer"
|
|
1095
|
+
? reviewVerdict(getResultOutput(lastStep.result))
|
|
1096
|
+
: undefined;
|
|
1097
|
+
const needsFullFinal = failed || (lastStep.result.agent === "reviewer" && finalVerdict !== "pass");
|
|
1098
|
+
if (needsFullFinal) {
|
|
1099
|
+
block += `\n\n${formatCompletionBlock(result, runConfig.maxResultLines, originalCwd)}`;
|
|
1100
|
+
}
|
|
1101
|
+
if (modelLevel) block += `\n\n${modelLevelTakeoverNote(result, { runId })}`;
|
|
1102
|
+
runtime.sendCompletionGroup([{
|
|
1103
|
+
agent: `${workflowOutcome.kind === "auto-fix" ? "auto-fix chain" : "managed workflow"} (${result.agent})`,
|
|
1104
|
+
block,
|
|
1105
|
+
triggerTurn: true,
|
|
1106
|
+
usage: sumUsage(workflowOutcome.steps.map((step) => step.result.usage)),
|
|
1107
|
+
}]);
|
|
1108
|
+
runtime.completionBatcher.flush();
|
|
1109
|
+
return;
|
|
1110
|
+
}
|
|
1111
|
+
|
|
955
1112
|
const completion: CompletionMessageItem = {
|
|
956
1113
|
agent: result.agent,
|
|
957
1114
|
block: modelLevel
|
|
@@ -975,7 +1132,18 @@ export function createBackgroundDispatcher(options: BackgroundDispatcherOptions)
|
|
|
975
1132
|
} finally {
|
|
976
1133
|
if (ownsSettlement()) thread.lifecycleOperation = undefined;
|
|
977
1134
|
}
|
|
978
|
-
}
|
|
1135
|
+
};
|
|
1136
|
+
const queuedGeneration = reserveManagedLane
|
|
1137
|
+
? async (backgroundSignal: AbortSignal): Promise<void> => {
|
|
1138
|
+
await runInManagedRepositoryLane(
|
|
1139
|
+
originalCwd,
|
|
1140
|
+
() => runGeneration(backgroundSignal),
|
|
1141
|
+
backgroundSignal,
|
|
1142
|
+
);
|
|
1143
|
+
}
|
|
1144
|
+
: runGeneration;
|
|
1145
|
+
const queueController = runtime.backgroundQueue.enqueue(
|
|
1146
|
+
queuedGeneration,
|
|
979
1147
|
() => {
|
|
980
1148
|
if (runtime.threads.get(runId)?.generation !== generation) return;
|
|
981
1149
|
// Queued park/stop owns publication and may still be finalizing an
|
|
@@ -1011,26 +1179,44 @@ export function createBackgroundDispatcher(options: BackgroundDispatcherOptions)
|
|
|
1011
1179
|
thread.lifecycleOperation === "settle" &&
|
|
1012
1180
|
!thread.retired;
|
|
1013
1181
|
try {
|
|
1014
|
-
const
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1182
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
1183
|
+
const latest = thread.lastResult;
|
|
1184
|
+
const crashed: SingleResult = latest
|
|
1185
|
+
? {
|
|
1186
|
+
...latest,
|
|
1187
|
+
runId,
|
|
1188
|
+
projectCwd: originalCwd,
|
|
1189
|
+
isolation,
|
|
1190
|
+
exitCode: 1,
|
|
1191
|
+
stopReason: "error",
|
|
1192
|
+
errorMessage: `Managed workflow dispatch failed: ${errorMessage}`,
|
|
1193
|
+
dispatchFailed: true,
|
|
1194
|
+
forkedFromRunId: thread.forkedFromRunId,
|
|
1195
|
+
}
|
|
1196
|
+
: {
|
|
1197
|
+
...dispatchFailedResult(route.agent, control.getObjective(), error, thinkingLevel),
|
|
1198
|
+
runId,
|
|
1199
|
+
projectCwd: originalCwd,
|
|
1200
|
+
isolation,
|
|
1201
|
+
forkedFromRunId: thread.forkedFromRunId,
|
|
1202
|
+
};
|
|
1203
|
+
thread.lastResult = crashed;
|
|
1204
|
+
runtime.retainSession(crashed);
|
|
1020
1205
|
await thread.finalizeIsolation(generation, crashed);
|
|
1021
1206
|
if (!ownsSettlement()) return;
|
|
1022
1207
|
thread.state = "failed";
|
|
1023
1208
|
monitor.setStatus(runId, "failed");
|
|
1209
|
+
persistElapsedTime();
|
|
1024
1210
|
finishRun(runId, "failed", { silent: true });
|
|
1025
1211
|
runtime.registerRunResult(runId, crashed);
|
|
1026
1212
|
runtime.runControllers.delete(runId);
|
|
1027
1213
|
thread.queueController = undefined;
|
|
1028
1214
|
if (!runtime.sessionActive || !ownsSettlement()) return;
|
|
1029
1215
|
try {
|
|
1030
|
-
runCtx.ui.notify(`✗ ${agent
|
|
1216
|
+
runCtx.ui.notify(`✗ ${crashed.agent} dispatch failed: ${crashed.errorMessage}`, "error");
|
|
1031
1217
|
runtime.sendCompletionGroup([
|
|
1032
1218
|
{
|
|
1033
|
-
agent: agent
|
|
1219
|
+
agent: crashed.agent,
|
|
1034
1220
|
block: formatCompletionBlock(crashed, runConfig.maxResultLines, crashed.projectCwd ?? originalCwd),
|
|
1035
1221
|
triggerTurn: true,
|
|
1036
1222
|
usage: crashed.usage,
|