pi-subagents 0.21.1 → 0.21.2

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 (73) hide show
  1. package/CHANGELOG.md +19 -0
  2. package/README.md +35 -16
  3. package/agents/context-builder.md +6 -3
  4. package/package.json +3 -4
  5. package/prompts/parallel-context-build.md +53 -0
  6. package/prompts/parallel-handoff-plan.md +59 -0
  7. package/prompts/parallel-review.md +4 -0
  8. package/skills/pi-subagents/SKILL.md +70 -4
  9. package/{agent-management.ts → src/agents/agent-management.ts} +1 -1
  10. package/{agents.ts → src/agents/agents.ts} +1 -1
  11. package/{doctor.ts → src/extension/doctor.ts} +5 -5
  12. package/{index.ts → src/extension/index.ts} +18 -17
  13. package/{schemas.ts → src/extension/schemas.ts} +25 -39
  14. package/{intercom-bridge.ts → src/intercom/intercom-bridge.ts} +2 -2
  15. package/{result-intercom.ts → src/intercom/result-intercom.ts} +33 -5
  16. package/{agent-manager-chain-detail.ts → src/manager-ui/agent-manager-chain-detail.ts} +3 -3
  17. package/{agent-manager-detail.ts → src/manager-ui/agent-manager-detail.ts} +7 -7
  18. package/{agent-manager-edit.ts → src/manager-ui/agent-manager-edit.ts} +4 -4
  19. package/{agent-manager-list.ts → src/manager-ui/agent-manager-list.ts} +2 -2
  20. package/{agent-manager-parallel.ts → src/manager-ui/agent-manager-parallel.ts} +3 -3
  21. package/{agent-manager.ts → src/manager-ui/agent-manager.ts} +9 -9
  22. package/{async-execution.ts → src/runs/background/async-execution.ts} +11 -11
  23. package/{async-job-tracker.ts → src/runs/background/async-job-tracker.ts} +29 -6
  24. package/src/runs/background/async-resume.ts +305 -0
  25. package/{async-status.ts → src/runs/background/async-status.ts} +14 -12
  26. package/{notify.ts → src/runs/background/notify.ts} +1 -1
  27. package/{result-watcher.ts → src/runs/background/result-watcher.ts} +3 -3
  28. package/{run-status.ts → src/runs/background/run-status.ts} +63 -28
  29. package/src/runs/background/stale-run-reconciler.ts +275 -0
  30. package/{subagent-runner.ts → src/runs/background/subagent-runner.ts} +38 -58
  31. package/{chain-clarify.ts → src/runs/foreground/chain-clarify.ts} +7 -7
  32. package/{chain-execution.ts → src/runs/foreground/chain-execution.ts} +10 -10
  33. package/{execution.ts → src/runs/foreground/execution.ts} +24 -28
  34. package/{subagent-executor.ts → src/runs/foreground/subagent-executor.ts} +155 -24
  35. package/{long-running-guard.ts → src/runs/shared/long-running-guard.ts} +3 -3
  36. package/{model-fallback.ts → src/runs/shared/model-fallback.ts} +1 -1
  37. package/{subagent-control.ts → src/runs/shared/subagent-control.ts} +4 -8
  38. package/src/shared/atomic-json.ts +16 -0
  39. package/{settings.ts → src/shared/settings.ts} +21 -14
  40. package/{types.ts → src/shared/types.ts} +5 -2
  41. package/{utils.ts → src/shared/utils.ts} +1 -15
  42. package/{slash-bridge.ts → src/slash/slash-bridge.ts} +2 -2
  43. package/{slash-commands.ts → src/slash/slash-commands.ts} +7 -7
  44. package/{slash-live-state.ts → src/slash/slash-live-state.ts} +2 -2
  45. package/{render.ts → src/tui/render.ts} +3 -3
  46. package/{subagents-status.ts → src/tui/subagents-status.ts} +34 -14
  47. /package/{agent-scope.ts → src/agents/agent-scope.ts} +0 -0
  48. /package/{agent-selection.ts → src/agents/agent-selection.ts} +0 -0
  49. /package/{agent-serializer.ts → src/agents/agent-serializer.ts} +0 -0
  50. /package/{agent-templates.ts → src/agents/agent-templates.ts} +0 -0
  51. /package/{chain-serializer.ts → src/agents/chain-serializer.ts} +0 -0
  52. /package/{frontmatter.ts → src/agents/frontmatter.ts} +0 -0
  53. /package/{skills.ts → src/agents/skills.ts} +0 -0
  54. /package/{completion-dedupe.ts → src/runs/background/completion-dedupe.ts} +0 -0
  55. /package/{top-level-async.ts → src/runs/background/top-level-async.ts} +0 -0
  56. /package/{completion-guard.ts → src/runs/shared/completion-guard.ts} +0 -0
  57. /package/{parallel-utils.ts → src/runs/shared/parallel-utils.ts} +0 -0
  58. /package/{pi-args.ts → src/runs/shared/pi-args.ts} +0 -0
  59. /package/{pi-spawn.ts → src/runs/shared/pi-spawn.ts} +0 -0
  60. /package/{run-history.ts → src/runs/shared/run-history.ts} +0 -0
  61. /package/{single-output.ts → src/runs/shared/single-output.ts} +0 -0
  62. /package/{subagent-prompt-runtime.ts → src/runs/shared/subagent-prompt-runtime.ts} +0 -0
  63. /package/{worktree.ts → src/runs/shared/worktree.ts} +0 -0
  64. /package/{artifacts.ts → src/shared/artifacts.ts} +0 -0
  65. /package/{file-coalescer.ts → src/shared/file-coalescer.ts} +0 -0
  66. /package/{fork-context.ts → src/shared/fork-context.ts} +0 -0
  67. /package/{formatters.ts → src/shared/formatters.ts} +0 -0
  68. /package/{jsonl-writer.ts → src/shared/jsonl-writer.ts} +0 -0
  69. /package/{post-exit-stdio-guard.ts → src/shared/post-exit-stdio-guard.ts} +0 -0
  70. /package/{session-tokens.ts → src/shared/session-tokens.ts} +0 -0
  71. /package/{prompt-template-bridge.ts → src/slash/prompt-template-bridge.ts} +0 -0
  72. /package/{render-helpers.ts → src/tui/render-helpers.ts} +0 -0
  73. /package/{text-editor.ts → src/tui/text-editor.ts} +0 -0
@@ -3,9 +3,9 @@
3
3
  */
4
4
 
5
5
  import { Type } from "typebox";
6
+ import { SUBAGENT_ACTIONS } from "../shared/types.ts";
6
7
 
7
8
  const SkillOverride = Type.Unsafe({
8
- type: ["string", "array", "boolean"],
9
9
  anyOf: [
10
10
  { type: "array", items: { type: "string" } },
11
11
  { type: "boolean" },
@@ -15,12 +15,14 @@ const SkillOverride = Type.Unsafe({
15
15
  });
16
16
 
17
17
  const OutputOverride = Type.Unsafe({
18
- type: ["string", "boolean"],
18
+ anyOf: [
19
+ { type: "string" },
20
+ { type: "boolean" },
21
+ ],
19
22
  description: "Output filename/path (string), or false to disable file output",
20
23
  });
21
24
 
22
25
  const ReadsOverride = Type.Unsafe({
23
- type: ["array", "boolean"],
24
26
  anyOf: [
25
27
  { type: "array", items: { type: "string" } },
26
28
  { type: "boolean" },
@@ -40,20 +42,6 @@ const TaskItem = Type.Object({
40
42
  skill: Type.Optional(SkillOverride),
41
43
  });
42
44
 
43
- // Sequential chain step (single agent)
44
- const SequentialStepSchema = Type.Object({
45
- agent: Type.String(),
46
- task: Type.Optional(Type.String({
47
- description: "Task template with variables: {task}=original request, {previous}=prior step's text response, {chain_dir}=shared folder. Required for first step, defaults to '{previous}' for subsequent steps."
48
- })),
49
- cwd: Type.Optional(Type.String()),
50
- output: Type.Optional(OutputOverride),
51
- reads: Type.Optional(ReadsOverride),
52
- progress: Type.Optional(Type.Boolean({ description: "Enable progress.md tracking in {chain_dir}" })),
53
- skill: Type.Optional(SkillOverride),
54
- model: Type.Optional(Type.String({ description: "Override model for this step" })),
55
- });
56
-
57
45
  // Parallel task item (within a parallel step)
58
46
  const ParallelTaskSchema = Type.Object({
59
47
  agent: Type.String(),
@@ -67,17 +55,7 @@ const ParallelTaskSchema = Type.Object({
67
55
  model: Type.Optional(Type.String({ description: "Override model for this task" })),
68
56
  });
69
57
 
70
- // Parallel chain step (multiple agents running concurrently)
71
- const ParallelStepSchema = Type.Object({
72
- parallel: Type.Array(ParallelTaskSchema, { minItems: 1, description: "Tasks to run in parallel" }),
73
- concurrency: Type.Optional(Type.Number({ description: "Max concurrent tasks (default: 4)" })),
74
- failFast: Type.Optional(Type.Boolean({ description: "Stop on first failure (default: false)" })),
75
- worktree: Type.Optional(Type.Boolean({
76
- description: "Create isolated git worktrees for each parallel task."
77
- })),
78
- });
79
-
80
- // Flattened so providers that reject anyOf/oneOf can still accept either sequential or parallel steps.
58
+ // Flattened so chain steps do not need an object-shape anyOf/oneOf union.
81
59
  const ChainItem = Type.Object({
82
60
  agent: Type.Optional(Type.String({ description: "Sequential step agent name" })),
83
61
  task: Type.Optional(Type.String({
@@ -100,9 +78,9 @@ const ChainItem = Type.Object({
100
78
  const ControlOverrides = Type.Object({
101
79
  enabled: Type.Optional(Type.Boolean({ description: "Enable/disable subagent control attention tracking for this run" })),
102
80
  needsAttentionAfterMs: Type.Optional(Type.Integer({ minimum: 1, description: "No-observed-activity window before a run needs attention" })),
103
- activeNoticeAfterMs: Type.Optional(Type.Integer({ minimum: 1, description: "Active-long-running notice threshold by elapsed ms (default: 300000)" })),
104
- activeNoticeAfterTurns: Type.Optional(Type.Integer({ minimum: 1, description: "Active-long-running notice threshold by assistant turns (default: 15)" })),
105
- activeNoticeAfterTokens: Type.Optional(Type.Integer({ minimum: 1, description: "Active-long-running notice threshold by total tokens (default: 150000)" })),
81
+ activeNoticeAfterMs: Type.Optional(Type.Integer({ minimum: 1, description: "Active-long-running notice threshold by elapsed ms (default: 240000)" })),
82
+ activeNoticeAfterTurns: Type.Optional(Type.Integer({ minimum: 1, description: "Optional active-long-running notice threshold by assistant turns (disabled by default)" })),
83
+ activeNoticeAfterTokens: Type.Optional(Type.Integer({ minimum: 1, description: "Optional active-long-running notice threshold by total tokens (disabled by default)" })),
106
84
  failedToolAttemptsBeforeAttention: Type.Optional(Type.Integer({ minimum: 1, description: "Consecutive mutating-tool failures before escalating to needs_attention (default: 3)" })),
107
85
  notifyOn: Type.Optional(Type.Array(Type.String({ enum: ["active_long_running", "needs_attention"] }), {
108
86
  description: "Control event types that should notify the parent/orchestrator. Defaults to active_long_running and needs_attention.",
@@ -117,26 +95,31 @@ export const SubagentParams = Type.Object({
117
95
  task: Type.Optional(Type.String({ description: "Task (SINGLE mode, optional for self-contained agents)" })),
118
96
  // Management action (when present, tool operates in management mode)
119
97
  action: Type.Optional(Type.String({
120
- description: "Action: 'list', 'get', 'create', 'update', 'delete', 'status', 'interrupt', or 'doctor' diagnostics. Omit for execution mode."
98
+ enum: [...SUBAGENT_ACTIONS],
99
+ description: "Management/control action. Omit for execution mode."
121
100
  })),
122
101
  id: Type.Optional(Type.String({
123
- description: "Run id or prefix for action='status' or action='interrupt'."
102
+ description: "Run id or prefix for action='status', action='interrupt', or action='resume'."
124
103
  })),
125
104
  runId: Type.Optional(Type.String({
126
- description: "Target run ID for action='interrupt'. Defaults to the most recently active controllable run in this session. Prefer id for new calls."
105
+ description: "Target run ID for action='interrupt' or action='resume'. Defaults to the most recently active controllable run for interrupt. Prefer id for new calls."
127
106
  })),
128
107
  dir: Type.Optional(Type.String({
129
- description: "Async run directory for action='status'."
108
+ description: "Async run directory for action='status' or action='resume'."
130
109
  })),
110
+ index: Type.Optional(Type.Integer({ minimum: 0, description: "Zero-based child index for actions that target a specific child." })),
111
+ message: Type.Optional(Type.String({ description: "Follow-up message for action='resume'." })),
131
112
  // Chain identifier for management (can't reuse 'chain' — that's the execution array)
132
113
  chainName: Type.Optional(Type.String({
133
114
  description: "Chain name for get/update/delete management actions"
134
115
  })),
135
116
  // Agent/chain configuration for create/update (nested to avoid conflicts with execution fields)
136
117
  config: Type.Optional(Type.Unsafe({
137
- type: ["object", "string"],
138
- additionalProperties: true,
139
- description: "Agent or chain config for create/update. Agent: name, description, scope ('user'|'project', default 'user'), systemPrompt, systemPromptMode, inheritProjectContext, inheritSkills, defaultContext ('fresh'|'fork'), model, tools (comma-separated), extensions (comma-separated), skills (comma-separated), thinking, output, reads, progress, maxSubagentDepth. Chain: name, description, scope, steps (array of {agent, task?, output?, reads?, model?, skills?, progress?}). Presence of 'steps' creates a chain instead of an agent. String values must be valid JSON."
118
+ anyOf: [
119
+ { type: "object", additionalProperties: true },
120
+ { type: "string" },
121
+ ],
122
+ description: "Agent or chain config for create/update. Agent: name, description, scope ('user'|'project', default 'user'), systemPrompt, systemPromptMode, inheritProjectContext, inheritSkills, defaultContext ('fresh'|'fork'), model, tools (comma-separated), extensions (comma-separated), skills (comma-separated), thinking, output, reads, progress, maxSubagentDepth. Chain: name, description, scope, steps (array of {agent, task?, output?, reads?, model?, skill?, progress?}). Presence of 'steps' creates a chain instead of an agent. String values must be valid JSON."
140
123
  })),
141
124
  tasks: Type.Optional(Type.Array(TaskItem, { description: "PARALLEL mode: [{agent, task, count?, output?, reads?, progress?}, ...]" })),
142
125
  concurrency: Type.Optional(Type.Integer({ minimum: 1, description: "Top-level PARALLEL mode only: max concurrent tasks. Defaults to config.parallel.concurrency or 4." })),
@@ -165,7 +148,10 @@ export const SubagentParams = Type.Object({
165
148
  control: Type.Optional(ControlOverrides),
166
149
  // Solo agent overrides
167
150
  output: Type.Optional(Type.Unsafe({
168
- type: ["string", "boolean"],
151
+ anyOf: [
152
+ { type: "string" },
153
+ { type: "boolean" },
154
+ ],
169
155
  description: "Output file for single agent (string), or false to disable. Relative paths resolve against cwd.",
170
156
  })),
171
157
  skill: Type.Optional(SkillOverride),
@@ -1,8 +1,8 @@
1
1
  import * as fs from "node:fs";
2
2
  import * as os from "node:os";
3
3
  import * as path from "node:path";
4
- import type { AgentConfig } from "./agents.ts";
5
- import type { ExtensionConfig, IntercomBridgeConfig, IntercomBridgeMode } from "./types.ts";
4
+ import type { AgentConfig } from "../agents/agents.ts";
5
+ import type { ExtensionConfig, IntercomBridgeConfig, IntercomBridgeMode } from "../shared/types.ts";
6
6
 
7
7
  function defaultIntercomExtensionDir(): string {
8
8
  return path.join(os.homedir(), ".pi", "agent", "extensions", "pi-intercom");
@@ -8,7 +8,7 @@ import {
8
8
  type SubagentResultStatus,
9
9
  SUBAGENT_RESULT_INTERCOM_DELIVERY_EVENT,
10
10
  SUBAGENT_RESULT_INTERCOM_EVENT,
11
- } from "./types.ts";
11
+ } from "../shared/types.ts";
12
12
 
13
13
  export function resolveSubagentResultStatus(input: {
14
14
  exitCode?: number;
@@ -69,10 +69,24 @@ interface GroupedResultIntercomMessageInput {
69
69
  chainSteps?: number;
70
70
  }
71
71
 
72
+ function asyncResumeGuidance(input: {
73
+ source: "foreground" | "async";
74
+ children: SubagentResultIntercomChild[];
75
+ asyncId?: string;
76
+ }): string | undefined {
77
+ if (input.source !== "async" || !input.asyncId) return undefined;
78
+ if (input.children.length === 1 && typeof input.children[0]?.sessionPath === "string") {
79
+ return `Revive: subagent({ action: "resume", id: "${input.asyncId}", message: "..." })`;
80
+ }
81
+ if (input.children.length > 1) return "Resume: unsupported for multi-child async runs until per-child session files are persisted.";
82
+ return "Resume: unavailable; no single child session file was persisted.";
83
+ }
84
+
72
85
  function formatSubagentResultIntercomMessage(input: {
73
86
  runId: string;
74
87
  mode: "single" | "parallel" | "chain";
75
88
  status: SubagentResultStatus;
89
+ source: "foreground" | "async";
76
90
  children: SubagentResultIntercomChild[];
77
91
  asyncId?: string;
78
92
  asyncDir?: string;
@@ -92,16 +106,20 @@ function formatSubagentResultIntercomMessage(input: {
92
106
  }
93
107
  if (input.asyncId) lines.push(`Async id: ${input.asyncId}`);
94
108
  if (input.asyncDir) lines.push(`Async dir: ${input.asyncDir}`);
109
+ const resumeGuidance = asyncResumeGuidance(input);
110
+ if (resumeGuidance) lines.push(resumeGuidance);
95
111
  if (input.children.some((child) => child.intercomTarget)) {
96
112
  lines.push("");
97
- lines.push("Intercom targets below identify child sessions used while they were running; completed child sessions may no longer be reachable. Inspect artifacts or session logs for follow-up.");
113
+ lines.push(input.source === "async"
114
+ ? "Previous intercom targets below identify child sessions used while they were running. Inspect artifacts or session logs if resume is unavailable."
115
+ : "Intercom targets below identify child sessions used while they were running; completed child sessions may no longer be reachable. Inspect artifacts or session logs for follow-up.");
98
116
  }
99
117
 
100
118
  for (let index = 0; index < input.children.length; index++) {
101
119
  const child = input.children[index]!;
102
120
  lines.push("");
103
121
  lines.push(`${index + 1}. ${child.agent} — ${child.status}`);
104
- if (child.intercomTarget) lines.push(`Run intercom target: ${child.intercomTarget}`);
122
+ if (child.intercomTarget) lines.push(`${input.source === "async" ? "Previous intercom target" : "Run intercom target"}: ${child.intercomTarget}`);
105
123
  if (child.artifactPath) lines.push(`Output artifact: ${child.artifactPath}`);
106
124
  if (child.sessionPath) lines.push(`Session: ${child.sessionPath}`);
107
125
  lines.push("Summary:");
@@ -144,9 +162,19 @@ export async function deliverSubagentResultIntercomEvent(
144
162
  events: IntercomEventBus,
145
163
  payload: SubagentResultIntercomPayload,
146
164
  timeoutMs = 500,
165
+ ): Promise<boolean> {
166
+ return deliverSubagentIntercomMessageEvent(events, payload.to, payload.message, timeoutMs, payload);
167
+ }
168
+
169
+ export async function deliverSubagentIntercomMessageEvent(
170
+ events: IntercomEventBus,
171
+ to: string,
172
+ message: string,
173
+ timeoutMs = 500,
174
+ extra: Record<string, unknown> = {},
147
175
  ): Promise<boolean> {
148
176
  if (typeof events.on !== "function" || typeof events.emit !== "function") return false;
149
- const requestId = payload.requestId ?? randomUUID();
177
+ const requestId = typeof extra.requestId === "string" ? extra.requestId : randomUUID();
150
178
  return new Promise((resolve) => {
151
179
  let settled = false;
152
180
  let unsubscribe: (() => void) | undefined;
@@ -166,7 +194,7 @@ export async function deliverSubagentResultIntercomEvent(
166
194
  });
167
195
  timer = setTimeout(() => finish(false), timeoutMs);
168
196
  try {
169
- events.emit(SUBAGENT_RESULT_INTERCOM_EVENT, { ...payload, requestId });
197
+ events.emit(SUBAGENT_RESULT_INTERCOM_EVENT, { ...extra, to, message, requestId });
170
198
  } catch {
171
199
  finish(false);
172
200
  }
@@ -1,8 +1,8 @@
1
1
  import type { Theme } from "@mariozechner/pi-coding-agent";
2
2
  import { matchesKey, truncateToWidth } from "@mariozechner/pi-tui";
3
- import type { ChainConfig, ChainStepConfig } from "./agents.ts";
4
- import { row, renderFooter, renderHeader, formatPath, formatScrollInfo } from "./render-helpers.ts";
5
- import { isParallelStep, type ChainStep } from "./settings.ts";
3
+ import type { ChainConfig, ChainStepConfig } from "../agents/agents.ts";
4
+ import { row, renderFooter, renderHeader, formatPath, formatScrollInfo } from "../tui/render-helpers.ts";
5
+ import { isParallelStep, type ChainStep } from "../shared/settings.ts";
6
6
 
7
7
  export interface ChainDetailState {
8
8
  scrollOffset: number;
@@ -1,12 +1,12 @@
1
1
  import type { Theme } from "@mariozechner/pi-coding-agent";
2
2
  import { matchesKey, truncateToWidth } from "@mariozechner/pi-tui";
3
- import type { AgentConfig } from "./agents.ts";
4
- import { formatDuration } from "./formatters.ts";
5
- import type { RunEntry } from "./run-history.ts";
6
- import { buildSkillInjection, resolveSkills } from "./skills.ts";
7
- import { ensureCursorVisible, getCursorDisplayPos, renderEditor, wrapText } from "./text-editor.ts";
8
- import type { TextEditorState } from "./text-editor.ts";
9
- import { pad, row, renderHeader, renderFooter, formatPath, formatScrollInfo } from "./render-helpers.ts";
3
+ import type { AgentConfig } from "../agents/agents.ts";
4
+ import { formatDuration } from "../shared/formatters.ts";
5
+ import type { RunEntry } from "../runs/shared/run-history.ts";
6
+ import { buildSkillInjection, resolveSkills } from "../agents/skills.ts";
7
+ import { ensureCursorVisible, getCursorDisplayPos, renderEditor, wrapText } from "../tui/text-editor.ts";
8
+ import type { TextEditorState } from "../tui/text-editor.ts";
9
+ import { pad, row, renderHeader, renderFooter, formatPath, formatScrollInfo } from "../tui/render-helpers.ts";
10
10
 
11
11
  export interface DetailState {
12
12
  resolved: boolean;
@@ -1,9 +1,9 @@
1
1
  import type { Theme } from "@mariozechner/pi-coding-agent";
2
2
  import { matchesKey, truncateToWidth } from "@mariozechner/pi-tui";
3
- import { defaultSystemPromptMode, type AgentConfig, type AgentDefaultContext, type BuiltinAgentOverrideBase } from "./agents.ts";
4
- import { createEditorState, ensureCursorVisible, getCursorDisplayPos, handleEditorInput, renderEditor, wrapText } from "./text-editor.ts";
5
- import type { TextEditorState } from "./text-editor.ts";
6
- import { pad, row, renderHeader, renderFooter, formatScrollInfo } from "./render-helpers.ts";
3
+ import { defaultSystemPromptMode, type AgentConfig, type AgentDefaultContext, type BuiltinAgentOverrideBase } from "../agents/agents.ts";
4
+ import { createEditorState, ensureCursorVisible, getCursorDisplayPos, handleEditorInput, renderEditor, wrapText } from "../tui/text-editor.ts";
5
+ import type { TextEditorState } from "../tui/text-editor.ts";
6
+ import { pad, row, renderHeader, renderFooter, formatScrollInfo } from "../tui/render-helpers.ts";
7
7
 
8
8
  export interface ModelInfo { provider: string; id: string; fullId: string; }
9
9
  export interface SkillInfo { name: string; source: string; description?: string; }
@@ -1,7 +1,7 @@
1
1
  import type { Theme } from "@mariozechner/pi-coding-agent";
2
- import type { AgentSource } from "./agents.ts";
2
+ import type { AgentSource } from "../agents/agents.ts";
3
3
  import { matchesKey, truncateToWidth, visibleWidth } from "@mariozechner/pi-tui";
4
- import { pad, row, renderHeader, renderFooter, fuzzyFilter, formatScrollInfo } from "./render-helpers.ts";
4
+ import { pad, row, renderHeader, renderFooter, fuzzyFilter, formatScrollInfo } from "../tui/render-helpers.ts";
5
5
 
6
6
  export interface ListAgent {
7
7
  id: string;
@@ -1,8 +1,8 @@
1
1
  import type { Theme } from "@mariozechner/pi-coding-agent";
2
2
  import { matchesKey, truncateToWidth, visibleWidth } from "@mariozechner/pi-tui";
3
- import type { TextEditorState } from "./text-editor.ts";
4
- import { createEditorState, handleEditorInput, renderEditor, wrapText, getCursorDisplayPos, ensureCursorVisible } from "./text-editor.ts";
5
- import { pad, row, renderHeader, renderFooter, fuzzyFilter } from "./render-helpers.ts";
3
+ import type { TextEditorState } from "../tui/text-editor.ts";
4
+ import { createEditorState, handleEditorInput, renderEditor, wrapText, getCursorDisplayPos, ensureCursorVisible } from "../tui/text-editor.ts";
5
+ import { pad, row, renderHeader, renderFooter, fuzzyFilter } from "../tui/render-helpers.ts";
6
6
 
7
7
  interface ParallelSlot {
8
8
  agentName: string;
@@ -14,20 +14,20 @@ import {
14
14
  type AgentConfig,
15
15
  type BuiltinAgentOverrideBase,
16
16
  type ChainConfig,
17
- } from "./agents.ts";
18
- import { serializeAgent } from "./agent-serializer.ts";
19
- import { TEMPLATE_ITEMS, type AgentTemplate, type TemplateItem } from "./agent-templates.ts";
20
- import { parseChain, serializeChain } from "./chain-serializer.ts";
17
+ } from "../agents/agents.ts";
18
+ import { serializeAgent } from "../agents/agent-serializer.ts";
19
+ import { TEMPLATE_ITEMS, type AgentTemplate, type TemplateItem } from "../agents/agent-templates.ts";
20
+ import { parseChain, serializeChain } from "../agents/chain-serializer.ts";
21
21
  import { DEFAULT_AGENT_MANAGER_NEW_SHORTCUT, renderList, handleListInput, type ListAgent, type ListShortcuts, type ListState, type ListAction } from "./agent-manager-list.ts";
22
22
  import { createParallelState, handleParallelInput, renderParallel, formatParallelTitle, type ParallelState, type AgentOption } from "./agent-manager-parallel.ts";
23
23
  import { renderDetail, handleDetailInput, renderTaskInput, type DetailState, type DetailAction, type LaunchToggleState } from "./agent-manager-detail.ts";
24
24
  import { renderChainDetail, handleChainDetailInput, type ChainDetailAction, type ChainDetailState } from "./agent-manager-chain-detail.ts";
25
25
  import { createEditState, handleEditInput, renderEdit, type EditField, type EditScreen, type EditState, type ModelInfo, type SkillInfo } from "./agent-manager-edit.ts";
26
- import { createEditorState, ensureCursorVisible, getCursorDisplayPos, handleEditorInput, renderEditor, wrapText } from "./text-editor.ts";
27
- import type { TextEditorState } from "./text-editor.ts";
28
- import { loadRunsForAgent } from "./run-history.ts";
29
- import { pad, row, renderHeader, renderFooter } from "./render-helpers.ts";
30
- import { isParallelStep, type ChainStep } from "./settings.ts";
26
+ import { createEditorState, ensureCursorVisible, getCursorDisplayPos, handleEditorInput, renderEditor, wrapText } from "../tui/text-editor.ts";
27
+ import type { TextEditorState } from "../tui/text-editor.ts";
28
+ import { loadRunsForAgent } from "../runs/shared/run-history.ts";
29
+ import { pad, row, renderHeader, renderFooter } from "../tui/render-helpers.ts";
30
+ import { isParallelStep, type ChainStep } from "../shared/settings.ts";
31
31
 
32
32
  export type ManagerResult =
33
33
  | { action: "launch"; agent: string; task: string; skipClarify?: boolean; fork?: boolean; background?: boolean }
@@ -9,16 +9,16 @@ import * as path from "node:path";
9
9
  import { fileURLToPath } from "node:url";
10
10
  import { createRequire } from "node:module";
11
11
  import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
12
- import type { AgentConfig } from "./agents.ts";
13
- import { applyThinkingSuffix } from "./pi-args.ts";
14
- import { injectSingleOutputInstruction, resolveSingleOutputPath } from "./single-output.ts";
15
- import { buildChainInstructions, isParallelStep, resolveStepBehavior, writeInitialProgressFile, type ChainStep, type ResolvedStepBehavior, type SequentialStep, type StepOverrides } from "./settings.ts";
16
- import type { RunnerStep } from "./parallel-utils.ts";
17
- import { resolvePiPackageRoot } from "./pi-spawn.ts";
18
- import { buildSkillInjection, normalizeSkillInput, resolveSkillsWithFallback } from "./skills.ts";
19
- import { resolveChildCwd } from "./utils.ts";
20
- import { buildModelCandidates, resolveModelCandidate, type AvailableModelInfo } from "./model-fallback.ts";
21
- import { resolveExpectedWorktreeAgentCwd } from "./worktree.ts";
12
+ import type { AgentConfig } from "../../agents/agents.ts";
13
+ import { applyThinkingSuffix } from "../shared/pi-args.ts";
14
+ import { injectSingleOutputInstruction, resolveSingleOutputPath } from "../shared/single-output.ts";
15
+ import { buildChainInstructions, isParallelStep, resolveStepBehavior, writeInitialProgressFile, type ChainStep, type ResolvedStepBehavior, type SequentialStep, type StepOverrides } from "../../shared/settings.ts";
16
+ import type { RunnerStep } from "../shared/parallel-utils.ts";
17
+ import { resolvePiPackageRoot } from "../shared/pi-spawn.ts";
18
+ import { buildSkillInjection, normalizeSkillInput, resolveSkillsWithFallback } from "../../agents/skills.ts";
19
+ import { resolveChildCwd } from "../../shared/utils.ts";
20
+ import { buildModelCandidates, resolveModelCandidate, type AvailableModelInfo } from "../shared/model-fallback.ts";
21
+ import { resolveExpectedWorktreeAgentCwd } from "../shared/worktree.ts";
22
22
  import {
23
23
  type ArtifactConfig,
24
24
  type Details,
@@ -30,7 +30,7 @@ import {
30
30
  TEMP_ROOT_DIR,
31
31
  getAsyncConfigPath,
32
32
  resolveChildMaxSubagentDepth,
33
- } from "./types.ts";
33
+ } from "../../shared/types.ts";
34
34
 
35
35
  const require = createRequire(import.meta.url);
36
36
  const piPackageRoot = resolvePiPackageRoot();
@@ -1,8 +1,8 @@
1
1
  import type { ExtensionAPI, ExtensionContext } from "@mariozechner/pi-coding-agent";
2
2
  import * as fs from "node:fs";
3
3
  import * as path from "node:path";
4
- import { renderWidget } from "./render.ts";
5
- import { formatControlNoticeMessage } from "./subagent-control.ts";
4
+ import { renderWidget } from "../../tui/render.ts";
5
+ import { formatControlNoticeMessage } from "../shared/subagent-control.ts";
6
6
  import {
7
7
  type AsyncJobState,
8
8
  type AsyncParallelGroupStatus,
@@ -10,10 +10,12 @@ import {
10
10
  type ControlEvent,
11
11
  type SubagentState,
12
12
  POLL_INTERVAL_MS,
13
+ RESULTS_DIR,
13
14
  SUBAGENT_CONTROL_EVENT,
14
15
  SUBAGENT_CONTROL_INTERCOM_EVENT,
15
- } from "./types.ts";
16
- import { readStatus } from "./utils.ts";
16
+ } from "../../shared/types.ts";
17
+ import { readStatus } from "../../shared/utils.ts";
18
+ import { reconcileAsyncRun } from "./stale-run-reconciler.ts";
17
19
 
18
20
 
19
21
  function isValidParallelGroup(group: AsyncParallelGroupStatus, stepCount: number, chainStepCount: number): boolean {
@@ -35,6 +37,9 @@ function normalizeParallelGroups(groups: AsyncParallelGroupStatus[] | undefined,
35
37
  interface AsyncJobTrackerOptions {
36
38
  completionRetentionMs?: number;
37
39
  pollIntervalMs?: number;
40
+ resultsDir?: string;
41
+ kill?: (pid: number, signal?: NodeJS.Signals | 0) => boolean;
42
+ now?: () => number;
38
43
  }
39
44
 
40
45
  export function createAsyncJobTracker(pi: Pick<ExtensionAPI, "events">, state: SubagentState, asyncDirRoot: string, options: AsyncJobTrackerOptions = {}): {
@@ -45,6 +50,7 @@ export function createAsyncJobTracker(pi: Pick<ExtensionAPI, "events">, state: S
45
50
  } {
46
51
  const completionRetentionMs = options.completionRetentionMs ?? 10000;
47
52
  const pollIntervalMs = options.pollIntervalMs ?? POLL_INTERVAL_MS;
53
+ const resultsDir = options.resultsDir ?? RESULTS_DIR;
48
54
  const rerenderWidget = (ctx: ExtensionContext, jobs = Array.from(state.asyncJobs.values())) => {
49
55
  renderWidget(ctx, jobs);
50
56
  ctx.ui.requestRender?.();
@@ -132,7 +138,20 @@ export function createAsyncJobTracker(pi: Pick<ExtensionAPI, "events">, state: S
132
138
  for (const job of state.asyncJobs.values()) {
133
139
  try {
134
140
  emitNewControlEvents(job);
135
- const status = readStatus(job.asyncDir);
141
+ const reconciliation = reconcileAsyncRun(job.asyncDir, {
142
+ resultsDir,
143
+ kill: options.kill,
144
+ now: options.now,
145
+ startedRun: {
146
+ runId: job.asyncId,
147
+ pid: job.pid,
148
+ mode: job.mode,
149
+ agents: job.agents,
150
+ startedAt: job.startedAt,
151
+ sessionFile: job.sessionFile,
152
+ },
153
+ });
154
+ const status = reconciliation.status ?? readStatus(job.asyncDir);
136
155
  if (status) {
137
156
  const previousStatus = job.status;
138
157
  job.status = status.state;
@@ -167,7 +186,7 @@ export function createAsyncJobTracker(pi: Pick<ExtensionAPI, "events">, state: S
167
186
  job.outputFile = status.outputFile ?? job.outputFile;
168
187
  job.totalTokens = status.totalTokens ?? job.totalTokens;
169
188
  job.sessionFile = status.sessionFile ?? job.sessionFile;
170
- if ((job.status === "complete" || job.status === "failed" || job.status === "paused") && previousStatus !== job.status) {
189
+ if ((job.status === "complete" || job.status === "failed" || job.status === "paused") && (previousStatus !== job.status || !state.cleanupTimers.has(job.asyncId))) {
171
190
  scheduleCleanup(job.asyncId);
172
191
  }
173
192
  continue;
@@ -178,6 +197,9 @@ export function createAsyncJobTracker(pi: Pick<ExtensionAPI, "events">, state: S
178
197
  console.error(`Failed to read async status for '${job.asyncDir}':`, error);
179
198
  job.status = "failed";
180
199
  job.updatedAt = Date.now();
200
+ if (!state.cleanupTimers.has(job.asyncId)) {
201
+ scheduleCleanup(job.asyncId);
202
+ }
181
203
  }
182
204
  }
183
205
 
@@ -202,6 +224,7 @@ export function createAsyncJobTracker(pi: Pick<ExtensionAPI, "events">, state: S
202
224
  asyncId: info.id,
203
225
  asyncDir,
204
226
  status: "queued",
227
+ pid: typeof info.pid === "number" ? info.pid : undefined,
205
228
  mode: info.chain ? "chain" : "single",
206
229
  agents,
207
230
  stepsTotal: firstGroupCount ?? agents?.length,