@ferris1225/pi-subagents 4.1.13 → 4.1.16
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 +333 -291
- package/agents/cleaner.md +24 -30
- package/agents/documenter.md +23 -20
- package/agents/explorer.md +7 -2
- package/agents/reviewer.md +82 -77
- package/agents/worker.md +45 -37
- package/package.json +1 -1
- package/src/announcements.ts +59 -54
- package/src/background.ts +26 -10
- package/src/completion.ts +7 -1
- package/src/config.ts +1 -1
- package/src/dispatch.ts +133 -133
- package/src/durable.ts +85 -19
- package/src/format.ts +179 -167
- package/src/monitor.ts +4 -2
- package/src/prompt.ts +14 -27
- package/src/runtime.ts +18 -14
- package/src/setup.ts +3 -3
- package/src/spawn.ts +650 -642
- package/src/temp-hygiene.ts +0 -28
- package/src/thread-lifecycle.ts +85 -99
- package/src/tools.ts +712 -708
- package/src/widget.ts +9 -0
- package/src/workflow.ts +199 -248
- package/src/worktree.ts +64 -37
package/src/temp-hygiene.ts
CHANGED
|
@@ -164,31 +164,3 @@ export function sweepOrphanTempDirs(
|
|
|
164
164
|
return removed;
|
|
165
165
|
}
|
|
166
166
|
|
|
167
|
-
/** Remove state-root directories no manifest record references. Fresh
|
|
168
|
-
* directories (a run just created but not yet recorded) are protected by the
|
|
169
|
-
* age cap, since the sweep only runs at extension load before new work. */
|
|
170
|
-
export function sweepUnreferencedState(
|
|
171
|
-
stateRoot: string,
|
|
172
|
-
referencedPaths: ReadonlySet<string>,
|
|
173
|
-
options: SweepOptions = {},
|
|
174
|
-
): number {
|
|
175
|
-
const now = options.now ?? Date.now();
|
|
176
|
-
const maxAgeMs = options.unmarkedMaxAgeMs ?? UNREFERENCED_STATE_MAX_AGE_MS;
|
|
177
|
-
const pathKey = (path: string): string => (process.platform === "win32" ? path.toLowerCase() : path);
|
|
178
|
-
const referenced = new Set([...referencedPaths].map(pathKey));
|
|
179
|
-
let entries: Dirent[];
|
|
180
|
-
try {
|
|
181
|
-
entries = readdirSync(stateRoot, { withFileTypes: true });
|
|
182
|
-
} catch {
|
|
183
|
-
return 0;
|
|
184
|
-
}
|
|
185
|
-
let removed = 0;
|
|
186
|
-
for (const entry of entries) {
|
|
187
|
-
if (!entry.isDirectory() || entry.isSymbolicLink()) continue;
|
|
188
|
-
const path = join(stateRoot, entry.name);
|
|
189
|
-
if (referenced.has(pathKey(path))) continue;
|
|
190
|
-
const ageMs = directoryAgeMs(entry, stateRoot, now);
|
|
191
|
-
if (ageMs !== undefined && ageMs > maxAgeMs && removeDir(path)) removed++;
|
|
192
|
-
}
|
|
193
|
-
return removed;
|
|
194
|
-
}
|
package/src/thread-lifecycle.ts
CHANGED
|
@@ -12,7 +12,7 @@ import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
|
12
12
|
import { existsSync } from "node:fs";
|
|
13
13
|
import { rm } from "node:fs/promises";
|
|
14
14
|
import { realpath } from "node:fs/promises";
|
|
15
|
-
import { resolve } from "node:path";
|
|
15
|
+
import { join, resolve } from "node:path";
|
|
16
16
|
import {
|
|
17
17
|
discoverAgents,
|
|
18
18
|
isWriteCapableAgent,
|
|
@@ -27,10 +27,9 @@ import {
|
|
|
27
27
|
type ThinkingLevel,
|
|
28
28
|
} from "./config.ts";
|
|
29
29
|
import {
|
|
30
|
-
getStateRoot,
|
|
31
30
|
readThreadRecords,
|
|
32
|
-
referencedDurablePaths,
|
|
33
31
|
removeThreadRecord,
|
|
32
|
+
pruneStaleProjectRoots,
|
|
34
33
|
pruneThreadRecords,
|
|
35
34
|
restoredResultFromSummary,
|
|
36
35
|
threadRecordFromThread,
|
|
@@ -42,6 +41,7 @@ import {
|
|
|
42
41
|
failedStartResult,
|
|
43
42
|
formatCompletionBlock,
|
|
44
43
|
modelLevelTakeoverNote,
|
|
44
|
+
reviewFailFollowUpNote,
|
|
45
45
|
queuedResult,
|
|
46
46
|
} from "./format.ts";
|
|
47
47
|
import {
|
|
@@ -66,6 +66,7 @@ import type { SubagentRuntime, SubagentThread, ThreadState } from "./runtime.ts"
|
|
|
66
66
|
import { forkRetainedSession } from "./session-fork.ts";
|
|
67
67
|
import {
|
|
68
68
|
buildResumePrompt,
|
|
69
|
+
getProjectRoot,
|
|
69
70
|
getResultOutput,
|
|
70
71
|
RpcRunControl,
|
|
71
72
|
isFailedResult,
|
|
@@ -81,7 +82,6 @@ import {
|
|
|
81
82
|
isProcessAlive,
|
|
82
83
|
killProcessTree,
|
|
83
84
|
sweepOrphanTempDirs,
|
|
84
|
-
sweepUnreferencedState,
|
|
85
85
|
} from "./temp-hygiene.ts";
|
|
86
86
|
import {
|
|
87
87
|
createWorktreeIsolation,
|
|
@@ -220,16 +220,19 @@ export function ownsResumeReservation(
|
|
|
220
220
|
);
|
|
221
221
|
}
|
|
222
222
|
|
|
223
|
-
/** Fire-and-forget durable checkpoint
|
|
224
|
-
*
|
|
223
|
+
/** Fire-and-forget durable checkpoint. Parked threads stay resumable across
|
|
224
|
+
* reloads; a settled thread drops its record so the manifest only exists
|
|
225
|
+
* while unfinished work needs it. The live session keeps working when the
|
|
226
|
+
* manifest is unwritable; only cross-reload resume is degraded. */
|
|
225
227
|
export function persistThreadCheckpoint(
|
|
226
228
|
runtime: SubagentRuntime,
|
|
227
229
|
thread: SubagentThread,
|
|
228
230
|
state: "parked" | "completed" | "failed",
|
|
229
231
|
): void {
|
|
230
|
-
|
|
231
|
-
(
|
|
232
|
-
|
|
232
|
+
const write = state === "parked"
|
|
233
|
+
? upsertThreadRecord(runtime.configPath, threadRecordFromThread(thread, state))
|
|
234
|
+
: removeThreadRecord(runtime.configPath, thread.id);
|
|
235
|
+
void write.catch(() => undefined);
|
|
233
236
|
}
|
|
234
237
|
|
|
235
238
|
const WORKTREE_ISOLATION_INSTRUCTIONS =
|
|
@@ -246,27 +249,6 @@ export function isWorktreeCapableAgent(agent: AgentConfig): boolean {
|
|
|
246
249
|
return isWriteCapableAgent(agent);
|
|
247
250
|
}
|
|
248
251
|
|
|
249
|
-
/** A direct reviewer otherwise cannot infer enabled-role availability from its
|
|
250
|
-
* isolated task. Managed internal gates receive the same contract in their
|
|
251
|
-
* generated briefs. Advisory reviews still emit neither machine marker. */
|
|
252
|
-
function withEnabledDocumenterReviewContract(agent: AgentConfig): AgentConfig {
|
|
253
|
-
return {
|
|
254
|
-
...agent,
|
|
255
|
-
systemPrompt: `${agent.systemPrompt.trimEnd()}\n\nRuntime workflow context: documenter is enabled. In gate reviews, documentation drift is non-gating: emit DOCUMENTATION: NEEDED with ## Documentation notes, or DOCUMENTATION: CLEAN when no sync is needed. Advisory reviews still emit neither VERDICT nor DOCUMENTATION markers.`.trim(),
|
|
256
|
-
};
|
|
257
|
-
}
|
|
258
|
-
|
|
259
|
-
/** The dispatcher explicitly requested a report-only review: forbid the gate
|
|
260
|
-
* markers at the source and (in dispatch) refuse to chain on them anyway. */
|
|
261
|
-
function withAdvisoryReviewContract(agent: AgentConfig): AgentConfig {
|
|
262
|
-
return {
|
|
263
|
-
...agent,
|
|
264
|
-
systemPrompt: `${agent.systemPrompt.trimEnd()}
|
|
265
|
-
|
|
266
|
-
Runtime workflow context: this dispatch is advisory. Report findings only; do not emit VERDICT or DOCUMENTATION markers — the runtime will not act on them.`.trim(),
|
|
267
|
-
};
|
|
268
|
-
}
|
|
269
|
-
|
|
270
252
|
export interface DispatchEnvironment {
|
|
271
253
|
ctx: ExtensionContext;
|
|
272
254
|
config: SubagentsConfig;
|
|
@@ -296,7 +278,6 @@ export interface StartBackgroundOptions {
|
|
|
296
278
|
environment?: DispatchEnvironment;
|
|
297
279
|
seed?: SessionSeed;
|
|
298
280
|
resumeReservation?: ResumeReservation;
|
|
299
|
-
advisoryReview?: boolean;
|
|
300
281
|
}
|
|
301
282
|
|
|
302
283
|
export type StartBackgroundInternal = (
|
|
@@ -421,20 +402,11 @@ export function createBackgroundDispatcher(options: BackgroundDispatcherOptions)
|
|
|
421
402
|
const runCtx = baseEnvironment.ctx;
|
|
422
403
|
const runConfig = baseEnvironment.config;
|
|
423
404
|
const runAgents = baseEnvironment.agents;
|
|
424
|
-
const stateRoot = getStateRoot(runtime.configPath);
|
|
425
405
|
const discoveredAgent = runAgents.find((candidate) => candidate.name === agentName);
|
|
426
406
|
if (!discoveredAgent) return failedStartResult(agentName, task, `Unknown agent: "${agentName}".`);
|
|
427
407
|
const resolveLiveAgentTools = (candidate: AgentConfig): AgentConfig =>
|
|
428
408
|
resolveAgentTools({ ...candidate, tools: discoveredAgent.tools }, runtime.getActiveTools());
|
|
429
|
-
const
|
|
430
|
-
const advisoryReview = startOptions.advisoryReview ?? existingThread?.advisoryReview ?? false;
|
|
431
|
-
const agent = agentName === "reviewer"
|
|
432
|
-
? advisoryReview
|
|
433
|
-
? withAdvisoryReviewContract(resolvedAgent)
|
|
434
|
-
: runAgents.some((candidate) => candidate.name === "documenter")
|
|
435
|
-
? withEnabledDocumenterReviewContract(resolvedAgent)
|
|
436
|
-
: resolvedAgent
|
|
437
|
-
: resolvedAgent;
|
|
409
|
+
const agent = resolveLiveAgentTools(discoveredAgent);
|
|
438
410
|
if (isolation === "worktree" && !isWorktreeCapableAgent(agent)) {
|
|
439
411
|
return {
|
|
440
412
|
...failedStartResult(agentName, task, `Agent "${agentName}" is read-only; worktree isolation is available only to write-capable agents such as worker, cleaner, or documenter.`),
|
|
@@ -443,6 +415,9 @@ export function createBackgroundDispatcher(options: BackgroundDispatcherOptions)
|
|
|
443
415
|
}
|
|
444
416
|
|
|
445
417
|
const originalCwd = resolve(cwd ?? runCtx.cwd);
|
|
418
|
+
const projectRoot = getProjectRoot(runtime.configPath, originalCwd);
|
|
419
|
+
const sessionsRoot = join(projectRoot, "sessions");
|
|
420
|
+
const worktreesRoot = join(projectRoot, "worktrees");
|
|
446
421
|
const previousWorktree = existingThread?.worktree;
|
|
447
422
|
let worktree = seed?.worktree ?? previousWorktree;
|
|
448
423
|
if (isolation === "worktree") {
|
|
@@ -455,7 +430,7 @@ export function createBackgroundDispatcher(options: BackgroundDispatcherOptions)
|
|
|
455
430
|
}
|
|
456
431
|
if (!worktree) {
|
|
457
432
|
try {
|
|
458
|
-
worktree = await createWorktreeIsolation(originalCwd, { tempBaseDir:
|
|
433
|
+
worktree = await createWorktreeIsolation(originalCwd, { tempBaseDir: worktreesRoot });
|
|
459
434
|
} catch (error) {
|
|
460
435
|
return {
|
|
461
436
|
...failedStartResult(agentName, task, error instanceof Error ? error.message : String(error)),
|
|
@@ -528,7 +503,6 @@ export function createBackgroundDispatcher(options: BackgroundDispatcherOptions)
|
|
|
528
503
|
thread.executionCwd = executionCwd;
|
|
529
504
|
thread.thinkingLevel = thinkingLevel;
|
|
530
505
|
thread.isolation = isolation;
|
|
531
|
-
thread.advisoryReview = advisoryReview;
|
|
532
506
|
thread.worktree = worktree;
|
|
533
507
|
thread.state = "queued";
|
|
534
508
|
thread.control = control;
|
|
@@ -552,7 +526,6 @@ export function createBackgroundDispatcher(options: BackgroundDispatcherOptions)
|
|
|
552
526
|
executionCwd,
|
|
553
527
|
thinkingLevel,
|
|
554
528
|
isolation,
|
|
555
|
-
advisoryReview,
|
|
556
529
|
worktree,
|
|
557
530
|
state: "queued",
|
|
558
531
|
control,
|
|
@@ -576,10 +549,7 @@ export function createBackgroundDispatcher(options: BackgroundDispatcherOptions)
|
|
|
576
549
|
const workflowAvailability = workflowAgentAvailability(runAgents);
|
|
577
550
|
const reserveManagedLane =
|
|
578
551
|
isolation === "shared" && canStartManagedWorkflow(agent, workflowAvailability);
|
|
579
|
-
|
|
580
|
-
// await, so the managed-continuation slot suspension always sees it.
|
|
581
|
-
let generationController: AbortController | undefined;
|
|
582
|
-
const runGeneration = async (backgroundSignal: AbortSignal): Promise<void> => {
|
|
552
|
+
const runGeneration = async (backgroundSignal: AbortSignal, controller: AbortController): Promise<void> => {
|
|
583
553
|
if (runtime.threads.get(runId)?.generation !== generation) return;
|
|
584
554
|
// Model/thinking config may have changed while this generation sat
|
|
585
555
|
// queued behind the concurrency limit. Re-resolve the route at actual
|
|
@@ -615,7 +585,7 @@ export function createBackgroundDispatcher(options: BackgroundDispatcherOptions)
|
|
|
615
585
|
control,
|
|
616
586
|
makeDetails: makeDetails("single", true),
|
|
617
587
|
idleTimeoutMs: activeIdleTimeoutMs,
|
|
618
|
-
sessionRoot:
|
|
588
|
+
sessionRoot: sessionsRoot,
|
|
619
589
|
...(priorSessionId && priorSessionDir
|
|
620
590
|
? {
|
|
621
591
|
sessionId: priorSessionId,
|
|
@@ -670,20 +640,20 @@ export function createBackgroundDispatcher(options: BackgroundDispatcherOptions)
|
|
|
670
640
|
if (thread.lifecycleOperation === "stop") return;
|
|
671
641
|
|
|
672
642
|
// A shutdown can win in the microtask gap after the top-level RPC
|
|
673
|
-
// settles. Do not launch an obsolete
|
|
643
|
+
// settles. Do not launch an obsolete downstream gate or replace the
|
|
674
644
|
// stable top-level session with an aborted downstream attempt.
|
|
675
645
|
if (backgroundSignal.aborted || lifecycleInterrupted() || !runtime.sessionActive) return;
|
|
676
646
|
|
|
677
647
|
if (thread.retireOnSettle) runtime.retireThreadSession(thread);
|
|
678
648
|
let workflowOutcome: ManagedWorkflowOutcome | undefined;
|
|
679
|
-
const workflowPlan = getManagedWorkflowPlan(result, workflowAvailability
|
|
649
|
+
const workflowPlan = getManagedWorkflowPlan(result, workflowAvailability);
|
|
680
650
|
if (workflowPlan && runtime.sessionActive) {
|
|
681
651
|
// The continuation is runtime-initiated (gate review,
|
|
682
652
|
// documentation sync): release this generation's
|
|
683
653
|
// concurrency slot so managed chains never starve manual
|
|
684
654
|
// dispatches. Cancellation and quiescence guarantees are
|
|
685
655
|
// unchanged — the task stays abortable and awaited.
|
|
686
|
-
runtime.backgroundQueue.suspend(
|
|
656
|
+
runtime.backgroundQueue.suspend(controller);
|
|
687
657
|
thread.state = "running";
|
|
688
658
|
// The stable parent row now represents workflow ownership, not whichever
|
|
689
659
|
// model stage ran most recently. Internal rows own their exact role/model/
|
|
@@ -799,7 +769,10 @@ export function createBackgroundDispatcher(options: BackgroundDispatcherOptions)
|
|
|
799
769
|
: undefined;
|
|
800
770
|
const needsFullFinal = failed || (lastStep.result.agent === "reviewer" && finalVerdict !== "pass");
|
|
801
771
|
if (needsFullFinal) {
|
|
802
|
-
block += `\n\n${formatCompletionBlock(result, runConfig.maxResultLines, originalCwd)}`;
|
|
772
|
+
block += `\n\n${formatCompletionBlock(result, runConfig.maxResultLines, { resultRoot: projectResultsRoot(runtime.configPath, result.projectCwd ?? originalCwd) })}`;
|
|
773
|
+
}
|
|
774
|
+
if (finalVerdict === "fail") {
|
|
775
|
+
block += `\n\n${reviewFailFollowUpNote()}`;
|
|
803
776
|
}
|
|
804
777
|
if (modelLevel) block += `\n\n${modelLevelTakeoverNote(result, { runId })}`;
|
|
805
778
|
runtime.sendCompletionGroup([{
|
|
@@ -815,8 +788,10 @@ export function createBackgroundDispatcher(options: BackgroundDispatcherOptions)
|
|
|
815
788
|
const completion: CompletionMessageItem = {
|
|
816
789
|
agent: result.agent,
|
|
817
790
|
block: modelLevel
|
|
818
|
-
? `${formatCompletionBlock(result, runConfig.maxResultLines, result.projectCwd ?? originalCwd)}\n\n${modelLevelTakeoverNote(result, { runId })}`
|
|
819
|
-
:
|
|
791
|
+
? `${formatCompletionBlock(result, runConfig.maxResultLines, { resultRoot: projectResultsRoot(runtime.configPath, result.projectCwd ?? originalCwd) })}\n\n${modelLevelTakeoverNote(result, { runId })}`
|
|
792
|
+
: result.agent === "reviewer" && reviewVerdict(getResultOutput(result)) === "fail"
|
|
793
|
+
? `${formatCompletionBlock(result, runConfig.maxResultLines, { resultRoot: projectResultsRoot(runtime.configPath, result.projectCwd ?? originalCwd) })}\n\n${reviewFailFollowUpNote()}`
|
|
794
|
+
: formatCompletionBlock(result, runConfig.maxResultLines, { resultRoot: projectResultsRoot(runtime.configPath, result.projectCwd ?? originalCwd) }),
|
|
820
795
|
triggerTurn: completionTriggersTurn(result, runConfig.notifyOnReviewPass),
|
|
821
796
|
usage: result.usage,
|
|
822
797
|
};
|
|
@@ -837,13 +812,21 @@ export function createBackgroundDispatcher(options: BackgroundDispatcherOptions)
|
|
|
837
812
|
}
|
|
838
813
|
};
|
|
839
814
|
const queuedGeneration = reserveManagedLane
|
|
840
|
-
? async (backgroundSignal: AbortSignal): Promise<void> => {
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
815
|
+
? async (backgroundSignal: AbortSignal, controller: AbortController): Promise<void> => {
|
|
816
|
+
// This task may spend its whole life waiting for the repository
|
|
817
|
+
// lane (every shared write-capable generation serializes on it).
|
|
818
|
+
// Holding a process slot while only waiting let a batch of shared
|
|
819
|
+
// writers park the entire pool and starve independent read-only
|
|
820
|
+
// dispatches that could have started immediately, so the slot is
|
|
821
|
+
// released up front; the lane itself still serializes same-repo
|
|
822
|
+
// writers, and abort/quiesce guarantees are unchanged.
|
|
823
|
+
runtime.backgroundQueue.suspend(controller);
|
|
824
|
+
await runInManagedRepositoryLane(
|
|
825
|
+
originalCwd,
|
|
826
|
+
() => runGeneration(backgroundSignal, controller),
|
|
827
|
+
backgroundSignal,
|
|
828
|
+
);
|
|
829
|
+
}
|
|
847
830
|
: runGeneration;
|
|
848
831
|
const queueController = runtime.backgroundQueue.enqueue(
|
|
849
832
|
queuedGeneration,
|
|
@@ -915,7 +898,7 @@ export function createBackgroundDispatcher(options: BackgroundDispatcherOptions)
|
|
|
915
898
|
runtime.sendCompletionGroup([
|
|
916
899
|
{
|
|
917
900
|
agent: crashed.agent,
|
|
918
|
-
block: formatCompletionBlock(crashed, runConfig.maxResultLines, crashed.projectCwd ?? originalCwd),
|
|
901
|
+
block: formatCompletionBlock(crashed, runConfig.maxResultLines, { resultRoot: projectResultsRoot(runtime.configPath, crashed.projectCwd ?? originalCwd) }),
|
|
919
902
|
triggerTurn: true,
|
|
920
903
|
usage: crashed.usage,
|
|
921
904
|
},
|
|
@@ -929,7 +912,6 @@ export function createBackgroundDispatcher(options: BackgroundDispatcherOptions)
|
|
|
929
912
|
}
|
|
930
913
|
},
|
|
931
914
|
);
|
|
932
|
-
generationController = queueController;
|
|
933
915
|
thread.queueController = queueController;
|
|
934
916
|
thread.generationCompletion = runtime.backgroundQueue.waitForTask(queueController);
|
|
935
917
|
runtime.runControllers.set(runId, queueController);
|
|
@@ -946,7 +928,9 @@ export function createBackgroundDispatcher(options: BackgroundDispatcherOptions)
|
|
|
946
928
|
export function installThreadLifecycle(thread: SubagentThread, deps: ThreadLifecycleDeps): void {
|
|
947
929
|
const { runtime, startBackground } = deps;
|
|
948
930
|
const runId = thread.id;
|
|
949
|
-
const
|
|
931
|
+
const projectRoot = getProjectRoot(runtime.configPath, thread.cwd);
|
|
932
|
+
const sessionsRoot = join(projectRoot, "sessions");
|
|
933
|
+
const worktreesRoot = join(projectRoot, "worktrees");
|
|
950
934
|
|
|
951
935
|
thread.notifyIsolationFailure = (finalization) => {
|
|
952
936
|
const paths = [finalization.worktreePath, finalization.patchPath].filter(Boolean).join(" · ");
|
|
@@ -1081,7 +1065,7 @@ export function installThreadLifecycle(thread: SubagentThread, deps: ThreadLifec
|
|
|
1081
1065
|
return createWorktreeIsolation(thread.cwd, {
|
|
1082
1066
|
seedCheckpoint,
|
|
1083
1067
|
seedIsIntegrated,
|
|
1084
|
-
tempBaseDir:
|
|
1068
|
+
tempBaseDir: worktreesRoot,
|
|
1085
1069
|
});
|
|
1086
1070
|
};
|
|
1087
1071
|
|
|
@@ -1169,7 +1153,7 @@ export function installThreadLifecycle(thread: SubagentThread, deps: ThreadLifec
|
|
|
1169
1153
|
targetCwd: continuationWorktree.cwd,
|
|
1170
1154
|
sessionDir: previousSessionDir,
|
|
1171
1155
|
sessionId: previousSessionId,
|
|
1172
|
-
targetRoot:
|
|
1156
|
+
targetRoot: sessionsRoot,
|
|
1173
1157
|
});
|
|
1174
1158
|
runtime.sessionDirs.add(clonedSession.sessionDir);
|
|
1175
1159
|
if (!ownsResumeReservation(runtime, thread, reservation)) {
|
|
@@ -1275,6 +1259,11 @@ async function discardRestoredRecord(runtime: SubagentRuntime, record: ThreadRec
|
|
|
1275
1259
|
await removeThreadRecord(runtime.configPath, record.runId).catch(() => undefined);
|
|
1276
1260
|
}
|
|
1277
1261
|
|
|
1262
|
+
/** Project-scoped <projectRoot>/results for a completion's artifacts. */
|
|
1263
|
+
export function projectResultsRoot(configPath: string, cwd: string | undefined): string {
|
|
1264
|
+
return join(getProjectRoot(configPath, cwd), "results");
|
|
1265
|
+
}
|
|
1266
|
+
|
|
1278
1267
|
function createRestoredThread(
|
|
1279
1268
|
runtime: SubagentRuntime,
|
|
1280
1269
|
record: ThreadRecord,
|
|
@@ -1290,7 +1279,6 @@ function createRestoredThread(
|
|
|
1290
1279
|
executionCwd: record.executionCwd,
|
|
1291
1280
|
...(record.thinkingLevel ? { thinkingLevel: record.thinkingLevel as ThinkingLevel } : {}),
|
|
1292
1281
|
isolation: record.isolation,
|
|
1293
|
-
advisoryReview: false,
|
|
1294
1282
|
worktree,
|
|
1295
1283
|
state,
|
|
1296
1284
|
control: new RpcRunControl(record.task, record.generation),
|
|
@@ -1320,15 +1308,20 @@ function createRestoredThread(
|
|
|
1320
1308
|
return thread;
|
|
1321
1309
|
}
|
|
1322
1310
|
|
|
1323
|
-
/** Rebuild parked
|
|
1324
|
-
* or restart. Orphaned children recorded by the previous process are
|
|
1325
|
-
* first; records whose retained session vanished
|
|
1326
|
-
*
|
|
1311
|
+
/** Rebuild interrupted (parked) threads from the durable manifest after a
|
|
1312
|
+
* reload or restart. Orphaned children recorded by the previous process are
|
|
1313
|
+
* killed first; records whose retained session vanished — and settled records
|
|
1314
|
+
* left by older versions, which hold no work worth resuming — drop out with
|
|
1315
|
+
* their artifacts. Returns the restored run ids. */
|
|
1327
1316
|
export async function restoreDurableThreads(runtime: SubagentRuntime): Promise<number[]> {
|
|
1328
1317
|
const records = await readThreadRecords(runtime.configPath);
|
|
1329
1318
|
const restoredIds: number[] = [];
|
|
1330
1319
|
for (const record of records) {
|
|
1331
1320
|
if (runtime.threads.has(record.runId) || monitor.findRun(record.runId)) continue;
|
|
1321
|
+
if (record.state !== "parked") {
|
|
1322
|
+
await discardRestoredRecord(runtime, record);
|
|
1323
|
+
continue;
|
|
1324
|
+
}
|
|
1332
1325
|
// A child orphaned by reload/crash may still hold the retained session.
|
|
1333
1326
|
// The on-disk session checkpoint is what survives; kill the writer.
|
|
1334
1327
|
for (const pid of record.childPids) {
|
|
@@ -1348,33 +1341,29 @@ export async function restoreDurableThreads(runtime: SubagentRuntime): Promise<n
|
|
|
1348
1341
|
// A worktree thread whose isolated filesystem is gone cannot continue its
|
|
1349
1342
|
// isolation invariant; surface it as failed instead of pretending.
|
|
1350
1343
|
const state: ThreadState = worktree
|
|
1351
|
-
?
|
|
1344
|
+
? "parked"
|
|
1352
1345
|
: record.isolation === "worktree" && record.worktree
|
|
1353
1346
|
? "failed"
|
|
1354
|
-
:
|
|
1347
|
+
: "parked";
|
|
1355
1348
|
const thread = createRestoredThread(runtime, record, worktree, state);
|
|
1356
1349
|
runtime.threads.set(record.runId, thread);
|
|
1357
1350
|
runtime.sessionDirs.add(record.sessionDir!);
|
|
1358
|
-
|
|
1359
|
-
|
|
1360
|
-
|
|
1361
|
-
|
|
1362
|
-
|
|
1363
|
-
|
|
1364
|
-
|
|
1365
|
-
|
|
1366
|
-
|
|
1367
|
-
|
|
1368
|
-
|
|
1369
|
-
|
|
1370
|
-
|
|
1371
|
-
|
|
1372
|
-
|
|
1373
|
-
|
|
1374
|
-
});
|
|
1375
|
-
} else if (thread.lastResult) {
|
|
1376
|
-
runtime.registerRunResult(record.runId, thread.lastResult);
|
|
1377
|
-
}
|
|
1351
|
+
monitor.restoreRun({
|
|
1352
|
+
id: record.runId,
|
|
1353
|
+
agent: record.agentName,
|
|
1354
|
+
task: record.task,
|
|
1355
|
+
status: "parked",
|
|
1356
|
+
elapsedMs: record.elapsedMs,
|
|
1357
|
+
isolation: record.isolation,
|
|
1358
|
+
...(record.worktree
|
|
1359
|
+
? {
|
|
1360
|
+
integrationStatus: record.worktree.state === "active"
|
|
1361
|
+
? ("pending" as const)
|
|
1362
|
+
: record.worktree.state,
|
|
1363
|
+
...(worktree ? { worktreeId: worktreeGroupId(worktree) } : {}),
|
|
1364
|
+
}
|
|
1365
|
+
: {}),
|
|
1366
|
+
});
|
|
1378
1367
|
restoredIds.push(record.runId);
|
|
1379
1368
|
}
|
|
1380
1369
|
return restoredIds;
|
|
@@ -1400,11 +1389,8 @@ export async function bootstrapDurableState(runtime: SubagentRuntime): Promise<v
|
|
|
1400
1389
|
/* temp hygiene is best-effort */
|
|
1401
1390
|
}
|
|
1402
1391
|
try {
|
|
1403
|
-
|
|
1404
|
-
getStateRoot(runtime.configPath),
|
|
1405
|
-
referencedDurablePaths(await readThreadRecords(runtime.configPath)),
|
|
1406
|
-
);
|
|
1392
|
+
await pruneStaleProjectRoots(runtime.configPath);
|
|
1407
1393
|
} catch {
|
|
1408
|
-
/*
|
|
1394
|
+
/* project-root hygiene is best-effort */
|
|
1409
1395
|
}
|
|
1410
1396
|
}
|