pi-subagents 0.46.0 → 0.47.1
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 +37 -0
- package/agents/reviewer.md +3 -4
- 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 +13 -0
- package/src/extension/fanout-child.ts +5 -4
- package/src/extension/index.ts +39 -14
- package/src/extension/rpc.ts +7 -6
- package/src/extension/schemas.ts +27 -7
- package/src/extension/tool-description.ts +26 -8
- package/src/inspectors/herdr/project-panes.ts +2 -1
- package/src/intercom/native-supervisor-channel.ts +102 -4
- package/src/missions/store.ts +2 -1
- package/src/missions/workflow-state.ts +19 -13
- package/src/runs/background/active-run-index.ts +42 -0
- package/src/runs/background/async-execution.ts +10 -5
- package/src/runs/background/async-job-tracker.ts +293 -133
- package/src/runs/background/async-resume.ts +19 -3
- package/src/runs/background/async-status.ts +33 -5
- package/src/runs/background/control-channel.ts +91 -17
- package/src/runs/background/result-watcher.ts +102 -7
- package/src/runs/background/run-status.ts +18 -0
- package/src/runs/background/scheduled-runs.ts +25 -1
- package/src/runs/background/stale-run-reconciler.ts +12 -24
- package/src/runs/background/subagent-runner.ts +68 -10
- package/src/runs/foreground/async-dismiss-action.ts +85 -0
- package/src/runs/foreground/async-steering-action.ts +6 -7
- package/src/runs/foreground/chain-execution.ts +3 -0
- package/src/runs/foreground/execution.ts +3 -0
- package/src/runs/foreground/subagent-executor.ts +129 -20
- 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/subagent-prompt-runtime.ts +24 -5
- package/src/runs/shared/worktree.ts +3 -2
- package/src/shared/artifacts.ts +15 -15
- package/src/shared/display-text.ts +100 -0
- package/src/shared/file-coalescer.ts +9 -0
- package/src/shared/formatters.ts +4 -6
- package/src/shared/settings.ts +15 -2
- package/src/shared/types.ts +16 -2
- 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
- package/src/workflows/scripted-workflow.ts +4 -1
package/src/extension/schemas.ts
CHANGED
|
@@ -254,7 +254,7 @@ const ControlOverrides = Type.Object({
|
|
|
254
254
|
})),
|
|
255
255
|
});
|
|
256
256
|
|
|
257
|
-
const
|
|
257
|
+
const SubagentParamProperties = {
|
|
258
258
|
agent: Type.Optional(Type.String({ description: "Agent target for management actions such as get, update, delete, and models." })),
|
|
259
259
|
resume: Type.Optional(Type.String({ description: "Retained child run id for a workflowScript runs.run/runs.all item. Mutually exclusive with agent; task supplies the follow-up." })),
|
|
260
260
|
// Management action (when present, tool operates in management mode)
|
|
@@ -263,10 +263,10 @@ const SubagentParamsSchema = Type.Object({
|
|
|
263
263
|
})),
|
|
264
264
|
name: Type.Optional(Type.String({ description: "Human-readable name for action='schedule.create'." })),
|
|
265
265
|
id: Type.Optional(Type.String({
|
|
266
|
-
description: "Run id or prefix for status, interrupt, stop, resume, steer, append-step, approve-checkpoint, reject-checkpoint, mission.attach-run, or the decision id for mission.resolve-decision."
|
|
266
|
+
description: "Run id or prefix for status, interrupt, stop, dismiss, resume, steer, append-step, approve-checkpoint, reject-checkpoint, mission.attach-run, or the decision id for mission.resolve-decision."
|
|
267
267
|
})),
|
|
268
268
|
runId: Type.Optional(Type.String({
|
|
269
|
-
description: "Target run ID for interrupt, stop, resume, steer, append-step, approve-checkpoint, reject-checkpoint, or mission.attach-run. Prefer id for new calls."
|
|
269
|
+
description: "Target run ID for interrupt, stop, dismiss, resume, steer, append-step, approve-checkpoint, reject-checkpoint, or mission.attach-run. Prefer id for new calls."
|
|
270
270
|
})),
|
|
271
271
|
dir: Type.Optional(Type.String({
|
|
272
272
|
description: "Async run directory for action='status', action='stop', action='resume', or action='steer'."
|
|
@@ -296,7 +296,7 @@ const SubagentParamsSchema = Type.Object({
|
|
|
296
296
|
overlap: Type.Optional(Type.String({ enum: ["skip"], description: "Overlap policy. This slice supports skip only." })),
|
|
297
297
|
catchUp: Type.Optional(Type.String({ enum: ["none", "latest"], description: "Missed occurrence policy for recurring schedules. Defaults to latest." })),
|
|
298
298
|
missionId: Type.Optional(Type.String({ description: "Mission id." })),
|
|
299
|
-
mission: Type.Optional(Type.Unsafe({ ...MissionLaunchOverride, description: "Mission object, or false for no mission.
|
|
299
|
+
mission: Type.Optional(Type.Unsafe({ ...MissionLaunchOverride, description: "Mission object, or false for no mission. Set exactly one non-empty title or summary; objective and labels are optional. goal may only be true and then requires budget.tokens." })),
|
|
300
300
|
missionUpdate: Type.Optional(Type.Unsafe({ ...MissionUpdateOverride, description: "Mission update: objective, goal false or {paused:boolean}, budget, summary, labels, decisions, artifacts, or delivery receipts." })),
|
|
301
301
|
missionStatus: Type.Optional(Type.String({ description: "Mission status." })),
|
|
302
302
|
missionScope: Type.Optional(Type.String({ description: "Mission list scope: project (default) or global pointer index." })),
|
|
@@ -324,8 +324,8 @@ const SubagentParamsSchema = Type.Object({
|
|
|
324
324
|
description: "'fresh' or 'fork' to branch from parent session. Explicit context overrides every child in the invocation. If omitted, each requested agent uses its own defaultContext; agents without defaultContext: 'fork' run fresh.",
|
|
325
325
|
})),
|
|
326
326
|
async: Type.Optional(Type.Boolean({ description: "Run in background (default: false, or per config)" })),
|
|
327
|
-
timeoutMs: Type.Optional(Type.Integer({ minimum: 1, description: "
|
|
328
|
-
maxRuntimeMs: Type.Optional(Type.Integer({ minimum: 1, description: "Alias timeoutMs
|
|
327
|
+
timeoutMs: Type.Optional(Type.Integer({ minimum: 1, description: "Run timeout. Foreground runs and async children default to 30m; async composites have no default parent deadline. Alias maxRuntimeMs." })),
|
|
328
|
+
maxRuntimeMs: Type.Optional(Type.Integer({ minimum: 1, description: "Alias timeoutMs. Foreground runs and async children default to 30m; async composites have no default parent deadline." })),
|
|
329
329
|
turnBudget: Type.Optional(TurnBudgetOverride),
|
|
330
330
|
toolBudget: Type.Optional(ToolBudgetOverride),
|
|
331
331
|
usageBudget: Type.Optional(UsageBudgetOverride),
|
|
@@ -353,9 +353,29 @@ const SubagentParamsSchema = Type.Object({
|
|
|
353
353
|
agentContract: Type.Optional(AgentContractOverride),
|
|
354
354
|
acceptance: Type.Optional(AcceptanceOverride),
|
|
355
355
|
gate: Type.Optional(Type.String({ minLength: 1, description: "Host gate command. Cannot be combined with acceptance." })),
|
|
356
|
-
}
|
|
356
|
+
};
|
|
357
|
+
|
|
358
|
+
const { step: _legacyChainStep, ...subagentParamPropertiesWithoutStep } = SubagentParamProperties;
|
|
359
|
+
const trimmedSubagentParamProperties = {
|
|
360
|
+
...subagentParamPropertiesWithoutStep,
|
|
361
|
+
id: Type.Optional(Type.String({
|
|
362
|
+
description: "Run id or prefix for status, interrupt, stop, resume, steer, mission.attach-run, or the decision id for mission.resolve-decision."
|
|
363
|
+
})),
|
|
364
|
+
runId: Type.Optional(Type.String({
|
|
365
|
+
description: "Target run ID for interrupt, stop, resume, steer, or mission.attach-run. Prefer id for new calls."
|
|
366
|
+
})),
|
|
367
|
+
};
|
|
368
|
+
const SubagentParamsSchema = Type.Object(SubagentParamProperties);
|
|
369
|
+
const TrimmedSubagentParamsSchema = Type.Object(trimmedSubagentParamProperties);
|
|
357
370
|
|
|
358
371
|
export const SubagentParams = keepTopLevelParameterDescriptions(SubagentParamsSchema);
|
|
372
|
+
export const SubagentParamsWithoutLegacyChainControls = keepTopLevelParameterDescriptions(TrimmedSubagentParamsSchema);
|
|
373
|
+
|
|
374
|
+
export function createSubagentParamsSchema(options: { legacyChainControls?: boolean } = {}): typeof SubagentParams | typeof SubagentParamsWithoutLegacyChainControls {
|
|
375
|
+
return options.legacyChainControls === true
|
|
376
|
+
? SubagentParams
|
|
377
|
+
: SubagentParamsWithoutLegacyChainControls;
|
|
378
|
+
}
|
|
359
379
|
|
|
360
380
|
const SubagentWaitParamsSchema = Type.Object({
|
|
361
381
|
id: Type.Optional(Type.String({
|
|
@@ -22,7 +22,7 @@ EXECUTION:
|
|
|
22
22
|
• Sequential example: { workflowScript: "const a = await runs.run('analyze', {agent:'agent-a', task:'Analyze the request'}); return (await runs.run('plan', {agent:'agent-b', task:'Plan from: '+a.output})).output" }
|
|
23
23
|
• Parallel example: { workflowScript: "const [a,b] = await runs.all([{key:'correctness',agent:'agent-a',task:'Review correctness'},{key:'tests',agent:'agent-b',task:'Review tests'}]); return {correctness:a.output,tests:b.output}" }
|
|
24
24
|
• Optional context is "fresh" or "fork". timeoutMs/maxRuntimeMs apply to foreground and async workflows; foreground workflows default to 30 minutes and async workflows have no default timeout. Omit acceptance for reviewer/read-only calls; evidence levels end at verified, and acceptance.review.required requests independent writer review.
|
|
25
|
-
• Durable mission attachment is automatic by default. Use missionId to attach an existing mission, mission:{...} to override auto-create, or mission:false for ephemeral work.
|
|
25
|
+
• Durable mission attachment is automatic by default. Use missionId to attach an existing mission, mission:{...} to override auto-create, or mission:false for ephemeral work. A mission object needs exactly one non-empty title or summary; objective and labels are optional. goal may only be true and requires budget:{tokens}.
|
|
26
26
|
|
|
27
27
|
MANAGEMENT / CONTROL (use action; omit execution fields):
|
|
28
28
|
• list, get, models, guide, children.list, create, update, delete, eject, disable, enable, reset, doctor, grant-spawn-budget, worktree.discard, refine/refine.show/refine.rollback, mission.create/list/show/update/resolve-decision/attach-run/close, inspector.open/status/close, project.open/status/close, and watchdog actions remain available. Use {action:"guide", topic:"overview"} for packaged current-version help; topics are overview, workflows, agents, missions, observability, tool-reference, configuration, models, watchdog, and extension-api.
|
|
@@ -44,6 +44,7 @@ EXECUTE:
|
|
|
44
44
|
MANAGE / CONTROL:
|
|
45
45
|
• Use action without execution fields for list/get/models/guide/authoring, refine/refine.show/refine.rollback, mission, watchdog, status, interrupt, stop, resume, steer, script-only scheduling, diagnostics, and other management actions. guide reads shipped current-version docs by topic.
|
|
46
46
|
• append-step uses step:{...} only for an already-running durable legacy chain; step is not an execution mode.
|
|
47
|
+
• A mission object needs exactly one non-empty title or summary; objective and labels are optional. goal may only be true and requires budget:{tokens}.
|
|
47
48
|
|
|
48
49
|
ASYNC / SAFETY:
|
|
49
50
|
• Omitted async detaches background work. Do not sleep or poll merely to wait; use subagent_wait only when this turn must receive results.
|
|
@@ -152,13 +153,30 @@ function withMandatorySafetyGuidance(description: string): string {
|
|
|
152
153
|
: SUBAGENT_SAFETY_GUIDANCE;
|
|
153
154
|
}
|
|
154
155
|
|
|
155
|
-
|
|
156
|
+
const LEGACY_CHAIN_CONTROL_GUIDANCE_LINES = new Set([
|
|
157
|
+
'• { action: "append-step", id: "...", step: {agent:"agent-c", task:"Use {previous}"} } appends one step to an already-running durable legacy chain. step is control-only, not an execution mode.',
|
|
158
|
+
"• approve-checkpoint and reject-checkpoint decide a paused durable legacy chain checkpoint.",
|
|
159
|
+
"• append-step uses step:{...} only for an already-running durable legacy chain; step is not an execution mode.",
|
|
160
|
+
]);
|
|
161
|
+
|
|
162
|
+
function withoutLegacyChainControlGuidance(description: string): string {
|
|
163
|
+
return description
|
|
164
|
+
.split("\n")
|
|
165
|
+
.filter((line) => !LEGACY_CHAIN_CONTROL_GUIDANCE_LINES.has(line.trim()))
|
|
166
|
+
.join("\n");
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
export function buildSubagentToolDescription(config: Pick<ExtensionConfig, "toolDescriptionMode" | "legacyChainControls"> = {}, options?: ToolDescriptionOptions): string {
|
|
156
170
|
const mode = resolveToolDescriptionMode(config, options);
|
|
157
|
-
|
|
158
|
-
if (mode === "
|
|
171
|
+
let description: string;
|
|
172
|
+
if (mode === "compact") description = COMPACT_SUBAGENT_TOOL_DESCRIPTION;
|
|
173
|
+
else if (mode === "custom") {
|
|
159
174
|
const custom = loadCustomToolDescription(options);
|
|
160
|
-
if (custom)
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
175
|
+
if (custom) description = withMandatorySafetyGuidance(custom);
|
|
176
|
+
else {
|
|
177
|
+
warn(options, `${CUSTOM_TOOL_DESCRIPTION_FILE} was not found or valid for toolDescriptionMode "custom"; using full description.`);
|
|
178
|
+
description = FULL_SUBAGENT_TOOL_DESCRIPTION;
|
|
179
|
+
}
|
|
180
|
+
} else description = FULL_SUBAGENT_TOOL_DESCRIPTION;
|
|
181
|
+
return config.legacyChainControls === true ? description : withoutLegacyChainControlGuidance(description);
|
|
164
182
|
}
|
|
@@ -2,6 +2,7 @@ import * as fs from "node:fs";
|
|
|
2
2
|
import * as path from "node:path";
|
|
3
3
|
import type { AgentToolResult } from "@earendil-works/pi-agent-core";
|
|
4
4
|
import { getPiSpawnCommand } from "../../runs/shared/pi-spawn.ts";
|
|
5
|
+
import { getProjectSubagentsDir } from "../../shared/artifacts.ts";
|
|
5
6
|
import { writeAtomicJson } from "../../shared/atomic-json.ts";
|
|
6
7
|
import type { Details } from "../../shared/types.ts";
|
|
7
8
|
import { createHerdrClient, detectHerdr, type HerdrClient, type HerdrErrorCode } from "./client.ts";
|
|
@@ -153,7 +154,7 @@ function formatProjectPaneError(input: { code: ProjectPaneErrorCode; message: st
|
|
|
153
154
|
}
|
|
154
155
|
|
|
155
156
|
function projectPaneDir(projectRoot: string): string {
|
|
156
|
-
return path.join(projectRoot, "
|
|
157
|
+
return path.join(getProjectSubagentsDir(projectRoot), "project-panes");
|
|
157
158
|
}
|
|
158
159
|
|
|
159
160
|
export function projectPaneBindingPath(projectRoot: string): string {
|
|
@@ -22,6 +22,7 @@ export const NATIVE_SUPERVISOR_TOOL_NAME = "subagent_supervisor";
|
|
|
22
22
|
const MAX_MESSAGE_BYTES = 64 * 1024;
|
|
23
23
|
const DEFAULT_ASK_TIMEOUT_MS = 10 * 60 * 1000;
|
|
24
24
|
const CHANNEL_POLL_MS = Math.min(POLL_INTERVAL_MS, 500);
|
|
25
|
+
const CHANNEL_SAFETY_POLL_MS = 5000;
|
|
25
26
|
const STALE_EMPTY_CHANNEL_AGE_MS = 60 * 1000;
|
|
26
27
|
const STALE_EMPTY_CHANNEL_CLEANUP_INTERVAL_MS = 60 * 1000;
|
|
27
28
|
|
|
@@ -69,6 +70,13 @@ interface IntercomParams {
|
|
|
69
70
|
replyTo?: string;
|
|
70
71
|
}
|
|
71
72
|
|
|
73
|
+
type SupervisorWatch = (filename: fs.PathLike, listener: fs.WatchListener<string>) => fs.FSWatcher;
|
|
74
|
+
|
|
75
|
+
interface NativeSupervisorChannelDeps {
|
|
76
|
+
platform?: NodeJS.Platform;
|
|
77
|
+
watch?: SupervisorWatch;
|
|
78
|
+
}
|
|
79
|
+
|
|
72
80
|
const ContactSupervisorParamsSchema = Type.Object({
|
|
73
81
|
reason: Type.String({ enum: ["need_decision", "interview_request", "progress_update"] }),
|
|
74
82
|
message: Type.Optional(Type.String()),
|
|
@@ -626,10 +634,16 @@ function buildParentIntercomTool(pending: Map<string, PendingSupervisorRequest>,
|
|
|
626
634
|
};
|
|
627
635
|
}
|
|
628
636
|
|
|
629
|
-
export function createNativeSupervisorChannel(pi: ExtensionAPI, state: SubagentState): { start: () => void; dispose: () => void; pending: Map<string, PendingSupervisorRequest> } {
|
|
637
|
+
export function createNativeSupervisorChannel(pi: ExtensionAPI, state: SubagentState, deps: NativeSupervisorChannelDeps = {}): { start: () => void; dispose: () => void; pending: Map<string, PendingSupervisorRequest> } {
|
|
638
|
+
const watch = deps.watch ?? fs.watch;
|
|
630
639
|
const pending = new Map<string, PendingSupervisorRequest>();
|
|
631
640
|
const seenFiles = new Set<string>();
|
|
641
|
+
const requestWatchers = new Map<string, fs.FSWatcher>();
|
|
642
|
+
let rootWatcher: fs.FSWatcher | undefined;
|
|
632
643
|
let poller: ReturnType<typeof setInterval> | undefined;
|
|
644
|
+
let safetyPoller: ReturnType<typeof setInterval> | undefined;
|
|
645
|
+
let deferredWatcherRefresh: ReturnType<typeof setImmediate> | undefined;
|
|
646
|
+
let started = false;
|
|
633
647
|
let lastStaleCleanupAt = 0;
|
|
634
648
|
|
|
635
649
|
const registerParentTools = (): void => {
|
|
@@ -696,17 +710,101 @@ export function createNativeSupervisorChannel(pi: ExtensionAPI, state: SubagentS
|
|
|
696
710
|
}
|
|
697
711
|
};
|
|
698
712
|
|
|
713
|
+
const startPolling = (): void => {
|
|
714
|
+
if (poller) return;
|
|
715
|
+
poller = setInterval(poll, CHANNEL_POLL_MS);
|
|
716
|
+
poller.unref?.();
|
|
717
|
+
};
|
|
718
|
+
const startSafetyPolling = (): void => {
|
|
719
|
+
if (safetyPoller) return;
|
|
720
|
+
safetyPoller = setInterval(() => {
|
|
721
|
+
watchExistingRequestDirs();
|
|
722
|
+
poll();
|
|
723
|
+
}, CHANNEL_SAFETY_POLL_MS);
|
|
724
|
+
safetyPoller.unref?.();
|
|
725
|
+
};
|
|
726
|
+
const watchRequestDir = (requestsDir: string): void => {
|
|
727
|
+
if (requestWatchers.has(requestsDir)) return;
|
|
728
|
+
try {
|
|
729
|
+
const watcher = watch(requestsDir, () => poll());
|
|
730
|
+
watcher.on("error", () => {
|
|
731
|
+
try { watcher.close(); } catch {}
|
|
732
|
+
requestWatchers.delete(requestsDir);
|
|
733
|
+
startPolling();
|
|
734
|
+
});
|
|
735
|
+
watcher.unref?.();
|
|
736
|
+
requestWatchers.set(requestsDir, watcher);
|
|
737
|
+
} catch (error) {
|
|
738
|
+
if ((error as NodeJS.ErrnoException).code !== "ENOENT") startPolling();
|
|
739
|
+
}
|
|
740
|
+
};
|
|
741
|
+
const watchExistingRequestDirs = (): void => {
|
|
742
|
+
let channelEntries: fs.Dirent[];
|
|
743
|
+
try {
|
|
744
|
+
channelEntries = fs.readdirSync(SUPERVISOR_CHANNEL_ROOT, { withFileTypes: true });
|
|
745
|
+
} catch (error) {
|
|
746
|
+
if ((error as NodeJS.ErrnoException).code === "ENOENT") return;
|
|
747
|
+
startPolling();
|
|
748
|
+
return;
|
|
749
|
+
}
|
|
750
|
+
for (const entry of channelEntries) {
|
|
751
|
+
if (entry.isDirectory()) watchRequestDir(path.join(SUPERVISOR_CHANNEL_ROOT, entry.name, REQUESTS_DIR));
|
|
752
|
+
}
|
|
753
|
+
};
|
|
754
|
+
const scheduleWatcherRefresh = (): void => {
|
|
755
|
+
if (deferredWatcherRefresh) return;
|
|
756
|
+
deferredWatcherRefresh = setImmediate(() => {
|
|
757
|
+
deferredWatcherRefresh = undefined;
|
|
758
|
+
if (!started) return;
|
|
759
|
+
watchExistingRequestDirs();
|
|
760
|
+
poll();
|
|
761
|
+
});
|
|
762
|
+
deferredWatcherRefresh.unref?.();
|
|
763
|
+
};
|
|
764
|
+
|
|
699
765
|
return {
|
|
700
766
|
start: () => {
|
|
701
|
-
if (
|
|
767
|
+
if (started) return;
|
|
768
|
+
started = true;
|
|
702
769
|
registerParentTools();
|
|
703
770
|
poll();
|
|
704
|
-
|
|
705
|
-
|
|
771
|
+
try {
|
|
772
|
+
fs.mkdirSync(SUPERVISOR_CHANNEL_ROOT, { recursive: true });
|
|
773
|
+
if ((deps.platform ?? process.platform) === "win32") {
|
|
774
|
+
startPolling();
|
|
775
|
+
return;
|
|
776
|
+
}
|
|
777
|
+
watchExistingRequestDirs();
|
|
778
|
+
rootWatcher = watch(SUPERVISOR_CHANNEL_ROOT, () => {
|
|
779
|
+
watchExistingRequestDirs();
|
|
780
|
+
poll();
|
|
781
|
+
scheduleWatcherRefresh();
|
|
782
|
+
});
|
|
783
|
+
rootWatcher.on("error", startPolling);
|
|
784
|
+
startSafetyPolling();
|
|
785
|
+
scheduleWatcherRefresh();
|
|
786
|
+
} catch {
|
|
787
|
+
startPolling();
|
|
788
|
+
}
|
|
706
789
|
},
|
|
707
790
|
dispose: () => {
|
|
791
|
+
started = false;
|
|
792
|
+
try {
|
|
793
|
+
rootWatcher?.close();
|
|
794
|
+
} catch {
|
|
795
|
+
// Best effort during shutdown.
|
|
796
|
+
}
|
|
797
|
+
rootWatcher = undefined;
|
|
798
|
+
for (const watcher of requestWatchers.values()) {
|
|
799
|
+
try { watcher.close(); } catch {}
|
|
800
|
+
}
|
|
801
|
+
requestWatchers.clear();
|
|
708
802
|
if (poller) clearInterval(poller);
|
|
709
803
|
poller = undefined;
|
|
804
|
+
if (safetyPoller) clearInterval(safetyPoller);
|
|
805
|
+
safetyPoller = undefined;
|
|
806
|
+
if (deferredWatcherRefresh) clearImmediate(deferredWatcherRefresh);
|
|
807
|
+
deferredWatcherRefresh = undefined;
|
|
710
808
|
pending.clear();
|
|
711
809
|
seenFiles.clear();
|
|
712
810
|
},
|
package/src/missions/store.ts
CHANGED
|
@@ -2,6 +2,7 @@ import { createHash, randomUUID } from "node:crypto";
|
|
|
2
2
|
import * as fs from "node:fs";
|
|
3
3
|
import * as os from "node:os";
|
|
4
4
|
import * as path from "node:path";
|
|
5
|
+
import { getProjectSubagentsDir } from "../shared/artifacts.ts";
|
|
5
6
|
import { writePrivateAtomicJson } from "../shared/atomic-json.ts";
|
|
6
7
|
import { getAgentDir } from "../shared/utils.ts";
|
|
7
8
|
import {
|
|
@@ -290,7 +291,7 @@ export function resolveMissionStoreLocation(input: {
|
|
|
290
291
|
const projectRoot = path.resolve(input.projectRoot);
|
|
291
292
|
const missionDir = input.config?.directory
|
|
292
293
|
? expandConfiguredPath(input.config.directory, projectRoot)
|
|
293
|
-
: path.join(projectRoot, "
|
|
294
|
+
: path.join(getProjectSubagentsDir(projectRoot), "missions");
|
|
294
295
|
const globalIndexDir = input.config?.globalIndexDir
|
|
295
296
|
? expandConfiguredPath(input.config.globalIndexDir, projectRoot)
|
|
296
297
|
: path.join(input.agentDir ?? getAgentDir(), "missions", "index");
|
|
@@ -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();
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import * as fs from "node:fs";
|
|
2
|
+
import * as path from "node:path";
|
|
3
|
+
import type { AsyncStatus } from "../../shared/types.ts";
|
|
4
|
+
|
|
5
|
+
export const ACTIVE_RUN_INDEX_DIR = ".active-runs";
|
|
6
|
+
|
|
7
|
+
function indexDir(asyncDirRoot: string): string {
|
|
8
|
+
return path.join(asyncDirRoot, ACTIVE_RUN_INDEX_DIR);
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
function markerPath(asyncDir: string): string {
|
|
12
|
+
return path.join(indexDir(path.dirname(asyncDir)), path.basename(asyncDir));
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function isActiveAsyncState(state: AsyncStatus["state"]): boolean {
|
|
16
|
+
return state === "queued" || state === "running";
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function updateActiveRunIndex(asyncDir: string, state: AsyncStatus["state"]): void {
|
|
20
|
+
const marker = markerPath(asyncDir);
|
|
21
|
+
if (isActiveAsyncState(state)) {
|
|
22
|
+
fs.mkdirSync(path.dirname(marker), { recursive: true });
|
|
23
|
+
fs.writeFileSync(marker, "", { flag: "a" });
|
|
24
|
+
return;
|
|
25
|
+
}
|
|
26
|
+
try {
|
|
27
|
+
fs.rmSync(marker);
|
|
28
|
+
} catch (error) {
|
|
29
|
+
if ((error as NodeJS.ErrnoException).code !== "ENOENT") throw error;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export function readActiveRunIndex(asyncDirRoot: string): string[] | undefined {
|
|
34
|
+
try {
|
|
35
|
+
return fs.readdirSync(indexDir(asyncDirRoot), { withFileTypes: true })
|
|
36
|
+
.filter((entry) => entry.isFile())
|
|
37
|
+
.map((entry) => entry.name);
|
|
38
|
+
} catch (error) {
|
|
39
|
+
if ((error as NodeJS.ErrnoException).code === "ENOENT") return undefined;
|
|
40
|
+
throw error;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
@@ -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 } : {}),
|