@ferris1225/pi-subagents 4.1.23 → 4.2.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/README.md +57 -114
- package/agents/executor.md +53 -0
- package/package.json +1 -1
- package/src/agents.ts +2 -2
- package/src/announcements.ts +0 -26
- package/src/completion.ts +0 -11
- package/src/config.ts +51 -13
- package/src/dispatch.ts +16 -310
- package/src/durable.ts +0 -5
- package/src/format.ts +0 -8
- package/src/monitor.ts +18 -143
- package/src/prompt.ts +7 -30
- package/src/recovery.ts +145 -145
- package/src/runtime.ts +3 -10
- package/src/setup.ts +13 -21
- package/src/spawn.ts +0 -10
- package/src/thread-lifecycle.ts +51 -219
- package/src/widget.ts +63 -274
- package/agents/cleaner.md +0 -50
- package/agents/documenter.md +0 -40
- package/agents/reviewer.md +0 -82
- package/agents/synthesizer.md +0 -39
- package/agents/worker.md +0 -43
- package/src/workflow.ts +0 -215
package/src/setup.ts
CHANGED
|
@@ -40,11 +40,7 @@ import { promptSelectMany, promptSelectOne } from "./ui.ts";
|
|
|
40
40
|
/** Short, selection-friendly descriptions for the built-in agents. */
|
|
41
41
|
const MODULE_HINTS: Record<string, string> = {
|
|
42
42
|
explorer: "read-only codebase recon (fast, read-only tools)",
|
|
43
|
-
|
|
44
|
-
cleaner: "apply proven cleanup and deduplicate code (full tools)",
|
|
45
|
-
documenter: "sync diff or whole-codebase comments/docs (docs write)",
|
|
46
|
-
synthesizer: "merge fan-out results/long sources into one brief (read-only)",
|
|
47
|
-
reviewer: "read-only audits and pre-commit gates",
|
|
43
|
+
executor: "implement / fix / clean up / docs sync / merge results (full tools)",
|
|
48
44
|
};
|
|
49
45
|
|
|
50
46
|
function moduleLabel(name: string): string {
|
|
@@ -248,10 +244,12 @@ async function runFullSetup(ctx: ExtensionCommandContext, configPath: string, ba
|
|
|
248
244
|
|
|
249
245
|
const next: SubagentsConfig = {
|
|
250
246
|
enabledAgents: enabled,
|
|
247
|
+
// The wizard surfaces every built-in, so an untoggled one was seen and
|
|
248
|
+
// deliberately left off — record them all as known.
|
|
249
|
+
knownAgents: [...BUILTIN_AGENT_NAMES],
|
|
251
250
|
agentModels,
|
|
252
251
|
// Full setup returns every agent to capability-aware Auto thinking.
|
|
253
252
|
agentThinkingLevels: {},
|
|
254
|
-
notifyOnReviewPass: base.notifyOnReviewPass,
|
|
255
253
|
maxResultLines: base.maxResultLines,
|
|
256
254
|
agentScope: base.agentScope,
|
|
257
255
|
idleTimeoutSec: base.idleTimeoutSec,
|
|
@@ -283,23 +281,17 @@ async function runMenu(ctx: ExtensionCommandContext, configPath: string, config:
|
|
|
283
281
|
const enabled = await pickEnabledAgents(ctx, config.enabledAgents);
|
|
284
282
|
if (enabled === undefined) continue;
|
|
285
283
|
next.enabledAgents = enabled;
|
|
286
|
-
// A newly enabled role inherits
|
|
284
|
+
// A newly enabled role inherits explorer's configured model and
|
|
287
285
|
// thinking level, so the file reflects what it will actually run
|
|
288
|
-
// instead of silently falling back to the current main model:
|
|
289
|
-
//
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
[
|
|
293
|
-
|
|
294
|
-
["synthesizer", "explorer"],
|
|
295
|
-
];
|
|
296
|
-
for (const [agent, from] of modelInheritance) {
|
|
297
|
-
if (config.enabledAgents.includes(agent) || !enabled.includes(agent)) continue;
|
|
298
|
-
if (!next.agentModels[agent] && config.agentModels[from]) {
|
|
299
|
-
next.agentModels[agent] = config.agentModels[from];
|
|
286
|
+
// instead of silently falling back to the current main model: these
|
|
287
|
+
// roles do light migration-grade work on the fast explorer lane.
|
|
288
|
+
for (const agent of enabled) {
|
|
289
|
+
if (agent === "explorer" || config.enabledAgents.includes(agent)) continue;
|
|
290
|
+
if (!next.agentModels[agent] && config.agentModels.explorer) {
|
|
291
|
+
next.agentModels[agent] = config.agentModels.explorer;
|
|
300
292
|
}
|
|
301
|
-
if (!next.agentThinkingLevels[agent] && config.agentThinkingLevels
|
|
302
|
-
next.agentThinkingLevels[agent] = config.agentThinkingLevels
|
|
293
|
+
if (!next.agentThinkingLevels[agent] && config.agentThinkingLevels.explorer) {
|
|
294
|
+
next.agentThinkingLevels[agent] = config.agentThinkingLevels.explorer;
|
|
303
295
|
}
|
|
304
296
|
}
|
|
305
297
|
next.agentModels = keepAgentEntries(next.agentModels, enabled);
|
package/src/spawn.ts
CHANGED
|
@@ -87,16 +87,6 @@ export function getFinalOutput(messages: Message[]): string {
|
|
|
87
87
|
return "";
|
|
88
88
|
}
|
|
89
89
|
|
|
90
|
-
/** Only the last standalone reviewer verdict line counts. */
|
|
91
|
-
export function reviewVerdict(output: string): "pass" | "fail" | undefined {
|
|
92
|
-
const lines = output.split("\n");
|
|
93
|
-
for (let index = lines.length - 1; index >= 0; index--) {
|
|
94
|
-
const match = /^\s*VERDICT:\s*REVIEW_(PASS|FAIL)\s*$/i.exec(lines[index]);
|
|
95
|
-
if (match) return match[1].toUpperCase() === "PASS" ? "pass" : "fail";
|
|
96
|
-
}
|
|
97
|
-
return undefined;
|
|
98
|
-
}
|
|
99
|
-
|
|
100
90
|
export const RESULT_LINE_MAX = 200;
|
|
101
91
|
|
|
102
92
|
export interface TruncatedOutput {
|
package/src/thread-lifecycle.ts
CHANGED
|
@@ -1,10 +1,9 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Stable logical-thread generation lifecycle for background sub-agents.
|
|
3
3
|
*
|
|
4
|
-
* Dispatch owns
|
|
5
|
-
* briefs; this module owns one
|
|
4
|
+
* Dispatch owns tool policy and role briefs; this module owns one
|
|
6
5
|
* stable parent generation end to end: managed-repository lane use,
|
|
7
|
-
* worktree setup/finalization
|
|
6
|
+
* worktree setup/finalization, queue/process ownership,
|
|
8
7
|
* retained-session resume, and guarded one-time terminal publication.
|
|
9
8
|
*/
|
|
10
9
|
|
|
@@ -19,7 +18,7 @@ import {
|
|
|
19
18
|
resolveAgentTools,
|
|
20
19
|
type AgentConfig,
|
|
21
20
|
} from "./agents.ts";
|
|
22
|
-
import {
|
|
21
|
+
import { type CompletionMessageItem } from "./completion.ts";
|
|
23
22
|
import {
|
|
24
23
|
DEFAULT_THINKING_LEVEL,
|
|
25
24
|
loadConfig,
|
|
@@ -43,18 +42,8 @@ import {
|
|
|
43
42
|
failedStartResult,
|
|
44
43
|
formatCompletionBlock,
|
|
45
44
|
modelLevelTakeoverNote,
|
|
46
|
-
reviewFailFollowUpNote,
|
|
47
45
|
queuedResult,
|
|
48
46
|
} from "./format.ts";
|
|
49
|
-
import {
|
|
50
|
-
canStartManagedWorkflow,
|
|
51
|
-
formatManagedWorkflowSummary,
|
|
52
|
-
getManagedWorkflowPlan,
|
|
53
|
-
workflowAgentAvailability,
|
|
54
|
-
type ManagedWorkflowOutcome,
|
|
55
|
-
type ManagedWorkflowPlan,
|
|
56
|
-
type ReviewMode,
|
|
57
|
-
} from "./workflow.ts";
|
|
58
47
|
import {
|
|
59
48
|
availableModelsInScope,
|
|
60
49
|
currentModelRef,
|
|
@@ -63,19 +52,17 @@ import {
|
|
|
63
52
|
resolveAgentModelRoute,
|
|
64
53
|
resolveThinkingLevel,
|
|
65
54
|
} from "./models.ts";
|
|
66
|
-
import { monitor
|
|
55
|
+
import { monitor } from "./monitor.ts";
|
|
67
56
|
import { persistRecoveryRecords, recoveryRecordFromFinalization } from "./recovery.ts";
|
|
68
57
|
import type { SubagentRuntime, SubagentThread, ThreadState } from "./runtime.ts";
|
|
69
58
|
import { forkRetainedSession } from "./session-fork.ts";
|
|
70
59
|
import {
|
|
71
60
|
buildResumePrompt,
|
|
72
61
|
getProjectRoot,
|
|
73
|
-
getResultOutput,
|
|
74
62
|
PROJECT_ROOTS_DIR_NAME,
|
|
75
63
|
RpcRunControl,
|
|
76
64
|
isFailedResult,
|
|
77
65
|
isModelLevelFailure,
|
|
78
|
-
reviewVerdict,
|
|
79
66
|
runSingleAgentWithMainFallback,
|
|
80
67
|
sessionExists,
|
|
81
68
|
sweepProjectResultArtifacts,
|
|
@@ -136,10 +123,10 @@ async function canonicalManagedRepositoryRoot(cwd: string): Promise<string> {
|
|
|
136
123
|
|
|
137
124
|
/** Run one operation under the canonical original-repository lane.
|
|
138
125
|
*
|
|
139
|
-
* Shared
|
|
140
|
-
*
|
|
141
|
-
*
|
|
142
|
-
*
|
|
126
|
+
* Shared write-capable generations use the abortable overload for their whole
|
|
127
|
+
* run. Isolated generations use the non-abortable overload only for their
|
|
128
|
+
* final worktree apply, so model work remains parallel while the original
|
|
129
|
+
* checkout mutation cannot race a shared writer.
|
|
143
130
|
*/
|
|
144
131
|
export async function runInManagedRepositoryLane<T>(
|
|
145
132
|
cwd: string,
|
|
@@ -285,8 +272,6 @@ export interface StartBackgroundOptions {
|
|
|
285
272
|
environment?: DispatchEnvironment;
|
|
286
273
|
seed?: SessionSeed;
|
|
287
274
|
resumeReservation?: ResumeReservation;
|
|
288
|
-
/** Gate intensity for this dispatch; a resume keeps the thread's mode. */
|
|
289
|
-
review?: ReviewMode;
|
|
290
275
|
}
|
|
291
276
|
|
|
292
277
|
export type StartBackgroundInternal = (
|
|
@@ -341,20 +326,6 @@ export function resolveDispatchModelRoute(
|
|
|
341
326
|
};
|
|
342
327
|
}
|
|
343
328
|
|
|
344
|
-
export interface ManagedWorkflowRequest extends DispatchEnvironment {
|
|
345
|
-
plan: ManagedWorkflowPlan;
|
|
346
|
-
initialResult: SingleResult;
|
|
347
|
-
groupId: string;
|
|
348
|
-
parentRunId: number;
|
|
349
|
-
executionCwd: string;
|
|
350
|
-
projectCwd: string;
|
|
351
|
-
isolation: IsolationMode;
|
|
352
|
-
/** Short identity of the isolated worktree shared by every workflow stage. */
|
|
353
|
-
worktreeId?: string;
|
|
354
|
-
signal: AbortSignal;
|
|
355
|
-
rememberLatest: (result: SingleResult) => void;
|
|
356
|
-
}
|
|
357
|
-
|
|
358
329
|
interface BackgroundDispatcherOptions {
|
|
359
330
|
runtime: SubagentRuntime;
|
|
360
331
|
/** Live dispatch environment; resolved lazily so control operations work
|
|
@@ -373,7 +344,6 @@ interface BackgroundDispatcherOptions {
|
|
|
373
344
|
mode: "single" | "parallel",
|
|
374
345
|
background?: boolean,
|
|
375
346
|
) => (results: SingleResult[]) => SubagentDetails;
|
|
376
|
-
runManagedWorkflow: (request: ManagedWorkflowRequest) => Promise<ManagedWorkflowOutcome>;
|
|
377
347
|
}
|
|
378
348
|
|
|
379
349
|
export function createBackgroundDispatcher(options: BackgroundDispatcherOptions): StartBackgroundInternal {
|
|
@@ -383,7 +353,6 @@ export function createBackgroundDispatcher(options: BackgroundDispatcherOptions)
|
|
|
383
353
|
finishRun,
|
|
384
354
|
makeLiveHandler,
|
|
385
355
|
makeDetails,
|
|
386
|
-
runManagedWorkflow,
|
|
387
356
|
} = options;
|
|
388
357
|
|
|
389
358
|
const startBackground: StartBackgroundInternal = async (
|
|
@@ -400,10 +369,6 @@ export function createBackgroundDispatcher(options: BackgroundDispatcherOptions)
|
|
|
400
369
|
seed,
|
|
401
370
|
resumeReservation,
|
|
402
371
|
} = startOptions;
|
|
403
|
-
// The dispatch-time gate choice is a thread property: resumes of a
|
|
404
|
-
// review-exempt task stay exempt instead of surprising the caller with a
|
|
405
|
-
// full gate after a reload.
|
|
406
|
-
const review: ReviewMode = startOptions.review ?? existingThread?.review ?? "gate";
|
|
407
372
|
if (!runtime.sessionActive) {
|
|
408
373
|
return failedStartResult(agentName, task, "Parent session shut down before this subagent generation could start.");
|
|
409
374
|
}
|
|
@@ -421,7 +386,7 @@ export function createBackgroundDispatcher(options: BackgroundDispatcherOptions)
|
|
|
421
386
|
const agent = resolveLiveAgentTools(discoveredAgent);
|
|
422
387
|
if (isolation === "worktree" && !isWorktreeCapableAgent(agent)) {
|
|
423
388
|
return {
|
|
424
|
-
...failedStartResult(agentName, task, `Agent "${agentName}" is read-only; worktree isolation is available only to write-capable agents such as
|
|
389
|
+
...failedStartResult(agentName, task, `Agent "${agentName}" is read-only; worktree isolation is available only to write-capable agents such as executor.`),
|
|
425
390
|
isolation,
|
|
426
391
|
};
|
|
427
392
|
}
|
|
@@ -516,7 +481,6 @@ export function createBackgroundDispatcher(options: BackgroundDispatcherOptions)
|
|
|
516
481
|
thread.executionCwd = executionCwd;
|
|
517
482
|
thread.thinkingLevel = thinkingLevel;
|
|
518
483
|
thread.isolation = isolation;
|
|
519
|
-
thread.review = review;
|
|
520
484
|
thread.worktree = worktree;
|
|
521
485
|
thread.state = "queued";
|
|
522
486
|
thread.control = control;
|
|
@@ -540,7 +504,6 @@ export function createBackgroundDispatcher(options: BackgroundDispatcherOptions)
|
|
|
540
504
|
executionCwd,
|
|
541
505
|
thinkingLevel,
|
|
542
506
|
isolation,
|
|
543
|
-
review,
|
|
544
507
|
worktree,
|
|
545
508
|
state: "queued",
|
|
546
509
|
control,
|
|
@@ -561,9 +524,10 @@ export function createBackgroundDispatcher(options: BackgroundDispatcherOptions)
|
|
|
561
524
|
});
|
|
562
525
|
|
|
563
526
|
const onLive = makeLiveHandler(runId, generation);
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
527
|
+
// Shared write-capable runs serialize on the repository lane so their
|
|
528
|
+
// edits cannot race; the lane wait releases the process slot because it
|
|
529
|
+
// is write serialization, not pool pacing.
|
|
530
|
+
const reserveManagedLane = isolation === "shared" && isWriteCapableAgent(agent);
|
|
567
531
|
const runGeneration = async (backgroundSignal: AbortSignal, controller: AbortController): Promise<void> => {
|
|
568
532
|
if (runtime.threads.get(runId)?.generation !== generation) return;
|
|
569
533
|
// The generation body owns a process slot from here (a lane wait, if
|
|
@@ -660,82 +624,12 @@ export function createBackgroundDispatcher(options: BackgroundDispatcherOptions)
|
|
|
660
624
|
// exactly one aborted result.
|
|
661
625
|
if (thread.lifecycleOperation === "stop") return;
|
|
662
626
|
|
|
663
|
-
// A shutdown can win in the microtask gap after the
|
|
664
|
-
// settles.
|
|
665
|
-
//
|
|
627
|
+
// A shutdown can win in the microtask gap after the child RPC
|
|
628
|
+
// settles. Never replace the stable top-level session with an
|
|
629
|
+
// aborted partial.
|
|
666
630
|
if (backgroundSignal.aborted || lifecycleInterrupted() || !runtime.sessionActive) return;
|
|
667
631
|
|
|
668
632
|
if (thread.retireOnSettle) runtime.retireThreadSession(thread);
|
|
669
|
-
let workflowOutcome: ManagedWorkflowOutcome | undefined;
|
|
670
|
-
const workflowPlan = getManagedWorkflowPlan(
|
|
671
|
-
result,
|
|
672
|
-
workflowAvailability,
|
|
673
|
-
review,
|
|
674
|
-
await producedWorkspaceChanges(thread),
|
|
675
|
-
);
|
|
676
|
-
if (workflowPlan && runtime.sessionActive) {
|
|
677
|
-
// The continuation is runtime-initiated (gate review,
|
|
678
|
-
// documentation sync): release this generation's
|
|
679
|
-
// concurrency slot so managed chains never starve manual
|
|
680
|
-
// dispatches. Cancellation and quiescence guarantees are
|
|
681
|
-
// unchanged — the task stays abortable and awaited.
|
|
682
|
-
runtime.backgroundQueue.suspend(controller);
|
|
683
|
-
thread.state = "running";
|
|
684
|
-
// The stable parent row now represents workflow ownership, not whichever
|
|
685
|
-
// model stage ran most recently. Internal rows own their exact role/model/
|
|
686
|
-
// thinking/timing telemetry and remain independently queryable.
|
|
687
|
-
monitor.setManagedWorkflow(runId, true);
|
|
688
|
-
monitor.setStatus(runId, "running");
|
|
689
|
-
monitor.setActivity(runId, "managed workflow running");
|
|
690
|
-
workflowOutcome = await runManagedWorkflow({
|
|
691
|
-
plan: workflowPlan,
|
|
692
|
-
initialResult: result,
|
|
693
|
-
groupId: `workflow-${runId}`,
|
|
694
|
-
parentRunId: runId,
|
|
695
|
-
executionCwd: thread.executionCwd,
|
|
696
|
-
projectCwd: originalCwd,
|
|
697
|
-
isolation,
|
|
698
|
-
...(worktree ? { worktreeId: worktreeGroupId(worktree) } : {}),
|
|
699
|
-
signal: backgroundSignal,
|
|
700
|
-
ctx: runCtx,
|
|
701
|
-
config: runConfig,
|
|
702
|
-
agents: runAgents,
|
|
703
|
-
rememberLatest: (latest) => {
|
|
704
|
-
if (runtime.threads.get(runId) !== thread || thread.generation !== generation) return;
|
|
705
|
-
thread.lastResult = latest;
|
|
706
|
-
// Retained control follows the newest child session, but the live parent
|
|
707
|
-
// row keeps the original top-level role/model/usage. The active internal
|
|
708
|
-
// row already owns the current stage's role and telemetry.
|
|
709
|
-
thread.agentName = latest.agent;
|
|
710
|
-
thread.task = latest.task;
|
|
711
|
-
thread.sessionId = latest.sessionId;
|
|
712
|
-
thread.sessionDir = latest.sessionDir;
|
|
713
|
-
runtime.retainSession(latest);
|
|
714
|
-
if (latest.sessionId && latest.sessionDir) {
|
|
715
|
-
persistThreadCheckpoint(runtime, thread, "parked");
|
|
716
|
-
}
|
|
717
|
-
},
|
|
718
|
-
});
|
|
719
|
-
|
|
720
|
-
// Park/stop/shutdown owns this generation once it cancels the queue
|
|
721
|
-
// signal. The newest internal partial is already on thread.lastResult;
|
|
722
|
-
// never replace it with the old top-level result or publish stale output.
|
|
723
|
-
if (backgroundSignal.aborted || lifecycleInterrupted() || !runtime.sessionActive) return;
|
|
724
|
-
|
|
725
|
-
const finalStep = workflowOutcome.steps[workflowOutcome.steps.length - 1]!;
|
|
726
|
-
result = {
|
|
727
|
-
...finalStep.result,
|
|
728
|
-
runId,
|
|
729
|
-
projectCwd: originalCwd,
|
|
730
|
-
isolation,
|
|
731
|
-
};
|
|
732
|
-
thread.lastResult = result;
|
|
733
|
-
thread.agentName = result.agent;
|
|
734
|
-
thread.task = result.task;
|
|
735
|
-
thread.sessionId = result.sessionId;
|
|
736
|
-
thread.sessionDir = result.sessionDir;
|
|
737
|
-
runtime.retainSession(result);
|
|
738
|
-
}
|
|
739
633
|
// Claim terminal settlement synchronously before the first slow await.
|
|
740
634
|
// Park therefore either wins while RPC is still active, or is rejected
|
|
741
635
|
// once settlement owns the generation. Destructive stop may supersede
|
|
@@ -749,89 +643,43 @@ export function createBackgroundDispatcher(options: BackgroundDispatcherOptions)
|
|
|
749
643
|
thread.lifecycleOperation === "settle" &&
|
|
750
644
|
!thread.retired;
|
|
751
645
|
try {
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
if (workflowOutcome && isolation === "worktree") {
|
|
758
|
-
for (const step of workflowOutcome.steps) {
|
|
759
|
-
step.result.integrationStatus = result.integrationStatus;
|
|
760
|
-
step.result.integrationApplied = result.integrationApplied;
|
|
761
|
-
step.result.integrationError = result.integrationError;
|
|
762
|
-
step.result.integrationWorktreePath = result.integrationWorktreePath;
|
|
763
|
-
step.result.integrationPatchPath = result.integrationPatchPath;
|
|
764
|
-
}
|
|
765
|
-
}
|
|
646
|
+
// For isolated writers the apply runs only after the child settled,
|
|
647
|
+
// so this lifecycle owner integrates the complete settled state
|
|
648
|
+
// exactly once under the repository lane.
|
|
649
|
+
await thread.finalizeIsolation(generation, result);
|
|
650
|
+
if (!ownsSettlement()) return;
|
|
766
651
|
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
const modelLevel = failed && isModelLevelFailure(result);
|
|
779
|
-
const dispatchFailed = result.dispatchFailed === true;
|
|
780
|
-
const ownedController = thread.queueController;
|
|
781
|
-
if (runtime.runControllers.get(runId) === ownedController) runtime.runControllers.delete(runId);
|
|
782
|
-
thread.queueController = undefined;
|
|
783
|
-
finishRun(
|
|
784
|
-
runId,
|
|
785
|
-
failed ? "failed" : "done",
|
|
786
|
-
workflowOutcome || modelLevel || dispatchFailed ? { silent: true } : undefined,
|
|
787
|
-
);
|
|
788
|
-
runtime.registerRunResult(runId, result);
|
|
652
|
+
const failed = isFailedResult(result);
|
|
653
|
+
thread.state = failed ? "failed" : "completed";
|
|
654
|
+
// Stamp the terminal monitor state before projecting it. This gives every
|
|
655
|
+
// path a fixed endedAt even when the row is removed immediately.
|
|
656
|
+
monitor.setStatus(runId, failed ? "failed" : "done");
|
|
657
|
+
thread.elapsedMs = monitor.getElapsedMs(runId) ?? thread.elapsedMs;
|
|
658
|
+
// Persist before the sessionActive check: a shutdown that won the
|
|
659
|
+
// lifecycle race must still leave the settled record on disk.
|
|
660
|
+
persistThreadCheckpoint(runtime, thread, failed ? "failed" : "completed");
|
|
661
|
+
if (!runtime.sessionActive || !ownsSettlement()) return;
|
|
789
662
|
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
runId: result.runId ?? workflowOutcome.steps[0]!.result.runId,
|
|
802
|
-
isolation: result.isolation,
|
|
803
|
-
integrationStatus: result.integrationStatus,
|
|
804
|
-
integrationApplied: result.integrationApplied,
|
|
805
|
-
integrationWorktreePath: result.integrationWorktreePath,
|
|
806
|
-
integrationPatchPath: result.integrationPatchPath,
|
|
807
|
-
integrationError: result.integrationError,
|
|
808
|
-
}
|
|
809
|
-
: result;
|
|
810
|
-
let block = `${formatManagedWorkflowSummary(workflowOutcome.steps, result)}\n\n${formatCompletionBlock(writerHandoff, runConfig.maxResultLines, { resultRoot: projectResultsRoot(runtime.configPath, result.projectCwd ?? originalCwd) })}`;
|
|
811
|
-
if (finalVerdict === "fail") {
|
|
812
|
-
block += `\n\n${reviewFailFollowUpNote()}`;
|
|
813
|
-
}
|
|
814
|
-
if (modelLevel) block += `\n\n${modelLevelTakeoverNote(result, { runId })}`;
|
|
815
|
-
runtime.sendCompletionGroup([{
|
|
816
|
-
agent: `managed workflow (${result.agent})`,
|
|
817
|
-
block,
|
|
818
|
-
triggerTurn: true,
|
|
819
|
-
usage: sumUsage(workflowOutcome.steps.map((step) => step.result.usage)),
|
|
820
|
-
}]);
|
|
821
|
-
runtime.completionBatcher.flush();
|
|
822
|
-
return;
|
|
823
|
-
}
|
|
663
|
+
const modelLevel = failed && isModelLevelFailure(result);
|
|
664
|
+
const dispatchFailed = result.dispatchFailed === true;
|
|
665
|
+
const ownedController = thread.queueController;
|
|
666
|
+
if (runtime.runControllers.get(runId) === ownedController) runtime.runControllers.delete(runId);
|
|
667
|
+
thread.queueController = undefined;
|
|
668
|
+
finishRun(
|
|
669
|
+
runId,
|
|
670
|
+
failed ? "failed" : "done",
|
|
671
|
+
modelLevel || dispatchFailed ? { silent: true } : undefined,
|
|
672
|
+
);
|
|
673
|
+
runtime.registerRunResult(runId, result);
|
|
824
674
|
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
usage: result.usage,
|
|
834
|
-
};
|
|
675
|
+
const completion: CompletionMessageItem = {
|
|
676
|
+
agent: result.agent,
|
|
677
|
+
block: modelLevel
|
|
678
|
+
? `${formatCompletionBlock(result, runConfig.maxResultLines, { resultRoot: projectResultsRoot(runtime.configPath, result.projectCwd ?? originalCwd) })}\n\n${modelLevelTakeoverNote(result, { runId })}`
|
|
679
|
+
: formatCompletionBlock(result, runConfig.maxResultLines, { resultRoot: projectResultsRoot(runtime.configPath, result.projectCwd ?? originalCwd) }),
|
|
680
|
+
triggerTurn: true,
|
|
681
|
+
usage: result.usage,
|
|
682
|
+
};
|
|
835
683
|
if (modelLevel) {
|
|
836
684
|
const detail = result.errorMessage?.trim() || "model unavailable or broken";
|
|
837
685
|
runCtx.ui.notify(`✗ ${result.agent} dispatch failed: ${detail} — task handed to the main window`, "error");
|
|
@@ -911,7 +759,7 @@ export function createBackgroundDispatcher(options: BackgroundDispatcherOptions)
|
|
|
911
759
|
isolation,
|
|
912
760
|
exitCode: 1,
|
|
913
761
|
stopReason: "error",
|
|
914
|
-
errorMessage: `
|
|
762
|
+
errorMessage: `Subagent dispatch failed: ${errorMessage}`,
|
|
915
763
|
dispatchFailed: true,
|
|
916
764
|
}
|
|
917
765
|
: {
|
|
@@ -1299,21 +1147,6 @@ async function discardRestoredRecord(runtime: SubagentRuntime, record: ThreadRec
|
|
|
1299
1147
|
await removeThreadRecord(runtime.configPath, record.runId).catch(() => undefined);
|
|
1300
1148
|
}
|
|
1301
1149
|
|
|
1302
|
-
/** Whether a settled generation left anything for a gate to review.
|
|
1303
|
-
*
|
|
1304
|
-
* Only an isolated worktree can answer: it starts from the integration base, so a
|
|
1305
|
-
* diff against that base is precisely this generation's own output. A shared
|
|
1306
|
-
* checkout is shared with the user and their editor, so nothing in it can be
|
|
1307
|
-
* attributed to one run, and undefined keeps the gate. Wrongly skipping a review
|
|
1308
|
-
* is the only failure here that costs correctness rather than a run, so an
|
|
1309
|
-
* undecidable case always pays for the run. */
|
|
1310
|
-
async function producedWorkspaceChanges(
|
|
1311
|
-
thread: SubagentThread,
|
|
1312
|
-
): Promise<boolean | undefined> {
|
|
1313
|
-
if (!thread.worktree) return undefined;
|
|
1314
|
-
return thread.worktree.hasPendingChanges().catch(() => undefined);
|
|
1315
|
-
}
|
|
1316
|
-
|
|
1317
1150
|
/** Project-scoped <projectRoot>/results for a completion's artifacts. */
|
|
1318
1151
|
export function projectResultsRoot(configPath: string, cwd: string | undefined): string {
|
|
1319
1152
|
return join(getProjectRoot(configPath, cwd), "results");
|
|
@@ -1334,7 +1167,6 @@ function createRestoredThread(
|
|
|
1334
1167
|
executionCwd: record.executionCwd,
|
|
1335
1168
|
...(record.thinkingLevel ? { thinkingLevel: record.thinkingLevel as ThinkingLevel } : {}),
|
|
1336
1169
|
isolation: record.isolation,
|
|
1337
|
-
...(record.review ? { review: record.review } : {}),
|
|
1338
1170
|
worktree,
|
|
1339
1171
|
state,
|
|
1340
1172
|
control: new RpcRunControl(record.task, record.generation),
|