pi-subagents 0.49.0 → 0.50.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.
Files changed (75) hide show
  1. package/CHANGELOG.md +35 -0
  2. package/agents/oracle.md +7 -5
  3. package/agents/researcher.md +1 -1
  4. package/agents/reviewer.md +2 -2
  5. package/agents/scout.md +1 -1
  6. package/agents/worker.md +1 -1
  7. package/docs/agents.md +2 -0
  8. package/docs/configuration.md +47 -3
  9. package/docs/extension-api.md +36 -0
  10. package/docs/missions.md +1 -1
  11. package/docs/observability.md +16 -0
  12. package/docs/tool-reference.md +19 -2
  13. package/docs/workflows.md +2 -2
  14. package/package.json +1 -1
  15. package/skills/pi-subagents/references/constraints-and-recipes.md +3 -2
  16. package/skills/pi-subagents/references/execution-controls.md +5 -4
  17. package/skills/pi-subagents/references/management-authoring-rpc.md +2 -2
  18. package/skills/pi-subagents/references/prompting-and-roles.md +31 -15
  19. package/src/agents/agent-serializer.ts +2 -0
  20. package/src/agents/agents.ts +10 -0
  21. package/src/api/external-runs.ts +174 -84
  22. package/src/api/preflight.ts +12 -6
  23. package/src/extension/config.ts +33 -0
  24. package/src/extension/index.ts +48 -5
  25. package/src/extension/public-execution.ts +3 -1
  26. package/src/extension/rpc.ts +5 -1
  27. package/src/extension/schemas.ts +2 -1
  28. package/src/extension/tool-description.ts +4 -2
  29. package/src/inspectors/herdr/actions.ts +3 -7
  30. package/src/inspectors/herdr/project-panes.ts +2 -6
  31. package/src/inspectors/herdr/shell-command.ts +16 -0
  32. package/src/intercom/intercom-bridge.ts +2 -3
  33. package/src/intercom/native-supervisor-channel.ts +19 -42
  34. package/src/missions/goal-driver.ts +3 -1
  35. package/src/runs/background/active-run-index.ts +71 -1
  36. package/src/runs/background/async-execution.ts +49 -7
  37. package/src/runs/background/async-job-tracker.ts +1 -0
  38. package/src/runs/background/async-resume.ts +3 -5
  39. package/src/runs/background/async-status-snapshot.ts +277 -0
  40. package/src/runs/background/async-status.ts +8 -3
  41. package/src/runs/background/chain-root-attachment.ts +2 -2
  42. package/src/runs/background/completion-replay.ts +11 -1
  43. package/src/runs/background/fleet-view.ts +3 -1
  44. package/src/runs/background/result-files.ts +437 -0
  45. package/src/runs/background/result-watcher.ts +188 -41
  46. package/src/runs/background/retained-children.ts +69 -20
  47. package/src/runs/background/run-id-resolver.ts +30 -24
  48. package/src/runs/background/run-status.ts +7 -2
  49. package/src/runs/background/scheduled-runs.ts +54 -28
  50. package/src/runs/background/stale-run-reconciler.ts +27 -13
  51. package/src/runs/background/subagent-runner.ts +294 -32
  52. package/src/runs/background/subagent-wait.ts +2 -0
  53. package/src/runs/background/wait-completions.ts +5 -2
  54. package/src/runs/foreground/async-dismiss-action.ts +2 -1
  55. package/src/runs/foreground/chain-execution.ts +16 -0
  56. package/src/runs/foreground/execution.ts +219 -15
  57. package/src/runs/foreground/subagent-executor.ts +114 -28
  58. package/src/runs/shared/completion-guard.ts +96 -6
  59. package/src/runs/shared/external-cli-runner.ts +4 -0
  60. package/src/runs/shared/model-fallback.ts +16 -2
  61. package/src/runs/shared/nested-events.ts +66 -62
  62. package/src/runs/shared/orca-progress-tabs.ts +375 -0
  63. package/src/runs/shared/parallel-utils.ts +2 -0
  64. package/src/runs/shared/subagent-control.ts +15 -0
  65. package/src/runs/shared/subagent-prompt-runtime.ts +1 -9
  66. package/src/runs/shared/subagent-startup-retry.ts +12 -0
  67. package/src/runs/shared/tool-timeout.ts +93 -0
  68. package/src/shared/types.ts +29 -2
  69. package/src/slash/slash-commands.ts +34 -25
  70. package/src/slash/slash-live-state.ts +3 -0
  71. package/src/tui/fleet-status.ts +160 -45
  72. package/src/tui/fleet.ts +125 -15
  73. package/src/tui/render.ts +41 -10
  74. package/src/workflows/chat-progress.ts +6 -4
  75. package/src/workflows/scripted-workflow.ts +227 -70
@@ -20,6 +20,7 @@ import { sanitizeDisplayText, truncateDisplayText } from "../shared/display-text
20
20
  import { readStatus } from "../shared/utils.ts";
21
21
  import { SubagentParams } from "./schemas.ts";
22
22
  import { normalizePublicSubagentExecution } from "./public-execution.ts";
23
+ import { ASYNC_STATUS_SNAPSHOT_KIND, ASYNC_STATUS_SNAPSHOT_VERSION, buildAsyncStatusSnapshotForState } from "../runs/background/async-status-snapshot.ts";
23
24
 
24
25
  export const SUBAGENT_RPC_PROTOCOL_VERSION = 1;
25
26
  export const SUBAGENT_RPC_REQUEST_EVENT = "subagents:rpc:v1:request";
@@ -375,6 +376,7 @@ function pingData(ctx: ExtensionContext | null) {
375
376
  capabilities: {
376
377
  status: true,
377
378
  fleetStatus: { version: 1 },
379
+ asyncStatusSnapshot: { kind: ASYNC_STATUS_SNAPSHOT_KIND, version: ASYNC_STATUS_SNAPSHOT_VERSION },
378
380
  asyncSpawn: true,
379
381
  steer: true,
380
382
  nonRecoveringSteer: true,
@@ -544,13 +546,15 @@ async function handleRequest(
544
546
  request.method,
545
547
  { action: "status", ...normalizeTargetParams(request.params, "status") },
546
548
  );
549
+ const sessionId = resolveCurrentSessionId(ctx.sessionManager);
547
550
  return {
548
551
  ...status,
549
552
  fleet: buildFleetStatus(
550
553
  options.state,
551
554
  fleetKeys,
552
- resolveCurrentSessionId(ctx.sessionManager),
555
+ sessionId,
553
556
  ),
557
+ asyncSnapshot: buildAsyncStatusSnapshotForState(options.state, sessionId),
554
558
  };
555
559
  }
556
560
  if (request.method === "steer") {
@@ -93,7 +93,7 @@ const AcceptanceOverride = Type.Unsafe({
93
93
  });
94
94
 
95
95
  const AgentContractOverride = Type.Object({
96
- version: Type.Integer({ enum: [1], description: "Enable compatibility behavior for this run/child." }),
96
+ version: Type.Integer({ minimum: 1, maximum: 1, description: "Enable compatibility behavior for this run/child." }),
97
97
  }, { additionalProperties: false, description: "Compatibility behavior. Omit for the default behavior." });
98
98
 
99
99
  const ChainGateOverride = Type.String({
@@ -327,6 +327,7 @@ const SubagentParamProperties = {
327
327
  async: Type.Optional(Type.Boolean({ description: "Run in background (default: false, or per config)" })),
328
328
  timeoutMs: Type.Optional(Type.Integer({ minimum: 1, description: "Timeout. Foreground and single async runs use config timeoutMs, else 30m; async composites have no default parent deadline. Alias maxRuntimeMs." })),
329
329
  maxRuntimeMs: Type.Optional(Type.Integer({ minimum: 1, description: "Alias timeoutMs. Foreground and single async runs use config timeoutMs, else 30m; async composites have no default parent deadline." })),
330
+ toolTimeoutMs: Type.Optional(Type.Integer({ minimum: 1, description: "Optional hard per-tool-call timeout in milliseconds; known-fast built-in tools have a five-minute default." })),
330
331
  turnBudget: Type.Optional(TurnBudgetOverride),
331
332
  toolBudget: Type.Optional(ToolBudgetOverride),
332
333
  usageBudget: Type.Optional(UsageBudgetOverride),
@@ -11,6 +11,7 @@ export const SUBAGENT_SAFETY_GUIDANCE = `SAFETY-CRITICAL SUBAGENT GUIDANCE:
11
11
  • Keep execution and management separate: omit action for structured single-child or workflowScript execution; use action only for management/control.
12
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
+ • Oracle/advisor consultations should use supervisor dialogue for material unknowns when available; request one-shot only when desired.
14
15
  • 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
16
  • 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
17
 
@@ -19,7 +20,7 @@ export const FULL_SUBAGENT_TOOL_DESCRIPTION = `Run one child with { agent, task?
19
20
  EXECUTION:
20
21
  • Before executing, use { action: "list" } and run only executable/non-disabled configured agents.
21
22
  • 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.
23
+ • 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 recent retained workflow children with resumable/not-resumable reasons. Resume only rows reported resumable. For a simple follow-up or implementation challenge, use {action:"resume", id:"run-id", message:"..."}. Resume keeps the stored agent/model/tool contract. If no resumable child is listed, launch a same-role fallback challenge and label it as fallback. Inside workflowScript, continue one with runs.run(key, {resume:"run-id", task:"follow-up"}); 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.
23
24
  • 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" }
24
25
  • 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}" }
25
26
  • 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.
@@ -39,7 +40,7 @@ export const COMPACT_SUBAGENT_TOOL_DESCRIPTION = `Run one child with { agent, ta
39
40
  EXECUTE:
40
41
  • Call { action:"list" } first and use only executable/non-disabled agents.
41
42
  • 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.
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.
43
+ • 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 recent retained workflow children and resume only rows reported resumable. Use {action:"resume",id:"run-id",message:"..."} for a simple follow-up or challenge; resume keeps the stored agent/model/tool contract. If none is resumable, launch a same-role fallback challenge and label it as fallback. Inside workflowScript use runs.run(key,{resume:"run-id",task:"follow-up"}) when the script must wait for completion and 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.
43
44
  • 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]"}
44
45
  • 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.
45
46
 
@@ -51,6 +52,7 @@ MANAGE / CONTROL:
51
52
  ASYNC / SAFETY:
52
53
  • 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.
53
54
  • Ordinary children are not orchestrators. Keep one writer per cwd/worktree and use fresh read-only reviewers for independent checks.
55
+ • Oracle/advisor consultations use available supervisor dialogue for material unknowns; request one-shot when desired.
54
56
  • Status and artifacts live under asyncId/asyncDir with status.json, events.jsonl, output logs, and {action:"status",id:"..."}.`;
55
57
 
56
58
 
@@ -12,6 +12,7 @@ import { readStatus } from "../../shared/utils.ts";
12
12
  import { resolveSubagentRunId } from "../../runs/background/run-id-resolver.ts";
13
13
  import { resolveNodeExecutable } from "../../shared/node-executable.ts";
14
14
  import { createHerdrClient, detectHerdr, type HerdrClient, type HerdrErrorCode, type HerdrResult } from "./client.ts";
15
+ import { formatShellCommand } from "./shell-command.ts";
15
16
 
16
17
  export const HERDR_INSPECTOR_ACTIONS = ["inspector.open", "inspector.status", "inspector.close"] as const;
17
18
  export type HerdrInspectorAction = typeof HERDR_INSPECTOR_ACTIONS[number];
@@ -86,16 +87,11 @@ function extractPaneId(value: unknown): string | undefined {
86
87
  return undefined;
87
88
  }
88
89
 
89
- function shellQuote(value: string): string {
90
- if (process.platform === "win32") return `"${value.replaceAll('"', '\\"')}"`;
91
- return `'${value.replaceAll("'", "'\\''")}'`;
92
- }
93
-
94
90
  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)];
91
+ const args = [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)];
96
92
  if (input.index !== undefined) args.push("--index", String(input.index));
97
93
  if (input.missionPath) args.push("--mission-path", input.missionPath);
98
- return `${process.platform === "win32" ? "& " : ""}${args.map(shellQuote).join(" ")}`;
94
+ return formatShellCommand(resolveNodeExecutable(), args);
99
95
  }
100
96
 
101
97
  function missionForRun(asyncDir: string, cwd: string, config: MissionStoreConfig | undefined, runId: string): { id: string; path: string } | undefined {
@@ -6,6 +6,7 @@ import { getProjectSubagentsDir } from "../../shared/artifacts.ts";
6
6
  import { writeAtomicJson } from "../../shared/atomic-json.ts";
7
7
  import type { Details } from "../../shared/types.ts";
8
8
  import { createHerdrClient, detectHerdr, type HerdrClient, type HerdrErrorCode } from "./client.ts";
9
+ import { formatShellCommand } from "./shell-command.ts";
9
10
 
10
11
  export const HERDR_PROJECT_PANE_ACTIONS = ["project.open", "project.status", "project.close"] as const;
11
12
  export type HerdrProjectPaneAction = typeof HERDR_PROJECT_PANE_ACTIONS[number];
@@ -257,11 +258,6 @@ function projectPaneRuntime(value: unknown): ProjectPaneRuntime | undefined {
257
258
  };
258
259
  }
259
260
 
260
- function shellQuote(value: string): string {
261
- if (process.platform === "win32") return `"${value.replaceAll('"', '\\"')}"`;
262
- return `'${value.replaceAll("'", "'\\''")}'`;
263
- }
264
-
265
261
  function resolveProjectRoot(requested: string): ProjectPaneResult<string> {
266
262
  const resolved = path.resolve(requested);
267
263
  try {
@@ -284,7 +280,7 @@ async function inspectPane(client: HerdrClient, paneId: string, signal?: AbortSi
284
280
  function projectPaneCommand(message: string | undefined): string {
285
281
  const args = message?.trim() ? [message.trim()] : [];
286
282
  const command = getPiSpawnCommand(args);
287
- return `${process.platform === "win32" ? "& " : ""}${[command.command, ...command.args].map(shellQuote).join(" ")}`;
283
+ return formatShellCommand(command.command, command.args);
288
284
  }
289
285
 
290
286
  function canonicalRuntimePath(value: string | undefined): string | undefined {
@@ -0,0 +1,16 @@
1
+ function shellQuote(value: string, platform: NodeJS.Platform): string {
2
+ if (platform === "win32") return `"${value.replaceAll('"', '\\"')}"`;
3
+ return `'${value.replaceAll("'", "'\\''")}'`;
4
+ }
5
+
6
+ function isBareExecutable(value: string): boolean {
7
+ return /^[\w./@:-]+$/.test(value);
8
+ }
9
+
10
+ export function formatShellCommand(exe: string, args: readonly string[], platform: NodeJS.Platform = process.platform): string {
11
+ const quotedArgs = args.map((arg) => shellQuote(arg, platform));
12
+ if (platform === "win32") return `& ${[shellQuote(exe, platform), ...quotedArgs].join(" ")}`;
13
+ // Nushell treats a leading quoted token as a string, so use a bare invoker for paths that need quoting.
14
+ const invocation = isBareExecutable(exe) ? exe : `sh -c 'exec "$0" "$@"' ${shellQuote(exe, platform)}`;
15
+ return [invocation, ...quotedArgs].join(" ");
16
+ }
@@ -26,9 +26,8 @@ Use contact_supervisor first. It resolves the supervisor session "{orchestratorT
26
26
  - After contact_supervisor with reason "need_decision" or "interview_request", stay alive and continue only after the reply arrives. Do not finish your final response with a choose-one question.
27
27
  - Do not ask for clarification when the only conflict is review-only/no-edit versus progress-writing or artifact-writing instructions. If an output path is configured but no write-capable tool is available, return the complete artifact in your final response; the runtime will persist it. Do not contact the supervisor merely because you cannot write that output path directly.
28
28
  - Meaningful progress or unexpected discoveries that change the plan: contact_supervisor({ reason: "progress_update", message: "UPDATE: <summary>" })
29
- - Generic intercom is lower-level plumbing/fallback only: intercom({ action: "ask", to: "{orchestratorTarget}", message: "<question>" })
30
29
 
31
- Do not use contact_supervisor or intercom for routine completion handoffs. If no coordination is needed, return a focused task result.`;
30
+ Do not use contact_supervisor for routine completion handoffs. If no coordination is needed, return a focused task result.`;
32
31
 
33
32
  export interface IntercomBridgeState {
34
33
  active: boolean;
@@ -171,7 +170,7 @@ export function resolveIntercomBridge(input: ResolveIntercomBridgeInput): Interc
171
170
  export function applyIntercomBridgeToAgent(agent: AgentConfig, bridge: IntercomBridgeState): AgentConfig {
172
171
  if (!bridge.active || !bridge.orchestratorTarget) return agent;
173
172
 
174
- const bridgeTools = ["intercom", "contact_supervisor"];
173
+ const bridgeTools = ["contact_supervisor"];
175
174
  const tools = agent.tools && agent.tools.length > 0
176
175
  ? [...agent.tools, ...bridgeTools.filter((tool) => !agent.tools?.includes(tool))]
177
176
  : agent.tools;
@@ -295,38 +295,18 @@ function hasTool(pi: ExtensionAPI, name: string): boolean {
295
295
  }
296
296
  }
297
297
 
298
- export function registerNativeSupervisorClient(pi: ExtensionAPI, options: { includeIntercomFallback?: boolean } = {}): void {
299
- if (!readChildMetadata()) return;
300
- const includeIntercomFallback = options.includeIntercomFallback !== false;
301
- if (!hasTool(pi, "contact_supervisor")) {
302
- const tool: ToolDefinition<typeof ContactSupervisorParamsSchema, Record<string, unknown>> = {
303
- name: "contact_supervisor",
304
- label: "Contact Supervisor",
305
- description: "Contact the parent/supervisor session for a blocking decision, structured interview, or progress update.",
306
- parameters: ContactSupervisorParamsSchema,
307
- execute(_id, params, signal) {
308
- return sendSupervisorRequest(params as ContactSupervisorParams, signal);
309
- },
310
- };
311
- pi.registerTool(tool);
312
- }
313
- if (includeIntercomFallback && !hasTool(pi, "intercom")) {
314
- const tool: ToolDefinition<typeof IntercomParamsSchema, Record<string, unknown>> = {
315
- name: "intercom",
316
- label: "Intercom",
317
- description: "Native supervisor-channel intercom fallback for subagents. Prefer contact_supervisor when available.",
318
- parameters: IntercomParamsSchema,
319
- async execute(_id, params, signal) {
320
- const action = (params as IntercomParams).action;
321
- if (action === "status") return { content: [{ type: "text", text: "Native supervisor channel is active." }], details: { active: true } };
322
- if (action === "list") return { content: [{ type: "text", text: "Supervisor session available through contact_supervisor." }], details: { sessions: [] } };
323
- if (action === "send") return sendSupervisorRequest({ reason: "progress_update", message: (params as IntercomParams).message ?? "" }, signal);
324
- if (action === "ask") return sendSupervisorRequest({ reason: "need_decision", message: (params as IntercomParams).message ?? "" }, signal);
325
- throw new Error("Native child intercom supports status, list, send, and ask. Use parent intercom reply from the supervisor session.");
326
- },
327
- };
328
- pi.registerTool(tool);
329
- }
298
+ export function registerNativeSupervisorClient(pi: ExtensionAPI): void {
299
+ if (!readChildMetadata() || hasTool(pi, "contact_supervisor")) return;
300
+ const tool: ToolDefinition<typeof ContactSupervisorParamsSchema, Record<string, unknown>> = {
301
+ name: "contact_supervisor",
302
+ label: "Contact Supervisor",
303
+ description: "Contact the parent/supervisor session for a blocking decision, structured interview, or progress update.",
304
+ parameters: ContactSupervisorParamsSchema,
305
+ execute(_id, params, signal) {
306
+ return sendSupervisorRequest(params as ContactSupervisorParams, signal);
307
+ },
308
+ };
309
+ pi.registerTool(tool);
330
310
  }
331
311
 
332
312
  function parseRequestFile(file: string, channelDir: string): PendingSupervisorRequest | undefined {
@@ -601,13 +581,11 @@ function publicPendingRequests(pending: Map<string, PendingSupervisorRequest>):
601
581
  }));
602
582
  }
603
583
 
604
- function buildParentIntercomTool(pending: Map<string, PendingSupervisorRequest>, state: SubagentState, name = "intercom"): ToolDefinition<typeof IntercomParamsSchema, Record<string, unknown>> {
584
+ function buildParentSupervisorTool(pending: Map<string, PendingSupervisorRequest>, state: SubagentState): ToolDefinition<typeof IntercomParamsSchema, Record<string, unknown>> {
605
585
  return {
606
- name,
607
- label: name === "intercom" ? "Intercom" : "Subagent Supervisor",
608
- description: name === "intercom"
609
- ? "Native pi-subagents supervisor channel. Use reply/pending/status to answer child subagent requests."
610
- : "Native pi-subagents supervisor channel. Use reply/pending/status to answer child subagent requests without overriding pi-intercom.",
586
+ name: NATIVE_SUPERVISOR_TOOL_NAME,
587
+ label: "Subagent Supervisor",
588
+ description: "Native pi-subagents supervisor channel. Use reply/pending/status to answer child subagent requests without overriding pi-intercom.",
611
589
  parameters: IntercomParamsSchema,
612
590
  async execute(_id, params) {
613
591
  refreshPendingRequests(pending, state, state.lastUiContext ?? undefined);
@@ -627,9 +605,9 @@ function buildParentIntercomTool(pending: Map<string, PendingSupervisorRequest>,
627
605
  return { content: [{ type: "text", text: `Replied to supervisor request ${request.id}.` }], details: { replyTo: request.id, runId: request.runId, agent: request.agent } };
628
606
  }
629
607
  if (input.action === "send" || input.action === "ask") {
630
- throw new Error("Native pi-subagents intercom currently handles supervisor replies. Child agents initiate asks with contact_supervisor.");
608
+ throw new Error("The native subagent supervisor handles replies only. Child agents initiate asks with contact_supervisor.");
631
609
  }
632
- throw new Error(`Unsupported intercom action: ${input.action}`);
610
+ throw new Error(`Unsupported supervisor action: ${input.action}`);
633
611
  },
634
612
  };
635
613
  }
@@ -647,8 +625,7 @@ export function createNativeSupervisorChannel(pi: ExtensionAPI, state: SubagentS
647
625
  let lastStaleCleanupAt = 0;
648
626
 
649
627
  const registerParentTools = (): void => {
650
- if (!hasTool(pi, NATIVE_SUPERVISOR_TOOL_NAME)) pi.registerTool(buildParentIntercomTool(pending, state, NATIVE_SUPERVISOR_TOOL_NAME));
651
- if (!hasTool(pi, "intercom")) pi.registerTool(buildParentIntercomTool(pending, state));
628
+ if (!hasTool(pi, NATIVE_SUPERVISOR_TOOL_NAME)) pi.registerTool(buildParentSupervisorTool(pending, state));
652
629
  };
653
630
 
654
631
  const cleanupStaleChannelsIfDue = (): void => {
@@ -99,7 +99,9 @@ function missionStateAction(location: MissionStoreLocation, record: MissionRecor
99
99
  function retainedResumeTarget(record: MissionRecord, retainedChildren: RetainedChild[]): RetainedChild | undefined {
100
100
  const latestRun = record.runs.at(-1);
101
101
  if (!latestRun) return undefined;
102
- return retainedChildren.find((child) => child.runId === latestRun.runId || child.parentRunId === latestRun.runId);
102
+ return retainedChildren.find((child) =>
103
+ child.resumability.state === "resumable" && (child.runId === latestRun.runId || child.parentRunId === latestRun.runId)
104
+ );
103
105
  }
104
106
 
105
107
  function nextReadyAction(location: MissionStoreLocation, record: MissionRecord, retainedChildren: RetainedChild[]): string {
@@ -3,37 +3,96 @@ import * as path from "node:path";
3
3
  import type { AsyncStatus } from "../../shared/types.ts";
4
4
 
5
5
  export const ACTIVE_RUN_INDEX_DIR = ".active-runs";
6
+ export const DEFAULT_STALE_TERMINAL_ACTIVE_MARKER_MS = 24 * 60 * 60 * 1000;
7
+
8
+ const TOOL_CALL_INDEX_DIR = "tool-calls";
6
9
 
7
10
  function indexDir(asyncDirRoot: string): string {
8
11
  return path.join(asyncDirRoot, ACTIVE_RUN_INDEX_DIR);
9
12
  }
10
13
 
14
+ function toolCallIndexDir(asyncDirRoot: string, toolCallId: string): string {
15
+ return path.join(indexDir(asyncDirRoot), TOOL_CALL_INDEX_DIR, encodeURIComponent(toolCallId));
16
+ }
17
+
18
+ function toolCallIndexPath(asyncDir: string, toolCallId: string): string {
19
+ return path.join(toolCallIndexDir(path.dirname(asyncDir), toolCallId), path.basename(asyncDir));
20
+ }
21
+
11
22
  function markerPath(asyncDir: string): string {
12
23
  return path.join(indexDir(path.dirname(asyncDir)), path.basename(asyncDir));
13
24
  }
14
25
 
26
+ function removeEmptyAncestors(start: string, stop: string): void {
27
+ let current = start;
28
+ while (current !== stop && current.startsWith(`${stop}${path.sep}`)) {
29
+ try {
30
+ fs.rmdirSync(current);
31
+ } catch {
32
+ return;
33
+ }
34
+ current = path.dirname(current);
35
+ }
36
+ }
37
+
15
38
  export function isActiveAsyncState(state: AsyncStatus["state"]): boolean {
16
39
  return state === "queued" || state === "running";
17
40
  }
18
41
 
42
+ function releaseToolCallAliases(asyncDir: string): void {
43
+ const root = path.join(indexDir(path.dirname(asyncDir)), TOOL_CALL_INDEX_DIR);
44
+ let entries: fs.Dirent[];
45
+ try {
46
+ entries = fs.readdirSync(root, { withFileTypes: true });
47
+ } catch (error) {
48
+ if ((error as NodeJS.ErrnoException).code === "ENOENT") return;
49
+ throw error;
50
+ }
51
+ for (const entry of entries) {
52
+ if (!entry.isDirectory()) continue;
53
+ const aliasMarker = path.join(root, entry.name, path.basename(asyncDir));
54
+ try {
55
+ fs.rmSync(aliasMarker, { force: true });
56
+ removeEmptyAncestors(path.dirname(aliasMarker), root);
57
+ } catch {
58
+ // Alias cleanup must not affect the authoritative active-run marker.
59
+ }
60
+ }
61
+ }
62
+
19
63
  export function releaseActiveRunIndex(asyncDir: string): void {
20
64
  try {
21
65
  fs.rmSync(markerPath(asyncDir));
22
66
  } catch (error) {
23
67
  if ((error as NodeJS.ErrnoException).code !== "ENOENT") throw error;
24
68
  }
69
+ releaseToolCallAliases(asyncDir);
25
70
  }
26
71
 
27
- export function updateActiveRunIndex(asyncDir: string, state: AsyncStatus["state"]): void {
72
+ export function updateActiveRunIndex(asyncDir: string, state: AsyncStatus["state"], toolCallId?: string): void {
28
73
  const marker = markerPath(asyncDir);
29
74
  if (isActiveAsyncState(state)) {
30
75
  fs.mkdirSync(path.dirname(marker), { recursive: true });
31
76
  fs.writeFileSync(marker, "", { flag: "a" });
77
+ if (toolCallId) {
78
+ const toolCallMarker = toolCallIndexPath(asyncDir, toolCallId);
79
+ fs.mkdirSync(path.dirname(toolCallMarker), { recursive: true });
80
+ fs.writeFileSync(toolCallMarker, "", { flag: "a" });
81
+ }
32
82
  return;
33
83
  }
34
84
  releaseActiveRunIndex(asyncDir);
35
85
  }
36
86
 
87
+ export function activeRunMarkerAgeMs(asyncDir: string, now = Date.now()): number | undefined {
88
+ try {
89
+ return Math.max(0, now - fs.statSync(markerPath(asyncDir)).mtimeMs);
90
+ } catch (error) {
91
+ if ((error as NodeJS.ErrnoException).code === "ENOENT") return undefined;
92
+ throw error;
93
+ }
94
+ }
95
+
37
96
  export function readActiveRunIndex(asyncDirRoot: string): string[] | undefined {
38
97
  try {
39
98
  return fs.readdirSync(indexDir(asyncDirRoot), { withFileTypes: true })
@@ -44,3 +103,14 @@ export function readActiveRunIndex(asyncDirRoot: string): string[] | undefined {
44
103
  throw error;
45
104
  }
46
105
  }
106
+
107
+ export function readActiveRunToolCallIndex(asyncDirRoot: string, toolCallId: string): string[] {
108
+ try {
109
+ return fs.readdirSync(toolCallIndexDir(asyncDirRoot, toolCallId), { withFileTypes: true })
110
+ .filter((entry) => entry.isFile())
111
+ .map((entry) => entry.name);
112
+ } catch (error) {
113
+ if ((error as NodeJS.ErrnoException).code === "ENOENT") return [];
114
+ throw error;
115
+ }
116
+ }
@@ -24,6 +24,7 @@ import { buildSkillInjection, normalizeSkillInput, resolveSkillsWithFallback } f
24
24
  import { buildAgentMemoryInjection } from "../../agents/agent-memory.ts";
25
25
  import { PI_CODING_AGENT_PACKAGE_ROOT_ENV, PROMPT_REDACTED, resolveChildCwd } from "../../shared/utils.ts";
26
26
  import { buildModelCandidates, resolveEffectiveSubagentModel, resolveModelCandidate, resolveSubagentModelOverride, type AvailableModelInfo, type ParentModel } from "../shared/model-fallback.ts";
27
+ import { resolveToolTimeoutMs, toolTimeoutFromEnv } from "../shared/tool-timeout.ts";
27
28
  import type { ModelScopeConfig } from "../shared/model-scope.ts";
28
29
  import { resolveEffectiveThinking } from "../../shared/model-info.ts";
29
30
  import { resolveExpectedWorktreeAgentCwd } from "../shared/worktree.ts";
@@ -58,6 +59,7 @@ import {
58
59
  resolveChildMaxSubagentDepth,
59
60
  } from "../../shared/types.ts";
60
61
  import { nestedResultsPath, nestedSummaryFromAsyncStatus, resolveInheritedNestedRouteFromEnv, resolveNestedParentAddressFromEnv, writeNestedEvent } from "../shared/nested-events.ts";
62
+ import { resultFilePath } from "./result-files.ts";
61
63
  import { appendTurnBudgetSystemPrompt, initialTurnBudgetState } from "../shared/turn-budget.ts";
62
64
  import { validateToolBudgetConfig } from "../shared/tool-budget.ts";
63
65
  import { usageBudgetState } from "../shared/usage-budget.ts";
@@ -174,6 +176,12 @@ interface AsyncChainParams {
174
176
  toolBudget?: ResolvedToolBudget;
175
177
  usageBudget?: UsageBudgetConfig;
176
178
  configToolBudget?: ResolvedToolBudget;
179
+ /** Optional per-call hard toolTimeoutMs override (highest precedence). */
180
+ callToolTimeoutMs?: number;
181
+ /** Global config.toolTimeoutMs (third precedence, after agent frontmatter). */
182
+ configToolTimeoutMs?: number;
183
+ /** PI_SUBAGENT_TOOL_TIMEOUT_MS override (lowest precedence). */
184
+ toolTimeoutMsEnv?: string | undefined;
177
185
  /** Global cap on simultaneously-running subagent tasks within the async run. */
178
186
  globalConcurrencyLimit?: number;
179
187
  capabilityCeiling?: ResolvedSubagentCapabilityCeiling;
@@ -225,10 +233,16 @@ interface AsyncSingleParams {
225
233
  acceptance?: AcceptanceInput;
226
234
  timeoutMs?: number;
227
235
  absoluteDeadlineAt?: number;
236
+ /** Optional per-call hard toolTimeoutMs override (highest precedence). */
237
+ toolTimeoutMs?: number;
228
238
  turnBudget?: { maxTurns: number; graceTurns?: number };
229
239
  toolBudget?: ResolvedToolBudget | ToolBudgetConfig;
230
240
  usageBudget?: UsageBudgetConfig;
231
241
  configToolBudget?: ResolvedToolBudget;
242
+ /** Global config.toolTimeoutMs (third precedence, after agent frontmatter). */
243
+ configToolTimeoutMs?: number;
244
+ /** PI_SUBAGENT_TOOL_TIMEOUT_MS override (lowest precedence). */
245
+ toolTimeoutMsEnv?: string | undefined;
232
246
  allowZeroToolBudget?: boolean;
233
247
  capabilityCeiling?: ResolvedSubagentCapabilityCeiling;
234
248
  runFanoutBudget?: RunFanoutBudgetDescriptor;
@@ -267,6 +281,12 @@ export interface AsyncRunnerStepBuildParams {
267
281
  validateOutputBindings?: boolean;
268
282
  toolBudget?: ResolvedToolBudget;
269
283
  configToolBudget?: ResolvedToolBudget;
284
+ /** Optional per-call hard toolTimeoutMs override from the subagent invocation. */
285
+ callToolTimeoutMs?: number;
286
+ /** Global config.toolTimeoutMs (third precedence, after agent frontmatter). */
287
+ configToolTimeoutMs?: number;
288
+ /** PI_SUBAGENT_TOOL_TIMEOUT_MS override (lowest precedence). */
289
+ toolTimeoutMsEnv?: string | undefined;
270
290
  capabilityCeiling?: ResolvedSubagentCapabilityCeiling;
271
291
  }
272
292
 
@@ -714,6 +734,13 @@ export function buildAsyncRunnerSteps(id: string, params: AsyncRunnerStepBuildPa
714
734
  const toolBudgetInput = s.toolBudget ?? params.toolBudget ?? a.toolBudget ?? params.configToolBudget;
715
735
  const resolvedToolBudget = validateToolBudgetConfig(toolBudgetInput, s.toolBudget ? "toolBudget" : a.toolBudget ? "agent.toolBudget" : "config.toolBudget");
716
736
  if (resolvedToolBudget.error) throw new AsyncStartValidationError(resolvedToolBudget.error);
737
+ const resolvedToolTimeout = resolveToolTimeoutMs({
738
+ callValue: params.callToolTimeoutMs,
739
+ agentValue: a.defaultToolTimeoutMs,
740
+ configValue: params.configToolTimeoutMs,
741
+ envValue: params.toolTimeoutMsEnv ?? toolTimeoutFromEnv(),
742
+ });
743
+ if (resolvedToolTimeout.error) throw new AsyncStartValidationError(resolvedToolTimeout.error);
717
744
  const stepCwd = resolveChildCwd(runnerCwd, s.cwd);
718
745
  const instructionCwd = behaviorCwd ?? stepCwd;
719
746
  const readExistenceCwd = behaviorCwd ? stepCwd : instructionCwd;
@@ -827,6 +854,7 @@ export function buildAsyncRunnerSteps(id: string, params: AsyncRunnerStepBuildPa
827
854
  sessionFile,
828
855
  maxSubagentDepth: resolveChildMaxSubagentDepth(maxSubagentDepth, a.maxSubagentDepth),
829
856
  timeoutMs: a.defaultTimeoutMs ?? DEFAULT_ASYNC_TIMEOUT_MS,
857
+ toolTimeoutMs: resolvedToolTimeout.toolTimeoutMs,
830
858
  waitToolEnabled: params.waitToolEnabled,
831
859
  effectiveAcceptance: resolveEffectiveAcceptance({
832
860
  explicit: s.acceptance,
@@ -1050,6 +1078,9 @@ export function executeAsyncChain(
1050
1078
  asyncDir,
1051
1079
  toolBudget: params.toolBudget,
1052
1080
  configToolBudget: params.configToolBudget,
1081
+ callToolTimeoutMs: params.callToolTimeoutMs,
1082
+ configToolTimeoutMs: params.configToolTimeoutMs,
1083
+ toolTimeoutMsEnv: params.toolTimeoutMsEnv ?? toolTimeoutFromEnv(),
1053
1084
  capabilityCeiling,
1054
1085
  });
1055
1086
  if ("error" in built) {
@@ -1086,7 +1117,7 @@ export function executeAsyncChain(
1086
1117
  {
1087
1118
  id,
1088
1119
  steps,
1089
- resultPath: inheritedNestedRoute ? nestedResultsPath(inheritedNestedRoute.rootRunId, id) : path.join(DIRS.results, `${id}.json`),
1120
+ resultPath: inheritedNestedRoute ? nestedResultsPath(inheritedNestedRoute.rootRunId, id) : resultFilePath(DIRS.results, id),
1090
1121
  cwd: runnerCwd,
1091
1122
  placeholder: "{previous}",
1092
1123
  maxOutput,
@@ -1384,17 +1415,27 @@ export function executeAsyncSingle(
1384
1415
  ? deadlineAt - Date.now()
1385
1416
  : params.timeoutMs;
1386
1417
  if (timeoutMs !== undefined && timeoutMs <= 0) return formatAsyncStartError("single", "The source run's absolute deadline expired before recovery could launch.");
1418
+ const resolvedToolTimeout = resolveToolTimeoutMs({
1419
+ callValue: params.toolTimeoutMs,
1420
+ agentValue: agentConfig.defaultToolTimeoutMs,
1421
+ configValue: params.configToolTimeoutMs,
1422
+ envValue: params.toolTimeoutMsEnv ?? toolTimeoutFromEnv(),
1423
+ });
1424
+ if (resolvedToolTimeout.error) return formatAsyncStartError("single", resolvedToolTimeout.error);
1425
+ const toolTimeoutMs = resolvedToolTimeout.toolTimeoutMs;
1387
1426
  const initialTurnBudget = params.turnBudget ? initialTurnBudgetState(params.turnBudget) : undefined;
1388
1427
  const initialUsageBudget = usageBudgetState(params.usageBudget, undefined);
1389
1428
  const resolvedSessionDir = params.sessionDir ?? (sessionRoot ? path.join(sessionRoot, `async-${id}`) : undefined);
1390
1429
  const structuredOutput = params.structuredOutputSchema
1391
1430
  ? createStructuredOutputRuntime(params.structuredOutputSchema, path.join(asyncDir, "structured-output"))
1392
1431
  : undefined;
1393
- const modelCandidates = buildModelCandidates(primaryModel, agentConfig.fallbackModels, availableModels, ctx.currentModelProvider, { scope: ctx.modelScope })
1394
- .flatMap((candidate) => {
1395
- const resolved = applyThinkingSuffix(candidate, effectiveThinking, params.thinkingOverride !== undefined);
1396
- return resolved ? [resolved] : [];
1397
- });
1432
+ const modelCandidates = externalRunner
1433
+ ? []
1434
+ : buildModelCandidates(primaryModel, agentConfig.fallbackModels, availableModels, ctx.currentModelProvider, { scope: ctx.modelScope })
1435
+ .flatMap((candidate) => {
1436
+ const resolved = applyThinkingSuffix(candidate, effectiveThinking, params.thinkingOverride !== undefined);
1437
+ return resolved ? [resolved] : [];
1438
+ });
1398
1439
  const effectiveSystemPrompt = appendTurnBudgetSystemPrompt(systemPrompt, params.turnBudget);
1399
1440
  const toolPlan = resolvePiLaunchToolPlan({
1400
1441
  tools: agentConfig.tools,
@@ -1533,7 +1574,7 @@ export function executeAsyncSingle(
1533
1574
  ],
1534
1575
  resultPath: params.parentWorkflowRunId !== undefined && params.revivalLease !== undefined
1535
1576
  ? workflowAwaitedAsyncResultPath(asyncDir)
1536
- : inheritedNestedRoute ? nestedResultsPath(inheritedNestedRoute.rootRunId, id) : path.join(DIRS.results, `${id}.json`),
1577
+ : inheritedNestedRoute ? nestedResultsPath(inheritedNestedRoute.rootRunId, id) : resultFilePath(DIRS.results, id),
1537
1578
  cwd: runnerCwd,
1538
1579
  placeholder: "{previous}",
1539
1580
  maxOutput,
@@ -1552,6 +1593,7 @@ export function executeAsyncSingle(
1552
1593
  controlConfig,
1553
1594
  timeoutMs,
1554
1595
  deadlineAt,
1596
+ toolTimeoutMs,
1555
1597
  turnBudget: params.turnBudget,
1556
1598
  toolBudget: params.toolBudget,
1557
1599
  usageBudget: params.usageBudget,
@@ -125,6 +125,7 @@ export function createAsyncJobTracker(pi: Pick<ExtensionAPI, "events">, state: S
125
125
  return {
126
126
  asyncId: run.id,
127
127
  asyncDir: run.asyncDir,
128
+ toolCallId: run.toolCallId,
128
129
  status: run.state,
129
130
  sessionId: run.sessionId,
130
131
  activityState: run.activityState,
@@ -8,6 +8,7 @@ import { intersectSubagentCapabilityCeilings, parseSubagentCapabilityCeiling, ty
8
8
  import { validateRunFanoutBudgetDescriptor } from "../shared/run-fanout-budget.ts";
9
9
  import { resolveTurnBudgetConfig } from "../shared/turn-budget.ts";
10
10
  import { reconcileAsyncRun } from "./stale-run-reconciler.ts";
11
+ import { resultFilePath } from "./result-files.ts";
11
12
 
12
13
  export interface AsyncResumeParams {
13
14
  id?: string;
@@ -172,7 +173,7 @@ function prefixedRunIds(dir: string, prefix: string, suffix = ""): string[] {
172
173
  }
173
174
 
174
175
  function exactResultPath(resultsDir: string, runId: string): string | null {
175
- const resultPath = path.join(resultsDir, `${runId}.json`);
176
+ const resultPath = resultFilePath(resultsDir, runId);
176
177
  assertInsideRoot(resultsDir, resultPath, "Async result file");
177
178
  return fs.existsSync(resultPath) ? resultPath : null;
178
179
  }
@@ -182,10 +183,7 @@ export function findAsyncRunPrefixMatches(prefix: string, asyncDirRoot: string,
182
183
  if (!requestedId) return [];
183
184
  const asyncRoot = path.resolve(asyncDirRoot);
184
185
  const resultRoot = path.resolve(resultsDir);
185
- const matchingIds = [...new Set([
186
- ...prefixedRunIds(asyncRoot, requestedId),
187
- ...prefixedRunIds(resultRoot, requestedId, ".json"),
188
- ])].sort();
186
+ const matchingIds = prefixedRunIds(asyncRoot, requestedId).sort();
189
187
  return matchingIds.map((id) => {
190
188
  const asyncDir = path.join(asyncRoot, id);
191
189
  assertInsideRoot(asyncRoot, asyncDir, "Async run directory");