@ferris1225/pi-subagents 4.1.15 → 4.1.17
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 +335 -302
- package/agents/cleaner.md +23 -29
- package/agents/documenter.md +21 -18
- package/agents/explorer.md +6 -1
- package/agents/reviewer.md +82 -85
- package/agents/worker.md +45 -37
- package/package.json +9 -9
- package/src/announcements.ts +75 -61
- package/src/background.ts +26 -10
- package/src/completion.ts +7 -1
- package/src/dispatch.ts +48 -16
- package/src/durable.ts +72 -7
- package/src/format.ts +179 -175
- package/src/monitor.ts +4 -2
- package/src/prompt.ts +2 -2
- package/src/rpc-run.ts +984 -962
- package/src/runtime.ts +3 -3
- package/src/spawn.ts +650 -642
- package/src/temp-hygiene.ts +0 -28
- package/src/thread-lifecycle.ts +42 -33
- package/src/tools.ts +712 -708
- package/src/widget.ts +9 -0
- package/src/workflow.ts +199 -200
- 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,
|
|
@@ -67,6 +66,7 @@ import type { SubagentRuntime, SubagentThread, ThreadState } from "./runtime.ts"
|
|
|
67
66
|
import { forkRetainedSession } from "./session-fork.ts";
|
|
68
67
|
import {
|
|
69
68
|
buildResumePrompt,
|
|
69
|
+
getProjectRoot,
|
|
70
70
|
getResultOutput,
|
|
71
71
|
RpcRunControl,
|
|
72
72
|
isFailedResult,
|
|
@@ -82,7 +82,6 @@ import {
|
|
|
82
82
|
isProcessAlive,
|
|
83
83
|
killProcessTree,
|
|
84
84
|
sweepOrphanTempDirs,
|
|
85
|
-
sweepUnreferencedState,
|
|
86
85
|
} from "./temp-hygiene.ts";
|
|
87
86
|
import {
|
|
88
87
|
createWorktreeIsolation,
|
|
@@ -403,7 +402,6 @@ export function createBackgroundDispatcher(options: BackgroundDispatcherOptions)
|
|
|
403
402
|
const runCtx = baseEnvironment.ctx;
|
|
404
403
|
const runConfig = baseEnvironment.config;
|
|
405
404
|
const runAgents = baseEnvironment.agents;
|
|
406
|
-
const stateRoot = getStateRoot(runtime.configPath);
|
|
407
405
|
const discoveredAgent = runAgents.find((candidate) => candidate.name === agentName);
|
|
408
406
|
if (!discoveredAgent) return failedStartResult(agentName, task, `Unknown agent: "${agentName}".`);
|
|
409
407
|
const resolveLiveAgentTools = (candidate: AgentConfig): AgentConfig =>
|
|
@@ -417,6 +415,9 @@ export function createBackgroundDispatcher(options: BackgroundDispatcherOptions)
|
|
|
417
415
|
}
|
|
418
416
|
|
|
419
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");
|
|
420
421
|
const previousWorktree = existingThread?.worktree;
|
|
421
422
|
let worktree = seed?.worktree ?? previousWorktree;
|
|
422
423
|
if (isolation === "worktree") {
|
|
@@ -429,7 +430,7 @@ export function createBackgroundDispatcher(options: BackgroundDispatcherOptions)
|
|
|
429
430
|
}
|
|
430
431
|
if (!worktree) {
|
|
431
432
|
try {
|
|
432
|
-
worktree = await createWorktreeIsolation(originalCwd, { tempBaseDir:
|
|
433
|
+
worktree = await createWorktreeIsolation(originalCwd, { tempBaseDir: worktreesRoot });
|
|
433
434
|
} catch (error) {
|
|
434
435
|
return {
|
|
435
436
|
...failedStartResult(agentName, task, error instanceof Error ? error.message : String(error)),
|
|
@@ -548,10 +549,7 @@ export function createBackgroundDispatcher(options: BackgroundDispatcherOptions)
|
|
|
548
549
|
const workflowAvailability = workflowAgentAvailability(runAgents);
|
|
549
550
|
const reserveManagedLane =
|
|
550
551
|
isolation === "shared" && canStartManagedWorkflow(agent, workflowAvailability);
|
|
551
|
-
|
|
552
|
-
// await, so the managed-continuation slot suspension always sees it.
|
|
553
|
-
let generationController: AbortController | undefined;
|
|
554
|
-
const runGeneration = async (backgroundSignal: AbortSignal): Promise<void> => {
|
|
552
|
+
const runGeneration = async (backgroundSignal: AbortSignal, controller: AbortController): Promise<void> => {
|
|
555
553
|
if (runtime.threads.get(runId)?.generation !== generation) return;
|
|
556
554
|
// Model/thinking config may have changed while this generation sat
|
|
557
555
|
// queued behind the concurrency limit. Re-resolve the route at actual
|
|
@@ -587,7 +585,7 @@ export function createBackgroundDispatcher(options: BackgroundDispatcherOptions)
|
|
|
587
585
|
control,
|
|
588
586
|
makeDetails: makeDetails("single", true),
|
|
589
587
|
idleTimeoutMs: activeIdleTimeoutMs,
|
|
590
|
-
sessionRoot:
|
|
588
|
+
sessionRoot: sessionsRoot,
|
|
591
589
|
...(priorSessionId && priorSessionDir
|
|
592
590
|
? {
|
|
593
591
|
sessionId: priorSessionId,
|
|
@@ -655,7 +653,7 @@ export function createBackgroundDispatcher(options: BackgroundDispatcherOptions)
|
|
|
655
653
|
// concurrency slot so managed chains never starve manual
|
|
656
654
|
// dispatches. Cancellation and quiescence guarantees are
|
|
657
655
|
// unchanged — the task stays abortable and awaited.
|
|
658
|
-
runtime.backgroundQueue.suspend(
|
|
656
|
+
runtime.backgroundQueue.suspend(controller);
|
|
659
657
|
thread.state = "running";
|
|
660
658
|
// The stable parent row now represents workflow ownership, not whichever
|
|
661
659
|
// model stage ran most recently. Internal rows own their exact role/model/
|
|
@@ -771,7 +769,7 @@ export function createBackgroundDispatcher(options: BackgroundDispatcherOptions)
|
|
|
771
769
|
: undefined;
|
|
772
770
|
const needsFullFinal = failed || (lastStep.result.agent === "reviewer" && finalVerdict !== "pass");
|
|
773
771
|
if (needsFullFinal) {
|
|
774
|
-
block += `\n\n${formatCompletionBlock(result, runConfig.maxResultLines, originalCwd)}`;
|
|
772
|
+
block += `\n\n${formatCompletionBlock(result, runConfig.maxResultLines, { resultRoot: projectResultsRoot(runtime.configPath, result.projectCwd ?? originalCwd) })}`;
|
|
775
773
|
}
|
|
776
774
|
if (finalVerdict === "fail") {
|
|
777
775
|
block += `\n\n${reviewFailFollowUpNote()}`;
|
|
@@ -790,10 +788,10 @@ export function createBackgroundDispatcher(options: BackgroundDispatcherOptions)
|
|
|
790
788
|
const completion: CompletionMessageItem = {
|
|
791
789
|
agent: result.agent,
|
|
792
790
|
block: modelLevel
|
|
793
|
-
? `${formatCompletionBlock(result, runConfig.maxResultLines, result.projectCwd ?? originalCwd)}\n\n${modelLevelTakeoverNote(result, { runId })}`
|
|
791
|
+
? `${formatCompletionBlock(result, runConfig.maxResultLines, { resultRoot: projectResultsRoot(runtime.configPath, result.projectCwd ?? originalCwd) })}\n\n${modelLevelTakeoverNote(result, { runId })}`
|
|
794
792
|
: result.agent === "reviewer" && reviewVerdict(getResultOutput(result)) === "fail"
|
|
795
|
-
? `${formatCompletionBlock(result, runConfig.maxResultLines, result.projectCwd ?? originalCwd)}\n\n${reviewFailFollowUpNote()}`
|
|
796
|
-
: formatCompletionBlock(result, runConfig.maxResultLines, result.projectCwd ?? originalCwd),
|
|
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) }),
|
|
797
795
|
triggerTurn: completionTriggersTurn(result, runConfig.notifyOnReviewPass),
|
|
798
796
|
usage: result.usage,
|
|
799
797
|
};
|
|
@@ -814,13 +812,21 @@ export function createBackgroundDispatcher(options: BackgroundDispatcherOptions)
|
|
|
814
812
|
}
|
|
815
813
|
};
|
|
816
814
|
const queuedGeneration = reserveManagedLane
|
|
817
|
-
? async (backgroundSignal: AbortSignal): Promise<void> => {
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
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
|
+
}
|
|
824
830
|
: runGeneration;
|
|
825
831
|
const queueController = runtime.backgroundQueue.enqueue(
|
|
826
832
|
queuedGeneration,
|
|
@@ -892,7 +898,7 @@ export function createBackgroundDispatcher(options: BackgroundDispatcherOptions)
|
|
|
892
898
|
runtime.sendCompletionGroup([
|
|
893
899
|
{
|
|
894
900
|
agent: crashed.agent,
|
|
895
|
-
block: formatCompletionBlock(crashed, runConfig.maxResultLines, crashed.projectCwd ?? originalCwd),
|
|
901
|
+
block: formatCompletionBlock(crashed, runConfig.maxResultLines, { resultRoot: projectResultsRoot(runtime.configPath, crashed.projectCwd ?? originalCwd) }),
|
|
896
902
|
triggerTurn: true,
|
|
897
903
|
usage: crashed.usage,
|
|
898
904
|
},
|
|
@@ -906,7 +912,6 @@ export function createBackgroundDispatcher(options: BackgroundDispatcherOptions)
|
|
|
906
912
|
}
|
|
907
913
|
},
|
|
908
914
|
);
|
|
909
|
-
generationController = queueController;
|
|
910
915
|
thread.queueController = queueController;
|
|
911
916
|
thread.generationCompletion = runtime.backgroundQueue.waitForTask(queueController);
|
|
912
917
|
runtime.runControllers.set(runId, queueController);
|
|
@@ -923,7 +928,9 @@ export function createBackgroundDispatcher(options: BackgroundDispatcherOptions)
|
|
|
923
928
|
export function installThreadLifecycle(thread: SubagentThread, deps: ThreadLifecycleDeps): void {
|
|
924
929
|
const { runtime, startBackground } = deps;
|
|
925
930
|
const runId = thread.id;
|
|
926
|
-
const
|
|
931
|
+
const projectRoot = getProjectRoot(runtime.configPath, thread.cwd);
|
|
932
|
+
const sessionsRoot = join(projectRoot, "sessions");
|
|
933
|
+
const worktreesRoot = join(projectRoot, "worktrees");
|
|
927
934
|
|
|
928
935
|
thread.notifyIsolationFailure = (finalization) => {
|
|
929
936
|
const paths = [finalization.worktreePath, finalization.patchPath].filter(Boolean).join(" · ");
|
|
@@ -1058,7 +1065,7 @@ export function installThreadLifecycle(thread: SubagentThread, deps: ThreadLifec
|
|
|
1058
1065
|
return createWorktreeIsolation(thread.cwd, {
|
|
1059
1066
|
seedCheckpoint,
|
|
1060
1067
|
seedIsIntegrated,
|
|
1061
|
-
tempBaseDir:
|
|
1068
|
+
tempBaseDir: worktreesRoot,
|
|
1062
1069
|
});
|
|
1063
1070
|
};
|
|
1064
1071
|
|
|
@@ -1146,7 +1153,7 @@ export function installThreadLifecycle(thread: SubagentThread, deps: ThreadLifec
|
|
|
1146
1153
|
targetCwd: continuationWorktree.cwd,
|
|
1147
1154
|
sessionDir: previousSessionDir,
|
|
1148
1155
|
sessionId: previousSessionId,
|
|
1149
|
-
targetRoot:
|
|
1156
|
+
targetRoot: sessionsRoot,
|
|
1150
1157
|
});
|
|
1151
1158
|
runtime.sessionDirs.add(clonedSession.sessionDir);
|
|
1152
1159
|
if (!ownsResumeReservation(runtime, thread, reservation)) {
|
|
@@ -1252,6 +1259,11 @@ async function discardRestoredRecord(runtime: SubagentRuntime, record: ThreadRec
|
|
|
1252
1259
|
await removeThreadRecord(runtime.configPath, record.runId).catch(() => undefined);
|
|
1253
1260
|
}
|
|
1254
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
|
+
|
|
1255
1267
|
function createRestoredThread(
|
|
1256
1268
|
runtime: SubagentRuntime,
|
|
1257
1269
|
record: ThreadRecord,
|
|
@@ -1377,11 +1389,8 @@ export async function bootstrapDurableState(runtime: SubagentRuntime): Promise<v
|
|
|
1377
1389
|
/* temp hygiene is best-effort */
|
|
1378
1390
|
}
|
|
1379
1391
|
try {
|
|
1380
|
-
|
|
1381
|
-
getStateRoot(runtime.configPath),
|
|
1382
|
-
referencedDurablePaths(await readThreadRecords(runtime.configPath)),
|
|
1383
|
-
);
|
|
1392
|
+
await pruneStaleProjectRoots(runtime.configPath);
|
|
1384
1393
|
} catch {
|
|
1385
|
-
/*
|
|
1394
|
+
/* project-root hygiene is best-effort */
|
|
1386
1395
|
}
|
|
1387
1396
|
}
|