pi-subagents 0.48.0 → 0.49.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 +47 -19
- package/docs/agents.md +1 -0
- package/docs/configuration.md +28 -5
- package/docs/missions.md +4 -2
- package/docs/observability.md +26 -2
- package/package.json +1 -1
- package/src/agents/agents.ts +27 -12
- package/src/api/preflight.ts +1 -1
- package/src/extension/config.ts +20 -0
- package/src/extension/index.ts +18 -8
- package/src/extension/public-execution.ts +30 -4
- package/src/extension/schemas.ts +8 -7
- package/src/extension/tool-description.ts +10 -8
- package/src/inspectors/herdr/actions.ts +11 -2
- package/src/inspectors/herdr/inspector-runner.ts +16 -3
- package/src/intercom/intercom-bridge.ts +3 -1
- package/src/missions/store.ts +8 -3
- package/src/runs/background/active-async-capacity.ts +82 -25
- package/src/runs/background/async-execution.ts +24 -36
- package/src/runs/background/async-job-tracker.ts +4 -0
- package/src/runs/background/async-resume.ts +11 -1
- package/src/runs/background/fleet-view.ts +18 -5
- package/src/runs/background/resume-guidance.ts +27 -7
- package/src/runs/background/retained-children.ts +14 -6
- package/src/runs/background/run-status.ts +96 -4
- package/src/runs/background/subagent-runner.ts +4 -1
- package/src/runs/foreground/prompt-audit.ts +4 -3
- package/src/runs/foreground/subagent-executor.ts +219 -20
- package/src/runs/shared/completion-guard.ts +17 -1
- package/src/runs/shared/llm-intent-arbiter.ts +39 -23
- package/src/shared/agent-stream-options.ts +5 -0
- package/src/shared/artifacts.ts +2 -6
- package/src/shared/display-text.ts +50 -0
- package/src/shared/node-executable.ts +21 -0
- package/src/shared/types.ts +23 -4
- package/src/tui/fleet-transcript.ts +1 -48
- package/src/tui/fleet.ts +3 -1
- package/src/tui/render.ts +86 -39
- package/src/watchdog/permission-arbiter.ts +2 -1
- package/src/watchdog/review.ts +4 -3
- package/src/workflows/chat-progress.ts +8 -2
- package/src/workflows/scripted-workflow.ts +46 -7
|
@@ -8,24 +8,25 @@ const CUSTOM_TOOL_DESCRIPTION_MAX_BYTES = 50 * 1024;
|
|
|
8
8
|
|
|
9
9
|
export const SUBAGENT_SAFETY_GUIDANCE = `SAFETY-CRITICAL SUBAGENT GUIDANCE:
|
|
10
10
|
• Use { action: "list" } before execution and only run executable/non-disabled agents.
|
|
11
|
-
• Keep execution and management separate: omit action for workflowScript execution; use action only for management/control.
|
|
12
|
-
• Async/background runs are the default. Use async:false only when a blocking foreground result is needed. Do not sleep or poll status just to wait; use subagent_wait only when the current request must finish in this turn.
|
|
11
|
+
• Keep execution and management separate: omit action for structured single-child or workflowScript execution; use action only for management/control.
|
|
12
|
+
• Async/background runs are the default. Use async:false only when a blocking foreground result is needed. After an async launch, continue independent work only until its next dependency barrier; consume the result before work that depends on it. Do not sleep or poll status just to wait; use subagent_wait only when the current request must finish in this turn.
|
|
13
13
|
• Ordinary child subagents are not orchestrators. Only explicitly configured fanout children may use the child-safe subagent tool, still bounded by depth/session limits.
|
|
14
14
|
• Keep one writer for the same cwd/worktree. Use fresh-context read-only reviewers for independent review, then have the parent synthesize and apply fixes.
|
|
15
|
-
• Async runs expose asyncId/asyncDir with status.json, events.jsonl, output logs,
|
|
15
|
+
• Async runs expose asyncId/asyncDir with status.json, events.jsonl, output logs, status via { action: "status", id }, and lifecycle diagnostics via { action: "debug.run", id }. Include output paths and residual risks when reporting results.`;
|
|
16
16
|
|
|
17
|
-
export const FULL_SUBAGENT_TOOL_DESCRIPTION = `Run
|
|
17
|
+
export const FULL_SUBAGENT_TOOL_DESCRIPTION = `Run one child with { agent, task? }; use { workflowScript } for orchestration. Omit action for execution. Use action only for management/control actions.
|
|
18
18
|
|
|
19
19
|
EXECUTION:
|
|
20
20
|
• Before executing, use { action: "list" } and run only executable/non-disabled configured agents.
|
|
21
|
-
•
|
|
21
|
+
• SINGLE CHILD: { agent:"worker", task:"..." }. This structured form starts exactly one child through the workflow runtime. Workflow-level fields such as model, context, cwd, worktree, output, budgets, acceptance, and async remain defaults for that child. Do not combine agent/task with action or workflowScript.
|
|
22
|
+
• WORKFLOW SCRIPT: { workflowScript: "return runs.run('main', {agent:'worker', task:'...'})" }. Use stable-key runs.run for one child and runs.all for parallel children; ordinary JavaScript provides sequence, branching, filtering, retries, and aggregation. workflowScript is an ordinary JavaScript statement body, so use an explicit return for a useful result. For task text with Markdown fences or shell blocks, build quoted lines instead of nesting raw template literals: \`const task=["Run:","\`\`\`bash","npm test","\`\`\`"].join("\\n")\`. Scripts start asynchronously by default; pass async:false only for a small foreground run. Same-repo foreground workflows default to a live in-chat card; set chatProgress to auto, off, or live-card to control that projection. Workflow-level child controls default onto each runs.run launch, and explicit child fields override them. Use await prompts.render("package:name" | "user:name" | "project:name", vars?) for reusable plain task text, then pass the result explicitly as task. Use {action:"children.list"} to list up to 10 completed retained children from this parent session, then continue one with runs.run(key, {resume:"run-id", task:"follow-up"}); resume and agent are mutually exclusive, resume keeps the stored agent/model/tool contract, workflow resumes wait for completed output, and loops must continue from each latest returned runId. For repository mutation lanes, set worktree:true on the workflow or individual runs.run/runs.all item for managed isolation; each parallel child gets a separate worktree and handoff artifact. A workflow usageBudget is enforced once across the workflow. Available globals are runs.run, runs.all, runs.status, runs.ref/refs, prompts.render, emit, console, and standard JavaScript only. Workflows get async state.get(key) and state.set(key, JSONValue) through their automatic or explicit mission; mission:false workflows do not have a state global. Scripts cannot access filesystem, shell, arbitrary Pi tools, or host globals.
|
|
22
23
|
• 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
24
|
• 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
25
|
• 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
26
|
• 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
27
|
|
|
27
28
|
MANAGEMENT / CONTROL (use action; omit execution fields):
|
|
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.
|
|
29
|
+
• list, get, models, guide, children.list, create, update, delete, eject, disable, enable, reset, status, debug.run, 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.
|
|
29
30
|
• status, interrupt, stop, resume, and steer manage live or persisted runs. Use status view:"fleet" for an overview or view:"transcript" with id and optional index to tail output.
|
|
30
31
|
• { 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.
|
|
31
32
|
• approve-checkpoint and reject-checkpoint decide a paused durable legacy chain checkpoint.
|
|
@@ -33,10 +34,11 @@ MANAGEMENT / CONTROL (use action; omit execution fields):
|
|
|
33
34
|
|
|
34
35
|
${SUBAGENT_SAFETY_GUIDANCE}`;
|
|
35
36
|
|
|
36
|
-
export const COMPACT_SUBAGENT_TOOL_DESCRIPTION = `Run
|
|
37
|
+
export const COMPACT_SUBAGENT_TOOL_DESCRIPTION = `Run one child with { agent, task? }; use { workflowScript } for orchestration. Omit action for execution. Use action only for management/control actions.
|
|
37
38
|
|
|
38
39
|
EXECUTE:
|
|
39
40
|
• Call { action:"list" } first and use only executable/non-disabled agents.
|
|
41
|
+
• SINGLE {agent:"worker",task:"..."} starts exactly one child through the workflow runtime. Workflow-level fields remain child defaults. Do not combine agent/task with action or workflowScript.
|
|
40
42
|
• SCRIPT {workflowScript:"return runs.run('main', {agent:'worker', task:'...'})"}. Use stable-key runs.run for one child and runs.all for parallel work. Use await prompts.render("package:name" | "user:name" | "project:name", vars?) for reusable task text and pass it explicitly to runs.run. Use {action:"children.list"} for the last 10 retained children in this parent session, then runs.run(key,{resume:"run-id",task:"follow-up"}) to continue one with its stored contract; workflow resumes wait for completion and loops continue from the latest returned runId. Workflows get async state.get/state.set through their automatic or explicit mission; mission:false does not. Scripts are ordinary JavaScript statement bodies; use explicit return for a useful result. For task text with Markdown fences or shell blocks, build quoted lines instead of nesting raw template literals: \`const task=["Run:","\`\`\`bash","npm test","\`\`\`"].join("\\n")\`. Use JavaScript for sequence, branching, retries, and aggregation. For repository mutation lanes, use worktree:true on the workflow or runs.run/runs.all item for managed isolation. Scripts start async by default; async:false is the foreground escape hatch and auto-enables a same-repo live chat card unless chatProgress is off.
|
|
41
43
|
• Example: {workflowScript:"const [a,b]=await runs.all([{key:'a',agent:'agent-a',task:'Implement A',worktree:true},{key:'b',agent:'agent-b',task:'Implement B',worktree:true}]); return [a.output,b.output]"}
|
|
42
44
|
• context can be 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.
|
|
@@ -47,7 +49,7 @@ MANAGE / CONTROL:
|
|
|
47
49
|
• 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}.
|
|
48
50
|
|
|
49
51
|
ASYNC / SAFETY:
|
|
50
|
-
• Omitted async detaches background work. Do not sleep or poll merely to wait; use subagent_wait only when this turn must receive results.
|
|
52
|
+
• Omitted async detaches background work. Continue independent work only until its next dependency barrier; consume the result before work that depends on it. Do not sleep or poll merely to wait; use subagent_wait only when this turn must receive results.
|
|
51
53
|
• Ordinary children are not orchestrators. Keep one writer per cwd/worktree and use fresh read-only reviewers for independent checks.
|
|
52
54
|
• Status and artifacts live under asyncId/asyncDir with status.json, events.jsonl, output logs, and {action:"status",id:"..."}.`;
|
|
53
55
|
|
|
@@ -10,6 +10,7 @@ import { writeAtomicJson } from "../../shared/atomic-json.ts";
|
|
|
10
10
|
import { DIRS, type Details, type SubagentState } from "../../shared/types.ts";
|
|
11
11
|
import { readStatus } from "../../shared/utils.ts";
|
|
12
12
|
import { resolveSubagentRunId } from "../../runs/background/run-id-resolver.ts";
|
|
13
|
+
import { resolveNodeExecutable } from "../../shared/node-executable.ts";
|
|
13
14
|
import { createHerdrClient, detectHerdr, type HerdrClient, type HerdrErrorCode, type HerdrResult } from "./client.ts";
|
|
14
15
|
|
|
15
16
|
export const HERDR_INSPECTOR_ACTIONS = ["inspector.open", "inspector.status", "inspector.close"] as const;
|
|
@@ -45,6 +46,7 @@ interface InspectorDeps {
|
|
|
45
46
|
client?: HerdrClient;
|
|
46
47
|
missions?: MissionStoreConfig;
|
|
47
48
|
authorityPolicy?: AuthorityPolicyConfig;
|
|
49
|
+
sessionRoots?: string[];
|
|
48
50
|
cwd: string;
|
|
49
51
|
signal?: AbortSignal;
|
|
50
52
|
now?: () => Date;
|
|
@@ -89,8 +91,8 @@ function shellQuote(value: string): string {
|
|
|
89
91
|
return `'${value.replaceAll("'", "'\\''")}'`;
|
|
90
92
|
}
|
|
91
93
|
|
|
92
|
-
function inspectorCommand(input: { runnerPath: string; asyncDir: string; runId: string; index?: number; missionPath?: string; allowSteer: boolean; allowStop: boolean }): string {
|
|
93
|
-
const args = [
|
|
94
|
+
function inspectorCommand(input: { runnerPath: string; asyncDir: string; runId: string; index?: number; missionPath?: string; allowSteer: boolean; allowStop: boolean; sessionRoots: string[] }): string {
|
|
95
|
+
const args = [resolveNodeExecutable(), input.runnerPath, "--async-dir", input.asyncDir, "--run-id", input.runId, "--allow-steer", String(input.allowSteer), "--allow-stop", String(input.allowStop), "--session-roots", JSON.stringify(input.sessionRoots)];
|
|
94
96
|
if (input.index !== undefined) args.push("--index", String(input.index));
|
|
95
97
|
if (input.missionPath) args.push("--mission-path", input.missionPath);
|
|
96
98
|
return `${process.platform === "win32" ? "& " : ""}${args.map(shellQuote).join(" ")}`;
|
|
@@ -114,6 +116,12 @@ function pathWithin(base: string, candidate: string): boolean {
|
|
|
114
116
|
return resolvedCandidate === resolvedBase || resolvedCandidate.startsWith(`${resolvedBase}${path.sep}`);
|
|
115
117
|
}
|
|
116
118
|
|
|
119
|
+
function herdrSessionRoots(target: { runId: string }, deps: InspectorDeps): string[] {
|
|
120
|
+
const roots = deps.sessionRoots ?? deps.state?.trustedSessionRoots ?? [];
|
|
121
|
+
const job = deps.state?.asyncJobs.get(target.runId) ?? deps.state?.fleetJobs?.get(target.runId);
|
|
122
|
+
return [...new Set([...roots, ...(job?.sessionRoot ? [job.sessionRoot] : [])])];
|
|
123
|
+
}
|
|
124
|
+
|
|
117
125
|
function isTrustedAsyncDir(asyncDir: string, deps: InspectorDeps): boolean {
|
|
118
126
|
try {
|
|
119
127
|
if (fs.lstatSync(asyncDir).isSymbolicLink() || !fs.statSync(asyncDir).isDirectory()) return false;
|
|
@@ -204,6 +212,7 @@ export async function handleHerdrInspectorAction(action: HerdrInspectorAction, p
|
|
|
204
212
|
missionPath: mission?.path,
|
|
205
213
|
allowSteer: resolveAuthorityDecision({ action: "steerRun", policy: deps.authorityPolicy }) === "auto",
|
|
206
214
|
allowStop: resolveAuthorityDecision({ action: "stopRun", policy: deps.authorityPolicy }) === "auto",
|
|
215
|
+
sessionRoots: herdrSessionRoots(target, deps),
|
|
207
216
|
});
|
|
208
217
|
const started = await client.run(["pane", "run", paneId, command], { timeoutMs: 15_000, signal: deps.signal });
|
|
209
218
|
if (started.ok === false) {
|
|
@@ -17,6 +17,7 @@ export interface RunnerOptions {
|
|
|
17
17
|
refreshMs: number;
|
|
18
18
|
allowSteer?: boolean;
|
|
19
19
|
allowStop?: boolean;
|
|
20
|
+
sessionRoots: string[];
|
|
20
21
|
}
|
|
21
22
|
|
|
22
23
|
function readMission(filePath: string | undefined): MissionRecord | undefined {
|
|
@@ -24,7 +25,7 @@ function readMission(filePath: string | undefined): MissionRecord | undefined {
|
|
|
24
25
|
try { return parseMissionRecord(JSON.parse(fs.readFileSync(filePath, "utf-8")), filePath); } catch { return undefined; }
|
|
25
26
|
}
|
|
26
27
|
|
|
27
|
-
export function formatInspectorDashboard(input: { status: AsyncStatus; asyncDir: string; index?: number; mission?: MissionRecord; allowSteer?: boolean; allowStop?: boolean }): string {
|
|
28
|
+
export function formatInspectorDashboard(input: { status: AsyncStatus; asyncDir: string; index?: number; mission?: MissionRecord; allowSteer?: boolean; allowStop?: boolean; sessionRoots?: string[] }): string {
|
|
28
29
|
const { status, asyncDir, mission } = input;
|
|
29
30
|
const lines = [
|
|
30
31
|
`pi-subagents inspector for ${status.runId}`,
|
|
@@ -37,7 +38,7 @@ export function formatInspectorDashboard(input: { status: AsyncStatus; asyncDir:
|
|
|
37
38
|
if (open.length) lines.push(`Open decisions: ${open.map((decision) => `${decision.id}: ${decision.title}`).join(" | ")}`);
|
|
38
39
|
lines.push("");
|
|
39
40
|
}
|
|
40
|
-
lines.push(formatAsyncRunTranscript(status, asyncDir, { index: input.index, lines: 60 }));
|
|
41
|
+
lines.push(formatAsyncRunTranscript(status, asyncDir, { index: input.index, lines: 60, sessionRoots: input.sessionRoots }));
|
|
41
42
|
const controls = [input.allowSteer === false ? undefined : "steer <message>", input.allowStop === false ? undefined : "stop", "status"].filter(Boolean);
|
|
42
43
|
lines.push("", `Controls: ${controls.join(" | ")}`, "Supervisor replies remain in the parent Pi session (subagent_supervisor/intercom).");
|
|
43
44
|
return lines.join("\n");
|
|
@@ -57,6 +58,17 @@ function parseArgs(argv: string[]): RunnerOptions {
|
|
|
57
58
|
const indexRaw = values.get("--index");
|
|
58
59
|
const childIndex = indexRaw === undefined ? undefined : Number(indexRaw);
|
|
59
60
|
if (childIndex !== undefined && (!Number.isInteger(childIndex) || childIndex < 0)) throw new Error("--index must be a non-negative integer.");
|
|
61
|
+
const sessionRootsRaw = values.get("--session-roots");
|
|
62
|
+
let sessionRoots: string[] = [];
|
|
63
|
+
if (sessionRootsRaw !== undefined) {
|
|
64
|
+
try {
|
|
65
|
+
const parsed = JSON.parse(sessionRootsRaw) as unknown;
|
|
66
|
+
if (!Array.isArray(parsed) || parsed.some((root) => typeof root !== "string")) throw new Error();
|
|
67
|
+
sessionRoots = parsed;
|
|
68
|
+
} catch {
|
|
69
|
+
throw new Error("--session-roots must be a JSON array of strings.");
|
|
70
|
+
}
|
|
71
|
+
}
|
|
60
72
|
const refreshRaw = values.get("--refresh-ms");
|
|
61
73
|
const refreshMs = refreshRaw === undefined ? 1_500 : Number(refreshRaw);
|
|
62
74
|
if (!Number.isInteger(refreshMs) || refreshMs < 250) throw new Error("--refresh-ms must be an integer >= 250.");
|
|
@@ -65,6 +77,7 @@ function parseArgs(argv: string[]): RunnerOptions {
|
|
|
65
77
|
runId,
|
|
66
78
|
...(childIndex !== undefined ? { index: childIndex } : {}),
|
|
67
79
|
...(values.get("--mission-path") ? { missionPath: path.resolve(values.get("--mission-path")!) } : {}),
|
|
80
|
+
sessionRoots,
|
|
68
81
|
refreshMs,
|
|
69
82
|
allowSteer: values.get("--allow-steer") !== "false",
|
|
70
83
|
allowStop: values.get("--allow-stop") !== "false",
|
|
@@ -115,7 +128,7 @@ export function runInspector(argv = process.argv.slice(2)): void {
|
|
|
115
128
|
process.stdout.write(`\x1b[2J\x1b[Hpi-subagents inspector\n\nLifecycle status for ${options.runId} is unavailable.\n`);
|
|
116
129
|
return;
|
|
117
130
|
}
|
|
118
|
-
process.stdout.write(`\x1b[2J\x1b[H${formatInspectorDashboard({ status, asyncDir: options.asyncDir, index: options.index, mission: readMission(options.missionPath), allowSteer: options.allowSteer, allowStop: options.allowStop })}${notice ? `\n\n${notice}` : ""}\n> `);
|
|
131
|
+
process.stdout.write(`\x1b[2J\x1b[H${formatInspectorDashboard({ status, asyncDir: options.asyncDir, index: options.index, mission: readMission(options.missionPath), allowSteer: options.allowSteer, allowStop: options.allowStop, sessionRoots: options.sessionRoots })}${notice ? `\n\n${notice}` : ""}\n> `);
|
|
119
132
|
if (isTerminal(status) && timer) {
|
|
120
133
|
clearInterval(timer);
|
|
121
134
|
timer = undefined;
|
|
@@ -51,6 +51,8 @@ export interface IntercomBridgeDiagnostic {
|
|
|
51
51
|
|
|
52
52
|
interface ResolveIntercomBridgeInput {
|
|
53
53
|
config: ExtensionConfig["intercomBridge"];
|
|
54
|
+
/** Per-run config replaces the global config when supplied. */
|
|
55
|
+
override?: IntercomBridgeConfig;
|
|
54
56
|
context: "fresh" | "fork" | undefined;
|
|
55
57
|
orchestratorTarget?: string;
|
|
56
58
|
cwd?: string;
|
|
@@ -143,7 +145,7 @@ export function diagnoseIntercomBridge(input: ResolveIntercomBridgeInput): Inter
|
|
|
143
145
|
}
|
|
144
146
|
|
|
145
147
|
export function resolveIntercomBridge(input: ResolveIntercomBridgeInput): IntercomBridgeState {
|
|
146
|
-
const config = resolveIntercomBridgeConfig(input.config);
|
|
148
|
+
const config = resolveIntercomBridgeConfig(input.override !== undefined ? input.override : input.config);
|
|
147
149
|
const mode = config.mode;
|
|
148
150
|
const orchestratorTarget = input.orchestratorTarget?.trim();
|
|
149
151
|
const agentDir = path.resolve(input.agentDir ?? defaultAgentDir());
|
package/src/missions/store.ts
CHANGED
|
@@ -2,7 +2,6 @@ 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";
|
|
6
5
|
import { writePrivateAtomicJson } from "../shared/atomic-json.ts";
|
|
7
6
|
import { getAgentDir } from "../shared/utils.ts";
|
|
8
7
|
import {
|
|
@@ -259,6 +258,11 @@ function expandConfiguredPath(value: string, projectRoot: string): string {
|
|
|
259
258
|
return path.isAbsolute(expanded) ? path.normalize(expanded) : path.resolve(projectRoot, expanded);
|
|
260
259
|
}
|
|
261
260
|
|
|
261
|
+
function projectMissionDirectory(agentDir: string, projectRoot: string): string {
|
|
262
|
+
const projectKey = createHash("sha256").update(projectRoot).digest("hex");
|
|
263
|
+
return path.join(agentDir, "missions", "projects", projectKey);
|
|
264
|
+
}
|
|
265
|
+
|
|
262
266
|
export function validateMissionStoreConfig(value: unknown, label = "config.missions"): MissionStoreConfig | undefined {
|
|
263
267
|
if (value === undefined) return undefined;
|
|
264
268
|
const input = asObject(value, label);
|
|
@@ -289,12 +293,13 @@ export function resolveMissionStoreLocation(input: {
|
|
|
289
293
|
agentDir?: string;
|
|
290
294
|
}): MissionStoreLocation {
|
|
291
295
|
const projectRoot = path.resolve(input.projectRoot);
|
|
296
|
+
const agentDir = input.agentDir ?? getAgentDir();
|
|
292
297
|
const missionDir = input.config?.directory
|
|
293
298
|
? expandConfiguredPath(input.config.directory, projectRoot)
|
|
294
|
-
:
|
|
299
|
+
: projectMissionDirectory(agentDir, projectRoot);
|
|
295
300
|
const globalIndexDir = input.config?.globalIndexDir
|
|
296
301
|
? expandConfiguredPath(input.config.globalIndexDir, projectRoot)
|
|
297
|
-
: path.join(
|
|
302
|
+
: path.join(agentDir, "missions", "index");
|
|
298
303
|
return {
|
|
299
304
|
projectRoot,
|
|
300
305
|
missionDir,
|
|
@@ -39,6 +39,18 @@ interface CapacityOptions {
|
|
|
39
39
|
afterSlotRename?: (releasedDir: string) => void;
|
|
40
40
|
}
|
|
41
41
|
|
|
42
|
+
export type ActiveAsyncCapacityReleaseVerdict =
|
|
43
|
+
| { state: "releasable"; reason: string }
|
|
44
|
+
| { state: "retained"; reason: string }
|
|
45
|
+
| { state: "not-owned"; reason: string };
|
|
46
|
+
|
|
47
|
+
export interface ActiveAsyncCapacityInspection {
|
|
48
|
+
owner?: ActiveAsyncCapacityOwnerV1;
|
|
49
|
+
relation: "current" | "source" | "none";
|
|
50
|
+
slotDir?: string;
|
|
51
|
+
release: ActiveAsyncCapacityReleaseVerdict;
|
|
52
|
+
}
|
|
53
|
+
|
|
42
54
|
export class ActiveAsyncCapacityError extends Error {
|
|
43
55
|
readonly snapshot: ActiveAsyncCapacitySnapshot;
|
|
44
56
|
|
|
@@ -160,55 +172,100 @@ function terminalState(state: AsyncStatus["state"]): boolean {
|
|
|
160
172
|
return state !== "queued" && state !== "running" && state !== "paused";
|
|
161
173
|
}
|
|
162
174
|
|
|
163
|
-
function
|
|
164
|
-
if (!
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
175
|
+
function runnerReleaseVerdict(owner: ActiveAsyncCapacityOwnerV1, status: AsyncStatus | null): ActiveAsyncCapacityReleaseVerdict {
|
|
176
|
+
if (!status) return { state: "retained", reason: "status file is missing or unreadable" };
|
|
177
|
+
if (!owner.runnerProcessInstanceId) return { state: "retained", reason: "runner process identity has not been recorded" };
|
|
178
|
+
if (status.sessionId !== owner.ownerSessionId) return { state: "retained", reason: `status session ${status.sessionId ?? "unknown"} does not match owner session ${owner.ownerSessionId}` };
|
|
179
|
+
if (status.runId !== owner.runId) return { state: "retained", reason: `status run ${status.runId} does not match owner run ${owner.runId}` };
|
|
180
|
+
if (!terminalState(status.state)) return { state: "retained", reason: `run is still ${status.state}` };
|
|
168
181
|
if (status.processTerminal?.state === "not-started"
|
|
169
182
|
&& status.processTerminal.runId === owner.runId
|
|
170
183
|
&& status.processTerminal.runnerProcessInstanceId === owner.runnerProcessInstanceId
|
|
171
184
|
&& typeof status.error === "string"
|
|
172
|
-
&& status.error) return
|
|
185
|
+
&& status.error) return { state: "releasable", reason: "run failed before child startup completed" };
|
|
173
186
|
const proof = readProcessTerminal(owner.asyncDir, {
|
|
174
187
|
runId: owner.runId,
|
|
175
188
|
runnerProcessInstanceId: owner.runnerProcessInstanceId,
|
|
176
189
|
});
|
|
177
190
|
return proof?.state === "observed"
|
|
178
191
|
&& proof.runId === owner.runId
|
|
179
|
-
&& proof.runnerProcessInstanceId === owner.runnerProcessInstanceId
|
|
192
|
+
&& proof.runnerProcessInstanceId === owner.runnerProcessInstanceId
|
|
193
|
+
? { state: "releasable", reason: "matching observed process-terminal proof is present" }
|
|
194
|
+
: { state: "retained", reason: `process-terminal proof is ${proof?.state ?? "missing"}` };
|
|
180
195
|
}
|
|
181
196
|
|
|
182
|
-
function
|
|
183
|
-
if (status
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
197
|
+
function workflowReleaseVerdict(owner: ActiveAsyncCapacityOwnerV1, status: AsyncStatus | null, liveWorkflowRunIds: ReadonlySet<string>): ActiveAsyncCapacityReleaseVerdict {
|
|
198
|
+
if (!status) return { state: "retained", reason: "status file is missing or unreadable" };
|
|
199
|
+
if (status.sessionId !== owner.ownerSessionId) return { state: "retained", reason: `status session ${status.sessionId ?? "unknown"} does not match owner session ${owner.ownerSessionId}` };
|
|
200
|
+
if (status.runId !== owner.runId) return { state: "retained", reason: `status run ${status.runId} does not match owner run ${owner.runId}` };
|
|
201
|
+
if (status.mode !== "workflow") return { state: "retained", reason: `status mode is ${status.mode}, not workflow` };
|
|
202
|
+
if (!terminalState(status.state)) return { state: "retained", reason: `workflow is still ${status.state}` };
|
|
203
|
+
if (liveWorkflowRunIds.has(owner.runId)) return { state: "retained", reason: "workflow controller is still live" };
|
|
188
204
|
for (const step of status.steps ?? []) {
|
|
189
|
-
|
|
190
|
-
if (
|
|
205
|
+
const label = step.workflowKey ?? step.agent;
|
|
206
|
+
if (step.status === "pending" || step.status === "running" || step.status === "paused") return { state: "retained", reason: `workflow child ${label} is still ${step.status}` };
|
|
207
|
+
if (typeof step.async !== "boolean") return { state: "retained", reason: `workflow child ${label} is missing async classification` };
|
|
191
208
|
if (!step.async) continue;
|
|
192
|
-
if (!step.runId) return
|
|
209
|
+
if (!step.runId) return { state: "retained", reason: `async workflow child ${label} is missing run id` };
|
|
193
210
|
const childDir = path.join(path.dirname(owner.asyncDir), step.runId);
|
|
194
|
-
if (!fs.existsSync(childDir)) return
|
|
211
|
+
if (!fs.existsSync(childDir)) return { state: "retained", reason: `async workflow child ${label} directory is missing` };
|
|
195
212
|
const childStatus = readStatus(childDir);
|
|
196
|
-
if (!childStatus
|
|
213
|
+
if (!childStatus) return { state: "retained", reason: `async workflow child ${label} status is missing or unreadable` };
|
|
214
|
+
if (!terminalState(childStatus.state)) return { state: "retained", reason: `async workflow child ${label} is still ${childStatus.state}` };
|
|
215
|
+
if (!childStatus.processTerminal?.runnerProcessInstanceId) return { state: "retained", reason: `async workflow child ${label} has no runner process identity` };
|
|
197
216
|
const proof = readProcessTerminal(childDir, {
|
|
198
217
|
runId: step.runId,
|
|
199
218
|
runnerProcessInstanceId: childStatus.processTerminal.runnerProcessInstanceId,
|
|
200
219
|
});
|
|
201
|
-
if (proof?.state !== "observed" || proof.runId !== step.runId) return
|
|
220
|
+
if (proof?.state !== "observed" || proof.runId !== step.runId) return { state: "retained", reason: `async workflow child ${label} process-terminal proof is ${proof?.state ?? "missing"}` };
|
|
202
221
|
}
|
|
203
|
-
return
|
|
222
|
+
return { state: "releasable", reason: "workflow is terminal, controller is gone, and async children have observed proof" };
|
|
204
223
|
}
|
|
205
224
|
|
|
206
|
-
function
|
|
225
|
+
function ownerReleaseVerdict(owner: ActiveAsyncCapacityOwnerV1, liveWorkflowRunIds: ReadonlySet<string>): ActiveAsyncCapacityReleaseVerdict {
|
|
207
226
|
const status = readStatus(owner.asyncDir);
|
|
208
|
-
if (!status) return false;
|
|
209
227
|
return owner.kind === "runner"
|
|
210
|
-
?
|
|
211
|
-
:
|
|
228
|
+
? runnerReleaseVerdict(owner, status)
|
|
229
|
+
: workflowReleaseVerdict(owner, status, liveWorkflowRunIds);
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
function capacitySessionDirs(rootDir: string, sessionId?: string): string[] {
|
|
233
|
+
if (sessionId) return [sessionDir(sessionId, rootDir)];
|
|
234
|
+
try {
|
|
235
|
+
return fs.readdirSync(rootDir, { withFileTypes: true })
|
|
236
|
+
.filter((entry) => entry.isDirectory())
|
|
237
|
+
.map((entry) => path.join(rootDir, entry.name));
|
|
238
|
+
} catch (error) {
|
|
239
|
+
if ((error as NodeJS.ErrnoException).code === "ENOENT") return [];
|
|
240
|
+
throw error;
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
export function inspectActiveAsyncCapacityOwner(
|
|
245
|
+
input: { runId: string; sessionId?: string; asyncDir?: string },
|
|
246
|
+
options: CapacityOptions & { liveWorkflowRunIds?: ReadonlySet<string> } = {},
|
|
247
|
+
): ActiveAsyncCapacityInspection {
|
|
248
|
+
const rootDir = options.rootDir ?? ACTIVE_ASYNC_CAPACITY_DIR;
|
|
249
|
+
const liveWorkflowRunIds = options.liveWorkflowRunIds ?? new Set<string>();
|
|
250
|
+
for (const poolDir of capacitySessionDirs(rootDir, input.sessionId)) {
|
|
251
|
+
for (const dir of occupiedSlots(poolDir)) {
|
|
252
|
+
const owner = readOwner(dir);
|
|
253
|
+
if (!owner) continue;
|
|
254
|
+
const sameRun = owner.runId === input.runId || (input.asyncDir !== undefined && path.resolve(owner.asyncDir) === path.resolve(input.asyncDir));
|
|
255
|
+
const sourceRun = owner.sourceRunId === input.runId;
|
|
256
|
+
if (!sameRun && !sourceRun) continue;
|
|
257
|
+
if (sourceRun && !sameRun) {
|
|
258
|
+
return {
|
|
259
|
+
owner,
|
|
260
|
+
relation: "source",
|
|
261
|
+
slotDir: dir,
|
|
262
|
+
release: { state: "not-owned", reason: `slot was transferred to ${owner.runId}` },
|
|
263
|
+
};
|
|
264
|
+
}
|
|
265
|
+
return { owner, relation: "current", slotDir: dir, release: ownerReleaseVerdict(owner, liveWorkflowRunIds) };
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
return { relation: "none", release: { state: "not-owned", reason: "no active-capacity slot records this run" } };
|
|
212
269
|
}
|
|
213
270
|
|
|
214
271
|
export function reconcileActiveAsyncCapacity(
|
|
@@ -225,7 +282,7 @@ export function reconcileActiveAsyncCapacity(
|
|
|
225
282
|
|| owner.ownerSessionId !== sessionId
|
|
226
283
|
|| owner.ownerSessionKey !== activeAsyncCapacitySessionKey(sessionId)
|
|
227
284
|
|| path.basename(dir) !== `slot-${owner.slot}`
|
|
228
|
-
||
|
|
285
|
+
|| ownerReleaseVerdict(owner, liveWorkflowRunIds).state !== "releasable") continue;
|
|
229
286
|
removeOwnedSlot(dir, owner, options);
|
|
230
287
|
}
|
|
231
288
|
return snapshotFor(sessionId, limit, rootDir);
|
|
@@ -19,6 +19,7 @@ import { buildChainInstructions, isCheckpointStep, isDynamicParallelStep, isPara
|
|
|
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";
|
|
22
|
+
import { resolveNodeExecutable } from "../../shared/node-executable.ts";
|
|
22
23
|
import { buildSkillInjection, normalizeSkillInput, resolveSkillsWithFallback } from "../../agents/skills.ts";
|
|
23
24
|
import { buildAgentMemoryInjection } from "../../agents/agent-memory.ts";
|
|
24
25
|
import { PI_CODING_AGENT_PACKAGE_ROOT_ENV, PROMPT_REDACTED, resolveChildCwd } from "../../shared/utils.ts";
|
|
@@ -37,6 +38,7 @@ import {
|
|
|
37
38
|
type AsyncStatus,
|
|
38
39
|
type ArtifactConfig,
|
|
39
40
|
type Details,
|
|
41
|
+
type IntercomBridgeConfig,
|
|
40
42
|
type JsonSchemaObject,
|
|
41
43
|
type MaxOutputConfig,
|
|
42
44
|
type NestedRouteInfo,
|
|
@@ -187,6 +189,8 @@ interface AsyncSingleParams {
|
|
|
187
189
|
/** Raw caller-facing goal used only by the started event. */
|
|
188
190
|
goal?: string;
|
|
189
191
|
agentConfig: AgentConfig;
|
|
192
|
+
/** Agent contract before per-run bridge injection, used only for recovery persistence. */
|
|
193
|
+
recoveryAgentConfig?: AgentConfig;
|
|
190
194
|
ctx: AsyncExecutionContext;
|
|
191
195
|
cwd?: string;
|
|
192
196
|
maxOutput?: MaxOutputConfig;
|
|
@@ -214,6 +218,7 @@ interface AsyncSingleParams {
|
|
|
214
218
|
worktreeSetupHookTimeoutMs?: number;
|
|
215
219
|
worktreeBaseDir?: string;
|
|
216
220
|
controlConfig?: ResolvedControlConfig;
|
|
221
|
+
intercomBridge?: IntercomBridgeConfig;
|
|
217
222
|
controlIntercomTarget?: string;
|
|
218
223
|
childIntercomTarget?: (agent: string, index: number) => string | undefined;
|
|
219
224
|
nestedRoute?: NestedRouteInfo;
|
|
@@ -299,27 +304,6 @@ export function isAsyncAvailable(): boolean {
|
|
|
299
304
|
return jitiCliPath !== undefined;
|
|
300
305
|
}
|
|
301
306
|
|
|
302
|
-
function isNodeExecutableName(execPath: string): boolean {
|
|
303
|
-
const basename = path.basename(execPath).toLowerCase();
|
|
304
|
-
return basename === "node" || basename === "node.exe" || basename === "nodejs" || basename === "nodejs.exe";
|
|
305
|
-
}
|
|
306
|
-
|
|
307
|
-
function canUseCurrentNodeExecutable(execPath: string): boolean {
|
|
308
|
-
try {
|
|
309
|
-
fs.accessSync(execPath, process.platform === "win32" ? fs.constants.F_OK : fs.constants.X_OK);
|
|
310
|
-
return true;
|
|
311
|
-
} catch {
|
|
312
|
-
return false;
|
|
313
|
-
}
|
|
314
|
-
}
|
|
315
|
-
|
|
316
|
-
function resolveAsyncRunnerNodeCommand(): string {
|
|
317
|
-
if (isNodeExecutableName(process.execPath) && canUseCurrentNodeExecutable(process.execPath)) {
|
|
318
|
-
return process.execPath;
|
|
319
|
-
}
|
|
320
|
-
return process.platform === "win32" ? "node.exe" : "node";
|
|
321
|
-
}
|
|
322
|
-
|
|
323
307
|
export function resolveAsyncRunnerLogPaths(cfg: object): { stdoutPath: string; stderrPath: string } | undefined {
|
|
324
308
|
const asyncDir = typeof (cfg as { asyncDir?: unknown }).asyncDir === "string"
|
|
325
309
|
? (cfg as { asyncDir: string }).asyncDir
|
|
@@ -486,7 +470,7 @@ function spawnRunner(cfg: object, suffix: string, cwd: string, onProcessTerminal
|
|
|
486
470
|
const launchConfig = { ...cfg, runnerProcessInstanceId };
|
|
487
471
|
fs.writeFileSync(cfgPath, JSON.stringify(launchConfig));
|
|
488
472
|
const runner = path.join(path.dirname(fileURLToPath(import.meta.url)), "subagent-runner.ts");
|
|
489
|
-
const nodeCommand =
|
|
473
|
+
const nodeCommand = resolveNodeExecutable();
|
|
490
474
|
const launchForStartup = launchConfig as typeof launchConfig & { asyncDir?: unknown; id?: unknown; sessionId?: unknown; revivalLease?: unknown };
|
|
491
475
|
const launchAsyncDir = typeof launchForStartup.asyncDir === "string" ? launchForStartup.asyncDir : undefined;
|
|
492
476
|
const launchRunId = typeof launchForStartup.id === "string" ? launchForStartup.id : suffix;
|
|
@@ -1251,6 +1235,7 @@ export function executeAsyncChain(
|
|
|
1251
1235
|
workflowGraph,
|
|
1252
1236
|
cwd: runnerCwd,
|
|
1253
1237
|
asyncDir,
|
|
1238
|
+
...(sessionRoot ? { sessionRoot } : {}),
|
|
1254
1239
|
...(params.timeoutMs !== undefined ? { timeoutMs: params.timeoutMs, deadlineAt } : {}),
|
|
1255
1240
|
...(initialTurnBudget ? { turnBudget: initialTurnBudget } : {}),
|
|
1256
1241
|
...(initialUsageBudget ? { usageBudget: initialUsageBudget } : {}),
|
|
@@ -1451,6 +1436,7 @@ export function executeAsyncSingle(
|
|
|
1451
1436
|
async: true,
|
|
1452
1437
|
agentContract: params.agentContract,
|
|
1453
1438
|
});
|
|
1439
|
+
const recoveryAgentConfig = params.recoveryAgentConfig ?? agentConfig;
|
|
1454
1440
|
const recoveryDescriptor: SteeringRecoveryDescriptor = {
|
|
1455
1441
|
version: 1,
|
|
1456
1442
|
launchContractDigest,
|
|
@@ -1462,30 +1448,31 @@ export function executeAsyncSingle(
|
|
|
1462
1448
|
...(sessionFile ? { sessionFile } : {}),
|
|
1463
1449
|
cwd: runnerCwd,
|
|
1464
1450
|
...(model ? { model } : {}),
|
|
1465
|
-
...(
|
|
1451
|
+
...(recoveryAgentConfig.fallbackModels ? { fallbackModels: [...recoveryAgentConfig.fallbackModels] } : {}),
|
|
1466
1452
|
...(effectiveThinking ? { thinking: resolveEffectiveThinking(model, effectiveThinking) } : {}),
|
|
1467
|
-
...(
|
|
1468
|
-
...(
|
|
1469
|
-
...(
|
|
1470
|
-
...(
|
|
1471
|
-
...(
|
|
1472
|
-
systemPromptMode:
|
|
1473
|
-
inheritProjectContext:
|
|
1474
|
-
inheritSkills:
|
|
1453
|
+
...(recoveryAgentConfig.tools ? { tools: [...recoveryAgentConfig.tools] } : {}),
|
|
1454
|
+
...(recoveryAgentConfig.extensions ? { extensions: [...recoveryAgentConfig.extensions] } : {}),
|
|
1455
|
+
...(recoveryAgentConfig.subagentOnlyExtensions ? { subagentOnlyExtensions: [...recoveryAgentConfig.subagentOnlyExtensions] } : {}),
|
|
1456
|
+
...(recoveryAgentConfig.mcpDirectTools ? { mcpDirectTools: [...recoveryAgentConfig.mcpDirectTools] } : {}),
|
|
1457
|
+
...(recoveryAgentConfig.systemPrompt ? { systemPrompt: recoveryAgentConfig.systemPrompt } : {}),
|
|
1458
|
+
systemPromptMode: recoveryAgentConfig.systemPromptMode,
|
|
1459
|
+
inheritProjectContext: recoveryAgentConfig.inheritProjectContext,
|
|
1460
|
+
inheritSkills: recoveryAgentConfig.inheritSkills,
|
|
1475
1461
|
...(resolvedSkills.length ? { skills: resolvedSkills.map((skill) => skill.name) } : {}),
|
|
1476
|
-
...(
|
|
1477
|
-
...(
|
|
1478
|
-
...(
|
|
1479
|
-
...(
|
|
1462
|
+
...(recoveryAgentConfig.skillPath ? { skillPath: [...recoveryAgentConfig.skillPath] } : {}),
|
|
1463
|
+
...(recoveryAgentConfig.filePath ? { agentFilePath: recoveryAgentConfig.filePath } : {}),
|
|
1464
|
+
...(recoveryAgentConfig.completionGuard !== undefined ? { completionGuard: recoveryAgentConfig.completionGuard } : {}),
|
|
1465
|
+
...(recoveryAgentConfig.memory ? { memory: { ...recoveryAgentConfig.memory } } : {}),
|
|
1480
1466
|
...(outputPath ? { outputPath } : {}),
|
|
1481
1467
|
outputMode,
|
|
1482
1468
|
...(params.structuredOutputSchema ? { structuredOutputSchema: params.structuredOutputSchema } : {}),
|
|
1483
1469
|
...(params.acceptance !== undefined ? { acceptance: params.acceptance } : {}),
|
|
1484
1470
|
...(controlConfig ? { controlConfig } : {}),
|
|
1471
|
+
...(params.intercomBridge !== undefined ? { intercomBridge: params.intercomBridge } : {}),
|
|
1485
1472
|
...(deadlineAt !== undefined ? { absoluteDeadlineAt: deadlineAt } : {}),
|
|
1486
1473
|
...(initialTurnBudget ? { initialTurnBudget: { maxTurns: initialTurnBudget.maxTurns, graceTurns: initialTurnBudget.graceTurns } } : {}),
|
|
1487
1474
|
...(resolvedToolBudget.budget ? { initialToolBudget: resolvedToolBudget.budget } : {}),
|
|
1488
|
-
maxSubagentDepth: resolveChildMaxSubagentDepth(maxSubagentDepth,
|
|
1475
|
+
maxSubagentDepth: resolveChildMaxSubagentDepth(maxSubagentDepth, recoveryAgentConfig.maxSubagentDepth),
|
|
1489
1476
|
...(maxOutput ? { maxOutput } : {}),
|
|
1490
1477
|
share: shareEnabled,
|
|
1491
1478
|
...(resolvedSessionDir ? { sessionDir: resolvedSessionDir } : {}),
|
|
@@ -1654,6 +1641,7 @@ export function executeAsyncSingle(
|
|
|
1654
1641
|
goal: (params.goal ?? task).trim() ? PROMPT_REDACTED : undefined,
|
|
1655
1642
|
cwd: runnerCwd,
|
|
1656
1643
|
asyncDir,
|
|
1644
|
+
...(sessionRoot ? { sessionRoot } : {}),
|
|
1657
1645
|
launchContractDigest,
|
|
1658
1646
|
launchResolvedExtensions,
|
|
1659
1647
|
...(params.parentWorkflowRunId ? { parentWorkflowRunId: params.parentWorkflowRunId } : {}),
|
|
@@ -557,10 +557,13 @@ export function createAsyncJobTracker(pi: Pick<ExtensionAPI, "events">, state: S
|
|
|
557
557
|
const agents = firstGroupCount && firstGroupCount > 0
|
|
558
558
|
? rawAgents?.slice(0, firstGroupCount)
|
|
559
559
|
: rawAgents;
|
|
560
|
+
const sessionRoot = state.liveAsyncSessionRoots?.get(info.id);
|
|
561
|
+
state.liveAsyncSessionRoots?.delete(info.id);
|
|
560
562
|
state.asyncJobs.set(info.id, {
|
|
561
563
|
asyncId: info.id,
|
|
562
564
|
asyncDir,
|
|
563
565
|
...(typeof info.cwd === "string" ? { cwd: path.resolve(info.cwd) } : {}),
|
|
566
|
+
...(sessionRoot ? { sessionRoot } : {}),
|
|
564
567
|
status: "queued",
|
|
565
568
|
pid: typeof info.pid === "number" ? info.pid : undefined,
|
|
566
569
|
...(typeof info.sessionId === "string" ? { sessionId: info.sessionId } : {}),
|
|
@@ -637,6 +640,7 @@ export function createAsyncJobTracker(pi: Pick<ExtensionAPI, "events">, state: S
|
|
|
637
640
|
state.asyncJobs.clear();
|
|
638
641
|
state.fleetJobs?.clear();
|
|
639
642
|
state.foregroundControls?.clear();
|
|
643
|
+
state.liveAsyncSessionRoots?.clear();
|
|
640
644
|
state.lastForegroundControlId = null;
|
|
641
645
|
state.resultFileCoalescer.clear();
|
|
642
646
|
if (ctx?.hasUI) {
|
|
@@ -307,7 +307,7 @@ export function readAsyncRecoveryDescriptor(asyncDir: string | undefined): Steer
|
|
|
307
307
|
"version", "launchContractDigest", "sourceRunId", "agentContract", "agent", "sessionFile", "cwd", "model", "fallbackModels", "thinking", "tools", "extensions",
|
|
308
308
|
"subagentOnlyExtensions", "mcpDirectTools", "systemPrompt", "systemPromptMode", "inheritProjectContext", "inheritSkills", "skills",
|
|
309
309
|
"skillPath", "agentFilePath", "completionGuard", "memory", "outputPath", "outputMode", "structuredOutputSchema", "acceptance", "sessionDir", "artifactConfig",
|
|
310
|
-
"artifactsDir", "maxOutput", "controlConfig", "absoluteDeadlineAt", "initialTurnBudget", "initialToolBudget", "maxSubagentDepth", "share", "capabilityCeiling",
|
|
310
|
+
"artifactsDir", "maxOutput", "controlConfig", "intercomBridge", "absoluteDeadlineAt", "initialTurnBudget", "initialToolBudget", "maxSubagentDepth", "share", "capabilityCeiling",
|
|
311
311
|
"launchResolvedExtensions", "runFanoutBudget",
|
|
312
312
|
]);
|
|
313
313
|
for (const field of Object.keys(parsed)) {
|
|
@@ -374,6 +374,16 @@ export function readAsyncRecoveryDescriptor(asyncDir: string | undefined): Steer
|
|
|
374
374
|
if (artifact.includeTranscript !== undefined && typeof artifact.includeTranscript !== "boolean") throw new Error(`Invalid async recovery descriptor '${descriptorPath}': artifactConfig.includeTranscript must be a boolean.`);
|
|
375
375
|
if (!Number.isInteger(artifact.cleanupDays) || (artifact.cleanupDays as number) < 0) throw new Error(`Invalid async recovery descriptor '${descriptorPath}': artifactConfig.cleanupDays must be a non-negative integer.`);
|
|
376
376
|
}
|
|
377
|
+
if (parsed.intercomBridge !== undefined) {
|
|
378
|
+
if (!parsed.intercomBridge || typeof parsed.intercomBridge !== "object" || Array.isArray(parsed.intercomBridge)) throw new Error(`Invalid async recovery descriptor '${descriptorPath}': intercomBridge must be an object.`);
|
|
379
|
+
const bridge = parsed.intercomBridge as Record<string, unknown>;
|
|
380
|
+
for (const field of Object.keys(bridge)) {
|
|
381
|
+
if (field !== "mode" && field !== "instructionFile" && field !== "resultDelivery") throw new Error(`Invalid async recovery descriptor '${descriptorPath}': intercomBridge.${field} is not supported.`);
|
|
382
|
+
}
|
|
383
|
+
if (bridge.mode !== undefined && bridge.mode !== "off" && bridge.mode !== "fork-only" && bridge.mode !== "always") throw new Error(`Invalid async recovery descriptor '${descriptorPath}': intercomBridge.mode is invalid.`);
|
|
384
|
+
if (bridge.instructionFile !== undefined && typeof bridge.instructionFile !== "string") throw new Error(`Invalid async recovery descriptor '${descriptorPath}': intercomBridge.instructionFile must be a string.`);
|
|
385
|
+
if (bridge.resultDelivery !== undefined && typeof bridge.resultDelivery !== "boolean") throw new Error(`Invalid async recovery descriptor '${descriptorPath}': intercomBridge.resultDelivery must be a boolean.`);
|
|
386
|
+
}
|
|
377
387
|
if (parsed.controlConfig !== undefined) {
|
|
378
388
|
if (!parsed.controlConfig || typeof parsed.controlConfig !== "object" || Array.isArray(parsed.controlConfig)) throw new Error(`Invalid async recovery descriptor '${descriptorPath}': controlConfig must be an object.`);
|
|
379
389
|
const control = parsed.controlConfig as Record<string, unknown>;
|