pi-subagents 0.64.0 → 0.65.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (106) hide show
  1. package/CHANGELOG.md +65 -0
  2. package/README.md +2 -2
  3. package/agents/reviewer.md +1 -1
  4. package/agents/scout.md +1 -1
  5. package/docs/agents.md +19 -17
  6. package/docs/configuration.md +23 -15
  7. package/docs/extension-api.md +18 -6
  8. package/docs/missions.md +2 -0
  9. package/docs/observability.md +10 -12
  10. package/docs/tool-reference.md +15 -8
  11. package/docs/watchdog.md +1 -1
  12. package/docs/workflows.md +13 -3
  13. package/package.json +5 -4
  14. package/runner-server-preload.mjs +13 -0
  15. package/skills/pi-subagents/SKILL.md +2 -1
  16. package/skills/pi-subagents/references/constraints-and-recipes.md +1 -1
  17. package/skills/pi-subagents/references/execution-controls.md +7 -3
  18. package/skills/pi-subagents/references/multi-lane-orchestration.md +2 -0
  19. package/src/agents/agent-management.ts +47 -15
  20. package/src/api/capability-ceiling.ts +0 -1
  21. package/src/api/{pi-args.ts → child-tool-plan.ts} +1 -1
  22. package/src/api/preflight.ts +7 -4
  23. package/src/extension/config.ts +4 -2
  24. package/src/extension/doctor.ts +2 -10
  25. package/src/extension/fanout-child.ts +9 -11
  26. package/src/extension/index.ts +58 -7
  27. package/src/extension/public-execution.ts +14 -0
  28. package/src/extension/rpc.ts +3 -2
  29. package/src/extension/schemas.ts +3 -2
  30. package/src/extension/tool-description.ts +13 -8
  31. package/src/integrations/pi-web-session-liveness.ts +73 -0
  32. package/src/intercom/native-supervisor-channel.ts +159 -95
  33. package/src/intercom/supervisor-ui.ts +244 -0
  34. package/src/missions/workflow-state.ts +37 -16
  35. package/src/runs/background/async-execution.ts +51 -25
  36. package/src/runs/background/async-job-tracker.ts +11 -0
  37. package/src/runs/background/async-resume.ts +14 -3
  38. package/src/runs/background/async-retention.ts +9 -0
  39. package/src/runs/background/control-channel.ts +2 -204
  40. package/src/runs/background/notify.ts +2 -0
  41. package/src/runs/background/process-terminal.ts +1 -1
  42. package/src/runs/background/retained-nested-route-tracker.ts +96 -0
  43. package/src/runs/background/run-child-session.ts +614 -0
  44. package/src/runs/background/run-status.ts +0 -1
  45. package/src/runs/background/runner-aliases.ts +152 -0
  46. package/src/runs/background/runner-child-sessions.ts +31 -0
  47. package/src/runs/background/scheduled-runs.ts +18 -4
  48. package/src/runs/background/subagent-runner.ts +203 -901
  49. package/src/runs/foreground/async-steering-action.ts +1 -17
  50. package/src/runs/foreground/execution.ts +204 -378
  51. package/src/runs/foreground/foreground-control.ts +4 -0
  52. package/src/runs/foreground/foreground-history.ts +3 -1
  53. package/src/runs/foreground/prompt-audit.ts +9 -5
  54. package/src/runs/foreground/subagent-executor.ts +161 -91
  55. package/src/runs/foreground/workflow-foreground-steering.ts +24 -98
  56. package/src/runs/shared/abort-recovery.ts +3 -3
  57. package/src/runs/shared/acceptance.ts +14 -1
  58. package/src/runs/shared/capability-ceiling.ts +1 -2
  59. package/src/runs/shared/child-hooks.ts +25 -0
  60. package/src/runs/shared/child-identity.ts +13 -2
  61. package/src/runs/shared/child-launch.ts +314 -0
  62. package/src/runs/shared/child-lifecycle.ts +25 -0
  63. package/src/runs/shared/child-runtime-config.ts +126 -0
  64. package/src/runs/shared/child-session.ts +337 -0
  65. package/src/runs/shared/child-tool-plan.ts +530 -0
  66. package/src/runs/shared/claude-code-adapter.ts +5 -1
  67. package/src/runs/shared/completion-guard.ts +1 -1
  68. package/src/runs/shared/external-cli-preflight.ts +16 -0
  69. package/src/runs/shared/llm-intent-arbiter.ts +20 -11
  70. package/src/runs/shared/mcp-direct-tool-allowlist.ts +5 -4
  71. package/src/runs/shared/model-exclusions.ts +84 -15
  72. package/src/runs/shared/model-fallback.ts +78 -6
  73. package/src/runs/shared/nested-events.ts +32 -48
  74. package/src/runs/shared/nested-path.ts +0 -14
  75. package/src/runs/shared/orca-progress-tabs.ts +12 -7
  76. package/src/runs/shared/parallel-utils.ts +1 -2
  77. package/src/runs/shared/permissions.ts +0 -13
  78. package/src/runs/shared/process-signal.ts +4 -1
  79. package/src/runs/shared/run-fanout-budget.ts +0 -13
  80. package/src/runs/shared/runtime-acknowledged-extensions.ts +0 -27
  81. package/src/runs/shared/structured-output.ts +17 -4
  82. package/src/runs/shared/subagent-control.ts +2 -0
  83. package/src/runs/shared/subagent-prompt-runtime.ts +87 -384
  84. package/src/runs/shared/tool-availability.ts +18 -62
  85. package/src/runs/shared/tool-budget.ts +0 -14
  86. package/src/runs/shared/worktree-cleanup-plan.ts +31 -9
  87. package/src/runs/shared/worktree.ts +192 -40
  88. package/src/shared/child-session-name.ts +1 -1
  89. package/src/shared/jsonl-writer.ts +11 -0
  90. package/src/shared/model-response-aliases.ts +13 -0
  91. package/src/shared/thinking-ceiling.ts +0 -6
  92. package/src/shared/types.ts +63 -28
  93. package/src/shared/utils.ts +6 -4
  94. package/src/shared/watch-strategy.ts +2 -0
  95. package/src/slash/slash-commands.ts +0 -6
  96. package/src/tui/fleet-status.ts +1 -1
  97. package/src/tui/fleet.ts +0 -1
  98. package/src/tui/render.ts +254 -41
  99. package/src/watchdog/child-status.ts +0 -1
  100. package/src/watchdog/register-child.ts +12 -12
  101. package/src/workflows/scripted-workflow.ts +80 -7
  102. package/src/workflows/workflow-checklist.ts +4 -3
  103. package/src/runs/shared/child-protocol.ts +0 -415
  104. package/src/runs/shared/pi-args.ts +0 -1059
  105. package/src/runs/shared/subagent-startup-retry.ts +0 -116
  106. package/src/shared/post-exit-stdio-guard.ts +0 -85
@@ -4,17 +4,17 @@ import * as path from "node:path";
4
4
  import type { AgentToolResult } from "@earendil-works/pi-agent-core";
5
5
  import type { ExtensionAPI, ExtensionContext, ToolDefinition } from "@earendil-works/pi-coding-agent";
6
6
  import { Type } from "typebox";
7
- import {
8
- SUBAGENT_CHILD_AGENT_ENV,
9
- SUBAGENT_CHILD_INDEX_ENV,
10
- SUBAGENT_ORCHESTRATOR_SESSION_ID_ENV,
11
- SUBAGENT_ORCHESTRATOR_TARGET_ENV,
12
- SUBAGENT_RUN_ID_ENV,
13
- SUBAGENT_SUPERVISOR_CHANNEL_DIR_ENV,
14
- } from "../runs/shared/pi-args.ts";
15
- import { INTERCOM_DETACH_REQUEST_EVENT, POLL_INTERVAL_MS, TEMP_ROOT_DIR, type IntercomEventBus, type SubagentState } from "../shared/types.ts";
7
+ import type { ChildSupervisorMetadata } from "../runs/shared/child-runtime-config.ts";
8
+ import { INTERCOM_DETACH_REQUEST_EVENT, POLL_INTERVAL_MS, TEMP_ROOT_DIR, type ControlEvent, type IntercomEventBus, type SubagentState } from "../shared/types.ts";
16
9
  import { writeAtomicJson } from "../shared/atomic-json.ts";
17
10
  import { shouldUseNativeFsWatch } from "../shared/watch-strategy.ts";
11
+ import {
12
+ SUPERVISOR_REQUEST_MESSAGE_TYPE,
13
+ SUPERVISOR_REPLY_ENTRY_TYPE,
14
+ supervisorReplyHint,
15
+ type SupervisorReason,
16
+ type SupervisorReplyEntryData,
17
+ } from "./supervisor-ui.ts";
18
18
 
19
19
  const SUPERVISOR_CHANNEL_ROOT = path.join(TEMP_ROOT_DIR, "supervisor-channels");
20
20
  const REQUESTS_DIR = "requests";
@@ -26,8 +26,10 @@ const CHANNEL_POLL_MS = Math.min(POLL_INTERVAL_MS, 500);
26
26
  const CHANNEL_SAFETY_POLL_MS = 5000;
27
27
  const STALE_EMPTY_CHANNEL_AGE_MS = 60 * 1000;
28
28
  const STALE_EMPTY_CHANNEL_CLEANUP_INTERVAL_MS = 60 * 1000;
29
+ const MAX_SUPERVISOR_REQUEST_CORRELATIONS = 256;
30
+ const SUPERVISOR_REQUEST_CORRELATION_RETENTION_MS = 10 * 60 * 1000;
29
31
 
30
- type SupervisorReason = "need_decision" | "interview_request" | "progress_update";
32
+ export type SupervisorRequestState = "pending" | "resolved" | "unknown";
31
33
 
32
34
  interface SupervisorRequest {
33
35
  type: "subagent.supervisor.request";
@@ -42,6 +44,7 @@ interface SupervisorRequest {
42
44
  runId: string;
43
45
  agent: string;
44
46
  childIndex: number;
47
+ toolCallId?: string;
45
48
  childTarget?: string;
46
49
  interview?: unknown;
47
50
  }
@@ -51,6 +54,12 @@ interface PendingSupervisorRequest extends SupervisorRequest {
51
54
  requestFile: string;
52
55
  }
53
56
 
57
+ interface SupervisorRequestCorrelation {
58
+ request: PendingSupervisorRequest;
59
+ state: Exclude<SupervisorRequestState, "unknown">;
60
+ updatedAt: number;
61
+ }
62
+
54
63
  interface SupervisorReply {
55
64
  type: "subagent.supervisor.reply";
56
65
  requestId: string;
@@ -113,71 +122,12 @@ function replyPath(channelDir: string, requestId: string): string {
113
122
  return path.join(channelDir, REPLIES_DIR, `${safeSegment(requestId)}.json`);
114
123
  }
115
124
 
116
- function readTextEnv(name: string): string | undefined {
117
- const value = process.env[name]?.trim();
118
- return value ? value : undefined;
119
- }
120
-
121
- function readChildMetadata(): {
122
- channelDir: string;
123
- runId: string;
124
- agent: string;
125
- childIndex: number;
126
- orchestratorTarget?: string;
127
- orchestratorSessionId?: string;
128
- childTarget?: string;
129
- } | undefined {
130
- const channelDir = readTextEnv(SUBAGENT_SUPERVISOR_CHANNEL_DIR_ENV);
131
- const runId = readTextEnv(SUBAGENT_RUN_ID_ENV);
132
- const agent = readTextEnv(SUBAGENT_CHILD_AGENT_ENV);
133
- const rawIndex = readTextEnv(SUBAGENT_CHILD_INDEX_ENV);
134
- const orchestratorSessionId = readTextEnv(SUBAGENT_ORCHESTRATOR_SESSION_ID_ENV);
135
- if (!channelDir || !runId || !agent || !orchestratorSessionId || rawIndex === undefined || !/^\d+$/.test(rawIndex)) return undefined;
136
- return {
137
- channelDir,
138
- runId,
139
- agent,
140
- childIndex: Number(rawIndex),
141
- orchestratorTarget: readTextEnv(SUBAGENT_ORCHESTRATOR_TARGET_ENV),
142
- orchestratorSessionId,
143
- childTarget: readTextEnv("PI_SUBAGENT_INTERCOM_SESSION_NAME"),
144
- };
145
- }
146
-
147
125
  function reasonHeading(reason: SupervisorReason): string {
148
126
  if (reason === "interview_request") return "Subagent requests a structured supervisor interview.";
149
127
  if (reason === "progress_update") return "Subagent progress update.";
150
128
  return "Subagent needs a supervisor decision.";
151
129
  }
152
130
 
153
- function formatChildMessage(input: {
154
- reason: SupervisorReason;
155
- message?: string;
156
- interview?: unknown;
157
- runId: string;
158
- agent: string;
159
- childIndex: number;
160
- childTarget?: string;
161
- }): string {
162
- const lines = [
163
- reasonHeading(input.reason),
164
- `Run: ${input.runId}`,
165
- `Agent: ${input.agent}`,
166
- `Child index: ${input.childIndex}`,
167
- ];
168
- if (input.childTarget) lines.push(`Child intercom target: ${input.childTarget}`);
169
- lines.push("");
170
- if (input.message?.trim()) lines.push(input.message.trim());
171
- if (input.reason === "interview_request") {
172
- lines.push(
173
- "",
174
- "Structured response requested. Reply with JSON, optionally fenced in ```json, matching the requested interview shape.",
175
- );
176
- if (input.interview !== undefined) lines.push(JSON.stringify(input.interview, null, "\t"));
177
- }
178
- return lines.join("\n").trimEnd();
179
- }
180
-
181
131
  function parseStructuredReply(message: string): { value?: unknown; error?: string } {
182
132
  const trimmed = message.trim();
183
133
  const fenced = trimmed.match(/^```(?:json)?\s*([\s\S]*?)\s*```$/i)?.[1]?.trim();
@@ -231,11 +181,9 @@ async function waitForReply(channelDir: string, requestId: string, deadline: num
231
181
  throw new Error("Timed out waiting for supervisor reply.");
232
182
  }
233
183
 
234
- async function sendSupervisorRequest(params: ContactSupervisorParams, signal?: AbortSignal): Promise<AgentToolResult<Record<string, unknown>>> {
235
- const metadata = readChildMetadata();
236
- if (!metadata) throw new Error("Native supervisor channel is not available for this subagent.");
237
- if (params.reason !== "progress_update" && !params.message?.trim() && params.reason !== "interview_request") {
238
- throw new Error("message is required for supervisor decisions.");
184
+ async function sendSupervisorRequest(params: ContactSupervisorParams, metadata: ChildSupervisorMetadata, signal?: AbortSignal, toolCallId?: string): Promise<AgentToolResult<Record<string, unknown>>> {
185
+ if (!params.message?.trim() && params.reason !== "interview_request") {
186
+ throw new Error("message is required for supervisor decisions and progress updates.");
239
187
  }
240
188
  ensureSupervisorChannelDir(metadata.channelDir);
241
189
  const requestId = randomUUID();
@@ -243,7 +191,8 @@ async function sendSupervisorRequest(params: ContactSupervisorParams, signal?: A
243
191
  const createdAt = Date.now();
244
192
  const replyDeadline = createdAt + askTimeoutMs();
245
193
  const expiresAt = expectsReply ? replyDeadline : undefined;
246
- const message = formatChildMessage({ ...metadata, reason: params.reason, message: params.message, interview: params.interview });
194
+ const message = params.message?.trim() ?? "";
195
+ const requestToolCallId = typeof toolCallId === "string" && toolCallId.length > 0 ? toolCallId : undefined;
247
196
  const request: SupervisorRequest = {
248
197
  type: "subagent.supervisor.request",
249
198
  id: requestId,
@@ -257,6 +206,7 @@ async function sendSupervisorRequest(params: ContactSupervisorParams, signal?: A
257
206
  runId: metadata.runId,
258
207
  agent: metadata.agent,
259
208
  childIndex: metadata.childIndex,
209
+ ...(requestToolCallId ? { toolCallId: requestToolCallId } : {}),
260
210
  ...(metadata.childTarget ? { childTarget: metadata.childTarget } : {}),
261
211
  ...(params.interview !== undefined ? { interview: params.interview } : {}),
262
212
  };
@@ -297,15 +247,19 @@ function hasTool(pi: ExtensionAPI, name: string): boolean {
297
247
  }
298
248
  }
299
249
 
300
- export function registerNativeSupervisorClient(pi: ExtensionAPI): void {
301
- if (!readChildMetadata() || hasTool(pi, "contact_supervisor")) return;
250
+ /**
251
+ * Register the child-side `contact_supervisor` tool. The host passes the
252
+ * channel metadata in the child runtime config.
253
+ */
254
+ export function registerNativeSupervisorClient(pi: ExtensionAPI, metadata: ChildSupervisorMetadata | undefined): void {
255
+ if (!metadata || hasTool(pi, "contact_supervisor")) return;
302
256
  const tool: ToolDefinition<typeof ContactSupervisorParamsSchema, Record<string, unknown>> = {
303
257
  name: "contact_supervisor",
304
258
  label: "Contact Supervisor",
305
259
  description: "Contact the parent/supervisor session for a blocking decision, structured interview, or progress update.",
306
260
  parameters: ContactSupervisorParamsSchema,
307
- execute(_id, params, signal) {
308
- return sendSupervisorRequest(params as ContactSupervisorParams, signal);
261
+ execute(id, params, signal) {
262
+ return sendSupervisorRequest(params as ContactSupervisorParams, metadata, signal, id);
309
263
  },
310
264
  };
311
265
  pi.registerTool(tool);
@@ -317,9 +271,14 @@ function parseRequestFile(file: string, channelDir: string): PendingSupervisorRe
317
271
  if (parsed.type !== "subagent.supervisor.request") return undefined;
318
272
  if (typeof parsed.id !== "string" || !parsed.id) return undefined;
319
273
  if (parsed.reason !== "need_decision" && parsed.reason !== "interview_request" && parsed.reason !== "progress_update") return undefined;
320
- if (typeof parsed.message !== "string" || !parsed.message) return undefined;
274
+ if (typeof parsed.message !== "string" || (!parsed.message.trim() && parsed.reason !== "interview_request")) return undefined;
321
275
  if (typeof parsed.runId !== "string" || typeof parsed.agent !== "string" || typeof parsed.childIndex !== "number") return undefined;
322
- return { ...parsed as SupervisorRequest, channelDir, requestFile: file };
276
+ return {
277
+ ...parsed as SupervisorRequest,
278
+ ...(typeof parsed.toolCallId === "string" && parsed.toolCallId.length > 0 ? { toolCallId: parsed.toolCallId } : { toolCallId: undefined }),
279
+ channelDir,
280
+ requestFile: file,
281
+ };
323
282
  } catch {
324
283
  return undefined;
325
284
  }
@@ -483,6 +442,7 @@ function removeRequestFile(file: string): void {
483
442
  }
484
443
 
485
444
  type SupervisorRequestLifecycle = "pending" | "resolved" | "expired" | "inactive" | "missing" | "wrong-session";
445
+ type SupervisorRequestLifecycleObserver = (request: PendingSupervisorRequest, lifecycle: SupervisorRequestLifecycle) => void;
486
446
 
487
447
  function requestExpiresAt(request: SupervisorRequest, now: number): number {
488
448
  const expiresAt = (request as { expiresAt?: unknown }).expiresAt;
@@ -515,30 +475,44 @@ function cleanupRequestLifecycle(request: PendingSupervisorRequest, lifecycle: S
515
475
  if (lifecycle === "resolved" || lifecycle === "expired" || lifecycle === "inactive") removeRequestFile(request.requestFile);
516
476
  }
517
477
 
518
- function refreshPendingRequests(pending: Map<string, PendingSupervisorRequest>, state: SubagentState, ctx: ExtensionContext | undefined): void {
478
+ function refreshPendingRequests(pending: Map<string, PendingSupervisorRequest>, state: SubagentState, ctx: ExtensionContext | undefined, onLifecycle: SupervisorRequestLifecycleObserver): void {
519
479
  const now = Date.now();
520
480
  for (const request of pending.values()) {
521
481
  const lifecycle = requestLifecycle(request, state, ctx, now);
522
482
  if (lifecycle === "pending") continue;
523
483
  pending.delete(request.id);
484
+ onLifecycle(request, lifecycle);
524
485
  cleanupRequestLifecycle(request, lifecycle);
525
486
  }
526
487
  }
527
488
 
528
489
  function formatPendingLine(request: PendingSupervisorRequest): string {
529
- const replyHint = request.expectsReply ? ` Reply: ${NATIVE_SUPERVISOR_TOOL_NAME}({ action: "reply", replyTo: "${request.id}", message: "..." })` : "";
490
+ const replyHint = request.expectsReply ? ` Reply: ${supervisorReplyHint(request.id)}` : "";
530
491
  return `- ${request.id}: ${request.agent} [${request.runId}#${request.childIndex}] ${request.reason}.${replyHint}`;
531
492
  }
532
493
 
533
494
  function requestVisibleText(request: PendingSupervisorRequest): string {
534
- const lines = [request.message];
535
- if (request.expectsReply) {
536
- lines.push("", `Reply with: ${NATIVE_SUPERVISOR_TOOL_NAME}({ action: "reply", replyTo: "${request.id}", message: "..." })`);
495
+ const lines = [
496
+ reasonHeading(request.reason),
497
+ `Run: ${request.runId}`,
498
+ `Agent: ${request.agent}`,
499
+ `Child index: ${request.childIndex}`,
500
+ ];
501
+ if (request.childTarget) lines.push(`Child intercom target: ${request.childTarget}`);
502
+ lines.push("");
503
+ if (request.message) lines.push(request.message);
504
+ if (request.reason === "interview_request") {
505
+ lines.push(
506
+ "",
507
+ "Structured response requested. Reply with JSON, optionally fenced in ```json, matching the requested interview shape.",
508
+ );
509
+ if (request.interview !== undefined) lines.push(JSON.stringify(request.interview, null, "\t"));
537
510
  }
538
- return lines.join("\n");
511
+ if (request.expectsReply) lines.push("", `Reply with: ${supervisorReplyHint(request.id)}`);
512
+ return lines.join("\n").trimEnd();
539
513
  }
540
514
 
541
- function writeReply(request: PendingSupervisorRequest, message: string): void {
515
+ function writeReply(request: PendingSupervisorRequest, message: string): SupervisorReply {
542
516
  if (!message.trim()) throw new Error("message is required for supervisor replies.");
543
517
  const reply: SupervisorReply = {
544
518
  type: "subagent.supervisor.reply",
@@ -548,6 +522,29 @@ function writeReply(request: PendingSupervisorRequest, message: string): void {
548
522
  };
549
523
  writeAtomicJson(replyPath(request.channelDir, request.id), reply);
550
524
  removeRequestFile(request.requestFile);
525
+ return reply;
526
+ }
527
+
528
+ function appendSupervisorReplyEntry(pi: ExtensionAPI, request: PendingSupervisorRequest, reply: SupervisorReply): void {
529
+ const appendEntry = (pi as unknown as {
530
+ appendEntry?: (customType: string, data?: SupervisorReplyEntryData) => void;
531
+ }).appendEntry;
532
+ if (typeof appendEntry !== "function") return;
533
+ try {
534
+ appendEntry.call(pi, SUPERVISOR_REPLY_ENTRY_TYPE, {
535
+ requestId: request.id,
536
+ reason: request.reason,
537
+ runId: request.runId,
538
+ agent: request.agent,
539
+ childIndex: request.childIndex,
540
+ ...(request.childTarget ? { childTarget: request.childTarget } : {}),
541
+ message: reply.message,
542
+ createdAt: reply.createdAt,
543
+ });
544
+ } catch (error) {
545
+ // The reply file is authoritative; a UI journal failure must not undo a delivered reply.
546
+ console.error("Failed to journal native supervisor reply:", error);
547
+ }
551
548
  }
552
549
 
553
550
  function resolvePendingRequest(pending: Map<string, PendingSupervisorRequest>, params: IntercomParams): PendingSupervisorRequest {
@@ -583,14 +580,14 @@ function publicPendingRequests(pending: Map<string, PendingSupervisorRequest>):
583
580
  }));
584
581
  }
585
582
 
586
- function buildParentSupervisorTool(pending: Map<string, PendingSupervisorRequest>, state: SubagentState): ToolDefinition<typeof IntercomParamsSchema, Record<string, unknown>> {
583
+ function buildParentSupervisorTool(pi: ExtensionAPI, pending: Map<string, PendingSupervisorRequest>, state: SubagentState, onLifecycle: SupervisorRequestLifecycleObserver): ToolDefinition<typeof IntercomParamsSchema, Record<string, unknown>> {
587
584
  return {
588
585
  name: NATIVE_SUPERVISOR_TOOL_NAME,
589
586
  label: "Subagent Supervisor",
590
587
  description: "Native pi-subagents supervisor channel. Use reply/pending/status to answer child subagent requests without overriding pi-intercom.",
591
588
  parameters: IntercomParamsSchema,
592
589
  async execute(_id, params) {
593
- refreshPendingRequests(pending, state, state.lastUiContext ?? undefined);
590
+ refreshPendingRequests(pending, state, state.lastUiContext ?? undefined, onLifecycle);
594
591
  const input = params as IntercomParams;
595
592
  if (input.action === "status") {
596
593
  return { content: [{ type: "text", text: `Native supervisor channel active. Pending replies: ${pending.size}.` }], details: { active: true, pending: pending.size, root: SUPERVISOR_CHANNEL_ROOT } };
@@ -601,7 +598,9 @@ function buildParentSupervisorTool(pending: Map<string, PendingSupervisorRequest
601
598
  }
602
599
  if (input.action === "reply") {
603
600
  const request = resolvePendingRequest(pending, input);
604
- writeReply(request, input.message ?? "");
601
+ const reply = writeReply(request, input.message ?? "");
602
+ appendSupervisorReplyEntry(pi, request, reply);
603
+ onLifecycle(request, "resolved");
605
604
  pending.delete(request.id);
606
605
  clearForegroundSupervisorAttention(request, pending, state);
607
606
  return { content: [{ type: "text", text: `Replied to supervisor request ${request.id}.` }], details: { replyTo: request.id, runId: request.runId, agent: request.agent } };
@@ -614,10 +613,66 @@ function buildParentSupervisorTool(pending: Map<string, PendingSupervisorRequest
614
613
  };
615
614
  }
616
615
 
617
- export function createNativeSupervisorChannel(pi: ExtensionAPI, state: SubagentState, deps: NativeSupervisorChannelDeps = {}): { start: () => void; activateTransport: () => void; dispose: () => void; pending: Map<string, PendingSupervisorRequest> } {
616
+ export function createNativeSupervisorChannel(pi: ExtensionAPI, state: SubagentState, deps: NativeSupervisorChannelDeps = {}): {
617
+ start: () => void;
618
+ activateTransport: () => void;
619
+ dispose: () => void;
620
+ pending: Map<string, PendingSupervisorRequest>;
621
+ getSupervisorRequestState: (event: ControlEvent) => SupervisorRequestState;
622
+ } {
618
623
  const watch = deps.watch ?? fs.watch;
619
624
  const timers = deps.timers ?? globalThis;
620
625
  const pending = new Map<string, PendingSupervisorRequest>();
626
+ const requestCorrelations = new Map<string, SupervisorRequestCorrelation>();
627
+ const correlationKey = (request: { runId: string; agent: string; childIndex: number; toolCallId?: string }): string | undefined => {
628
+ if (!request.toolCallId) return undefined;
629
+ return JSON.stringify([request.runId, request.agent, request.childIndex, request.toolCallId]);
630
+ };
631
+ const pruneRequestCorrelations = (now = Date.now()): void => {
632
+ for (const [key, correlation] of requestCorrelations) {
633
+ if (correlation.state === "resolved" && now - correlation.updatedAt > SUPERVISOR_REQUEST_CORRELATION_RETENTION_MS) requestCorrelations.delete(key);
634
+ }
635
+ while (requestCorrelations.size > MAX_SUPERVISOR_REQUEST_CORRELATIONS) {
636
+ const oldest = requestCorrelations.keys().next().value;
637
+ if (oldest === undefined) break;
638
+ requestCorrelations.delete(oldest);
639
+ }
640
+ };
641
+ const rememberPendingRequest = (request: PendingSupervisorRequest): void => {
642
+ const key = correlationKey(request);
643
+ if (!key || !request.expectsReply) return;
644
+ requestCorrelations.set(key, { request, state: "pending", updatedAt: Date.now() });
645
+ pruneRequestCorrelations();
646
+ };
647
+ const rememberResolvedRequest = (request: PendingSupervisorRequest): void => {
648
+ const key = correlationKey(request);
649
+ if (!key || !request.expectsReply) return;
650
+ requestCorrelations.set(key, { request, state: "resolved", updatedAt: Date.now() });
651
+ pruneRequestCorrelations();
652
+ };
653
+ const observeRequestLifecycle: SupervisorRequestLifecycleObserver = (request, lifecycle) => {
654
+ if (lifecycle !== "wrong-session") rememberResolvedRequest(request);
655
+ };
656
+ const getSupervisorRequestState = (event: ControlEvent): SupervisorRequestState => {
657
+ if (event.currentTool === "intercom" || event.index === undefined) return "unknown";
658
+ const toolCallId = typeof event.toolCallId === "string" && event.toolCallId.length > 0 ? event.toolCallId : undefined;
659
+ if (!toolCallId) return "unknown";
660
+ pruneRequestCorrelations();
661
+ const key = correlationKey({ runId: event.runId, agent: event.agent, childIndex: event.index, toolCallId });
662
+ if (!key) return "unknown";
663
+ const correlation = requestCorrelations.get(key);
664
+ if (!correlation) return "unknown";
665
+ if (correlation.state === "resolved") return "resolved";
666
+ const now = Date.now();
667
+ if (!fs.existsSync(correlation.request.requestFile)
668
+ || fs.existsSync(replyPath(correlation.request.channelDir, correlation.request.id))
669
+ || now > requestExpiresAt(correlation.request, now)
670
+ || requestRunInactive(correlation.request, state)) {
671
+ rememberResolvedRequest(correlation.request);
672
+ return "resolved";
673
+ }
674
+ return "pending";
675
+ };
621
676
  const seenFiles = new Set<string>();
622
677
  const requestWatchers = new Map<string, fs.FSWatcher>();
623
678
  let rootWatcher: fs.FSWatcher | undefined;
@@ -635,7 +690,7 @@ export function createNativeSupervisorChannel(pi: ExtensionAPI, state: SubagentS
635
690
  };
636
691
 
637
692
  const registerParentTools = (): void => {
638
- if (!hasTool(pi, NATIVE_SUPERVISOR_TOOL_NAME)) pi.registerTool(buildParentSupervisorTool(pending, state));
693
+ if (!hasTool(pi, NATIVE_SUPERVISOR_TOOL_NAME)) pi.registerTool(buildParentSupervisorTool(pi, pending, state, observeRequestLifecycle));
639
694
  };
640
695
 
641
696
  const cleanupStaleChannelsIfDue = (): void => {
@@ -653,7 +708,7 @@ export function createNativeSupervisorChannel(pi: ExtensionAPI, state: SubagentS
653
708
  cleanupStaleChannelsIfDue();
654
709
  const ctx = state.lastUiContext;
655
710
  if (!ctx) return;
656
- refreshPendingRequests(pending, state, ctx);
711
+ refreshPendingRequests(pending, state, ctx, observeRequestLifecycle);
657
712
  const now = Date.now();
658
713
  for (const { channelDir, file } of listRequestFiles()) {
659
714
  if (seenFiles.has(file)) continue;
@@ -662,11 +717,13 @@ export function createNativeSupervisorChannel(pi: ExtensionAPI, state: SubagentS
662
717
  const lifecycle = requestLifecycle(request, state, undefined, now);
663
718
  if (lifecycle !== "pending") {
664
719
  seenFiles.add(file);
720
+ observeRequestLifecycle(request, lifecycle);
665
721
  cleanupRequestLifecycle(request, lifecycle);
666
722
  continue;
667
723
  }
668
724
  seenFiles.add(file);
669
725
  if (request.expectsReply) {
726
+ rememberPendingRequest(request);
670
727
  pending.set(request.id, request);
671
728
  markForegroundSupervisorAttention(request, state);
672
729
  }
@@ -674,16 +731,21 @@ export function createNativeSupervisorChannel(pi: ExtensionAPI, state: SubagentS
674
731
  removeRequestFile(request.requestFile);
675
732
  }
676
733
  pi.sendMessage({
677
- customType: "subagent_supervisor_request",
734
+ customType: SUPERVISOR_REQUEST_MESSAGE_TYPE,
678
735
  content: requestVisibleText(request),
679
736
  display: true,
680
737
  details: {
681
738
  id: request.id,
739
+ requestId: request.id,
682
740
  reason: request.reason,
683
741
  expectsReply: request.expectsReply,
684
742
  runId: request.runId,
685
743
  agent: request.agent,
686
744
  childIndex: request.childIndex,
745
+ ...(request.childTarget ? { childTarget: request.childTarget } : {}),
746
+ ...(request.interview !== undefined ? { interview: request.interview } : {}),
747
+ requestBody: request.message,
748
+ ...(request.expectsReply ? { replyHint: supervisorReplyHint(request.id) } : {}),
687
749
  },
688
750
  }, { triggerTurn: true });
689
751
  if (request.expectsReply) {
@@ -805,8 +867,10 @@ export function createNativeSupervisorChannel(pi: ExtensionAPI, state: SubagentS
805
867
  if (deferredWatcherRefresh) timers.clearImmediate(deferredWatcherRefresh);
806
868
  deferredWatcherRefresh = undefined;
807
869
  pending.clear();
870
+ requestCorrelations.clear();
808
871
  seenFiles.clear();
809
872
  },
810
873
  pending,
874
+ getSupervisorRequestState,
811
875
  };
812
876
  }