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.
Files changed (42) hide show
  1. package/CHANGELOG.md +47 -19
  2. package/docs/agents.md +1 -0
  3. package/docs/configuration.md +28 -5
  4. package/docs/missions.md +4 -2
  5. package/docs/observability.md +26 -2
  6. package/package.json +1 -1
  7. package/src/agents/agents.ts +27 -12
  8. package/src/api/preflight.ts +1 -1
  9. package/src/extension/config.ts +20 -0
  10. package/src/extension/index.ts +18 -8
  11. package/src/extension/public-execution.ts +30 -4
  12. package/src/extension/schemas.ts +8 -7
  13. package/src/extension/tool-description.ts +10 -8
  14. package/src/inspectors/herdr/actions.ts +11 -2
  15. package/src/inspectors/herdr/inspector-runner.ts +16 -3
  16. package/src/intercom/intercom-bridge.ts +3 -1
  17. package/src/missions/store.ts +8 -3
  18. package/src/runs/background/active-async-capacity.ts +82 -25
  19. package/src/runs/background/async-execution.ts +24 -36
  20. package/src/runs/background/async-job-tracker.ts +4 -0
  21. package/src/runs/background/async-resume.ts +11 -1
  22. package/src/runs/background/fleet-view.ts +18 -5
  23. package/src/runs/background/resume-guidance.ts +27 -7
  24. package/src/runs/background/retained-children.ts +14 -6
  25. package/src/runs/background/run-status.ts +96 -4
  26. package/src/runs/background/subagent-runner.ts +4 -1
  27. package/src/runs/foreground/prompt-audit.ts +4 -3
  28. package/src/runs/foreground/subagent-executor.ts +219 -20
  29. package/src/runs/shared/completion-guard.ts +17 -1
  30. package/src/runs/shared/llm-intent-arbiter.ts +39 -23
  31. package/src/shared/agent-stream-options.ts +5 -0
  32. package/src/shared/artifacts.ts +2 -6
  33. package/src/shared/display-text.ts +50 -0
  34. package/src/shared/node-executable.ts +21 -0
  35. package/src/shared/types.ts +23 -4
  36. package/src/tui/fleet-transcript.ts +1 -48
  37. package/src/tui/fleet.ts +3 -1
  38. package/src/tui/render.ts +86 -39
  39. package/src/watchdog/permission-arbiter.ts +2 -1
  40. package/src/watchdog/review.ts +4 -3
  41. package/src/workflows/chat-progress.ts +8 -2
  42. package/src/workflows/scripted-workflow.ts +46 -7
@@ -1,6 +1,7 @@
1
1
  import * as fs from "node:fs";
2
2
  import * as path from "node:path";
3
3
  import type { AgentToolResult } from "@earendil-works/pi-agent-core";
4
+ import { safeTerminalText } from "../../shared/display-text.ts";
4
5
  import { formatDuration, formatModelThinking, formatTokens, shortenPath } from "../../shared/formatters.ts";
5
6
  import { formatActivityLabel } from "../../shared/status-format.ts";
6
7
  import {
@@ -196,7 +197,7 @@ function readSessionTranscriptTail(sessionFile: string, maxLines: number, truste
196
197
  try {
197
198
  const parsed = JSON.parse(line) as unknown;
198
199
  const messageLine = sessionMessageLine(parsed);
199
- if (messageLine) lines.push(messageLine);
200
+ if (messageLine) lines.push(...messageLine.split(/\r?\n/));
200
201
  } catch {
201
202
  malformed++;
202
203
  }
@@ -410,6 +411,18 @@ function appendKnownArtifacts(lines: string[], input: { outputPaths: string[]; s
410
411
  for (const artifact of artifacts) lines.push(` ${artifact}`);
411
412
  }
412
413
 
414
+ /**
415
+ * Sanitizes each assembled line independently.
416
+ *
417
+ * Sanitizing the joined response would let a single binary fragment in child
418
+ * output collapse the whole transcript, including run state, artifact paths, and
419
+ * warnings, into the binary placeholder. Per-line sanitization keeps that damage
420
+ * to the offending line.
421
+ */
422
+ function safeTranscriptLines(lines: string[]): string {
423
+ return lines.map((line) => safeTerminalText(line)).join("\n");
424
+ }
425
+
413
426
  function appendTranscriptBody(lines: string[], sourceLabel: string, sourceLines: string[], truncated: boolean): void {
414
427
  lines.push(`${sourceLabel}${truncated ? " (tail truncated)" : ""}:`);
415
428
  if (sourceLines.length === 0) {
@@ -470,7 +483,7 @@ export function formatAsyncRunTranscript(status: AsyncStatus, asyncDir: string,
470
483
  for (const warning of warnings) lines.push(` ${warning}`);
471
484
  }
472
485
  appendTranscriptBody(lines, transcriptSource, transcriptLines, truncated);
473
- return lines.join("\n");
486
+ return safeTranscriptLines(lines);
474
487
  }
475
488
 
476
489
  export function formatNestedRunTranscript(run: NestedRunSummary, options: TranscriptOptions = {}): string {
@@ -488,7 +501,7 @@ export function formatNestedRunTranscript(run: NestedRunSummary, options: Transc
488
501
  appendKnownArtifacts(lines, { outputPaths: [], sessionFile: run.sessionFile });
489
502
  if (!run.sessionFile) {
490
503
  appendTranscriptBody(lines, "Transcript tail", [], false);
491
- return lines.join("\n");
504
+ return safeTranscriptLines(lines);
492
505
  }
493
506
  const sessionTail = readSessionTranscriptTail(run.sessionFile, lineLimit, options.sessionRoots ?? []);
494
507
  if (sessionTail.warnings.length) {
@@ -496,7 +509,7 @@ export function formatNestedRunTranscript(run: NestedRunSummary, options: Transc
496
509
  for (const warning of sessionTail.warnings) lines.push(` ${warning}`);
497
510
  }
498
511
  appendTranscriptBody(lines, `Session transcript tail from ${run.sessionFile}`, sessionTail.lines, false);
499
- return lines.join("\n");
512
+ return safeTranscriptLines(lines);
500
513
  }
501
514
 
502
515
  export function formatAsyncResultTranscript(data: {
@@ -536,5 +549,5 @@ export function formatAsyncResultTranscript(data: {
536
549
  ].filter((line): line is string => Boolean(line));
537
550
  appendKnownArtifacts(lines, { outputPaths: [], sessionFile, resultPath });
538
551
  appendTranscriptBody(lines, "Result transcript tail", transcriptLines.filter((line) => line.trim()), output.split(/\r?\n/).length > lineLimit);
539
- return lines.join("\n");
552
+ return safeTranscriptLines(lines);
540
553
  }
@@ -1,6 +1,19 @@
1
1
  import * as fs from "node:fs";
2
2
  import type { AsyncRunSummary } from "./async-status.ts";
3
3
 
4
+ const INTERCOM_DETACH_ERROR = "detached for intercom coordination";
5
+
6
+ function isIntercomDetached(run: AsyncRunSummary): boolean {
7
+ return run.steps.some((step) => step.execution?.status === "detached"
8
+ || step.error?.toLowerCase().includes(INTERCOM_DETACH_ERROR) === true)
9
+ || run.error?.toLowerCase().includes(INTERCOM_DETACH_ERROR) === true;
10
+ }
11
+
12
+ function formatIntercomDetachGuidance(run: AsyncRunSummary): string | undefined {
13
+ if (!isIntercomDetached(run)) return undefined;
14
+ return `Run "${run.id}" detached for intercom coordination. Reply to the supervisor request first, then wait with subagent_wait({ id: "${run.id}" }). Use subagent({ action: "status", id: "${run.id}" }) to recover the result; do not resume or launch a replacement while it remains detached.`;
15
+ }
16
+
4
17
  export function formatAsyncReviveCommand(run: AsyncRunSummary): string | undefined {
5
18
  const step = run.steps.find((candidate) => candidate.status === "failed" && candidate.sessionFile && fs.existsSync(candidate.sessionFile));
6
19
  if (!step) {
@@ -15,19 +28,26 @@ export function formatAsyncReviveCommand(run: AsyncRunSummary): string | undefin
15
28
 
16
29
  export function formatResumeFirstFailedRunDetail(run: AsyncRunSummary): string | undefined {
17
30
  if (run.state !== "failed") return undefined;
31
+ const detachGuidance = formatIntercomDetachGuidance(run);
32
+ if (detachGuidance) return detachGuidance;
18
33
  const command = formatAsyncReviveCommand(run);
19
34
  if (!command) return undefined;
20
35
  return `Resume-first: failed run "${run.id}" has a persisted child session. Revive the original run with ${command} before reporting failure or launching a replacement. Launch a replacement only if revive fails or the user explicitly asks for one.`;
21
36
  }
22
37
 
23
38
  export function formatResumeFirstFailedRunsNote(runs: AsyncRunSummary[]): string {
24
- const resumable = runs
25
- .filter((run) => run.state === "failed")
39
+ const failedRuns = runs.filter((run) => run.state === "failed");
40
+ const detachGuidance = failedRuns
41
+ .map(formatIntercomDetachGuidance)
42
+ .filter((guidance): guidance is string => Boolean(guidance));
43
+ const resumable = failedRuns
44
+ .filter((run) => !isIntercomDetached(run))
26
45
  .map((run) => ({ run, command: formatAsyncReviveCommand(run) }))
27
46
  .filter((entry): entry is { run: AsyncRunSummary; command: string } => Boolean(entry.command));
28
- if (resumable.length === 0) return "";
29
- const guidance = resumable.length === 1
30
- ? `failed run "${resumable[0]!.run.id}" has a persisted child session. Revive the original run with ${resumable[0]!.command}`
31
- : `${resumable.length} failed runs have persisted child sessions. Inspect status and revive each original run before retrying`;
32
- return ` Resume-first: ${guidance} before reporting failure or launching a replacement. Launch a replacement only if revive fails or the user explicitly asks for one.`;
47
+ const resumeGuidance = resumable.length === 0
48
+ ? ""
49
+ : resumable.length === 1
50
+ ? ` Resume-first: failed run "${resumable[0]!.run.id}" has a persisted child session. Revive the original run with ${resumable[0]!.command} before reporting failure or launching a replacement. Launch a replacement only if revive fails or the user explicitly asks for one.`
51
+ : ` Resume-first: ${resumable.length} failed runs have persisted child sessions. Inspect status and revive each original run before reporting failure or launching a replacement. Launch a replacement only if revive fails or the user explicitly asks for one.`;
52
+ return `${detachGuidance.length > 0 ? ` ${detachGuidance.join(" ")}` : ""}${resumeGuidance}`;
33
53
  }
@@ -8,6 +8,8 @@ const MAX_TASK_SUMMARY_LENGTH = 120;
8
8
  export interface RetainedChild {
9
9
  runId: string;
10
10
  parentRunId?: string;
11
+ workflowKey?: string;
12
+ state: "complete" | "failed" | "paused";
11
13
  agent: string;
12
14
  taskSummary: string;
13
15
  completedAt: number;
@@ -33,16 +35,20 @@ function boundedTaskSummary(value: string | undefined): string {
33
35
  }
34
36
 
35
37
  export function listRetainedChildren(asyncDirRoot: string, sessionId: string): RetainedChild[] {
36
- return listAsyncRuns(asyncDirRoot, { sessionId, states: ["complete"], reconcile: false })
38
+ return listAsyncRuns(asyncDirRoot, { sessionId, states: ["complete", "failed", "paused"], reconcile: false })
37
39
  .flatMap((run) => {
38
40
  if (!run.parentWorkflowRunId || run.steps.length !== 1) return [];
41
+ if (run.state !== "complete" && run.state !== "failed" && run.state !== "paused") return [];
39
42
  const step = run.steps[0]!;
40
- if ((step.status !== "complete" && step.status !== "completed") || !retainedSessionFile(step.sessionFile ?? run.sessionFile)) return [];
41
- const completedAt = run.endedAt;
43
+ const terminalStep = step.status === "complete" || step.status === "completed" || step.status === "failed" || step.status === "paused";
44
+ if (!terminalStep || !retainedSessionFile(step.sessionFile ?? run.sessionFile)) return [];
45
+ const completedAt = run.endedAt ?? run.lastUpdate;
42
46
  if (completedAt === undefined) return [];
43
47
  return [{
44
48
  runId: run.id,
45
49
  ...(run.parentWorkflowRunId ? { parentRunId: run.parentWorkflowRunId } : {}),
50
+ ...(run.workflowKey ? { workflowKey: run.workflowKey } : {}),
51
+ state: run.state,
46
52
  agent: step.agent,
47
53
  taskSummary: boundedTaskSummary(step.description),
48
54
  completedAt,
@@ -55,13 +61,15 @@ export function listRetainedChildren(asyncDirRoot: string, sessionId: string): R
55
61
  }
56
62
 
57
63
  export function formatRetainedChildren(children: RetainedChild[]): string {
58
- if (children.length === 0) return "No completed retained children in the active parent session.";
64
+ if (children.length === 0) return "No recoverable retained children in the active parent session.";
59
65
  return [
60
- `Completed retained children (newest first, last ${MAX_RETAINED_CHILDREN}):`,
66
+ `Recoverable retained children (newest first, last ${MAX_RETAINED_CHILDREN}):`,
61
67
  ...children.flatMap((child) => [
62
- `- ${child.runId} | ${child.agent} | ${new Date(child.completedAt).toISOString()}`,
68
+ `- ${child.runId} | ${child.agent} | ${child.state} | ${new Date(child.completedAt).toISOString()}`,
69
+ ...(child.parentRunId ? [` workflow: ${child.parentRunId}${child.workflowKey ? ` (${child.workflowKey})` : ""}`] : []),
63
70
  ` task: ${child.taskSummary || "(no task summary)"}`,
64
71
  ` session: ${child.sessionPath}`,
72
+ ` resume: subagent({ action: "resume", id: "${child.runId}", message: "..." })`,
65
73
  ...(child.tokenTotals ? [` tokens: input ${child.tokenTotals.input}, output ${child.tokenTotals.output}, total ${child.tokenTotals.total}`] : []),
66
74
  ]),
67
75
  ].join("\n");
@@ -1,12 +1,14 @@
1
1
  import * as fs from "node:fs";
2
2
  import * as path from "node:path";
3
3
  import type { AgentToolResult } from "@earendil-works/pi-agent-core";
4
+ import { safeTerminalText } from "../../shared/display-text.ts";
4
5
  import { formatAsyncRunList, formatAsyncRunOutputPath, formatAsyncRunProgressLabel, listAsyncRuns } from "./async-status.ts";
5
6
  import { formatAsyncResultTranscript, formatAsyncRunTranscript, formatNestedRunTranscript, inspectSubagentFleet } from "./fleet-view.ts";
6
7
  import { formatNestedRunStatusLines } from "../shared/nested-render.ts";
7
8
  import { formatModelThinking } from "../../shared/formatters.ts";
8
9
  import { formatActivityLabel } from "../../shared/status-format.ts";
9
10
  import { DIRS, type AsyncStatus, type Details, type ForegroundResumeRun, type NestedRunSummary, type SteeringStatus, type SubagentState } from "../../shared/types.ts";
11
+ import { inspectActiveAsyncCapacityOwner, type ActiveAsyncCapacityInspection } from "./active-async-capacity.ts";
10
12
  import { readStatus } from "../../shared/utils.ts";
11
13
  import { resolveSubagentIntercomTarget } from "../../intercom/intercom-bridge.ts";
12
14
  import { resolveSubagentResultStatus } from "../../intercom/result-intercom.ts";
@@ -31,6 +33,66 @@ interface RunStatusParams {
31
33
  lines?: number;
32
34
  }
33
35
 
36
+ function formatProcessTerminal(value: AsyncStatus["processTerminal"] | undefined): string {
37
+ if (!value) return "missing";
38
+ return `${value.state}${value.reason ? ` (${value.reason})` : ""}${value.runnerProcessInstanceId ? ` · runner ${value.runnerProcessInstanceId}` : ""}`;
39
+ }
40
+
41
+ function debugProcessTerminal(asyncDir: string, status: AsyncStatus): { sidecar?: AsyncStatus["processTerminal"]; overlay?: AsyncStatus["processTerminal"] } {
42
+ const expected = { runId: status.runId, runnerProcessInstanceId: status.processTerminal?.runnerProcessInstanceId };
43
+ return {
44
+ sidecar: readProcessTerminal(asyncDir, expected),
45
+ overlay: sanitizeProcessTerminal(status.processTerminal, expected, path.join(asyncDir, "status.json")),
46
+ };
47
+ }
48
+
49
+ function formatCapacityOwner(inspect: ActiveAsyncCapacityInspection): string[] {
50
+ if (!inspect.owner) return [`Active capacity: ${inspect.release.state} — ${inspect.release.reason}`];
51
+ const owner = inspect.owner;
52
+ return [
53
+ `Active capacity: ${inspect.release.state} — ${inspect.release.reason}`,
54
+ `Capacity owner: ${inspect.relation} slot ${owner.slot}, ${owner.kind}, generation ${owner.generation}`,
55
+ `Capacity session: ${owner.ownerSessionId}`,
56
+ owner.sourceRunId ? `Capacity source run: ${owner.sourceRunId}` : undefined,
57
+ `Capacity async dir: ${owner.asyncDir}`,
58
+ owner.runnerProcessInstanceId ? `Capacity runner: ${owner.runnerProcessInstanceId}` : undefined,
59
+ owner.runnerStartedAt !== undefined ? `Capacity runner started: ${new Date(owner.runnerStartedAt).toISOString()}` : undefined,
60
+ ].filter((line): line is string => line !== undefined);
61
+ }
62
+
63
+ function formatWorkflowDebug(status: AsyncStatus): string[] {
64
+ if (status.mode !== "workflow" && !status.parentWorkflowRunId && !status.workflowKey) return [];
65
+ const lines = [
66
+ status.parentWorkflowRunId ? `Workflow parent: ${status.parentWorkflowRunId}${status.workflowKey ? ` (${status.workflowKey})` : ""}` : undefined,
67
+ status.mode === "workflow" ? `Workflow children: ${(status.steps ?? []).length}` : undefined,
68
+ ].filter((line): line is string => line !== undefined);
69
+ for (const [index, step] of (status.steps ?? []).entries()) {
70
+ lines.push(` ${index + 1}. key ${step.workflowKey ?? "n/a"} · ${step.agent} · ${step.status} · async ${step.async === undefined ? "unknown" : step.async ? "yes" : "no"}${step.runId ? ` · run ${step.runId}` : ""}`);
71
+ }
72
+ return lines;
73
+ }
74
+
75
+ function formatRunLifecycleDebug(input: { status: AsyncStatus; asyncDir: string; sidecarProcessTerminal: AsyncStatus["processTerminal"] | undefined; overlayProcessTerminal: AsyncStatus["processTerminal"] | undefined; capacity: ActiveAsyncCapacityInspection }): string {
76
+ const { status, asyncDir, sidecarProcessTerminal, overlayProcessTerminal, capacity } = input;
77
+ const lines = [
78
+ "Run lifecycle debug",
79
+ `Run: ${status.runId}`,
80
+ `Dir: ${asyncDir}`,
81
+ `Status file: ${path.join(asyncDir, "status.json")}`,
82
+ `Process terminal file: ${path.join(asyncDir, "process-terminal.json")}`,
83
+ `Session: ${status.sessionId ?? "unknown"}`,
84
+ `State: ${status.state}`,
85
+ `Mode: ${status.mode}`,
86
+ status.parentWorkflowRunId ? `Workflow parent: ${status.parentWorkflowRunId}` : undefined,
87
+ status.workflowKey ? `Workflow key: ${status.workflowKey}` : undefined,
88
+ `Status process terminal: ${formatProcessTerminal(overlayProcessTerminal)}`,
89
+ `Sidecar process terminal: ${formatProcessTerminal(sidecarProcessTerminal)}`,
90
+ ...formatCapacityOwner(capacity),
91
+ ...formatWorkflowDebug(status),
92
+ ].filter((line): line is string => line !== undefined);
93
+ return lines.join("\n");
94
+ }
95
+
34
96
  interface RunStatusDeps {
35
97
  asyncDirRoot?: string;
36
98
  resultsDir?: string;
@@ -39,6 +101,7 @@ interface RunStatusDeps {
39
101
  state?: SubagentState;
40
102
  nested?: NestedRunResolutionScope;
41
103
  sessionRoots?: string[];
104
+ activeCapacityRoot?: string;
42
105
  }
43
106
 
44
107
  function hasExistingSessionFile(value: unknown): value is string {
@@ -50,12 +113,16 @@ function formatCheckpointGuidance(runId: string | undefined, checkpoint: AsyncSt
50
113
  return `Checkpoint: ${checkpoint.name}${checkpoint.message ? ` — ${checkpoint.message}` : ""}\nApprove: subagent({ action: "approve-checkpoint", id: "${runId}" })\nReject: subagent({ action: "reject-checkpoint", id: "${runId}" })`;
51
114
  }
52
115
 
53
- function formatResumeGuidance(runId: string | undefined, children: Array<{ agent?: unknown; sessionFile?: unknown }>, fallbackSessionFile?: unknown, options: { stopped?: boolean } = {}): string {
116
+ function formatResumeGuidance(runId: string | undefined, children: Array<{ agent?: unknown; sessionFile?: unknown; runId?: unknown; workflowKey?: unknown }>, fallbackSessionFile?: unknown, options: { stopped?: boolean } = {}): string {
54
117
  if (options.stopped) return "Resume: unavailable; stopped runs are not resumable. Start a new run instead.";
55
118
  const knownChildren = children
56
119
  .map((child, index) => ({ child, index }))
57
120
  .filter(({ child }) => typeof child.agent === "string");
58
121
  if (!runId || knownChildren.length === 0) return "Resume: unavailable; no child session file was persisted.";
122
+ const workflowChildren = knownChildren.filter(({ child }) => typeof child.runId === "string" && child.runId.trim() && hasExistingSessionFile(child.sessionFile));
123
+ if (workflowChildren.length > 0) {
124
+ return workflowChildren.map(({ child }) => `Revive workflow child${typeof child.workflowKey === "string" && child.workflowKey.trim() ? ` '${child.workflowKey}'` : ""}: subagent({ action: "resume", id: "${child.runId}", message: "..." })`).join("\n");
125
+ }
59
126
  const singleSessionFile = knownChildren[0]?.child.sessionFile ?? fallbackSessionFile;
60
127
  if (children.length === 1 && knownChildren.length === 1 && hasExistingSessionFile(singleSessionFile)) {
61
128
  return `Revive: subagent({ action: "resume", id: "${runId}", message: "..." })`;
@@ -178,7 +245,7 @@ function formatRememberedForegroundTranscript(run: ForegroundResumeRun, options:
178
245
  lines.push("Result transcript tail:");
179
246
  if (outputLines.length === 0) lines.push(" (no recovered final output available yet)");
180
247
  else for (const line of outputLines) lines.push(` ${line}`);
181
- return lines.join("\n");
248
+ return lines.map((line) => safeTerminalText(line)).join("\n");
182
249
  }
183
250
 
184
251
  function formatNestedExactStatus(rootRunId: string, run: NestedRunSummary): string {
@@ -263,7 +330,9 @@ export function inspectSubagentStatus(params: RunStatusParams, deps: RunStatusDe
263
330
  let location;
264
331
  try {
265
332
  const requestedId = params.id ?? params.runId;
266
- if (!params.dir && requestedId) {
333
+ if (params.action === "debug.run") {
334
+ location = resolveAsyncRunLocation(params, asyncDirRoot, resultsDir);
335
+ } else if (!params.dir && requestedId) {
267
336
  const resolved = resolveSubagentRunId(requestedId, { asyncDirRoot, resultsDir, state: deps.state, nested: deps.nested });
268
337
  if (resolved?.kind === "foreground") {
269
338
  const run = deps.state?.foregroundRuns?.get(resolved.id);
@@ -331,6 +400,14 @@ export function inspectSubagentStatus(params: RunStatusParams, deps: RunStatusDe
331
400
  }
332
401
  const status = reconciliation.status;
333
402
  if (!status && diskStatus?.displayDismissedAt !== undefined) {
403
+ if (params.action === "debug.run") {
404
+ const { sidecar, overlay } = debugProcessTerminal(asyncDir, diskStatus);
405
+ const capacity = inspectActiveAsyncCapacityOwner({ runId: diskStatus.runId, sessionId: diskStatus.sessionId, asyncDir }, { rootDir: deps.activeCapacityRoot, liveWorkflowRunIds: new Set(deps.state?.workflowControllers?.keys() ?? []) });
406
+ return {
407
+ content: [{ type: "text", text: formatRunLifecycleDebug({ status: diskStatus, asyncDir, sidecarProcessTerminal: sidecar, overlayProcessTerminal: overlay, capacity }) }],
408
+ details: { mode: "single", results: [], ...((sidecar ?? overlay) ? { lifecycleStatus: { processTerminal: sidecar ?? overlay } } : {}) },
409
+ };
410
+ }
334
411
  if (params.view === "transcript") {
335
412
  if (currentSessionId && diskStatus.sessionId !== currentSessionId) {
336
413
  return {
@@ -350,6 +427,14 @@ export function inspectSubagentStatus(params: RunStatusParams, deps: RunStatusDe
350
427
  const logPath = path.join(asyncDir, `subagent-log-${effectiveRunId}.md`);
351
428
  const eventsPath = path.join(asyncDir, "events.jsonl");
352
429
  if (status) {
430
+ if (params.action === "debug.run") {
431
+ const { sidecar, overlay } = debugProcessTerminal(asyncDir, status);
432
+ const capacity = inspectActiveAsyncCapacityOwner({ runId: status.runId, sessionId: status.sessionId, asyncDir }, { rootDir: deps.activeCapacityRoot, liveWorkflowRunIds: new Set(deps.state?.workflowControllers?.keys() ?? []) });
433
+ return {
434
+ content: [{ type: "text", text: formatRunLifecycleDebug({ status, asyncDir, sidecarProcessTerminal: sidecar, overlayProcessTerminal: overlay, capacity }) }],
435
+ details: { mode: "single", results: [], ...((sidecar ?? overlay) ? { lifecycleStatus: { processTerminal: sidecar ?? overlay } } : {}) },
436
+ };
437
+ }
353
438
  if (params.view === "transcript") {
354
439
  if (currentSessionId && status.sessionId !== currentSessionId) {
355
440
  return {
@@ -475,9 +560,16 @@ export function inspectSubagentStatus(params: RunStatusParams, deps: RunStatusDe
475
560
  }
476
561
 
477
562
  if (resultPath) {
563
+ if (params.action === "debug.run") {
564
+ return {
565
+ content: [{ type: "text", text: "Run lifecycle debug needs an async run directory with status.json." }],
566
+ isError: true,
567
+ details: { mode: "single", results: [] },
568
+ };
569
+ }
478
570
  try {
479
571
  const raw = fs.readFileSync(resultPath, "utf-8");
480
- const data = JSON.parse(raw) as { id?: string; runId?: string; toolCallId?: string; agent?: string; success?: boolean; summary?: string; output?: string; exitCode?: number; state?: string; stopped?: boolean; timedOut?: boolean; turnBudgetExceeded?: boolean; processSignal?: string | null; sessionFile?: string; parallelHandoff?: { path?: string }; results?: Array<{ agent?: string; output?: string; summary?: string; sessionFile?: string; state?: string; success?: boolean; exitCode?: number | null; stopped?: boolean; timedOut?: boolean; turnBudgetExceeded?: boolean; interrupted?: boolean; processSignal?: string | null }> };
572
+ const data = JSON.parse(raw) as { id?: string; runId?: string; toolCallId?: string; agent?: string; success?: boolean; summary?: string; output?: string; exitCode?: number; state?: string; stopped?: boolean; timedOut?: boolean; turnBudgetExceeded?: boolean; processSignal?: string | null; sessionFile?: string; parallelHandoff?: { path?: string }; results?: Array<{ agent?: string; runId?: string; workflowKey?: string; output?: string; summary?: string; sessionFile?: string; state?: string; success?: boolean; exitCode?: number | null; stopped?: boolean; timedOut?: boolean; turnBudgetExceeded?: boolean; interrupted?: boolean; processSignal?: string | null }> };
481
573
  if (params.view === "transcript") {
482
574
  try {
483
575
  return { content: [{ type: "text", text: formatAsyncResultTranscript(data, resultPath, { index: params.index, lines: params.lines }) }], details: { mode: "single", results: [] } };
@@ -143,6 +143,8 @@ import {
143
143
  type ChildWatchdogStateSnapshot,
144
144
  } from "../../watchdog/child-status.ts";
145
145
 
146
+ const INTERCOM_DETACH_RECEIPT = "Detached for intercom coordination before task completion.";
147
+
146
148
  interface SubagentRunConfig {
147
149
  id: string;
148
150
  steps: RunnerStep[];
@@ -1693,6 +1695,7 @@ async function runSingleStep(
1693
1695
  const acceptanceFailure = effectiveAcceptance ? acceptanceFailureMessage(effectiveAcceptance) : undefined;
1694
1696
  const acceptanceCanFailRun = acceptanceFailure && effectiveAcceptance?.explicit && (finalResult?.exitCode ?? 1) === 0 && !finalResult?.interrupted && !timedOutAfterAcceptance && !stoppedAfterAcceptance && !turnBudgetExceeded && !isAgentContractV1(step.agentContract);
1695
1697
  const effectiveFinalExitCode = timedOutAfterAcceptance || stoppedAfterAcceptance || turnBudgetExceeded ? 1 : acceptanceCanFailRun ? 1 : finalResult?.exitCode ?? 1;
1698
+ const intercomDetachReceipt = finalResult?.finalOutput === INTERCOM_DETACH_RECEIPT;
1696
1699
  const effectiveFinalError = stoppedAfterAcceptance
1697
1700
  ? ctx.stopMessage ?? "Subagent stopped by user."
1698
1701
  : timedOutAfterAcceptance
@@ -1701,7 +1704,7 @@ async function runSingleStep(
1701
1704
  ? finalResult?.error ?? (turnBudget ? turnBudgetExceededMessage(turnBudget, turnBudget.turnCount) : "Subagent exceeded turn budget.")
1702
1705
  : acceptanceCanFailRun
1703
1706
  ? (finalResult?.error ? `${finalResult.error}\n${acceptanceFailure}` : acceptanceFailure)
1704
- : finalResult?.error;
1707
+ : finalResult?.error ?? (intercomDetachReceipt ? INTERCOM_DETACH_RECEIPT : undefined);
1705
1708
 
1706
1709
  const artifactErrors = artifactPaths && ctx.artifactConfig?.enabled !== false
1707
1710
  ? persistStepArtifacts({
@@ -1,7 +1,8 @@
1
1
  import { Agent, type StreamFn, type ThinkingLevel } from "@earendil-works/pi-agent-core";
2
2
  import { convertToLlm, type ExtensionContext } from "@earendil-works/pi-coding-agent";
3
3
  import { streamSimple } from "@earendil-works/pi-ai/compat";
4
- import type { Model } from "@earendil-works/pi-ai";
4
+ import type { Model, ProviderHeaders } from "@earendil-works/pi-ai";
5
+ import { agentStreamOptions } from "../../shared/agent-stream-options.ts";
5
6
  import type { ForegroundRunControl } from "../../shared/types.ts";
6
7
  export type PromptAuditView = "authored" | "runtime" | "effective";
7
8
 
@@ -42,7 +43,7 @@ function finalAssistantText(agent: Agent): string {
42
43
  return "";
43
44
  }
44
45
 
45
- async function resolveRewriteAuth(ctx: ExtensionContext, model: RegistryModel): Promise<{ apiKey?: string; headers?: Record<string, string>; env?: Record<string, string> }> {
46
+ async function resolveRewriteAuth(ctx: ExtensionContext, model: RegistryModel): Promise<{ apiKey?: string; headers?: ProviderHeaders; env?: Record<string, string> }> {
46
47
  const auth = await ctx.modelRegistry.getApiKeyAndHeaders(model);
47
48
  if (auth.ok === false) throw new Error(`Prompt redo model auth failed for ${fullModelId(model)}: ${auth.error}`);
48
49
  return {
@@ -91,7 +92,7 @@ export async function rewritePromptWithGuidance(input: {
91
92
  tools: [],
92
93
  },
93
94
  convertToLlm,
94
- streamFunction: streamFn,
95
+ ...agentStreamOptions(streamFn),
95
96
  getApiKey: (providerName) => providerName === model.provider ? auth.apiKey : undefined,
96
97
  toolExecution: "sequential",
97
98
  });