pi-subagents 0.46.0 → 0.47.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/CHANGELOG.md +23 -0
- package/docs/agents.md +1 -1
- package/docs/configuration.md +14 -6
- package/docs/extension-api.md +1 -1
- package/docs/missions.md +5 -3
- package/docs/models.md +3 -1
- package/docs/observability.md +3 -3
- package/docs/tool-reference.md +2 -2
- package/docs/watchdog.md +1 -1
- package/package.json +1 -1
- package/skills/pi-subagents/references/execution-controls.md +4 -4
- package/skills/pi-subagents/references/management-authoring-rpc.md +1 -1
- package/src/extension/config.ts +3 -0
- package/src/extension/fanout-child.ts +5 -4
- package/src/extension/index.ts +30 -3
- package/src/extension/rpc.ts +3 -6
- package/src/extension/schemas.ts +25 -5
- package/src/extension/tool-description.ts +26 -8
- package/src/inspectors/herdr/project-panes.ts +2 -1
- package/src/missions/store.ts +2 -1
- package/src/missions/workflow-state.ts +19 -13
- package/src/runs/background/async-execution.ts +10 -5
- package/src/runs/background/async-job-tracker.ts +15 -0
- package/src/runs/background/async-resume.ts +19 -3
- package/src/runs/background/async-status.ts +6 -1
- package/src/runs/background/control-channel.ts +36 -0
- package/src/runs/background/result-watcher.ts +16 -2
- package/src/runs/background/scheduled-runs.ts +2 -1
- package/src/runs/background/stale-run-reconciler.ts +2 -21
- package/src/runs/background/subagent-runner.ts +47 -6
- package/src/runs/foreground/async-steering-action.ts +1 -1
- package/src/runs/foreground/chain-execution.ts +3 -0
- package/src/runs/foreground/execution.ts +3 -0
- package/src/runs/foreground/subagent-executor.ts +95 -12
- package/src/runs/foreground/workflow-foreground-steering.ts +187 -0
- package/src/runs/shared/dynamic-fanout.ts +1 -1
- package/src/runs/shared/model-fallback.ts +8 -4
- package/src/runs/shared/model-scope.ts +12 -2
- package/src/runs/shared/parallel-utils.ts +1 -0
- package/src/runs/shared/worktree.ts +3 -2
- package/src/shared/artifacts.ts +14 -14
- package/src/shared/display-text.ts +100 -0
- package/src/shared/formatters.ts +4 -6
- package/src/shared/settings.ts +15 -2
- package/src/shared/types.ts +11 -1
- package/src/shared/utils.ts +43 -33
- package/src/slash/slash-commands.ts +3 -1
- package/src/tui/fleet-status.ts +14 -10
- package/src/tui/render.ts +30 -26
- package/src/watchdog/change-signature.ts +4 -3
|
@@ -3,7 +3,7 @@ import { randomUUID } from "node:crypto";
|
|
|
3
3
|
import * as fs from "node:fs";
|
|
4
4
|
import * as path from "node:path";
|
|
5
5
|
import { writePrivateAtomicJson } from "../shared/atomic-json.ts";
|
|
6
|
-
import { DEFAULT_FILE_SYSTEM_RETRY_DELAYS_MS, waitForFileSystemRetry } from "../shared/file-system-retry.ts";
|
|
6
|
+
import { DEFAULT_FILE_SYSTEM_RETRY_DELAYS_MS, isRetryableFileSystemError, waitForFileSystemRetry } from "../shared/file-system-retry.ts";
|
|
7
7
|
import { assertWorkflowJsonValue } from "../workflows/scripted-workflow.ts";
|
|
8
8
|
import type { MissionStoreLocation } from "./types.ts";
|
|
9
9
|
import { validateMissionId } from "./store.ts";
|
|
@@ -162,24 +162,30 @@ function withStateFileLock<T>(filePath: string, operation: () => T): T {
|
|
|
162
162
|
waitForStateLock(DEFAULT_FILE_SYSTEM_RETRY_DELAYS_MS[attempt], lockPath);
|
|
163
163
|
continue;
|
|
164
164
|
}
|
|
165
|
+
let acquired = false;
|
|
165
166
|
try {
|
|
166
|
-
|
|
167
|
-
owner = { pid: process.pid, token: randomUUID(), createdAt: Date.now(), ...(CURRENT_PROCESS_KEY ? { processKey: CURRENT_PROCESS_KEY } : {}) };
|
|
168
|
-
try {
|
|
169
|
-
fs.writeFileSync(path.join(lockPath, "owner.json"), JSON.stringify(owner), { encoding: "utf-8", mode: 0o600 });
|
|
170
|
-
} catch (error) {
|
|
171
|
-
removeOwnedStateLock(lockPath, owner);
|
|
172
|
-
owner = undefined;
|
|
173
|
-
throw error;
|
|
174
|
-
}
|
|
175
|
-
break;
|
|
167
|
+
acquired = tryMakeDirectory(lockPath, 0o700);
|
|
176
168
|
} catch (error) {
|
|
177
|
-
if ((error
|
|
178
|
-
|
|
169
|
+
if (isRetryableFileSystemError(error)) {
|
|
170
|
+
waitForStateLock(DEFAULT_FILE_SYSTEM_RETRY_DELAYS_MS[attempt], lockPath);
|
|
171
|
+
continue;
|
|
179
172
|
}
|
|
173
|
+
throw new Error(`Failed to acquire mission state lock '${lockPath}': ${error instanceof Error ? error.message : String(error)}`);
|
|
174
|
+
}
|
|
175
|
+
if (!acquired) {
|
|
180
176
|
if (reclaimStaleStateLock(lockPath, reclaimPath)) continue;
|
|
181
177
|
waitForStateLock(DEFAULT_FILE_SYSTEM_RETRY_DELAYS_MS[attempt], lockPath);
|
|
178
|
+
continue;
|
|
179
|
+
}
|
|
180
|
+
owner = { pid: process.pid, token: randomUUID(), createdAt: Date.now(), ...(CURRENT_PROCESS_KEY ? { processKey: CURRENT_PROCESS_KEY } : {}) };
|
|
181
|
+
try {
|
|
182
|
+
fs.writeFileSync(path.join(lockPath, "owner.json"), JSON.stringify(owner), { encoding: "utf-8", mode: 0o600 });
|
|
183
|
+
} catch (error) {
|
|
184
|
+
fs.rmSync(lockPath, { recursive: true, force: true });
|
|
185
|
+
owner = undefined;
|
|
186
|
+
throw error;
|
|
182
187
|
}
|
|
188
|
+
break;
|
|
183
189
|
}
|
|
184
190
|
try {
|
|
185
191
|
return operation();
|
|
@@ -15,7 +15,7 @@ import { appendAgentRefinementOverlay } from "../../agents/agent-refinements.ts"
|
|
|
15
15
|
import { writePrivateAtomicJson } from "../../shared/atomic-json.ts";
|
|
16
16
|
import { applyThinkingSuffix, projectLaunchResolvedChildExtensions, resolvePiLaunchToolPlan } from "../shared/pi-args.ts";
|
|
17
17
|
import { injectOutputPathSystemPrompt, injectSingleOutputInstruction, normalizeSingleOutputOverride, resolveSingleOutputPath, validateFileOnlyOutputMode } from "../shared/single-output.ts";
|
|
18
|
-
import { buildChainInstructions, isCheckpointStep, isDynamicParallelStep, isParallelStep, resolveChainPath, resolveStepBehavior, suppressProgressForReadOnlyTask, writeInitialProgressFile, type ChainStep, type ResolvedStepBehavior, type SequentialStep, type StepOverrides } from "../../shared/settings.ts";
|
|
18
|
+
import { buildChainInstructions, isCheckpointStep, isDynamicParallelStep, isParallelStep, resolveChainPath, resolveExistingReadPaths, resolveStepBehavior, suppressProgressForReadOnlyTask, writeInitialProgressFile, type ChainStep, type ResolvedStepBehavior, type SequentialStep, type StepOverrides } from "../../shared/settings.ts";
|
|
19
19
|
import type { RunnerStep } from "../shared/parallel-utils.ts";
|
|
20
20
|
import type { ContextMode } from "../shared/context-mode.ts";
|
|
21
21
|
import { resolvePiPackageRoot } from "../shared/pi-spawn.ts";
|
|
@@ -128,6 +128,8 @@ interface AsyncExecutionContext {
|
|
|
128
128
|
interactive?: boolean;
|
|
129
129
|
}
|
|
130
130
|
|
|
131
|
+
export const DEFAULT_ASYNC_TIMEOUT_MS = 30 * 60 * 1000;
|
|
132
|
+
|
|
131
133
|
interface AsyncChainParams {
|
|
132
134
|
chain: ChainStep[];
|
|
133
135
|
task?: string;
|
|
@@ -668,6 +670,7 @@ export function buildAsyncRunnerSteps(id: string, params: AsyncRunnerStepBuildPa
|
|
|
668
670
|
if (resolvedToolBudget.error) throw new AsyncStartValidationError(resolvedToolBudget.error);
|
|
669
671
|
const stepCwd = resolveChildCwd(runnerCwd, s.cwd);
|
|
670
672
|
const instructionCwd = behaviorCwd ?? stepCwd;
|
|
673
|
+
const readExistenceCwd = behaviorCwd ? stepCwd : instructionCwd;
|
|
671
674
|
let behavior = suppressProgressForReadOnlyTask(resolvedBehavior ?? resolveStepBehavior(a, buildStepOverrides(s), chainSkills), s.task, originalTask);
|
|
672
675
|
const inheritedRelativeParallelOutput = parallelOutputNamespace && s.output === undefined && typeof behavior.output === "string" && !path.isAbsolute(behavior.output);
|
|
673
676
|
if (inheritedRelativeParallelOutput && parallelOutputNamespace.taskIndex !== undefined) {
|
|
@@ -698,7 +701,7 @@ export function buildAsyncRunnerSteps(id: string, params: AsyncRunnerStepBuildPa
|
|
|
698
701
|
}
|
|
699
702
|
systemPrompt = appendAgentRefinementOverlay(systemPrompt, { cwd: stepCwd, agentName: a.name });
|
|
700
703
|
|
|
701
|
-
const readInstructions = buildChainInstructions({ ...behavior, output: false, progress: false }, instructionCwd, false);
|
|
704
|
+
const readInstructions = buildChainInstructions({ ...behavior, output: false, progress: false }, instructionCwd, false, undefined, readExistenceCwd);
|
|
702
705
|
const isFirstProgressAgent = behavior.progress && !progressPrecreated && !progressInstructionCreated;
|
|
703
706
|
if (behavior.progress) progressInstructionCreated = true;
|
|
704
707
|
const progressInstructions = buildChainInstructions({ ...behavior, output: false, reads: false }, progressDir, isFirstProgressAgent);
|
|
@@ -776,6 +779,7 @@ export function buildAsyncRunnerSteps(id: string, params: AsyncRunnerStepBuildPa
|
|
|
776
779
|
outputMode: behavior.outputMode,
|
|
777
780
|
sessionFile,
|
|
778
781
|
maxSubagentDepth: resolveChildMaxSubagentDepth(maxSubagentDepth, a.maxSubagentDepth),
|
|
782
|
+
timeoutMs: a.defaultTimeoutMs ?? DEFAULT_ASYNC_TIMEOUT_MS,
|
|
779
783
|
waitToolEnabled: params.waitToolEnabled,
|
|
780
784
|
effectiveAcceptance: resolveEffectiveAcceptance({
|
|
781
785
|
explicit: s.acceptance,
|
|
@@ -1296,8 +1300,9 @@ export function executeAsyncSingle(
|
|
|
1296
1300
|
// Reads: caller override > agent defaultReads > none. `~`/`~/` expand to home;
|
|
1297
1301
|
// absolute paths pass through; relative paths resolve against the child cwd.
|
|
1298
1302
|
const reads = params.reads !== undefined ? params.reads : agentConfig.defaultReads ?? false;
|
|
1299
|
-
const
|
|
1300
|
-
|
|
1303
|
+
const readPaths = Array.isArray(reads) ? resolveExistingReadPaths(reads, runnerCwd) : [];
|
|
1304
|
+
const readsInstruction = readPaths.length > 0
|
|
1305
|
+
? `[Read from: ${readPaths.join(", ")}]\n\n`
|
|
1301
1306
|
: "";
|
|
1302
1307
|
const taskText = readsInstruction + taskWithOutputInstruction;
|
|
1303
1308
|
const primaryModel = externalRunner ? undefined : resolveSubagentModelOverride(
|
|
@@ -1399,7 +1404,7 @@ export function executeAsyncSingle(
|
|
|
1399
1404
|
...(params.acceptance !== undefined ? { acceptance: params.acceptance } : {}),
|
|
1400
1405
|
...(controlConfig ? { controlConfig } : {}),
|
|
1401
1406
|
...(deadlineAt !== undefined ? { absoluteDeadlineAt: deadlineAt } : {}),
|
|
1402
|
-
...(initialTurnBudget ? { initialTurnBudget } : {}),
|
|
1407
|
+
...(initialTurnBudget ? { initialTurnBudget: { maxTurns: initialTurnBudget.maxTurns, graceTurns: initialTurnBudget.graceTurns } } : {}),
|
|
1403
1408
|
...(resolvedToolBudget.budget ? { initialToolBudget: resolvedToolBudget.budget } : {}),
|
|
1404
1409
|
maxSubagentDepth: resolveChildMaxSubagentDepth(maxSubagentDepth, agentConfig.maxSubagentDepth),
|
|
1405
1410
|
...(maxOutput ? { maxOutput } : {}),
|
|
@@ -57,6 +57,7 @@ export function createAsyncJobTracker(pi: Pick<ExtensionAPI, "events">, state: S
|
|
|
57
57
|
const resultsDir = options.resultsDir ?? DIRS.results;
|
|
58
58
|
const steeringNoticeSeen = new Map<string, number>();
|
|
59
59
|
const rerenderWidget = (ctx: ExtensionContext, jobs = Array.from(state.asyncJobs.values())) => {
|
|
60
|
+
if (state.widgetsSuspended) return;
|
|
60
61
|
renderWidget(ctx, options.widgetEnabled === false ? [] : jobs);
|
|
61
62
|
(ctx.ui as { requestRender?: () => void }).requestRender?.();
|
|
62
63
|
};
|
|
@@ -73,6 +74,19 @@ export function createAsyncJobTracker(pi: Pick<ExtensionAPI, "events">, state: S
|
|
|
73
74
|
throw error;
|
|
74
75
|
}
|
|
75
76
|
};
|
|
77
|
+
const requestLastWidgetRender = () => {
|
|
78
|
+
const ctx = state.lastUiContext;
|
|
79
|
+
if (!ctx || state.widgetsSuspended || options.widgetEnabled === false) return;
|
|
80
|
+
try {
|
|
81
|
+
if (ctx.hasUI) (ctx.ui as { requestRender?: () => void }).requestRender?.();
|
|
82
|
+
} catch (error) {
|
|
83
|
+
if (error instanceof Error && error.message.includes("extension ctx is stale")) {
|
|
84
|
+
state.lastUiContext = null;
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
87
|
+
throw error;
|
|
88
|
+
}
|
|
89
|
+
};
|
|
76
90
|
const refreshWidget = (ctx: ExtensionContext) => rerenderWidget(ctx);
|
|
77
91
|
const restoredControlEventCursor = (asyncDir: string) => {
|
|
78
92
|
try {
|
|
@@ -400,6 +414,7 @@ export function createAsyncJobTracker(pi: Pick<ExtensionAPI, "events">, state: S
|
|
|
400
414
|
}
|
|
401
415
|
|
|
402
416
|
if (widgetChanged) rerenderLastWidget();
|
|
417
|
+
else if (Array.from(state.asyncJobs.values()).some((job) => job.status === "running")) requestLastWidgetRender();
|
|
403
418
|
}, pollIntervalMs);
|
|
404
419
|
state.poller.unref?.();
|
|
405
420
|
};
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import * as fs from "node:fs";
|
|
2
2
|
import * as path from "node:path";
|
|
3
|
-
import { DIRS, type AcceptanceInput, type AsyncStatus, type SteeringRecoveryDescriptor } from "../../shared/types.ts";
|
|
3
|
+
import { DIRS, type AcceptanceInput, type AsyncStatus, type ResolvedTurnBudget, type SteeringRecoveryDescriptor } from "../../shared/types.ts";
|
|
4
4
|
import type { AgentConfig } from "../../agents/agents.ts";
|
|
5
5
|
import { validateAcceptanceInput } from "../shared/acceptance.ts";
|
|
6
6
|
import { validateToolBudgetConfig } from "../shared/tool-budget.ts";
|
|
@@ -273,6 +273,23 @@ function normalizeRecoveryAcceptance(value: unknown, descriptorPath: string): Ac
|
|
|
273
273
|
return value as AcceptanceInput;
|
|
274
274
|
}
|
|
275
275
|
|
|
276
|
+
function normalizeRecoveryTurnBudget(value: unknown, descriptorPath: string): ResolvedTurnBudget {
|
|
277
|
+
if (value && typeof value === "object" && !Array.isArray(value)) {
|
|
278
|
+
const {
|
|
279
|
+
outcome: _outcome,
|
|
280
|
+
turnCount: _turnCount,
|
|
281
|
+
wrapUpRequestedAtTurn: _wrapUpRequestedAtTurn,
|
|
282
|
+
terminationDeferredAtTurn: _terminationDeferredAtTurn,
|
|
283
|
+
exceededAtTurn: _exceededAtTurn,
|
|
284
|
+
...publicTurnBudget
|
|
285
|
+
} = value as Record<string, unknown>;
|
|
286
|
+
value = publicTurnBudget;
|
|
287
|
+
}
|
|
288
|
+
const result = resolveTurnBudgetConfig(value, "recoveryDescriptor.initialTurnBudget");
|
|
289
|
+
if (result.error || !result.turnBudget) throw new Error(`Invalid async recovery descriptor '${descriptorPath}': ${result.error ?? "recoveryDescriptor.initialTurnBudget is invalid."}`);
|
|
290
|
+
return result.turnBudget;
|
|
291
|
+
}
|
|
292
|
+
|
|
276
293
|
export function readAsyncRecoveryDescriptor(asyncDir: string | undefined): SteeringRecoveryDescriptor | undefined {
|
|
277
294
|
if (!asyncDir) return undefined;
|
|
278
295
|
const descriptorPath = path.join(asyncDir, "recovery-descriptor.json");
|
|
@@ -329,8 +346,7 @@ export function readAsyncRecoveryDescriptor(asyncDir: string | undefined): Steer
|
|
|
329
346
|
}
|
|
330
347
|
if (parsed.absoluteDeadlineAt !== undefined && (!Number.isFinite(parsed.absoluteDeadlineAt) || (parsed.absoluteDeadlineAt as number) <= 0)) throw new Error(`Invalid async recovery descriptor '${descriptorPath}': absoluteDeadlineAt must be a positive timestamp.`);
|
|
331
348
|
if (parsed.initialTurnBudget !== undefined) {
|
|
332
|
-
|
|
333
|
-
if (result.error) throw new Error(`Invalid async recovery descriptor '${descriptorPath}': ${result.error}`);
|
|
349
|
+
parsed.initialTurnBudget = normalizeRecoveryTurnBudget(parsed.initialTurnBudget, descriptorPath);
|
|
334
350
|
}
|
|
335
351
|
if (parsed.initialToolBudget !== undefined) {
|
|
336
352
|
const result = validateToolBudgetConfig(parsed.initialToolBudget, "recoveryDescriptor.initialToolBudget");
|
|
@@ -4,7 +4,7 @@ import { formatDuration, formatModelThinking, formatTokens, shortenPath } from "
|
|
|
4
4
|
import { formatActivityLabel, formatParallelOutcome } from "../../shared/status-format.ts";
|
|
5
5
|
import { type ActivityState, type AsyncJobStep, type AsyncParallelGroupStatus, type AsyncStatus, type CostSummary, type Details, type LaunchResolvedChildExtensionsV1, type RuntimeAcknowledgedChildExtensionsV1, type NestedRunSummary, type SteeringStatus, type SubagentRunMode, type TokenUsage, type TurnBudgetState, type UsageBudgetState, type ChainCheckpointState } from "../../shared/types.ts";
|
|
6
6
|
import type { ResolvedSubagentCapabilityCeiling, SubagentCapabilityAudit } from "../shared/capability-ceiling.ts";
|
|
7
|
-
import { readStatus } from "../../shared/utils.ts";
|
|
7
|
+
import { pruneStatusCacheForAsyncRoot, readStatus } from "../../shared/utils.ts";
|
|
8
8
|
import { attachRootChildrenToSteps, buildNestedRouteIndex, type NestedRoute, projectNestedEvents } from "../shared/nested-events.ts";
|
|
9
9
|
import { formatNestedRunStatusLines } from "../shared/nested-render.ts";
|
|
10
10
|
import { flatToLogicalStepIndex, normalizeParallelGroups } from "./parallel-groups.ts";
|
|
@@ -370,6 +370,7 @@ function sortRuns(runs: AsyncRunSummary[]): AsyncRunSummary[] {
|
|
|
370
370
|
|
|
371
371
|
export function listAsyncRuns(asyncDirRoot: string, options: AsyncRunListOptions = {}): AsyncRunSummary[] {
|
|
372
372
|
let entries: string[];
|
|
373
|
+
let scannedCompleteRoot = false;
|
|
373
374
|
try {
|
|
374
375
|
if (options.runId !== undefined) {
|
|
375
376
|
const resolution = resolveTargetedAsyncRun(asyncDirRoot, options.runId, options.sessionId);
|
|
@@ -383,6 +384,7 @@ export function listAsyncRuns(asyncDirRoot: string, options: AsyncRunListOptions
|
|
|
383
384
|
: [];
|
|
384
385
|
} else {
|
|
385
386
|
entries = fs.readdirSync(asyncDirRoot).filter((entry) => isAsyncRunDir(asyncDirRoot, entry));
|
|
387
|
+
scannedCompleteRoot = true;
|
|
386
388
|
}
|
|
387
389
|
} catch (error) {
|
|
388
390
|
if (isNotFoundError(error)) return [];
|
|
@@ -392,6 +394,7 @@ export function listAsyncRuns(asyncDirRoot: string, options: AsyncRunListOptions
|
|
|
392
394
|
}
|
|
393
395
|
|
|
394
396
|
if (options.entryLimit !== undefined) {
|
|
397
|
+
scannedCompleteRoot = false;
|
|
395
398
|
const limit = Math.max(0, Math.floor(options.entryLimit));
|
|
396
399
|
entries = entries
|
|
397
400
|
.map((entry) => {
|
|
@@ -410,6 +413,8 @@ export function listAsyncRuns(asyncDirRoot: string, options: AsyncRunListOptions
|
|
|
410
413
|
.map((candidate) => candidate.entry);
|
|
411
414
|
}
|
|
412
415
|
|
|
416
|
+
if (scannedCompleteRoot) pruneStatusCacheForAsyncRoot(asyncDirRoot, entries);
|
|
417
|
+
|
|
413
418
|
const allowedStates = options.states ? new Set(options.states) : undefined;
|
|
414
419
|
const runs: AsyncRunSummary[] = [];
|
|
415
420
|
// Route resolution for every run shares a single index built from the
|
|
@@ -21,6 +21,16 @@ import { POLL_INTERVAL_MS } from "../../shared/types.ts";
|
|
|
21
21
|
import { resolveWatchPath } from "../../shared/utils.ts";
|
|
22
22
|
|
|
23
23
|
export type ControlChannelFs = Pick<typeof fs, "mkdirSync" | "existsSync" | "rmSync" | "watch" | "readdirSync" | "readFileSync" | "realpathSync">;
|
|
24
|
+
|
|
25
|
+
function writeJsonToExistingDir(filePath: string, payload: object): void {
|
|
26
|
+
const tempPath = path.join(path.dirname(filePath), `.${path.basename(filePath)}.${process.pid}.${Date.now()}.${randomUUID()}.tmp`);
|
|
27
|
+
try {
|
|
28
|
+
fs.writeFileSync(tempPath, JSON.stringify(payload, null, 2), { encoding: "utf-8", flag: "wx" });
|
|
29
|
+
fs.renameSync(tempPath, filePath);
|
|
30
|
+
} finally {
|
|
31
|
+
fs.rmSync(tempPath, { force: true });
|
|
32
|
+
}
|
|
33
|
+
}
|
|
24
34
|
export type ControlChannelTimers = { setInterval: typeof setInterval; clearInterval: typeof clearInterval };
|
|
25
35
|
type KillFn = (pid: number, signal?: NodeJS.Signals | 0) => unknown;
|
|
26
36
|
|
|
@@ -205,6 +215,13 @@ export function writeSteerRequestToDir(dir: string, request: SteerRequest): stri
|
|
|
205
215
|
return requestPath;
|
|
206
216
|
}
|
|
207
217
|
|
|
218
|
+
export function writeSteerRequestToExistingDir(dir: string, request: SteerRequest): string {
|
|
219
|
+
if (!validSteerRequest(request)) throw new Error("steer request is malformed or exceeds transport limits.");
|
|
220
|
+
const requestPath = path.join(dir, steerRequestFileName(request));
|
|
221
|
+
writeJsonToExistingDir(requestPath, request);
|
|
222
|
+
return requestPath;
|
|
223
|
+
}
|
|
224
|
+
|
|
208
225
|
export function writeSteerCapabilityAt(filePath: string, capability: Omit<SteerCapability, "type" | "protocolVersion">): string {
|
|
209
226
|
assertChildIndex(capability.index);
|
|
210
227
|
if (!Number.isInteger(capability.pid) || capability.pid <= 0) throw new Error("steer capability pid must be a positive integer.");
|
|
@@ -385,6 +402,25 @@ export function consumeSteerCapabilities(asyncDir: string, fsImpl: Pick<typeof f
|
|
|
385
402
|
return capabilities;
|
|
386
403
|
}
|
|
387
404
|
|
|
405
|
+
export function consumeSteerAckFromDir(
|
|
406
|
+
dir: string,
|
|
407
|
+
requestId: string,
|
|
408
|
+
fsImpl: Pick<typeof fs, "existsSync" | "readdirSync" | "readFileSync" | "rmSync"> = fs,
|
|
409
|
+
): SteerAck | undefined {
|
|
410
|
+
if (!fsImpl.existsSync(dir)) return undefined;
|
|
411
|
+
let entries: string[];
|
|
412
|
+
try { entries = fsImpl.readdirSync(dir).filter((name) => name.endsWith(".json")).sort(); } catch { return undefined; }
|
|
413
|
+
for (const entry of entries) {
|
|
414
|
+
const target = path.join(dir, entry);
|
|
415
|
+
let ack: SteerAck | undefined;
|
|
416
|
+
try { ack = parseSteerAck(JSON.parse(fsImpl.readFileSync(target, "utf-8"))); } catch { ack = undefined; }
|
|
417
|
+
if (ack?.requestId !== requestId) continue;
|
|
418
|
+
try { fsImpl.rmSync(target, { force: true }); } catch { return undefined; }
|
|
419
|
+
return ack;
|
|
420
|
+
}
|
|
421
|
+
return undefined;
|
|
422
|
+
}
|
|
423
|
+
|
|
388
424
|
export function consumeSteerAcks(asyncDir: string, fsImpl: Pick<typeof fs, "existsSync" | "readdirSync" | "readFileSync" | "rmSync"> = fs): SteerAck[] {
|
|
389
425
|
const root = path.join(controlInboxDir(asyncDir), STEER_ACKS_DIR);
|
|
390
426
|
if (!fsImpl.existsSync(root)) return [];
|
|
@@ -126,6 +126,7 @@ export function createResultWatcher(
|
|
|
126
126
|
const processing = new Set<string>();
|
|
127
127
|
let deliveryActive = true;
|
|
128
128
|
let deliveryEpoch = 0;
|
|
129
|
+
let resultScanTimer: ReturnType<typeof setInterval> | null = null;
|
|
129
130
|
// The sole in-memory ownership lease. It is acquired for one active session
|
|
130
131
|
// and revoked before the watcher, queues, or callbacks are torn down.
|
|
131
132
|
let activeSessionId: string | null = null;
|
|
@@ -343,9 +344,15 @@ export function createResultWatcher(
|
|
|
343
344
|
}
|
|
344
345
|
};
|
|
345
346
|
|
|
347
|
+
const clearResultScan = () => {
|
|
348
|
+
if (resultScanTimer) timers.clearInterval(resultScanTimer);
|
|
349
|
+
resultScanTimer = null;
|
|
350
|
+
};
|
|
351
|
+
|
|
346
352
|
const startPolling = (reason: unknown) => {
|
|
347
353
|
state.watcher?.close();
|
|
348
354
|
state.watcher = null;
|
|
355
|
+
clearResultScan();
|
|
349
356
|
if (state.watcherRestartTimer) return;
|
|
350
357
|
console.error(`Subagent result watcher for '${resultsDir}' fell back to polling because native fs.watch is unavailable (${errorCode(reason) ?? "unknown error"}).`);
|
|
351
358
|
primeExistingResults();
|
|
@@ -354,6 +361,7 @@ export function createResultWatcher(
|
|
|
354
361
|
};
|
|
355
362
|
|
|
356
363
|
const scheduleRestart = () => {
|
|
364
|
+
clearResultScan();
|
|
357
365
|
if (state.watcherRestartTimer) return;
|
|
358
366
|
state.watcherRestartTimer = timers.setTimeout(() => {
|
|
359
367
|
state.watcherRestartTimer = null;
|
|
@@ -381,8 +389,11 @@ export function createResultWatcher(
|
|
|
381
389
|
}
|
|
382
390
|
try {
|
|
383
391
|
const watchDir = resolveWatchPath(resultsDir, fsApi.realpathSync.native);
|
|
384
|
-
state.watcher = fsApi.watch(watchDir, (
|
|
385
|
-
if (
|
|
392
|
+
state.watcher = fsApi.watch(watchDir, (_event, file) => {
|
|
393
|
+
if (!file) {
|
|
394
|
+
primeExistingResults();
|
|
395
|
+
return;
|
|
396
|
+
}
|
|
386
397
|
const fileName = file.toString();
|
|
387
398
|
if (fileName.endsWith(".json")) scheduleResult(fileName, true);
|
|
388
399
|
});
|
|
@@ -394,6 +405,8 @@ export function createResultWatcher(
|
|
|
394
405
|
scheduleRestart();
|
|
395
406
|
});
|
|
396
407
|
state.watcher.unref?.();
|
|
408
|
+
resultScanTimer = timers.setInterval(primeExistingResults, POLL_INTERVAL_MS);
|
|
409
|
+
resultScanTimer.unref?.();
|
|
397
410
|
} catch (error) {
|
|
398
411
|
if (shouldPoll(error)) return startPolling(error);
|
|
399
412
|
console.error(`Failed to start subagent result watcher for '${resultsDir}':`, error);
|
|
@@ -413,6 +426,7 @@ export function createResultWatcher(
|
|
|
413
426
|
timers.clearInterval(state.watcherRestartTimer);
|
|
414
427
|
}
|
|
415
428
|
state.watcherRestartTimer = null;
|
|
429
|
+
clearResultScan();
|
|
416
430
|
state.resultFileCoalescer.clear();
|
|
417
431
|
pendingTriggerTurn.clear();
|
|
418
432
|
processing.clear();
|
|
@@ -3,6 +3,7 @@ import * as fs from "node:fs";
|
|
|
3
3
|
import * as path from "node:path";
|
|
4
4
|
import type { AgentToolResult } from "@earendil-works/pi-agent-core";
|
|
5
5
|
import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
6
|
+
import { getProjectSubagentsDir } from "../../shared/artifacts.ts";
|
|
6
7
|
import { writePrivateAtomicJson } from "../../shared/atomic-json.ts";
|
|
7
8
|
import { shortenPath } from "../../shared/formatters.ts";
|
|
8
9
|
import type { AsyncStatus, Details, ExtensionConfig } from "../../shared/types.ts";
|
|
@@ -87,7 +88,7 @@ export function scheduledRunsEnabled(config: ExtensionConfig): boolean {
|
|
|
87
88
|
}
|
|
88
89
|
|
|
89
90
|
export function scheduledRunStorePath(cwd: string, _sessionId?: string, root?: string): string {
|
|
90
|
-
if (!root) return path.join(path.resolve(cwd), "
|
|
91
|
+
if (!root) return path.join(getProjectSubagentsDir(path.resolve(cwd)), "schedules");
|
|
91
92
|
const projectKey = createHash("sha256").update(path.resolve(cwd)).digest("hex").slice(0, 20);
|
|
92
93
|
return path.join(root, projectKey);
|
|
93
94
|
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import * as fs from "node:fs";
|
|
2
2
|
import * as path from "node:path";
|
|
3
3
|
import { writeAtomicJson } from "../../shared/atomic-json.ts";
|
|
4
|
+
import { readStatus } from "../../shared/utils.ts";
|
|
4
5
|
import { DIRS, type AsyncParallelGroupStatus, type AsyncStatus, type NestedRunSummary, type SubagentRunMode } from "../../shared/types.ts";
|
|
5
6
|
import { resolveEffectiveThinking } from "../../shared/model-info.ts";
|
|
6
7
|
import { normalizeParallelGroups } from "./parallel-groups.ts";
|
|
@@ -84,26 +85,6 @@ function appendJsonlBestEffort(filePath: string, payload: object): void {
|
|
|
84
85
|
}
|
|
85
86
|
}
|
|
86
87
|
|
|
87
|
-
function readStatusFile(asyncDir: string): AsyncStatus | null {
|
|
88
|
-
const statusPath = path.join(asyncDir, "status.json");
|
|
89
|
-
let content: string;
|
|
90
|
-
try {
|
|
91
|
-
content = fs.readFileSync(statusPath, "utf-8");
|
|
92
|
-
} catch (error) {
|
|
93
|
-
if (isNotFoundError(error)) return null;
|
|
94
|
-
throw new Error(`Failed to read async status file '${statusPath}': ${getErrorMessage(error)}`, {
|
|
95
|
-
cause: error instanceof Error ? error : undefined,
|
|
96
|
-
});
|
|
97
|
-
}
|
|
98
|
-
try {
|
|
99
|
-
return JSON.parse(content) as AsyncStatus;
|
|
100
|
-
} catch (error) {
|
|
101
|
-
throw new Error(`Failed to parse async status file '${statusPath}': ${getErrorMessage(error)}`, {
|
|
102
|
-
cause: error instanceof Error ? error : undefined,
|
|
103
|
-
});
|
|
104
|
-
}
|
|
105
|
-
}
|
|
106
|
-
|
|
107
88
|
interface ResultChildOutcome {
|
|
108
89
|
agent?: string;
|
|
109
90
|
success?: boolean;
|
|
@@ -348,7 +329,7 @@ export function checkPidLiveness(pid: number, kill: KillFn = process.kill): PidL
|
|
|
348
329
|
|
|
349
330
|
export function reconcileAsyncRun(asyncDir: string, options: ReconcileAsyncRunOptions = {}): ReconcileAsyncRunResult {
|
|
350
331
|
const now = options.now?.() ?? Date.now();
|
|
351
|
-
const status =
|
|
332
|
+
const status = readStatus(asyncDir);
|
|
352
333
|
const startedStatus = !status && options.startedRun ? buildStartedStatus(asyncDir, options.startedRun, now) : undefined;
|
|
353
334
|
const effectiveStatus = status ?? startedStatus;
|
|
354
335
|
if (!effectiveStatus) return { status: null, repaired: false };
|
|
@@ -1955,6 +1955,47 @@ function combinedAbortSignal(signals: Array<AbortSignal | undefined>): AbortSign
|
|
|
1955
1955
|
return controller.signal;
|
|
1956
1956
|
}
|
|
1957
1957
|
|
|
1958
|
+
async function runSingleStepWithTimeout(
|
|
1959
|
+
step: SubagentStep,
|
|
1960
|
+
ctx: SingleStepContext,
|
|
1961
|
+
parentDeadlineAt?: number,
|
|
1962
|
+
): Promise<SingleStepResult> {
|
|
1963
|
+
if (step.timeoutMs === undefined) return runSingleStep(step, ctx);
|
|
1964
|
+
|
|
1965
|
+
const parentRemainingMs = parentDeadlineAt === undefined ? undefined : Math.max(0, parentDeadlineAt - Date.now());
|
|
1966
|
+
const timeoutMs = parentRemainingMs === undefined ? step.timeoutMs : Math.min(step.timeoutMs, parentRemainingMs);
|
|
1967
|
+
const timeoutMessage = parentRemainingMs !== undefined && parentRemainingMs <= step.timeoutMs
|
|
1968
|
+
? ctx.timeoutMessage
|
|
1969
|
+
: `Subagent timed out after ${step.timeoutMs}ms.`;
|
|
1970
|
+
const timeoutController = new AbortController();
|
|
1971
|
+
let timeoutAction: (() => void) | undefined;
|
|
1972
|
+
let timeoutTriggered = false;
|
|
1973
|
+
const triggerTimeout = (): void => {
|
|
1974
|
+
if (timeoutTriggered) return;
|
|
1975
|
+
timeoutTriggered = true;
|
|
1976
|
+
timeoutController.abort();
|
|
1977
|
+
timeoutAction?.();
|
|
1978
|
+
};
|
|
1979
|
+
const registerTimeout = (action: (() => void) | undefined): void => {
|
|
1980
|
+
timeoutAction = action;
|
|
1981
|
+
ctx.registerTimeout?.(action ? triggerTimeout : undefined);
|
|
1982
|
+
if (action && timeoutTriggered) action();
|
|
1983
|
+
};
|
|
1984
|
+
const timer = setTimeout(triggerTimeout, timeoutMs);
|
|
1985
|
+
timer.unref?.();
|
|
1986
|
+
try {
|
|
1987
|
+
return await runSingleStep(step, {
|
|
1988
|
+
...ctx,
|
|
1989
|
+
registerTimeout,
|
|
1990
|
+
timeoutSignal: combinedAbortSignal([ctx.timeoutSignal, timeoutController.signal]),
|
|
1991
|
+
timeoutMessage,
|
|
1992
|
+
});
|
|
1993
|
+
} finally {
|
|
1994
|
+
clearTimeout(timer);
|
|
1995
|
+
ctx.registerTimeout?.(undefined);
|
|
1996
|
+
}
|
|
1997
|
+
}
|
|
1998
|
+
|
|
1958
1999
|
async function runSubagent(
|
|
1959
2000
|
config: SubagentRunConfig,
|
|
1960
2001
|
onWriterProcess?: (writer: { state: "none" | "spawning" } | { state: "running"; pid: number }) => void,
|
|
@@ -3449,7 +3490,7 @@ async function runSubagent(
|
|
|
3449
3490
|
writeStatusPayload();
|
|
3450
3491
|
appendJsonl(eventsPath, JSON.stringify({ type: "subagent.step.started", ts: taskStartTime, runId: id, stepIndex: fi, agent: task.agent }));
|
|
3451
3492
|
flushPendingStepSteers(fi);
|
|
3452
|
-
const singleResult = await
|
|
3493
|
+
const singleResult = await runSingleStepWithTimeout(task, compactOptional<SingleStepContext>({
|
|
3453
3494
|
previousOutput, placeholder, cwd, sessionEnabled,
|
|
3454
3495
|
outputs,
|
|
3455
3496
|
sessionDir: config.sessionDir ? path.join(config.sessionDir, `dynamic-${stepIndex}-${taskIdx}`) : undefined,
|
|
@@ -3479,7 +3520,7 @@ async function runSubagent(
|
|
|
3479
3520
|
onWriterProcess,
|
|
3480
3521
|
onExternalProcess: (process) => updateExternalProcess(fi, process),
|
|
3481
3522
|
skipAcceptance: () => timedOut || stopped,
|
|
3482
|
-
}));
|
|
3523
|
+
}), config.deadlineAt);
|
|
3483
3524
|
const taskEndTime = Date.now();
|
|
3484
3525
|
const childInterrupted = singleResult.interrupted === true;
|
|
3485
3526
|
const childStopped = singleResult.stopped === true;
|
|
@@ -3831,7 +3872,7 @@ async function runSubagent(
|
|
|
3831
3872
|
const { taskForRun, taskCwd } = prepareParallelTaskRun(task, cwd, worktreeSetup, taskIdx);
|
|
3832
3873
|
flushPendingStepSteers(fi);
|
|
3833
3874
|
|
|
3834
|
-
const singleResult = await
|
|
3875
|
+
const singleResult = await runSingleStepWithTimeout(taskForRun, compactOptional<SingleStepContext>({
|
|
3835
3876
|
previousOutput, placeholder, cwd: taskCwd, sessionEnabled,
|
|
3836
3877
|
outputs,
|
|
3837
3878
|
sessionDir: taskSessionDir,
|
|
@@ -3861,7 +3902,7 @@ async function runSubagent(
|
|
|
3861
3902
|
onWriterProcess,
|
|
3862
3903
|
onExternalProcess: (process) => updateExternalProcess(fi, process),
|
|
3863
3904
|
skipAcceptance: () => timedOut || stopped,
|
|
3864
|
-
}));
|
|
3905
|
+
}), config.deadlineAt);
|
|
3865
3906
|
if (task.sessionFile) {
|
|
3866
3907
|
latestSessionFile = task.sessionFile;
|
|
3867
3908
|
}
|
|
@@ -4120,7 +4161,7 @@ async function runSubagent(
|
|
|
4120
4161
|
}));
|
|
4121
4162
|
|
|
4122
4163
|
flushPendingStepSteers(flatIndex);
|
|
4123
|
-
const singleResult = await
|
|
4164
|
+
const singleResult = await runSingleStepWithTimeout(seqStep, compactOptional<SingleStepContext>({
|
|
4124
4165
|
previousOutput, placeholder, cwd, sessionEnabled,
|
|
4125
4166
|
outputs: statusPayload.mode === "single" ? undefined : outputs,
|
|
4126
4167
|
sessionDir: config.sessionDir,
|
|
@@ -4150,7 +4191,7 @@ async function runSubagent(
|
|
|
4150
4191
|
onWriterProcess,
|
|
4151
4192
|
onExternalProcess: (process) => updateExternalProcess(flatIndex, process),
|
|
4152
4193
|
skipAcceptance: () => timedOut || stopped,
|
|
4153
|
-
}));
|
|
4194
|
+
}), config.deadlineAt);
|
|
4154
4195
|
if (seqStep.sessionFile) {
|
|
4155
4196
|
latestSessionFile = seqStep.sessionFile;
|
|
4156
4197
|
}
|
|
@@ -220,7 +220,7 @@ export async function steerAsyncRun(input: {
|
|
|
220
220
|
const revived = await input.recover(limits);
|
|
221
221
|
if (revived.isError || !revived.details.asyncId) throw new Error(revived.content[0]?.type === "text" ? revived.content[0].text : "Replacement launch failed; source run remains paused.");
|
|
222
222
|
const sourceStatus = readStatus(asyncDir);
|
|
223
|
-
const targetIndex = input.index ?? status.steps?.findIndex((step) => step.status === "running") ?? -1;
|
|
223
|
+
const targetIndex = input.index ?? sourceStatus?.steering?.recent.find((request) => request.id === requestId)?.targets[0]?.index ?? status.steps?.findIndex((step) => step.status === "running") ?? -1;
|
|
224
224
|
if (sourceStatus?.state === "paused" && sourceStatus.steering && targetIndex >= 0) {
|
|
225
225
|
updateSteeringTarget(sourceStatus.steering, requestId, targetIndex, "recovered", Date.now(), { replacementRunId: revived.details.asyncId });
|
|
226
226
|
const stepSteering = sourceStatus.steps?.[targetIndex]?.steering;
|
|
@@ -34,6 +34,7 @@ import {
|
|
|
34
34
|
import { discoverAvailableSkills, normalizeSkillInput } from "../../agents/skills.ts";
|
|
35
35
|
import { INTERCOM_BRIDGE_MARKER } from "../../intercom/intercom-bridge.ts";
|
|
36
36
|
import { runSync } from "./execution.ts";
|
|
37
|
+
import { workflowForegroundSteeringLaunchOptions } from "./workflow-foreground-steering.ts";
|
|
37
38
|
import {
|
|
38
39
|
beginForegroundChild,
|
|
39
40
|
finishForegroundChild,
|
|
@@ -387,6 +388,7 @@ async function runParallelChainTasks(input: ParallelChainRunInput): Promise<Sing
|
|
|
387
388
|
result = await runSync(input.ctx.cwd, input.agents, task.agent, taskStr, {
|
|
388
389
|
permissions: input.permissions,
|
|
389
390
|
parentSessionId: input.ctx.sessionManager.getSessionId() ?? undefined,
|
|
391
|
+
...workflowForegroundSteeringLaunchOptions(input.foregroundControl, childIndex),
|
|
390
392
|
capabilityCeiling: input.capabilityCeiling,
|
|
391
393
|
context: input.contextForAgent?.(task.agent),
|
|
392
394
|
cwd: taskCwd,
|
|
@@ -1367,6 +1369,7 @@ ${step.message}` : ""}` }],
|
|
|
1367
1369
|
r = await runSync(ctx.cwd, agents, seqStep.agent, stepTask, {
|
|
1368
1370
|
permissions: params.permissions,
|
|
1369
1371
|
parentSessionId: ctx.sessionManager.getSessionId() ?? undefined,
|
|
1372
|
+
...workflowForegroundSteeringLaunchOptions(params.foregroundControl, childIndex),
|
|
1370
1373
|
capabilityCeiling: params.capabilityCeiling,
|
|
1371
1374
|
context: params.contextForAgent?.(seqStep.agent),
|
|
1372
1375
|
cwd: resolveChildCwd(cwd ?? ctx.cwd, seqStep.cwd),
|
|
@@ -338,6 +338,9 @@ async function runSingleAttempt(
|
|
|
338
338
|
parentRootRunId: options.nestedRoute?.rootRunId,
|
|
339
339
|
parentCapabilityToken: options.nestedRoute?.capabilityToken,
|
|
340
340
|
parentSessionId: options.parentSessionId,
|
|
341
|
+
steerInboxDir: options.steerInboxDir,
|
|
342
|
+
steerCapabilityPath: options.steerCapabilityPath,
|
|
343
|
+
steerAckDir: options.steerAckDir,
|
|
341
344
|
structuredOutput: options.structuredOutput,
|
|
342
345
|
toolBudget: options.toolBudget,
|
|
343
346
|
allowZeroToolBudget: options.allowZeroToolBudget,
|