@ferris1225/pi-subagents 4.1.24 → 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 +47 -116
- package/agents/executor.md +53 -0
- package/package.json +55 -55
- package/src/agents.ts +2 -2
- package/src/announcements.ts +0 -26
- package/src/completion.ts +0 -11
- package/src/config.ts +1 -11
- package/src/dispatch.ts +16 -310
- package/src/durable.ts +0 -5
- package/src/format.ts +0 -8
- package/src/monitor.ts +7 -143
- package/src/prompt.ts +7 -30
- package/src/rpc-run.ts +993 -993
- package/src/runtime.ts +3 -10
- package/src/setup.ts +1 -6
- package/src/spawn.ts +0 -10
- package/src/thread-lifecycle.ts +51 -219
- package/src/widget.ts +33 -240
- 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/runtime.ts
CHANGED
|
@@ -25,7 +25,6 @@ import { isRunActiveStatus, monitor } from "./monitor.ts";
|
|
|
25
25
|
import type { RpcRunControl } from "./rpc-run.ts";
|
|
26
26
|
import type { StartBackgroundInternal } from "./thread-lifecycle.ts";
|
|
27
27
|
import { isFailedResult, type SingleResult } from "./spawn.ts";
|
|
28
|
-
import type { ReviewMode } from "./workflow.ts";
|
|
29
28
|
import type { IsolationMode, WorktreeFinalization, WorktreeIsolation } from "./worktree.ts";
|
|
30
29
|
|
|
31
30
|
export type ThreadState =
|
|
@@ -51,15 +50,12 @@ export interface SubagentThread {
|
|
|
51
50
|
executionCwd: string;
|
|
52
51
|
thinkingLevel?: ThinkingLevel;
|
|
53
52
|
isolation: IsolationMode;
|
|
54
|
-
/** Dispatch-time gate intensity; "none" skips the automatic post-writer
|
|
55
|
-
* reviewer for this thread (kept across resumes and reloads). */
|
|
56
|
-
review?: ReviewMode;
|
|
57
53
|
worktree?: WorktreeIsolation;
|
|
58
54
|
state: ThreadState;
|
|
59
55
|
control: RpcRunControl;
|
|
60
56
|
queueController?: AbortController;
|
|
61
|
-
/** Resolves only after the current generation's
|
|
62
|
-
*
|
|
57
|
+
/** Resolves only after the current generation's child process, isolation
|
|
58
|
+
* finalization, and queue work have fully quiesced and released their
|
|
63
59
|
* concurrency slot. */
|
|
64
60
|
generationCompletion: Promise<void>;
|
|
65
61
|
/** Synchronous CAS used by lifecycle controls across their async preflight. */
|
|
@@ -122,8 +118,7 @@ export interface SubagentRuntime {
|
|
|
122
118
|
/** Resume setup that has claimed a thread but has not yet enqueued its
|
|
123
119
|
* next generation. Shutdown invalidates these claims and waits for cleanup. */
|
|
124
120
|
preflightOperations: Set<Promise<void>>;
|
|
125
|
-
/** Every session directory retained for this parent session
|
|
126
|
-
* managed-workflow internals that are not directly controllable. */
|
|
121
|
+
/** Every session directory retained for this parent session. */
|
|
127
122
|
sessionDirs: Set<string>;
|
|
128
123
|
retainSession: (result: Pick<SingleResult, "sessionDir">) => void;
|
|
129
124
|
retireThreadSession: (thread: SubagentThread) => void;
|
|
@@ -148,8 +143,6 @@ export function createRuntime(pi: ExtensionAPI, configPath: string): SubagentRun
|
|
|
148
143
|
// Computing this at delivery (emit) time — not when the item was
|
|
149
144
|
// pushed — reflects the current monitor state, since finishing runs
|
|
150
145
|
// are removed from the monitor before their completion is pushed.
|
|
151
|
-
// Managed-workflow parents remain "running" through reviewer and
|
|
152
|
-
// documenter stages, so they are included without a special case.
|
|
153
146
|
const active = monitor
|
|
154
147
|
.getRuns()
|
|
155
148
|
.filter((run) => isRunActiveStatus(run.status))
|
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 {
|
|
@@ -254,7 +250,6 @@ async function runFullSetup(ctx: ExtensionCommandContext, configPath: string, ba
|
|
|
254
250
|
agentModels,
|
|
255
251
|
// Full setup returns every agent to capability-aware Auto thinking.
|
|
256
252
|
agentThinkingLevels: {},
|
|
257
|
-
notifyOnReviewPass: base.notifyOnReviewPass,
|
|
258
253
|
maxResultLines: base.maxResultLines,
|
|
259
254
|
agentScope: base.agentScope,
|
|
260
255
|
idleTimeoutSec: base.idleTimeoutSec,
|
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),
|