pi-subagents 0.49.0 → 0.51.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 (117) hide show
  1. package/CHANGELOG.md +97 -0
  2. package/agents/gpt-pro.md +17 -0
  3. package/agents/oracle.md +7 -5
  4. package/agents/researcher.md +1 -1
  5. package/agents/reviewer.md +2 -2
  6. package/agents/scout.md +1 -1
  7. package/agents/worker.md +1 -1
  8. package/async-retention-discovery-worker.mjs +180 -0
  9. package/docs/agents.md +37 -2
  10. package/docs/configuration.md +76 -14
  11. package/docs/extension-api.md +78 -1
  12. package/docs/missions.md +1 -1
  13. package/docs/observability.md +20 -4
  14. package/docs/tool-reference.md +55 -39
  15. package/docs/workflows.md +171 -5
  16. package/package.json +4 -2
  17. package/skills/pi-subagents/SKILL.md +5 -4
  18. package/skills/pi-subagents/references/constraints-and-recipes.md +9 -6
  19. package/skills/pi-subagents/references/execution-controls.md +22 -18
  20. package/skills/pi-subagents/references/management-authoring-rpc.md +5 -5
  21. package/skills/pi-subagents/references/prompting-and-roles.md +33 -17
  22. package/src/agents/agent-management.ts +100 -345
  23. package/src/agents/agent-serializer.ts +2 -0
  24. package/src/agents/agents.ts +135 -25
  25. package/src/api/external-job-provider.ts +185 -0
  26. package/src/api/external-runs.ts +174 -84
  27. package/src/api/preflight.ts +42 -11
  28. package/src/api/shared-types.ts +2 -0
  29. package/src/extension/config.ts +36 -3
  30. package/src/extension/doctor.ts +3 -6
  31. package/src/extension/fanout-child.ts +2 -2
  32. package/src/extension/index.ts +210 -90
  33. package/src/extension/public-execution.ts +31 -2
  34. package/src/extension/rpc.ts +5 -1
  35. package/src/extension/schemas.ts +14 -36
  36. package/src/extension/tool-description.ts +37 -24
  37. package/src/inspectors/herdr/actions.ts +5 -9
  38. package/src/inspectors/herdr/inspector-runner.ts +2 -1
  39. package/src/inspectors/herdr/project-panes.ts +4 -8
  40. package/src/inspectors/herdr/shell-command.ts +16 -0
  41. package/src/intercom/intercom-bridge.ts +2 -3
  42. package/src/intercom/native-supervisor-channel.ts +49 -51
  43. package/src/missions/goal-driver.ts +3 -1
  44. package/src/missions/lifecycle.ts +6 -1
  45. package/src/missions/store.ts +4 -9
  46. package/src/profiles/profiles.ts +3 -1
  47. package/src/runs/background/active-run-index.ts +94 -1
  48. package/src/runs/background/async-execution.ts +98 -38
  49. package/src/runs/background/async-job-tracker.ts +21 -4
  50. package/src/runs/background/async-resume.ts +30 -17
  51. package/src/runs/background/async-retention.ts +888 -0
  52. package/src/runs/background/async-status-snapshot.ts +277 -0
  53. package/src/runs/background/async-status.ts +47 -56
  54. package/src/runs/background/chain-append.ts +3 -33
  55. package/src/runs/background/chain-root-attachment.ts +2 -2
  56. package/src/runs/background/completion-replay.ts +11 -1
  57. package/src/runs/background/control-channel.ts +14 -68
  58. package/src/runs/background/fleet-view.ts +3 -1
  59. package/src/runs/background/index-segment.ts +59 -0
  60. package/src/runs/background/notify.ts +3 -1
  61. package/src/runs/background/result-files.ts +505 -0
  62. package/src/runs/background/result-watcher.ts +250 -51
  63. package/src/runs/background/retained-children.ts +79 -20
  64. package/src/runs/background/run-id-query.ts +7 -0
  65. package/src/runs/background/run-id-resolver.ts +37 -29
  66. package/src/runs/background/run-status.ts +34 -20
  67. package/src/runs/background/scheduled-runs.ts +71 -27
  68. package/src/runs/background/stale-run-reconciler.ts +33 -16
  69. package/src/runs/background/steering.ts +11 -1
  70. package/src/runs/background/subagent-runner.ts +535 -161
  71. package/src/runs/background/subagent-wait.ts +9 -7
  72. package/src/runs/background/terminal-run-index.ts +129 -0
  73. package/src/runs/background/wait-completions.ts +22 -2
  74. package/src/runs/background/wait-subscriptions.ts +80 -1
  75. package/src/runs/foreground/async-dismiss-action.ts +2 -1
  76. package/src/runs/foreground/async-steering-action.ts +21 -14
  77. package/src/runs/foreground/execution.ts +221 -15
  78. package/src/runs/foreground/subagent-executor.ts +529 -1519
  79. package/src/runs/foreground/workflow-foreground-steering.ts +6 -5
  80. package/src/runs/shared/chain-outputs.ts +1 -3
  81. package/src/runs/shared/completion-guard.ts +96 -6
  82. package/src/runs/shared/external-cli-runner.ts +4 -0
  83. package/src/runs/shared/external-job-bridge.ts +450 -0
  84. package/src/runs/shared/external-job-runner.ts +286 -0
  85. package/src/runs/shared/mcp-direct-tool-allowlist.ts +14 -0
  86. package/src/runs/shared/model-fallback.ts +34 -3
  87. package/src/runs/shared/nested-events.ts +66 -62
  88. package/src/runs/shared/orca-progress-tabs.ts +437 -0
  89. package/src/runs/shared/parallel-handoff.ts +46 -4
  90. package/src/runs/shared/parallel-utils.ts +6 -15
  91. package/src/runs/shared/permissions.ts +5 -1
  92. package/src/runs/shared/pi-args.ts +8 -1
  93. package/src/runs/shared/subagent-control.ts +41 -4
  94. package/src/runs/shared/subagent-prompt-runtime.ts +13 -15
  95. package/src/runs/shared/subagent-startup-retry.ts +12 -0
  96. package/src/runs/shared/tool-timeout.ts +93 -0
  97. package/src/runs/shared/workflow-graph.ts +1 -23
  98. package/src/runs/shared/worktree.ts +12 -1
  99. package/src/shared/atomic-json.ts +22 -2
  100. package/src/shared/capacity-resilient-json.ts +102 -0
  101. package/src/shared/completion-owner.ts +14 -0
  102. package/src/shared/file-system-retry.ts +49 -1
  103. package/src/shared/fork-context.ts +42 -0
  104. package/src/shared/prompt-resources.ts +0 -40
  105. package/src/shared/settings.ts +3 -27
  106. package/src/shared/types.ts +88 -27
  107. package/src/shared/utils.ts +8 -0
  108. package/src/shared/watch-strategy.ts +10 -0
  109. package/src/slash/slash-commands.ts +45 -28
  110. package/src/slash/slash-live-state.ts +3 -0
  111. package/src/tui/fleet-status.ts +160 -45
  112. package/src/tui/fleet.ts +186 -28
  113. package/src/tui/render.ts +41 -10
  114. package/src/workflows/chat-progress.ts +7 -5
  115. package/src/workflows/scripted-workflow.ts +424 -125
  116. package/src/runs/foreground/chain-clarify.ts +0 -1354
  117. package/src/runs/foreground/chain-execution.ts +0 -1565
@@ -18,6 +18,7 @@ import * as fs from "node:fs";
18
18
  import * as path from "node:path";
19
19
  import { writeAtomicJson } from "../../shared/atomic-json.ts";
20
20
  import { POLL_INTERVAL_MS } from "../../shared/types.ts";
21
+ import { shouldUseNativeFsWatch } from "../../shared/watch-strategy.ts";
21
22
  import { resolveWatchPath } from "../../shared/utils.ts";
22
23
 
23
24
  export type ControlChannelFs = Pick<typeof fs, "mkdirSync" | "existsSync" | "rmSync" | "watch" | "readdirSync" | "readFileSync" | "realpathSync">;
@@ -56,13 +57,6 @@ export interface StopRequest {
56
57
  reason?: string;
57
58
  }
58
59
 
59
- export interface CheckpointDecisionRequest {
60
- type: "approve-checkpoint" | "reject-checkpoint";
61
- ts?: number;
62
- source?: string;
63
- reason?: string;
64
- }
65
-
66
60
  export type SteerDeliveryMode = "steer" | "follow_up" | "auto";
67
61
  export type SteerDeliveryStatus = "delivered" | "queued";
68
62
 
@@ -127,14 +121,6 @@ export function stopRequestPath(asyncDir: string): string {
127
121
  return path.join(controlInboxDir(asyncDir), "stop.json");
128
122
  }
129
123
 
130
- export function approveCheckpointRequestPath(asyncDir: string): string {
131
- return path.join(controlInboxDir(asyncDir), "approve-checkpoint.json");
132
- }
133
-
134
- export function rejectCheckpointRequestPath(asyncDir: string): string {
135
- return path.join(controlInboxDir(asyncDir), "reject-checkpoint.json");
136
- }
137
-
138
124
  /** Directory of parent-to-runner steering requests. */
139
125
  export function steerRequestsDir(asyncDir: string): string {
140
126
  return path.join(controlInboxDir(asyncDir), STEER_REQUESTS_DIR);
@@ -144,8 +130,8 @@ export function steerInboxClosedPath(asyncDir: string): string {
144
130
  return path.join(controlInboxDir(asyncDir), STEER_INBOX_CLOSED_FILE);
145
131
  }
146
132
 
147
- export function closeSteerInbox(asyncDir: string, state: string): void {
148
- writeAtomicJson(steerInboxClosedPath(asyncDir), { version: 1, closedAt: Date.now(), state });
133
+ export function closeSteerInbox(asyncDir: string, state: string, write: (filePath: string, payload: object) => void = writeAtomicJson): void {
134
+ write(steerInboxClosedPath(asyncDir), { version: 1, closedAt: Date.now(), state });
149
135
  }
150
136
 
151
137
  /** Per-child inbox consumed by the child prompt runtime inside the Pi process. */
@@ -299,18 +285,6 @@ export function requestAsyncStop(
299
285
  return requestPath;
300
286
  }
301
287
 
302
- export function requestAsyncCheckpointDecision(
303
- asyncDir: string,
304
- type: CheckpointDecisionRequest["type"],
305
- payload: Omit<CheckpointDecisionRequest, "type"> = {},
306
- deps: { now?: () => number } = {},
307
- ): string {
308
- const requestPath = type === "approve-checkpoint" ? approveCheckpointRequestPath(asyncDir) : rejectCheckpointRequestPath(asyncDir);
309
- const request: CheckpointDecisionRequest = { ...payload, ts: payload.ts ?? deps.now?.() ?? Date.now(), type };
310
- writeAtomicJson(requestPath, request);
311
- return requestPath;
312
- }
313
-
314
288
  export function requestAsyncSteer(
315
289
  asyncDir: string,
316
290
  payload: { message: string; mode?: SteerDeliveryMode; targetIndex?: number; targetIndexes?: number[]; source?: string; id?: string; ts?: number },
@@ -559,21 +533,6 @@ export function consumeStopRequest(
559
533
  return true;
560
534
  }
561
535
 
562
- export function consumeCheckpointDecisionRequest(
563
- asyncDir: string,
564
- fsImpl: Pick<typeof fs, "existsSync" | "rmSync"> = fs,
565
- ): "approved" | "rejected" | undefined {
566
- if (fsImpl.existsSync(rejectCheckpointRequestPath(asyncDir))) {
567
- try { fsImpl.rmSync(rejectCheckpointRequestPath(asyncDir), { force: true, recursive: true }); } catch {}
568
- return "rejected";
569
- }
570
- if (fsImpl.existsSync(approveCheckpointRequestPath(asyncDir))) {
571
- try { fsImpl.rmSync(approveCheckpointRequestPath(asyncDir), { force: true, recursive: true }); } catch {}
572
- return "approved";
573
- }
574
- return undefined;
575
- }
576
-
577
536
  /** Parent side: write the authoritative portable interrupt request. */
578
537
  export function deliverInterruptRequest(input: {
579
538
  asyncDir: string;
@@ -605,21 +564,6 @@ export function deliverStopRequest(input: {
605
564
  requestAsyncStop(input.asyncDir, input.source ? { source: input.source } : {}, { now: input.now });
606
565
  }
607
566
 
608
- export function deliverCheckpointDecisionRequest(input: {
609
- asyncDir: string;
610
- decision: "approved" | "rejected";
611
- now?: () => number;
612
- source?: string;
613
- reason?: string;
614
- }): void {
615
- requestAsyncCheckpointDecision(
616
- input.asyncDir,
617
- input.decision === "approved" ? "approve-checkpoint" : "reject-checkpoint",
618
- { ...(input.source ? { source: input.source } : {}), ...(input.reason ? { reason: input.reason } : {}) },
619
- { now: input.now },
620
- );
621
- }
622
-
623
567
  /**
624
568
  * Runner side: watch the control inbox and route interrupt requests into
625
569
  * `onInterrupt`. Uses `fs.watch` when available and starts interval polling
@@ -633,11 +577,11 @@ export function watchAsyncControlInbox(
633
577
  onTimeout?: () => void;
634
578
  onStop?: () => void;
635
579
  onSteer?: (request: SteerRequest) => void;
636
- onCheckpointDecision?: (decision: "approved" | "rejected") => void;
637
580
  onSteerCapability?: (capability: SteerCapability) => void;
638
581
  onSteerAck?: (ack: SteerAck) => void;
639
582
  pollIntervalMs?: number;
640
583
  safetyPollIntervalMs?: number;
584
+ platform?: NodeJS.Platform;
641
585
  fs?: ControlChannelFs;
642
586
  timers?: ControlChannelTimers;
643
587
  },
@@ -658,8 +602,6 @@ export function watchAsyncControlInbox(
658
602
  if (consumeStopRequest(asyncDir, fsImpl)) opts.onStop?.();
659
603
  if (consumeTimeoutRequest(asyncDir, fsImpl)) opts.onTimeout?.();
660
604
  if (consumeInterruptRequest(asyncDir, fsImpl)) opts.onInterrupt();
661
- const checkpointDecision = consumeCheckpointDecisionRequest(asyncDir, fsImpl);
662
- if (checkpointDecision) opts.onCheckpointDecision?.(checkpointDecision);
663
605
  for (const request of consumeSteerRequests(asyncDir, fsImpl)) opts.onSteer?.(request);
664
606
  for (const capability of consumeSteerCapabilities(asyncDir, fsImpl)) opts.onSteerCapability?.(capability);
665
607
  for (const ack of consumeSteerAcks(asyncDir, fsImpl)) opts.onSteerAck?.(ack);
@@ -707,12 +649,16 @@ export function watchAsyncControlInbox(
707
649
  for (const entry of entries) watchDir(path.join(ackRoot, entry));
708
650
  };
709
651
  try {
710
- watchDir(dir);
711
- watchDir(steerRequestsDir(asyncDir), true);
712
- watchDir(steerCapabilitiesDir(asyncDir), true);
713
- watchDir(path.join(dir, STEER_ACKS_DIR), true);
714
- watchExistingSteerAckDirs();
715
- startSafetyPolling();
652
+ if (shouldUseNativeFsWatch("runner-control-inbox", opts.platform)) {
653
+ watchDir(dir);
654
+ watchDir(steerRequestsDir(asyncDir), true);
655
+ watchDir(steerCapabilitiesDir(asyncDir), true);
656
+ watchDir(path.join(dir, STEER_ACKS_DIR), true);
657
+ watchExistingSteerAckDirs();
658
+ startSafetyPolling();
659
+ } else {
660
+ startPolling();
661
+ }
716
662
  } catch {
717
663
  startPolling();
718
664
  }
@@ -443,6 +443,8 @@ export function formatAsyncRunTranscript(status: AsyncStatus, asyncDir: string,
443
443
  : uniqueStrings([runOutputPath]);
444
444
  const sessionFile = selected.index !== undefined ? selected.step?.sessionFile : status.sessionFile;
445
445
  const eventsPath = path.join(asyncDir, "events.jsonl");
446
+ // Synthesized output paths can name files that were never written. Keep the unfiltered paths
447
+ // below so a present-but-unreadable file still reports its read error.
446
448
 
447
449
  const context = contextModeLabel(status.context ?? summarizeContextModes((status.steps ?? []).map((step) => step.context)));
448
450
  const lines = [
@@ -452,7 +454,7 @@ export function formatAsyncRunTranscript(status: AsyncStatus, asyncDir: string,
452
454
  stepStateLine(status.mode, selected.index, selected.step),
453
455
  selected.hint,
454
456
  ].filter((line): line is string => Boolean(line));
455
- appendKnownArtifacts(lines, { outputPaths, sessionFile, eventsPath: fs.existsSync(eventsPath) ? eventsPath : undefined, logPath: fs.existsSync(logPath) ? logPath : undefined });
457
+ appendKnownArtifacts(lines, { outputPaths: outputPaths.filter((outputPath) => fs.existsSync(outputPath)), sessionFile, eventsPath: fs.existsSync(eventsPath) ? eventsPath : undefined, logPath: fs.existsSync(logPath) ? logPath : undefined });
456
458
 
457
459
  const warnings: string[] = [];
458
460
  let transcriptLines: string[] = [];
@@ -0,0 +1,59 @@
1
+ import { createHash } from "node:crypto";
2
+
3
+ /**
4
+ * Keep opaque index keys below common filesystem component limits.
5
+ *
6
+ * Portable short values retain the historical URI-encoded representation.
7
+ * Oversized or non-portable values use a deterministic digest so callers can
8
+ * resolve the same index without persisting a separate lookup table.
9
+ *
10
+ * Encoded backslashes and trailing file extensions are not portable. Pi session
11
+ * ids are often the full session `.jsonl` path. On Windows, `readdir` of a
12
+ * directory whose name ends in `.jsonl` or contains `%5C` can fail with EPERM.
13
+ */
14
+ export const MAX_INDEX_SEGMENT_BYTES = 255;
15
+ const HASHED_INDEX_SEGMENT_PREFIX = "~sha256-";
16
+ const WINDOWS_RESERVED_NAME = /^(?:con|prn|aux|nul|com[1-9]|lpt[1-9])(?:\.|$)/i;
17
+
18
+ function hashedSegment(value: string): string {
19
+ return `${HASHED_INDEX_SEGMENT_PREFIX}${createHash("sha256").update(value).digest("hex")}`;
20
+ }
21
+
22
+ function isPortableSegment(value: string): boolean {
23
+ return value.length > 0
24
+ && value !== "."
25
+ && value !== ".."
26
+ && !value.endsWith(".")
27
+ && !WINDOWS_RESERVED_NAME.test(value)
28
+ && !/%5C/i.test(value)
29
+ && !/\.[A-Za-z][A-Za-z0-9]{0,7}$/.test(value);
30
+ }
31
+
32
+ export function encodeIndexSegment(value: string, maxBytes = MAX_INDEX_SEGMENT_BYTES): string {
33
+ let encoded: string;
34
+ try {
35
+ encoded = encodeURIComponent(value);
36
+ } catch {
37
+ return hashedSegment(value);
38
+ }
39
+ if (Buffer.byteLength(encoded, "utf-8") <= maxBytes && isPortableSegment(encoded)) return encoded;
40
+ return hashedSegment(value);
41
+ }
42
+
43
+ function historicallyReadableSegment(value: string, maxBytes: number): string | undefined {
44
+ try {
45
+ const encoded = encodeURIComponent(value);
46
+ if (encoded.length === 0 || encoded === "." || encoded === ".." || encoded.endsWith(".") || WINDOWS_RESERVED_NAME.test(encoded)) return undefined;
47
+ if (Buffer.byteLength(encoded, "utf-8") > maxBytes) return undefined;
48
+ return encoded;
49
+ } catch {
50
+ return undefined;
51
+ }
52
+ }
53
+
54
+ /** Current write key first, then the pre-hash URI-encoded key when it still fits. */
55
+ export function indexSegmentAliases(value: string, maxBytes = MAX_INDEX_SEGMENT_BYTES): string[] {
56
+ const current = encodeIndexSegment(value, maxBytes);
57
+ const historical = historicallyReadableSegment(value, maxBytes);
58
+ return historical && historical !== current ? [current, historical] : [current];
59
+ }
@@ -63,6 +63,7 @@ export interface CompletionNotification {
63
63
  taskIndex?: number;
64
64
  totalTasks?: number;
65
65
  sessionId?: string | null;
66
+ completionOwnerId?: string | null;
66
67
  triggerTurn?: boolean;
67
68
  /** True when an acknowledged grouped intercom relay already delivered this run. */
68
69
  intercomDelivered?: boolean;
@@ -239,7 +240,7 @@ export function buildCompletionDetails(result: CompletionNotification): Subagent
239
240
 
240
241
  export default function registerSubagentNotify(
241
242
  pi: ExtensionAPI,
242
- state: Pick<SubagentState, "currentSessionId">,
243
+ state: Pick<SubagentState, "currentSessionId" | "completionOwnerId">,
243
244
  options: RegisterSubagentNotifyOptions = {},
244
245
  ): CompletionNotifier {
245
246
  const seen = new Map<string, number>();
@@ -275,6 +276,7 @@ export default function registerSubagentNotify(
275
276
 
276
277
  const deliver = (result: CompletionNotification): Promise<boolean> => {
277
278
  if (disposed || typeof result.sessionId !== "string" || result.sessionId !== state.currentSessionId) return Promise.resolve(false);
279
+ if (result.source !== "foreground" && (!state.completionOwnerId || result.completionOwnerId !== state.completionOwnerId)) return Promise.resolve(false);
278
280
  if (result.intercomDelivered === true) return Promise.resolve(true);
279
281
  const key = buildCompletionKey(result, "notify");
280
282
  const seenAt = seen.get(key);