pi-subagents 0.82.1 → 0.83.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 (77) hide show
  1. package/CHANGELOG.md +29 -0
  2. package/bundle/agents.js +4 -4
  3. package/bundle/background-work.js +1 -1
  4. package/bundle/child-tool-plan.js +3 -3
  5. package/bundle/{chunk-FXUXCX2K.js → chunk-4BW55BTL.js} +6 -6
  6. package/bundle/{chunk-PBK2I7WY.js → chunk-527E3D2J.js} +6 -6
  7. package/bundle/{chunk-46XKCPJR.js → chunk-5ESXK62X.js} +7 -1073
  8. package/bundle/{chunk-UTT254JE.js → chunk-6B4ITO2W.js} +8 -4
  9. package/bundle/{chunk-CIZQXJ5Q.js → chunk-7F5SK2U3.js} +39 -6
  10. package/bundle/{chunk-OYF6AFDY.js → chunk-BKFWQ642.js} +2 -2
  11. package/bundle/{chunk-ZIV4J4HG.js → chunk-CDI3GMNV.js} +47 -30
  12. package/bundle/{chunk-3JCDWF4V.js → chunk-CGXWSNPA.js} +12 -18
  13. package/bundle/{chunk-JYUKW2SY.js → chunk-CNLDYEH4.js} +12 -12
  14. package/bundle/{chunk-4SDU2BZX.js → chunk-CSSZRTKY.js} +2 -2
  15. package/bundle/{chunk-VSUSLDNW.js → chunk-CZTMUBUI.js} +10 -8
  16. package/bundle/{chunk-IVAE5Z6Q.js → chunk-HJJAVHFB.js} +28 -24
  17. package/bundle/{chunk-GJECHSAR.js → chunk-HLE22FSH.js} +2 -2
  18. package/bundle/{chunk-4Y2ROR7L.js → chunk-HXVKGS33.js} +5 -5
  19. package/bundle/{chunk-5LFLVSTM.js → chunk-JEZOSEWG.js} +2 -0
  20. package/bundle/{chunk-YKEWIB4M.js → chunk-JHZ6DZ5M.js} +1 -1
  21. package/bundle/{chunk-QE4AGIMG.js → chunk-LDMXJIA6.js} +457 -657
  22. package/bundle/{chunk-43SCHIMD.js → chunk-MBDOYZ2V.js} +1 -1
  23. package/bundle/{chunk-XSWOTIA3.js → chunk-MHX22USZ.js} +1 -1
  24. package/bundle/{chunk-6XJB7PIX.js → chunk-OOFW75PV.js} +4 -4
  25. package/bundle/{chunk-RMQFMMAT.js → chunk-P2NCCTZP.js} +231 -10
  26. package/bundle/{chunk-UCPP4GNU.js → chunk-RZJN63GU.js} +12 -5
  27. package/bundle/{chunk-G4GZD3OJ.js → chunk-TJQJ3MEA.js} +10 -10
  28. package/bundle/{chunk-5TYMSZX3.js → chunk-VTMNBPCQ.js} +2 -2
  29. package/bundle/{chunk-52UBALJL.js → chunk-WG2OTTAK.js} +2 -2
  30. package/bundle/{chunk-N2BN7BF5.js → chunk-XA3DY6UG.js} +7 -5
  31. package/bundle/{chunk-PDZWJCHD.js → chunk-XXKG3P2W.js} +9 -6
  32. package/bundle/{chunk-PLCCBLPY.js → chunk-Y3I6H6DV.js} +2 -2
  33. package/bundle/control-channel.js +3 -3
  34. package/bundle/index.js +1 -1
  35. package/bundle/intercom-bridge.js +3 -3
  36. package/bundle/preflight.js +9 -9
  37. package/bundle/project-panes.js +4 -4
  38. package/bundle/shared-types.js +1 -1
  39. package/docs/configuration.md +1 -1
  40. package/docs/extension-api.md +19 -4
  41. package/docs/observability.md +1 -0
  42. package/docs/tool-reference.md +1 -1
  43. package/package.json +1 -1
  44. package/skills/pi-subagents/SKILL.md +6 -6
  45. package/src/agents/agent-management.js +8 -14
  46. package/src/agents/agent-serializer.js +1 -2
  47. package/src/agents/agents.js +19 -3
  48. package/src/api/background-work.d.ts +9 -6
  49. package/src/api/background-work.js +14 -7
  50. package/src/extension/index.js +4 -1
  51. package/src/extension/public-execution.d.ts +2 -2
  52. package/src/extension/public-execution.js +3 -3
  53. package/src/extension/reply-workflow-script.js +4 -5
  54. package/src/extension/rpc.js +3 -1
  55. package/src/extension/subagent-options.js +17 -4
  56. package/src/runs/background/async-execution.js +4 -1
  57. package/src/runs/background/async-job-tracker.js +2 -1
  58. package/src/runs/background/auto-drain.js +2 -3
  59. package/src/runs/background/fleet-view.js +1 -1
  60. package/src/runs/background/run-child-session.js +22 -6
  61. package/src/runs/background/subagent-wait.d.ts +2 -1
  62. package/src/runs/background/subagent-wait.js +5 -2
  63. package/src/runs/foreground/execution.js +22 -6
  64. package/src/runs/foreground/subagent-executor.js +2 -5
  65. package/src/runs/shared/child-launch.js +5 -2
  66. package/src/runs/shared/child-session.d.ts +6 -0
  67. package/src/runs/shared/child-session.js +20 -4
  68. package/src/runs/shared/herdr-machine.d.ts +2 -0
  69. package/src/runs/shared/herdr-machine.js +2 -0
  70. package/src/runs/shared/herdr-placed-run.js +11 -4
  71. package/src/runs/shared/subagent-prompt-runtime.js +11 -17
  72. package/src/shared/disabled-features.d.ts +3 -1
  73. package/src/shared/disabled-features.js +11 -3
  74. package/src/shared/types.d.ts +3 -1
  75. package/src/shared/types.js +2 -0
  76. package/src/workflows/scripted-workflow.js +9 -1070
  77. package/workflow-script-worker.cjs +1067 -0
@@ -69,6 +69,8 @@ export function runChildSession(input) {
69
69
  let queuedDrainHold = false;
70
70
  let compactionStartedReceived = false;
71
71
  let afterCompactionSettlement = false;
72
+ let terminalAgentEndReceived = false;
73
+ let compactionAfterFinalOpen = false;
72
74
  let finalDrainTimer;
73
75
  let finalHardFinishTimer;
74
76
  let watchdogTailTimer;
@@ -216,7 +218,8 @@ export function runChildSession(input) {
216
218
  finalDrainTimer = setTimeout(() => {
217
219
  if (settled || promptSettled)
218
220
  return;
219
- if (input.launch.capture.finalDrainHeld() || observeQueuedDrainHold()) {
221
+ // Pi runs auto-compaction after the final agent_end and only then emits agent_settled.
222
+ if (input.launch.capture.finalDrainHeld() || observeQueuedDrainHold() || compactionAfterFinalOpen) {
220
223
  finalDrainTimer = undefined;
221
224
  armFinalDrainTimer();
222
225
  return;
@@ -231,16 +234,22 @@ export function runChildSession(input) {
231
234
  return;
232
235
  settle(undefined, true);
233
236
  }, HARD_FINISH_MS);
234
- finalHardFinishTimer.unref?.();
235
237
  }, FINAL_STOP_GRACE_MS);
236
- finalDrainTimer.unref?.();
237
238
  }
238
- const applyChildLifecycle = (action) => {
239
+ const applyChildLifecycle = (action, event) => {
239
240
  if (action === "cancel-drain") {
240
- cleanTerminalAssistantStopReceived = false;
241
+ // After a clean final stop, a non-retrying agent_end may be the child's last event
242
+ // (Pi may not emit agent_settled), so it re-arms the drain instead of cancelling it.
243
+ terminalAgentEndReceived = event?.type === "agent_end" && event.willRetry !== true && cleanTerminalAssistantStopReceived;
241
244
  agentSettledReceived = false;
242
245
  clearFinalDrainTimers();
243
246
  clearWatchdogTailTimer();
247
+ if (terminalAgentEndReceived) {
248
+ startFinalDrain();
249
+ return;
250
+ }
251
+ cleanTerminalAssistantStopReceived = false;
252
+ compactionAfterFinalOpen = false;
244
253
  return;
245
254
  }
246
255
  if (action === "start-drain")
@@ -314,11 +323,18 @@ export function runChildSession(input) {
314
323
  compactionStartedReceived = true;
315
324
  if (agentSettledReceived)
316
325
  afterCompactionSettlement = true;
326
+ if (terminalAgentEndReceived)
327
+ compactionAfterFinalOpen = true;
317
328
  }
318
329
  if (event.type === "compaction_end" && event.willRetry === true) {
319
330
  compactionStartedReceived = false;
320
331
  afterCompactionSettlement = false;
321
332
  }
333
+ if (event.type === "compaction_end" && event.willRetry !== true && compactionAfterFinalOpen) {
334
+ compactionAfterFinalOpen = false;
335
+ clearFinalDrainTimers();
336
+ startFinalDrain();
337
+ }
322
338
  if (event.type === "turn_start" || event.type === "agent_start" || event.type === "auto_retry_start") {
323
339
  queuedDrainHold = false;
324
340
  }
@@ -331,7 +347,7 @@ export function runChildSession(input) {
331
347
  agentSettledReceived = true;
332
348
  afterCompactionSettlement = compactionStartedReceived;
333
349
  }
334
- applyChildLifecycle(lifecycleAction);
350
+ applyChildLifecycle(lifecycleAction, event);
335
351
  if (isChildWatchdogStatusEvent(event)) {
336
352
  if (!input.childWatchdog)
337
353
  return;
@@ -100,7 +100,7 @@ export interface SubagentWaitDeps {
100
100
  };
101
101
  /** Injectable provider protocol surfaces for deterministic tests. */
102
102
  backgroundWork?: {
103
- snapshot(sessionId: string, nowMs: number): BackgroundWorkSnapshot;
103
+ snapshot(sessionIds: readonly string[], nowMs: number): BackgroundWorkSnapshot;
104
104
  wakeChannels(): readonly string[];
105
105
  };
106
106
  /**
@@ -111,6 +111,7 @@ export interface SubagentWaitDeps {
111
111
  */
112
112
  events?: WaitEventBus;
113
113
  }
114
+ export declare function backgroundWorkForSession(deps: Pick<SubagentWaitDeps, "state" | "backgroundWork">, nowMs: number): BackgroundWorkSnapshot;
114
115
  export declare function waitRunScopes(deps: Pick<SubagentWaitDeps, "asyncDirRoot" | "resultsDir" | "nestedRootRunId">): Array<{
115
116
  asyncDirRoot: string;
116
117
  resultsDir: string;
@@ -181,11 +181,14 @@ function hasSupervisorTool(run) {
181
181
  function backgroundWorkIdentity(item) {
182
182
  return `${item.provider}\0${item.sessionId}\0${item.id}`;
183
183
  }
184
- function backgroundWorkForSession(deps, nowMs) {
184
+ export function backgroundWorkForSession(deps, nowMs) {
185
185
  const sessionId = deps.state.currentSessionId;
186
186
  if (!sessionId)
187
187
  throw new Error("bg_wait requires an active session identity to scope background work safely.");
188
- return deps.backgroundWork?.snapshot(sessionId, nowMs) ?? snapshotBackgroundWork(sessionId, nowMs);
188
+ // Providers may key items by the session file path or by getSessionId(); accept both.
189
+ const piSessionId = deps.state.supervisorOwnerSessionId;
190
+ const sessionIds = piSessionId ? [sessionId, piSessionId] : [sessionId];
191
+ return deps.backgroundWork?.snapshot(sessionIds, nowMs) ?? snapshotBackgroundWork(sessionIds, nowMs);
189
192
  }
190
193
  export function waitRunScopes(deps) {
191
194
  return [
@@ -561,6 +561,8 @@ async function runSingleAttempt(runtimeCwd, agent, task, model, options, shared)
561
561
  let agentSettledReceived = false;
562
562
  let queuedDrainHold = false;
563
563
  let compactionStartedReceived = false;
564
+ let terminalAgentEndReceived = false;
565
+ let compactionAfterFinalOpen = false;
564
566
  let finalDrainTimer;
565
567
  let finalHardFinishTimer;
566
568
  let watchdogTailTimer;
@@ -608,7 +610,8 @@ async function runSingleAttempt(runtimeCwd, agent, task, model, options, shared)
608
610
  finalDrainTimer = setTimeout(() => {
609
611
  if (lifecycleFinished || sessionSettled)
610
612
  return;
611
- if (capture.finalDrainHeld() || observeQueuedDrainHold()) {
613
+ // Pi runs auto-compaction after the final agent_end and only then emits agent_settled.
614
+ if (capture.finalDrainHeld() || observeQueuedDrainHold() || compactionAfterFinalOpen) {
612
615
  finalDrainTimer = undefined;
613
616
  armFinalDrainTimer();
614
617
  return;
@@ -623,9 +626,7 @@ async function runSingleAttempt(runtimeCwd, agent, task, model, options, shared)
623
626
  return;
624
627
  settle(undefined, true);
625
628
  }, HARD_FINISH_MS);
626
- finalHardFinishTimer.unref?.();
627
629
  }, FINAL_STOP_GRACE_MS);
628
- finalDrainTimer.unref?.();
629
630
  };
630
631
  function armWatchdogTail() {
631
632
  if ((!cleanTerminalAssistantStopReceived && !agentSettledReceived) || watchdogTailTimer || lifecycleFinished || sessionSettled)
@@ -644,12 +645,20 @@ async function runSingleAttempt(runtimeCwd, agent, task, model, options, shared)
644
645
  }, childWatchdog?.watchdogTailTimeoutMs ?? 120_000);
645
646
  watchdogTailTimer.unref?.();
646
647
  }
647
- const applyChildLifecycle = (action) => {
648
+ const applyChildLifecycle = (action, event) => {
648
649
  if (action === "cancel-drain") {
649
- cleanTerminalAssistantStopReceived = false;
650
+ // After a clean final stop, a non-retrying agent_end may be the child's last event
651
+ // (Pi may not emit agent_settled), so it re-arms the drain instead of cancelling it.
652
+ terminalAgentEndReceived = event?.type === "agent_end" && event.willRetry !== true && cleanTerminalAssistantStopReceived;
650
653
  agentSettledReceived = false;
651
654
  clearFinalDrainTimers();
652
655
  clearWatchdogTailTimer();
656
+ if (terminalAgentEndReceived) {
657
+ startFinalDrain();
658
+ return;
659
+ }
660
+ cleanTerminalAssistantStopReceived = false;
661
+ compactionAfterFinalOpen = false;
653
662
  return;
654
663
  }
655
664
  if (action === "start-drain")
@@ -925,11 +934,18 @@ async function runSingleAttempt(runtimeCwd, agent, task, model, options, shared)
925
934
  compactionStartedReceived = true;
926
935
  if (agentSettledReceived)
927
936
  afterCompactionSettlement = true;
937
+ if (terminalAgentEndReceived)
938
+ compactionAfterFinalOpen = true;
928
939
  }
929
940
  if (evt.type === "compaction_end" && evt.willRetry === true) {
930
941
  compactionStartedReceived = false;
931
942
  afterCompactionSettlement = false;
932
943
  }
944
+ if (evt.type === "compaction_end" && evt.willRetry !== true && compactionAfterFinalOpen) {
945
+ compactionAfterFinalOpen = false;
946
+ clearFinalDrainTimers();
947
+ startFinalDrain();
948
+ }
933
949
  if (evt.type === "turn_start" || evt.type === "agent_start" || evt.type === "auto_retry_start") {
934
950
  queuedDrainHold = false;
935
951
  }
@@ -956,7 +972,7 @@ async function runSingleAttempt(runtimeCwd, agent, task, model, options, shared)
956
972
  agentSettledReceived = true;
957
973
  afterCompactionSettlement = compactionStartedReceived;
958
974
  }
959
- applyChildLifecycle(lifecycleAction);
975
+ applyChildLifecycle(lifecycleAction, evt);
960
976
  if (isChildWatchdogStatusEvent(evt)) {
961
977
  if (!childWatchdog)
962
978
  return;
@@ -17,7 +17,7 @@ import { buildDoctorReport } from "../../extension/doctor.js";
17
17
  import { readSubagentGuide } from "../../extension/subagent-guide.js";
18
18
  import { isWorkflowScriptPath, normalizePublicSubagentExecution, validateWorkflowCapacityOverrides } from "../../extension/public-execution.js";
19
19
  import { readReplyWorkflowScript } from "../../extension/reply-workflow-script.js";
20
- import { disabledFeatureNotice, disabledFeatureUseError, resolveDisabledFeatureSurface } from "../../shared/disabled-features.js";
20
+ import { disabledFeatureNotice, disabledFeatureSectionNotice, disabledFeatureUseError, resolveDisabledFeatureSurface } from "../../shared/disabled-features.js";
21
21
  import { runSync } from "./execution.js";
22
22
  import { commandAction } from "./command-action.js";
23
23
  import { handleWatchdogToolAction, WATCHDOG_TOOL_ACTIONS } from "../../watchdog/tool-actions.js";
@@ -6689,10 +6689,7 @@ export function createSubagentExecutor(deps) {
6689
6689
  if (action === "guide") {
6690
6690
  try {
6691
6691
  const guide = readSubagentGuide(paramsWithResolvedCwd.topic);
6692
- const [topic, section] = paramsWithResolvedCwd.topic?.split("/") ?? [];
6693
- const fullNotice = topic === "tool-reference" ? disabledFeatureNotice(disabledFeatures) : undefined;
6694
- // Sections are capped at 8,000 chars, so they get a one-line pointer instead of the full list.
6695
- const notice = fullNotice && section !== undefined ? "Some options and actions documented here are disabled by config in this session; guide topic tool-reference lists them." : fullNotice;
6692
+ const notice = paramsWithResolvedCwd.topic?.includes("/") ? disabledFeatureSectionNotice(disabledFeatures) : disabledFeatureNotice(disabledFeatures);
6696
6693
  return {
6697
6694
  content: [{ type: "text", text: notice ? `${notice}\n\n${guide}` : guide }],
6698
6695
  details: { mode: "management", results: [] },
@@ -45,7 +45,7 @@ function escapeXmlAttr(value) {
45
45
  function inheritedCapabilityCeiling(inherited) {
46
46
  return inherited?.capabilityCeiling;
47
47
  }
48
- /** Environment values external child extensions read; only the runner applies them. */
48
+ /** Environment values external child extensions read in the runner, which hosts only children. */
49
49
  function childProcessEnv(input, toolPlan) {
50
50
  const env = {};
51
51
  env[PI_SUBAGENT_EXTENSION_BINDINGS_ENV] = encodeExtensionBindings(input.extensionBindings);
@@ -222,7 +222,10 @@ export function buildInProcessChildLaunch(input) {
222
222
  requiredExtensions: toolPlan.requiredExtensions,
223
223
  ambientExtensions,
224
224
  hooks: capturedHooks.hooks,
225
- ...(input.host === "runner" ? { processEnv: childProcessEnv(input, toolPlan) } : {}),
225
+ // A foreground child gets only its own bindings, so it never sees those of the session that launched it.
226
+ ...(input.host === "runner"
227
+ ? { processEnv: childProcessEnv(input, toolPlan) }
228
+ : { processEnv: { [PI_SUBAGENT_EXTENSION_BINDINGS_ENV]: encodeExtensionBindings(input.extensionBindings) }, restoreProcessEnv: true }),
226
229
  runtime: config,
227
230
  noSkills: !input.inheritSkills,
228
231
  noContextFiles: !input.inheritProjectContext,
@@ -80,6 +80,12 @@ export interface ChildSessionLaunch {
80
80
  * window one at a time. An undefined value removes the variable.
81
81
  */
82
82
  processEnv?: Record<string, string | undefined>;
83
+ /**
84
+ * Restore the hosting process's previous values of `processEnv` once that
85
+ * window ends, so a child hosted in the parent's own process leaves the
86
+ * parent's environment as it found it.
87
+ */
88
+ restoreProcessEnv?: boolean;
83
89
  /** The typed runtime config the hooks were built from; informational for factories. */
84
90
  runtime: ChildRuntimeConfig;
85
91
  onExtensionError?: (error: ChildSessionExtensionError) => void;
@@ -153,15 +153,18 @@ function resetExtensionCacheOnReload(loader) {
153
153
  loader.loaded = true;
154
154
  return true;
155
155
  }
156
+ /** Applies `values` to `process.env` and returns a function that puts back the values they replaced. */
156
157
  function applyProcessEnv(values) {
157
158
  if (!values)
158
- return;
159
+ return () => { };
160
+ const previous = Object.fromEntries(Object.keys(values).map((name) => [name, process.env[name]]));
159
161
  for (const [name, value] of Object.entries(values)) {
160
162
  if (value === undefined)
161
163
  delete process.env[name];
162
164
  else
163
165
  process.env[name] = value;
164
166
  }
167
+ return () => applyProcessEnv(previous);
165
168
  }
166
169
  /**
167
170
  * Pi's built-in MCP extension registers server tools with the configured exposure, `codemode` by
@@ -458,7 +461,11 @@ export function createDefaultChildSessionFactory(options = {}) {
458
461
  await commands?.shutdown();
459
462
  const runner = session.extensionRunner;
460
463
  if (runner.hasHandlers("session_shutdown")) {
461
- await Promise.race([runner.emit({ type: "session_shutdown", reason: "quit" }), new Promise((resolve) => setTimeout(resolve, shutdownTimeoutMs).unref?.())]);
464
+ // Ref'd so a detached runner with nothing else pending stays alive until the bound
465
+ // ends and still publishes its result; cleared as soon as the handlers finish.
466
+ let bound;
467
+ await Promise.race([runner.emit({ type: "session_shutdown", reason: "quit" }), new Promise((resolve) => { bound = setTimeout(resolve, shutdownTimeoutMs); })])
468
+ .finally(() => clearTimeout(bound));
462
469
  }
463
470
  }
464
471
  catch (error) {
@@ -469,8 +476,7 @@ export function createDefaultChildSessionFactory(options = {}) {
469
476
  }
470
477
  };
471
478
  let inheritedProviders = new Set();
472
- const open = async () => {
473
- applyProcessEnv(launch.processEnv);
479
+ const openSession = async () => {
474
480
  if (!resetExtensionCacheOnReload(loader) && (launch.ambientExtensions || launch.extensionPaths.length))
475
481
  launch.onExtensionError?.({ extensionPath: "<loader>", event: "load", error: new Error("pi's extension cache reset is unavailable; extensions loaded into this child share module state with other sessions in this process.") });
476
482
  await loader.reload();
@@ -559,6 +565,16 @@ export function createDefaultChildSessionFactory(options = {}) {
559
565
  }
560
566
  return session;
561
567
  };
568
+ const open = async () => {
569
+ const restoreEnv = applyProcessEnv(launch.processEnv);
570
+ try {
571
+ return await openSession();
572
+ }
573
+ finally {
574
+ if (launch.restoreProcessEnv)
575
+ restoreEnv();
576
+ }
577
+ };
562
578
  const opened = loading.catch(() => { }).then(open);
563
579
  loading = opened;
564
580
  const session = await opened;
@@ -42,6 +42,8 @@ export interface PreparedHerdrMachineExternalCliRun {
42
42
  export declare function resolveHerdrMachinePlacement(input: ResolveHerdrMachinePlacementInput): HerdrMachinePlacement;
43
43
  /** `local` resolves to a Rex pane on this machine; a name with a `rex` entry in user settings to a Rex pane on that SSH host; any other value to a Herdr saved machine. */
44
44
  export declare function resolveMachinePlacement(input: ResolveHerdrMachinePlacementInput): MachinePlacement;
45
+ /** A placed Pi keeps its transcript on the pane's machine, so it can't start from a local one. */
46
+ export declare const PLACED_RESTORE_UNSUPPORTED = "Pane-native remote Pi does not support fork, resume, or revival; use fresh context.";
45
47
  export declare function formatHerdrMachineRunnerUnsupported(input: {
46
48
  machine?: string;
47
49
  agentName: string;
@@ -233,6 +233,8 @@ export function resolveMachinePlacement(input) {
233
233
  return { machine: { provider: "rex", id: requested, target: settings.rex.ssh, cwd: validateRemoteCwd(remoteMachineCwd(input.stepCwd, settings.cwd, requested), requested) } };
234
234
  return resolveHerdrMachinePlacement(input);
235
235
  }
236
+ /** A placed Pi keeps its transcript on the pane's machine, so it can't start from a local one. */
237
+ export const PLACED_RESTORE_UNSUPPORTED = "Pane-native remote Pi does not support fork, resume, or revival; use fresh context.";
236
238
  export function formatHerdrMachineRunnerUnsupported(input) {
237
239
  if (input.machine === undefined)
238
240
  return undefined;
@@ -8,6 +8,7 @@ import { encodeHerdrPiFrame, HERDR_PI_MODE_ENV, HERDR_PI_PROTOCOL, HERDR_PI_RUN_
8
8
  import { discoverPlacedBridgeManifest } from "./placed-run-owner.js";
9
9
  import { RexPlacedRunOwner } from "./rex-pane-host.js";
10
10
  import { isRexLocalMachine } from "./rex-machine.js";
11
+ import { PLACED_RESTORE_UNSUPPORTED } from "./herdr-machine.js";
11
12
  /** The pane half comes from the host; the bridge half (run, package, protocol, native session, evidence cursor) is host-neutral. */
12
13
  export function reconcileHerdrReconnect(identity, candidate, lastEvidenceCursor, host = herdrPaneHost) {
13
14
  return host.reconcile(identity, candidate, () => { const bridge = candidate.bridge; if (bridge.runId !== identity.runId || bridge.nativeSessionId !== identity.nativeSessionId || bridge.packageVersion !== packageJson.version || bridge.protocol !== HERDR_PI_PROTOCOL)
@@ -35,16 +36,18 @@ export async function boundedHerdrReconnect(identity, lastEvidenceCursor, attemp
35
36
  }
36
37
  function safeRunId(value) { const prefix = value?.replace(/[^A-Za-z0-9_-]/gu, "_").slice(0, 48) ?? "run"; return validateHerdrPiRunId(`${prefix}_${randomBytes(8).toString("hex")}`); }
37
38
  export function serializeHerdrPiLaunch(launch) {
38
- if (launch.storage.kind === "file")
39
- throw new Error("Pane-native remote Pi does not support fork, resume, or revival; use fresh context.");
39
+ // Every child is handed a local session file path; only one that already exists (a fork or a
40
+ // resumed transcript) asks the pane to restore history. A fresh child's path is never written.
41
+ if (launch.storage.kind === "file" && fs.existsSync(launch.storage.sessionFile))
42
+ throw new Error(PLACED_RESTORE_UNSUPPORTED);
40
43
  if (launch.extensionPaths.length || launch.requiredExtensions?.length)
41
44
  throw new Error("Pane-native remote Pi cannot transfer local extension paths; install/configure pi-subagents on the remote machine.");
42
45
  if (launch.processEnv && Object.values(launch.processEnv).some((value) => value !== undefined && value !== "__none__"))
43
46
  throw new Error("Pane-native remote Pi cannot transfer local bindings, MCP selections, or process environment.");
44
47
  if (launch.runtime.nestedRoute || launch.runtime.fanoutChild || launch.runtime.depth > 1)
45
48
  throw new Error("Nested delegation from a pane-native remote Pi is not supported.");
46
- if (launch.excludeTools?.length || launch.runtime.permissions || launch.runtime.toolBudget || launch.runtime.requiredExtensions || launch.runtime.requiredTools?.length || launch.runtime.mcpDirectTools?.length)
47
- throw new Error("Pane-native remote Pi cannot yet represent exclusions, permission rules, tool budgets, MCP direct tools, or required-tool/extension contracts; remove machine placement.");
49
+ if (launch.excludeTools?.length || launch.runtime.permissions || launch.runtime.toolBudget || launch.runtime.requiredExtensions || launch.runtime.mcpDirectTools?.length)
50
+ throw new Error("Pane-native remote Pi cannot yet represent exclusions, permission rules, tool budgets, MCP direct tools, or required-extension contracts; remove machine placement.");
48
51
  const resources = launch.remoteResources;
49
52
  if (!resources || !/^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/u.test(resources.agent) || resources.skills?.some((name) => !/^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/u.test(name)) || resources.toolCeiling?.some((name) => !/^[A-Za-z0-9_-]{1,64}$/u.test(name)) || (resources.reads !== undefined && resources.reads !== false && resources.reads.some((read) => typeof read !== "string" || Buffer.byteLength(read) > 4096)))
50
53
  throw new Error("Pane-native remote resources require bounded logical agent, skill, tool, and read names.");
@@ -350,6 +353,10 @@ async function startPlacedPiSession(launch, owner, policy) {
350
353
  throw new Error("Remote Pi resource resolution identity mismatch.");
351
354
  if (!Array.isArray(configured.tools) || (policy.resources.toolCeiling && configured.tools.some((tool) => !policy.resources.toolCeiling.includes(String(tool)))))
352
355
  throw new Error("Remote Pi active-tool ceiling acknowledgement mismatch.");
356
+ // The remote Pi never receives the runtime contract, so required tools are checked here, against what it activated.
357
+ const missingRequired = (launch.runtime.requiredTools ?? []).filter((tool) => !configured.tools.includes(tool));
358
+ if (missingRequired.length)
359
+ throw new Error(`Remote Pi agent '${policy.resources.agent}' did not activate required tools: ${missingRequired.join(", ")}.`);
353
360
  if (launch.model && configured.model !== launch.model.split(":")[0])
354
361
  throw new Error(`Remote Pi model '${launch.model}' did not resolve to the requested provider/model.`);
355
362
  const identity = { runId, machineId: machine.id, target: isRexLocalMachine(machine) ? machine.id : machine.target, session: owner.host.sessionOf(connection), workspaceId: owned.workspaceId, tabId: owned.tabId, paneId: owned.paneId, terminalId, agentName, nativeSessionId: manifest.nativeSessionId, cwd: machine.cwd, runtimeDir };
@@ -669,6 +669,7 @@ export default function registerSubagentPromptRuntime(pi, config, drainObservati
669
669
  onRuntimeEvent("session_start", (_event, ctx) => {
670
670
  const sessionManager = ctx?.sessionManager;
671
671
  waitState.currentSessionId = sessionManager ? resolveCurrentSessionId(sessionManager) : null;
672
+ waitState.supervisorOwnerSessionId = sessionManager?.getSessionId() || null;
672
673
  registerNativeSupervisorClientOnce();
673
674
  });
674
675
  onRuntimeEvent("agent_start", () => {
@@ -724,30 +725,23 @@ export default function registerSubagentPromptRuntime(pi, config, drainObservati
724
725
  onRuntimeEvent("before_provider_headers", (event, ctx) => {
725
726
  applyForkSessionAffinity(event?.headers, ctx?.model, config.parentSessionId, readInherited(ctx)?.model);
726
727
  });
727
- if (config.forkCacheKey)
728
- onRuntimeEvent("context_with_system", (event, ctx) => {
729
- if (!event || typeof event !== "object" || !("messages" in event) || !Array.isArray(event.messages))
730
- return undefined;
731
- const fork = readInherited(ctx);
732
- if (!fork)
733
- return undefined;
734
- const messages = projectForkRequest(event.messages, fork.system, fork.forkedAt, { boundary });
735
- return messages === event.messages ? undefined : { messages };
736
- });
737
- onRuntimeEvent("context", (event, ctx) => {
728
+ // The strip runs on context_with_system, not context: Pi treats any change a context handler
729
+ // makes to the message list as a rewritten conversation and folds every mid-conversation system
730
+ // message into the leading one, so every later request would miss the prompt cache. The strip
731
+ // passes system messages through unchanged.
732
+ onRuntimeEvent("context_with_system", (event, ctx) => {
738
733
  if (!event || typeof event !== "object" || !("messages" in event) || !Array.isArray(event.messages))
739
734
  return undefined;
740
- const forkedAt = config.forkCacheKey ? Date.parse(ctx?.sessionManager?.getHeader()?.timestamp ?? "") : Number.NaN;
741
- const messages = stripParentOnlySubagentMessages(event.messages, {
735
+ const fork = config.forkCacheKey ? readInherited(ctx) : undefined;
736
+ const stripped = stripParentOnlySubagentMessages(event.messages, {
742
737
  sanitizeToolIds: !COMPOSITE_TOOL_ID_APIS.has(ctx?.model?.api ?? ""),
743
738
  // Providers that reuse the Codex Responses wire under their own api id (e.g. cliproxyapi-codex-responses) split call_id|item_id the same way.
744
739
  preserveBoundedCompositeToolIds: ctx?.model?.api?.endsWith("-codex-responses") === true,
745
740
  preserveFanoutToolHistory: config.fanoutChild,
746
- ...(Number.isFinite(forkedAt) ? { inheritedBefore: forkedAt } : {}),
741
+ ...(fork ? { inheritedBefore: fork.forkedAt } : {}),
747
742
  });
748
- if (messages === event.messages)
749
- return undefined;
750
- return { messages };
743
+ const messages = fork ? projectForkRequest(stripped, fork.system, fork.forkedAt, { boundary }) : stripped;
744
+ return messages === event.messages ? undefined : { messages };
751
745
  });
752
746
  // pi-intercom asks at session start for this session's intercom ID. Claiming
753
747
  // the route there frees the session name for the readable label. An older
@@ -86,6 +86,8 @@ interface FeatureConfig {
86
86
  export declare function resolveDisabledFeatureSurface(config: FeatureConfig): DisabledFeatureSurface;
87
87
  /** Returns why a request uses a disabled feature, or undefined when every requested field is enabled. */
88
88
  export declare function disabledFeatureUseError(request: object, surface: DisabledFeatureSurface, label?: string): string | undefined;
89
- /** Lists what config disabled, for prepending to static reference docs that describe the full tool. */
89
+ /** Lists what config disabled, for prepending to static guide docs that describe the full tool. */
90
90
  export declare function disabledFeatureNotice(surface: DisabledFeatureSurface): string | undefined;
91
+ /** One-line form for guide sections, which are capped at 8,000 chars. */
92
+ export declare function disabledFeatureSectionNotice(surface: DisabledFeatureSurface): string | undefined;
91
93
  export {};
@@ -99,14 +99,22 @@ export function disabledFeatureUseError(request, surface, label = "subagent") {
99
99
  }
100
100
  return undefined;
101
101
  }
102
- /** Lists what config disabled, for prepending to static reference docs that describe the full tool. */
102
+ const WORKFLOW_SCRIPTS_REPLACEMENT = "Instead of workflow scripts, use the top-level tasks or chain fields.";
103
+ /** Lists what config disabled, for prepending to static guide docs that describe the full tool. */
103
104
  export function disabledFeatureNotice(surface) {
104
105
  if (surface.features.size === 0)
105
106
  return undefined;
106
107
  const lines = [...surface.features].map((feature) => {
107
108
  const { actions, params, disabledBy } = featureSurface(feature);
108
109
  const parts = [`options ${params.join(", ")}`, ...(actions.length > 0 ? [`actions ${actions.join(", ")}`] : [])];
109
- return `- ${disabledBy}: ${parts.join("; ")}`;
110
+ return `- ${disabledBy}: ${parts.join("; ")}${feature === "workflow-scripts" ? `. ${WORKFLOW_SCRIPTS_REPLACEMENT}` : ""}`;
110
111
  });
111
- return `Disabled by config in this session. The reference below still lists these, but calls that use them are rejected:\n${lines.join("\n")}`;
112
+ return `Disabled by config in this session. The guide below may still describe these, but calls that use them are rejected:\n${lines.join("\n")}`;
113
+ }
114
+ /** One-line form for guide sections, which are capped at 8,000 chars. */
115
+ export function disabledFeatureSectionNotice(surface) {
116
+ if (surface.features.size === 0)
117
+ return undefined;
118
+ const replacement = surface.features.has("workflow-scripts") ? ` ${WORKFLOW_SCRIPTS_REPLACEMENT}` : "";
119
+ return `Some options and actions are disabled by config in this session; guide topic tool-reference lists them.${replacement}`;
112
120
  }
@@ -2278,7 +2278,7 @@ export interface ActiveAsyncCapacitySnapshot {
2278
2278
  export interface SubagentState {
2279
2279
  baseCwd: string;
2280
2280
  currentSessionId: string | null;
2281
- /** Exact SDK runtime session ID for supervisor ownership; never a session file path. */
2281
+ /** Exact SDK runtime session ID (`getSessionId()`) for supervisor ownership and background-work matching; never a session file path. */
2282
2282
  supervisorOwnerSessionId?: string | null;
2283
2283
  /** Session for which active status projections were restored successfully. */
2284
2284
  statusProjectionSessionId?: string | null;
@@ -2410,6 +2410,8 @@ export declare const SUBAGENT_CONTROL_EVENT = "subagent:control-event";
2410
2410
  export declare const SUBAGENT_CONTROL_INTERCOM_EVENT = "subagent:control-intercom";
2411
2411
  export declare const SUBAGENT_STEERING_NOTICE_EVENT = "subagent:steering-notice";
2412
2412
  export declare const SUBAGENT_CHILD_STATUS_EVENT = "subagent:child-status";
2413
+ /** Payload is only `{ version: 1 }`: a hint for hosts to read `status` again, not a description of the change. */
2414
+ export declare const SUBAGENT_JOBS_CHANGED_EVENT = "subagent:jobs-changed";
2413
2415
  export declare const SUBAGENT_RESULT_INTERCOM_EVENT = "subagent:result-intercom";
2414
2416
  export declare const SUBAGENT_RESULT_INTERCOM_DELIVERY_EVENT = "subagent:result-intercom-delivery";
2415
2417
  export interface SubagentChildStatusEvent {
@@ -19,6 +19,8 @@ export const SUBAGENT_CONTROL_EVENT = "subagent:control-event";
19
19
  export const SUBAGENT_CONTROL_INTERCOM_EVENT = "subagent:control-intercom";
20
20
  export const SUBAGENT_STEERING_NOTICE_EVENT = "subagent:steering-notice";
21
21
  export const SUBAGENT_CHILD_STATUS_EVENT = "subagent:child-status";
22
+ /** Payload is only `{ version: 1 }`: a hint for hosts to read `status` again, not a description of the change. */
23
+ export const SUBAGENT_JOBS_CHANGED_EVENT = "subagent:jobs-changed";
22
24
  export const SUBAGENT_RESULT_INTERCOM_EVENT = "subagent:result-intercom";
23
25
  export const SUBAGENT_RESULT_INTERCOM_DELIVERY_EVENT = "subagent:result-intercom-delivery";
24
26
  export const FLEET_KEYBINDING_ACTIONS = [