pi-subagents 0.81.0 → 0.82.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 (141) hide show
  1. package/CHANGELOG.md +38 -0
  2. package/bundle/agents.js +3 -3
  3. package/bundle/child-tool-plan.js +2 -2
  4. package/bundle/{chunk-LSRUTHT7.js → chunk-43SCHIMD.js} +1 -1
  5. package/bundle/{chunk-G5DKFLLI.js → chunk-4SDU2BZX.js} +15 -10
  6. package/bundle/{chunk-V2JWDB5U.js → chunk-52UBALJL.js} +2 -2
  7. package/bundle/{chunk-3DC5KBVP.js → chunk-5TYMSZX3.js} +1 -1
  8. package/bundle/{chunk-CEKS3TFO.js → chunk-7IVSUMQQ.js} +6 -6
  9. package/bundle/{chunk-KSWQVGDW.js → chunk-7PCKZHR5.js} +225 -18
  10. package/bundle/{chunk-563BHAZ5.js → chunk-7YMXOYEU.js} +33 -25
  11. package/bundle/{chunk-XMFLCKMQ.js → chunk-B5EKXDHE.js} +336 -130
  12. package/bundle/{chunk-ZBC44JJ3.js → chunk-CIZQXJ5Q.js} +22 -9
  13. package/bundle/{chunk-L5ZHE4L4.js → chunk-GC4DSPMH.js} +4 -4
  14. package/bundle/{chunk-ARRKBB2B.js → chunk-GJECHSAR.js} +2 -2
  15. package/bundle/{chunk-2ZSNH26H.js → chunk-M2CTFYT3.js} +7 -7
  16. package/bundle/{chunk-TUWLXTAM.js → chunk-MHZ4G23A.js} +681 -320
  17. package/bundle/{chunk-WSBNENFS.js → chunk-NGGAFDL6.js} +14 -4
  18. package/bundle/{chunk-VJHOELQP.js → chunk-NRQ75MGH.js} +160 -78
  19. package/bundle/{chunk-BJN5LUQY.js → chunk-OYF6AFDY.js} +1 -1
  20. package/bundle/{chunk-EYNJ6VL6.js → chunk-PLCCBLPY.js} +2 -2
  21. package/bundle/{chunk-74RT5YQE.js → chunk-RMQFMMAT.js} +123 -33
  22. package/bundle/{chunk-V6MR5TOQ.js → chunk-SIJ4S3EN.js} +25 -19
  23. package/bundle/{chunk-NNLASTQE.js → chunk-VH5OAWVG.js} +19 -5
  24. package/bundle/{chunk-BPS2AGBZ.js → chunk-W2SZ22EM.js} +93 -52
  25. package/bundle/{chunk-V3BIUPLU.js → chunk-WVD4UEL4.js} +256 -52
  26. package/bundle/{chunk-ZP3HODFH.js → chunk-XSWOTIA3.js} +1 -1
  27. package/bundle/{chunk-3IF2PGW7.js → chunk-YKEWIB4M.js} +1 -1
  28. package/bundle/{chunk-26C3WLYQ.js → chunk-ZLINIYTP.js} +497 -272
  29. package/bundle/{chunk-2XYWN7QJ.js → chunk-ZQGSDHL6.js} +2 -2
  30. package/bundle/control-channel.js +2 -2
  31. package/bundle/index.js +1 -1
  32. package/bundle/intercom-bridge.js +2 -2
  33. package/bundle/preflight.js +8 -8
  34. package/bundle/project-panes.js +3 -3
  35. package/docs/agents.md +31 -0
  36. package/docs/configuration.md +9 -1
  37. package/docs/tool-reference.md +3 -1
  38. package/docs/workflows.md +16 -0
  39. package/package.json +2 -2
  40. package/skills/pi-subagents/references/execution-controls.md +5 -4
  41. package/src/agents/agent-management.js +3 -3
  42. package/src/agents/agent-memory.js +3 -1
  43. package/src/agents/agents.js +6 -6
  44. package/src/agents/frontmatter.d.ts +2 -0
  45. package/src/agents/frontmatter.js +15 -1
  46. package/src/agents/skills.js +2 -1
  47. package/src/extension/config.js +3 -0
  48. package/src/extension/herdr-pi-bridge.js +17 -10
  49. package/src/extension/index.js +82 -2
  50. package/src/extension/schemas.js +3 -3
  51. package/src/intercom/native-supervisor-channel.js +20 -0
  52. package/src/missions/lifecycle.js +7 -1
  53. package/src/runs/background/active-async-capacity.d.ts +1 -0
  54. package/src/runs/background/active-async-capacity.js +9 -3
  55. package/src/runs/background/async-execution.js +20 -10
  56. package/src/runs/background/async-job-tracker.d.ts +6 -1
  57. package/src/runs/background/async-job-tracker.js +9 -4
  58. package/src/runs/background/async-status.d.ts +2 -0
  59. package/src/runs/background/async-status.js +21 -2
  60. package/src/runs/background/await-async-run.js +3 -7
  61. package/src/runs/background/boot-id.d.ts +12 -0
  62. package/src/runs/background/boot-id.js +47 -0
  63. package/src/runs/background/interrupted-elsewhere.d.ts +22 -0
  64. package/src/runs/background/interrupted-elsewhere.js +68 -0
  65. package/src/runs/background/notify.d.ts +2 -0
  66. package/src/runs/background/notify.js +13 -0
  67. package/src/runs/background/result-delivery-ownership.d.ts +6 -0
  68. package/src/runs/background/result-delivery-ownership.js +19 -1
  69. package/src/runs/background/result-watcher.d.ts +3 -1
  70. package/src/runs/background/result-watcher.js +24 -8
  71. package/src/runs/background/run-child-session.d.ts +1 -6
  72. package/src/runs/background/run-child-session.js +1 -1
  73. package/src/runs/background/run-status.js +2 -1
  74. package/src/runs/background/stale-run-reconciler.d.ts +25 -0
  75. package/src/runs/background/stale-run-reconciler.js +52 -13
  76. package/src/runs/background/subagent-runner-bootstrap.js +3 -0
  77. package/src/runs/background/subagent-runner.d.ts +4 -0
  78. package/src/runs/background/subagent-runner.js +13 -5
  79. package/src/runs/background/subagent-wait.d.ts +1 -1
  80. package/src/runs/background/subagent-wait.js +6 -1
  81. package/src/runs/background/terminal-run-index.d.ts +1 -0
  82. package/src/runs/background/terminal-run-index.js +4 -2
  83. package/src/runs/foreground/async-stop-action.js +3 -1
  84. package/src/runs/foreground/execution.js +6 -3
  85. package/src/runs/foreground/foreground-history.d.ts +16 -2
  86. package/src/runs/foreground/foreground-history.js +133 -31
  87. package/src/runs/foreground/subagent-executor.d.ts +11 -0
  88. package/src/runs/foreground/subagent-executor.js +277 -74
  89. package/src/runs/shared/child-launch.d.ts +2 -2
  90. package/src/runs/shared/child-session.d.ts +3 -2
  91. package/src/runs/shared/child-session.js +51 -8
  92. package/src/runs/shared/child-tool-plan.d.ts +7 -0
  93. package/src/runs/shared/child-tool-plan.js +21 -6
  94. package/src/runs/shared/herdr-connection.d.ts +17 -8
  95. package/src/runs/shared/herdr-connection.js +19 -7
  96. package/src/runs/shared/herdr-external-adapters.d.ts +3 -2
  97. package/src/runs/shared/herdr-external-adapters.js +1 -1
  98. package/src/runs/shared/herdr-machine.d.ts +12 -2
  99. package/src/runs/shared/herdr-machine.js +55 -9
  100. package/src/runs/shared/herdr-pane-host.d.ts +88 -0
  101. package/src/runs/shared/herdr-pane-host.js +155 -0
  102. package/src/runs/shared/herdr-placed-run.d.ts +27 -150
  103. package/src/runs/shared/herdr-placed-run.js +48 -395
  104. package/src/runs/shared/local-machine-access.d.ts +5 -0
  105. package/src/runs/shared/local-machine-access.js +28 -0
  106. package/src/runs/shared/orca-progress-tabs.d.ts +0 -1
  107. package/src/runs/shared/orca-progress-tabs.js +1 -24
  108. package/src/runs/shared/parallel-utils.d.ts +1 -1
  109. package/src/runs/shared/placed-run-owner.d.ts +49 -0
  110. package/src/runs/shared/placed-run-owner.js +93 -0
  111. package/src/runs/shared/placement-host.d.ts +99 -0
  112. package/src/runs/shared/placement-host.js +157 -0
  113. package/src/runs/shared/rex-machine.d.ts +25 -0
  114. package/src/runs/shared/rex-machine.js +83 -0
  115. package/src/runs/shared/rex-pane-host.d.ts +62 -0
  116. package/src/runs/shared/rex-pane-host.js +327 -0
  117. package/src/shared/completion-owner.d.ts +5 -0
  118. package/src/shared/completion-owner.js +5 -0
  119. package/src/shared/session-identity.d.ts +2 -0
  120. package/src/shared/session-identity.js +26 -0
  121. package/src/shared/types.d.ts +45 -6
  122. package/src/tui/fleet.d.ts +1 -0
  123. package/src/tui/fleet.js +24 -15
  124. package/src/tui/render.d.ts +6 -0
  125. package/src/tui/render.js +3 -3
  126. package/src/tui/subagent-messages.js +1 -0
  127. package/src/workflows/scripted-workflow.d.ts +1 -1
  128. package/src/workflows/workflow-auto-resume.d.ts +32 -0
  129. package/src/workflows/workflow-auto-resume.js +102 -0
  130. package/src/workflows/workflow-launch-record.d.ts +27 -0
  131. package/src/workflows/workflow-launch-record.js +48 -0
  132. package/src/workflows/workflow-resume-claim.d.ts +82 -0
  133. package/src/workflows/workflow-resume-claim.js +210 -0
  134. package/src/workflows/workflow-resume.d.ts +56 -0
  135. package/src/workflows/workflow-resume.js +106 -0
  136. package/src/workflows/workflow-reuse.d.ts +15 -9
  137. package/src/workflows/workflow-reuse.js +77 -14
  138. package/src/workflows/workflow-revival.d.ts +2 -0
  139. package/src/workflows/workflow-revival.js +2 -1
  140. package/src/workflows/workflow-stop-cause.d.ts +6 -0
  141. package/src/workflows/workflow-stop-cause.js +8 -0
@@ -49,7 +49,7 @@ import { nestedSummaryFromAsyncStatus, projectNestedEvents, resolveNestedAsyncDi
49
49
  import { isContextOverflow } from "../shared/model-resolution.js";
50
50
  import { processTerminalPath, writeProcessTerminalCandidate } from "./process-terminal.js";
51
51
  import { persistRunnerStartupFailure } from "./runner-startup-failure.js";
52
- import { currentPidNamespaceScope } from "./pid-namespace.js";
52
+ import { currentRecordedPid } from "./boot-id.js";
53
53
  import { createSteeringStatus, recordSteeringRequest, steeringStatus, terminalSteeringNoticeState, unconsumedSteerReason, updateSteeringTarget } from "./steering.js";
54
54
  import { PROMPT_REDACTED, SUBAGENT_STOPPED_MESSAGE, SUBAGENT_TIMED_OUT_MESSAGE, detectSubagentError, extractTextFromContent, extractToolArgsPreview, formatEmptyTerminalAssistantResponseError, getAgentDir, hasEmptyTerminalAssistantResponse, readStatus } from "../../shared/utils.js";
55
55
  import { planAbortRecovery } from "../shared/abort-recovery.js";
@@ -77,6 +77,7 @@ import { resolveCodexExecLaunch } from "../shared/codex-exec-adapter.js";
77
77
  import { resolveCursorAgentLaunch } from "../shared/cursor-agent-adapter.js";
78
78
  import { resolveExternalCliRunnerStatus } from "../shared/external-cli-contract.js";
79
79
  import { formatHerdrMachineHint, prepareHerdrMachineExternalCliRun } from "../shared/herdr-machine.js";
80
+ import { isRexLocalMachine } from "../shared/rex-machine.js";
80
81
  import { HerdrExternalNeedsAttentionError, createHerdrExternalAdapter, prepareInternalHerdrExternalAdapter } from "../shared/herdr-external-adapters.js";
81
82
  import { runExternalJob } from "../shared/external-job-runner.js";
82
83
  import { createOrcaProgressTab } from "../shared/orca-progress-tabs.js";
@@ -522,6 +523,7 @@ export async function runSingleStepInner(step, ctx) {
522
523
  capabilityCeiling: step.capabilityCeiling ?? ctx.capabilityCeiling,
523
524
  inheritedCapabilityCeiling: ctx.inheritedChildRuntime?.capabilityCeiling,
524
525
  requiredExtensions: step.requiredExtensions ?? ctx.inheritedChildRuntime?.requiredExtensions,
526
+ agentName: step.agent,
525
527
  permissionRules: step.permissionRules,
526
528
  }));
527
529
  }
@@ -608,6 +610,8 @@ export async function runSingleStepInner(step, ctx) {
608
610
  return omitUndefinedProperties({ agent: step.agent, context: step.context, output: message, error: message, exitCode: 1, stopped: stopped || undefined, timedOut: stopped ? undefined : true });
609
611
  }
610
612
  if (step.machine) {
613
+ if (step.machine.provider !== "herdr")
614
+ throw new Error("Rex supports native Pi children only.");
611
615
  const runner = resolveExternalCliRunnerStatus({ ...step.runner, machine: step.machine });
612
616
  const outputSnapshot = captureSingleOutputSnapshot(step.outputPath);
613
617
  const placedRunId = `${ctx.id}-${ctx.flatIndex}`;
@@ -781,7 +785,8 @@ export async function runSingleStepInner(step, ctx) {
781
785
  let toolBudget = step.toolBudget ? initialToolBudgetState(step.toolBudget) : undefined;
782
786
  let toolBudgetBlocked = false;
783
787
  let actualLaunchContractDigest = step.launchContractDigest;
784
- const mutationSnapshot = step.machine ? { source: "tracked-files", trackedOnly: true, cwd: step.cwd ?? ctx.cwd, dirtyFiles: [], fingerprints: {}, unavailable: "Local Git evidence is not authoritative for a pane-native remote run." } : snapshotTrackedMutations(step.cwd ?? ctx.cwd);
788
+ // A machine 'local' child edits this disk, so its local Git evidence is authoritative, as for an unplaced run.
789
+ const mutationSnapshot = step.machine && !isRexLocalMachine(step.machine) ? { source: "tracked-files", trackedOnly: true, cwd: step.cwd ?? ctx.cwd, dirtyFiles: [], fingerprints: {}, unavailable: "Local Git evidence is not authoritative for a pane-native remote run." } : snapshotTrackedMutations(step.cwd ?? ctx.cwd);
785
790
  let finalMutationEvidence = collectTrackedMutationEvidence(mutationSnapshot, step.cwd ?? ctx.cwd);
786
791
  let contextOverflow = false;
787
792
  let launchWarningsEmitted = false;
@@ -1004,7 +1009,7 @@ export async function runSingleStepInner(step, ctx) {
1004
1009
  ? formatEmptyTerminalAssistantResponseError(run.messages)
1005
1010
  : undefined;
1006
1011
  const remoteGitChanged = run.nativeMachine?.initialGit && run.nativeMachine.finalGit ? run.nativeMachine.initialGit.head !== run.nativeMachine.finalGit.head || run.nativeMachine.initialGit.dirty !== run.nativeMachine.finalGit.dirty : undefined;
1007
- const mutationEvidence = run.nativeMachine ? { source: "tracked-files", trackedOnly: true, changedFiles: [], attemptedMutation: remoteGitChanged === true, ...(remoteGitChanged === undefined ? { unavailable: "Remote Git before/after evidence was incomplete." } : {}) } : collectTrackedMutationEvidence(mutationSnapshot, step.cwd ?? ctx.cwd);
1012
+ const mutationEvidence = run.nativeMachine && !isRexLocalMachine(step.machine) ? { source: "tracked-files", trackedOnly: true, changedFiles: [], attemptedMutation: remoteGitChanged === true, ...(remoteGitChanged === undefined ? { unavailable: "Remote Git before/after evidence was incomplete." } : {}) } : collectTrackedMutationEvidence(mutationSnapshot, step.cwd ?? ctx.cwd);
1008
1013
  finalMutationEvidence = mutationEvidence;
1009
1014
  const mutationAttemptObserved = run.observedMutationAttempt === true || mutationEvidence.attemptedMutation === true;
1010
1015
  // Validated structured output is persistable file content even when the final text is only
@@ -1669,6 +1674,7 @@ export async function runSubagent(config, childSessions) {
1669
1674
  ...(config.toolCallId ? { toolCallId: config.toolCallId } : {}),
1670
1675
  ...(config.sessionId ? { sessionId: config.sessionId } : {}),
1671
1676
  ...(config.completionOwnerId ? { completionOwnerId: config.completionOwnerId } : {}),
1677
+ ...(config.completionOwnerProcess ? { completionOwnerProcess: config.completionOwnerProcess } : {}),
1672
1678
  mode: config.resultMode ?? (flatSteps.length > 1 ? "chain" : "single"),
1673
1679
  ...(config.nestedSelf ? { isNested: true } : {}),
1674
1680
  state: "running",
@@ -1680,8 +1686,7 @@ export async function runSubagent(config, childSessions) {
1680
1686
  ...(config.deadlineAt !== undefined ? { deadlineAt: config.deadlineAt } : {}),
1681
1687
  ...(config.toolBudget ? { toolBudget: initialToolBudgetState(config.toolBudget) } : {}),
1682
1688
  ...(config.usageBudget ? { usageBudget: usageBudgetState(config.usageBudget, undefined) } : {}),
1683
- pid: process.pid,
1684
- pidNamespaceScope: currentPidNamespaceScope(),
1689
+ ...currentRecordedPid(),
1685
1690
  cwd,
1686
1691
  currentStep: 0,
1687
1692
  chainStepCount: steps.length,
@@ -1909,6 +1914,8 @@ export async function runSubagent(config, childSessions) {
1909
1914
  cwd,
1910
1915
  sessionId: config.sessionId,
1911
1916
  completionOwnerId: config.completionOwnerId,
1917
+ completionOwnerProcess: config.completionOwnerProcess,
1918
+ parentWorkflowRunId: config.parentWorkflowRunId,
1912
1919
  sessionFile: statusPayload.sessionFile ?? latestSessionFile,
1913
1920
  }), (filePath, payload) => publishRunResult(filePath, () => writePendingAsyncResultFile(filePath, payload)));
1914
1921
  };
@@ -4797,6 +4804,7 @@ export async function runSubagent(config, childSessions) {
4797
4804
  runtimeAcknowledgedExtensions: singleRuntimeAcknowledgedExtensions,
4798
4805
  sessionId: config.sessionId,
4799
4806
  completionOwnerId: config.completionOwnerId,
4807
+ completionOwnerProcess: config.completionOwnerProcess,
4800
4808
  sessionFile: effectiveSessionFile,
4801
4809
  intercomTarget: config.controlIntercomTarget,
4802
4810
  shareUrl,
@@ -54,7 +54,7 @@ export interface SubagentWaitParams {
54
54
  all?: boolean;
55
55
  /** Give up after this many milliseconds. Defaults to waitTool.defaultTimeoutMs, then 30 minutes. */
56
56
  timeoutMs?: number;
57
- /** False keeps a blocking wait open through idle attention; supervisor/contact requests still stop the wait. */
57
+ /** False keeps a blocking wait open through all attention except supervisor/contact requests, such as idle time, a tool held open, or repeated tool failures. */
58
58
  stopOnAttention?: boolean;
59
59
  }
60
60
  /** Minimal event-bus surface wait subscribes to (matches pi.events). */
@@ -707,8 +707,13 @@ export async function waitForSubagents(params, signal, deps) {
707
707
  const supervisorAttentionHint = relevantAttention.some(hasSupervisorTool)
708
708
  ? " Reply to any pending supervisor request. If subagent_supervisor({ action: \"pending\" }) is empty, check intercom({ action: \"pending\" }) because an external intercom tool may own the request."
709
709
  : "";
710
+ // Only when watchdog attention alone ended the wait: stopOnAttention: false would have kept blocking.
711
+ const keepWaitingHint = stopOnAttention && supervisorAttentionHint === ""
712
+ && (waitForAll || finishedAsyncCount + providerFinishedCount === 0)
713
+ ? " These runs are still running and none is waiting on a supervisor reply. To keep waiting through this kind of attention, call bg_wait again with stopOnAttention: false; supervisor requests still end that wait."
714
+ : "";
710
715
  const attentionNote = relevantAttention.length > 0
711
- ? ` ${relevantAttention.length} run(s) need attention: ${relevantAttention.map((run) => run.id).join(", ")} —${supervisorAttentionHint} inspect with subagent({ action: "status" }) then steer a top-level live async child, resume a paused/completed/failed child, or interrupt explicitly.`
716
+ ? ` ${relevantAttention.length} run(s) need attention: ${relevantAttention.map((run) => run.id).join(", ")} —${supervisorAttentionHint} inspect with subagent({ action: "status" }) then steer a top-level live async child, resume a paused/completed/failed child, or interrupt explicitly.${keepWaitingHint}`
712
717
  : "";
713
718
  const stillRunning = active.filter((run) => initialAsyncIds.has(run.id)).length
714
719
  + providerActive.filter((item) => initialProviderIds.has(backgroundWorkIdentity(item))).length;
@@ -5,4 +5,5 @@ export declare function readTerminalRunToolCallIndex(asyncDirRoot: string, toolC
5
5
  export declare function readRecentTerminalRunIndex(asyncDirRoot: string, options?: {
6
6
  sessionId?: string;
7
7
  limit?: number;
8
+ endedAfter?: number;
8
9
  }): string[];
@@ -99,8 +99,10 @@ function sessionDirs(asyncDirRoot, sessionId) {
99
99
  throw error;
100
100
  }
101
101
  }
102
- function recentMarkerFiles(dirs, limit) {
102
+ function recentMarkerFiles(dirs, limit, endedAfter) {
103
103
  return dirs.flatMap(markerFiles)
104
+ // The name starts with the run's end time, so an old run is skipped without reading its marker.
105
+ .filter((marker) => endedAfter === undefined || !(Number(marker.name.slice(0, TIMESTAMP_WIDTH)) < endedAfter))
104
106
  .sort((left, right) => right.name.localeCompare(left.name))
105
107
  .slice(0, limit)
106
108
  .map((marker) => path.join(marker.dir, marker.name));
@@ -135,7 +137,7 @@ export function readTerminalRunToolCallIndex(asyncDirRoot, toolCallId) {
135
137
  }
136
138
  export function readRecentTerminalRunIndex(asyncDirRoot, options = {}) {
137
139
  const limit = options.limit === undefined ? Number.POSITIVE_INFINITY : Math.max(0, Math.floor(options.limit));
138
- const candidates = recentMarkerFiles(sessionDirs(asyncDirRoot, options.sessionId), limit);
140
+ const candidates = recentMarkerFiles(sessionDirs(asyncDirRoot, options.sessionId), limit, options.endedAfter);
139
141
  const runIds = [];
140
142
  const seen = new Set();
141
143
  for (const marker of candidates) {
@@ -44,6 +44,8 @@ function sealPausedRun(asyncDir, status, resultsDir) {
44
44
  sessionId: status.sessionId,
45
45
  asyncDir,
46
46
  ...(status.completionOwnerId ? { completionOwnerId: status.completionOwnerId } : {}),
47
+ ...(status.completionOwnerProcess ? { completionOwnerProcess: status.completionOwnerProcess } : {}),
48
+ ...(status.parentWorkflowRunId ? { parentWorkflowRunId: status.parentWorkflowRunId } : {}),
47
49
  ...(status.toolCallId ? { toolCallId: status.toolCallId } : {}),
48
50
  results: (status.steps ?? []).map((step) => ({
49
51
  agent: step.agent,
@@ -146,7 +148,7 @@ export function stopAsyncRun(state, runId, kill, location, childId) {
146
148
  if (!target)
147
149
  return null;
148
150
  const status = reconcileAsyncRun(target.asyncDir, { kill }).status;
149
- // A workflow relaunched with options.reuseFrom stops the exact child run it re-attached, whichever session launched it.
151
+ // A relaunched workflow stops the exact child run it re-attached from its relaunch chain, whichever session launched it.
150
152
  if (state.currentSessionId && !ownsRun(state.currentSessionId, status?.sessionId) && location?.reattachedRunId !== target.asyncId) {
151
153
  return {
152
154
  content: [{ type: "text", text: `Async run '${target.asyncId}' was not found in the active session.` }],
@@ -46,6 +46,7 @@ import { buildInProcessChildLaunch, createReportedChildSessionInput } from "../s
46
46
  import { childSessionFactory, childSessionHasQueuedMessages, projectChildSessionEventForJson } from "../shared/child-session.js";
47
47
  import { reconcileAttemptUsage } from "../shared/usage-reconciliation.js";
48
48
  import { clearProviderRetry, observeProviderDiagnostics, snapshotProviderDiagnostics } from "../../shared/provider-diagnostics.js";
49
+ import { isRexLocalMachine } from "../shared/rex-machine.js";
49
50
  const artifactOutputByResult = new WeakMap();
50
51
  const acceptanceOutputByResult = new WeakMap();
51
52
  function emptyUsage() {
@@ -441,7 +442,9 @@ async function runSingleAttempt(runtimeCwd, agent, task, model, options, shared)
441
442
  };
442
443
  return result;
443
444
  }
444
- const mutationSnapshot = options.machine ? { source: "tracked-files", trackedOnly: true, cwd: options.cwd ?? runtimeCwd, dirtyFiles: [], fingerprints: {}, unavailable: "Local Git evidence is not authoritative for a pane-native remote run." } : snapshotTrackedMutations(options.cwd ?? runtimeCwd);
445
+ // A machine 'local' child edits this disk, so its local Git evidence is authoritative, as for an unplaced run.
446
+ const mutationCwd = isRexLocalMachine(options.machine) ? options.machine.cwd : options.cwd ?? runtimeCwd;
447
+ const mutationSnapshot = options.machine && !isRexLocalMachine(options.machine) ? { source: "tracked-files", trackedOnly: true, cwd: mutationCwd, dirtyFiles: [], fingerprints: {}, unavailable: "Local Git evidence is not authoritative for a pane-native remote run." } : snapshotTrackedMutations(mutationCwd);
445
448
  let structuredOutputToolInvoked = false;
446
449
  let structuredOutputMessageStartIndex;
447
450
  let toolAvailabilityError;
@@ -1259,7 +1262,7 @@ async function runSingleAttempt(runtimeCwd, agent, task, model, options, shared)
1259
1262
  toolAvailabilityError = toolDiagnosticError;
1260
1263
  result.runtimeAcknowledgedExtensions = capture.runtimeAcknowledgedExtensions();
1261
1264
  if (session?.machineEvidence)
1262
- result.nativeMachine = { provider: "herdr", machineId: session.machineEvidence.machineId, ...(session.machineEvidence.initial ? { initialGit: session.machineEvidence.initial } : {}), ...(session.machineEvidence.final ? { finalGit: session.machineEvidence.final } : {}) };
1265
+ result.nativeMachine = { provider: session.machineEvidence.provider, machineId: session.machineEvidence.machineId, ...(session.machineEvidence.initial ? { initialGit: session.machineEvidence.initial } : {}), ...(session.machineEvidence.final ? { finalGit: session.machineEvidence.final } : {}) };
1263
1266
  let closeError = result.error ?? toolDiagnosticError ?? assistantError;
1264
1267
  const promptErrorMessage = promptError === undefined ? undefined : promptError instanceof Error ? promptError.message : String(promptError);
1265
1268
  if (promptError !== undefined)
@@ -1533,7 +1536,7 @@ async function runSingleAttempt(runtimeCwd, agent, task, model, options, shared)
1533
1536
  durationMs: progress.durationMs,
1534
1537
  };
1535
1538
  const remoteGitChanged = result.nativeMachine?.initialGit && result.nativeMachine.finalGit ? result.nativeMachine.initialGit.head !== result.nativeMachine.finalGit.head || result.nativeMachine.initialGit.dirty !== result.nativeMachine.finalGit.dirty : undefined;
1536
- const mutationEvidence = result.nativeMachine ? { source: "tracked-files", trackedOnly: true, changedFiles: [], attemptedMutation: remoteGitChanged === true, ...(remoteGitChanged === undefined ? { unavailable: "Remote Git before/after evidence was incomplete." } : {}) } : collectTrackedMutationEvidence(mutationSnapshot, options.cwd ?? runtimeCwd);
1539
+ const mutationEvidence = result.nativeMachine && !isRexLocalMachine(options.machine) ? { source: "tracked-files", trackedOnly: true, changedFiles: [], attemptedMutation: remoteGitChanged === true, ...(remoteGitChanged === undefined ? { unavailable: "Remote Git before/after evidence was incomplete." } : {}) } : collectTrackedMutationEvidence(mutationSnapshot, mutationCwd);
1537
1540
  const acceptanceOutput = getFinalOutput(result.messages ?? []);
1538
1541
  const structuredText = result.structuredOutput === undefined ? undefined : JSON.stringify(result.structuredOutput, null, 2);
1539
1542
  let fullOutput = stripAcceptanceReport(acceptanceOutput);
@@ -1,11 +1,25 @@
1
- import type { SubagentState } from "../../shared/types.ts";
1
+ import type { ForegroundResumeChild, ForegroundResumeRun, SubagentState } from "../../shared/types.ts";
2
2
  export declare const MAX_REMEMBERED_FOREGROUND_RUNS = 50;
3
- export declare function persistForegroundRunHistory(state: SubagentState, options?: {
3
+ export declare const FOREGROUND_OWNER_EXITED_STOP_ERROR = "Subagent stopped because the Pi process that ran it exited before the run finished.";
4
+ /**
5
+ * Saves one foreground run to its own history file. A run with a `running` child is a launch-time
6
+ * record; the run's final save replaces it.
7
+ */
8
+ export declare function persistForegroundRun(run: ForegroundResumeRun, options?: {
4
9
  resultsDir?: string;
5
10
  limit?: number;
6
11
  }): void;
12
+ /** Drops this process's `running` record for a launch that threw, so it never loads as interrupted. */
13
+ export declare function discardRunningForegroundRun(runId: string, options?: {
14
+ resultsDir?: string;
15
+ }): void;
16
+ /** For a record with a `running` child: its owning Pi process is gone. */
17
+ export declare function ownerConfirmedDead(run: ForegroundResumeRun): boolean;
18
+ /** State text for a remembered child; a child cut off by a Pi exit names the resume target. */
19
+ export declare function foregroundChildStateLabel(run: Pick<ForegroundResumeRun, "runId">, child: Pick<ForegroundResumeChild, "status" | "error" | "sessionFile">): string;
7
20
  export declare function restoreForegroundRunHistory(state: SubagentState, options?: {
8
21
  resultsDir?: string;
9
22
  sessionId?: string | null;
10
23
  limit?: number;
24
+ observeForeign?: (run: ForegroundResumeRun) => void;
11
25
  }): number;
@@ -5,12 +5,18 @@ import { writePrivateAtomicJson } from "../../shared/atomic-json.js";
5
5
  import { utf8Tail } from "../../shared/utf8.js";
6
6
  import { validateAcceptanceInput } from "../shared/acceptance.js";
7
7
  import { validateModelResponseAliases } from "../../shared/model-response-aliases.js";
8
+ import { checkRecordedPidLiveness, isRecordedPid } from "../background/stale-run-reconciler.js";
9
+ import { encodeIndexSegment } from "../background/index-segment.js";
8
10
  export const MAX_REMEMBERED_FOREGROUND_RUNS = 50;
9
- const HISTORY_VERSION = 1;
11
+ export const FOREGROUND_OWNER_EXITED_STOP_ERROR = "Subagent stopped because the Pi process that ran it exited before the run finished.";
10
12
  const MAX_INLINE_OUTPUT_BYTES = 64 * 1024;
11
13
  const MAX_RESUME_CONTRACT_BYTES = 64 * 1024;
12
- function historyPath(resultsDir) {
13
- return path.join(resultsDir, "foreground-history.json");
14
+ // One file per run, so Pi processes sharing the results dir never rewrite each other's records.
15
+ function historyDir(resultsDir) {
16
+ return path.join(resultsDir, "foreground-history");
17
+ }
18
+ function runHistoryPath(resultsDir, runId) {
19
+ return path.join(historyDir(resultsDir), `${encodeIndexSegment(runId)}.json`);
14
20
  }
15
21
  function boundedTail(value) {
16
22
  return utf8Tail(value, MAX_INLINE_OUTPUT_BYTES).text;
@@ -57,6 +63,13 @@ function compactChild(child) {
57
63
  function isRestorableForegroundStatus(status) {
58
64
  return status === "completed" || status === "failed" || status === "paused" || status === "stopped";
59
65
  }
66
+ function hasRunningChild(run) {
67
+ return run.children.some((child) => child.status === "running");
68
+ }
69
+ /** A `running` record is kept only for a single run whose owning Pi process can be probed later. */
70
+ function isAdmittedChildStatus(run, status) {
71
+ return isRestorableForegroundStatus(status) || (status === "running" && run.mode === "single" && isRecordedPid(run.owner));
72
+ }
60
73
  function isRestorableResumeContract(value) {
61
74
  if (value === undefined)
62
75
  return true;
@@ -86,8 +99,9 @@ function isRestorableResumeContract(value) {
86
99
  function compactRun(run) {
87
100
  if (!run.sessionId)
88
101
  return undefined;
89
- if (run.children.length === 0 || !run.children.every((child) => isRestorableForegroundStatus(child.status) && isRestorableResumeContract(child.resumeContract)))
102
+ if (run.children.length === 0 || !run.children.every((child) => isAdmittedChildStatus(run, child.status) && isRestorableResumeContract(child.resumeContract)))
90
103
  return undefined;
104
+ const running = hasRunningChild(run);
91
105
  return {
92
106
  runId: run.runId,
93
107
  mode: run.mode,
@@ -95,23 +109,26 @@ function compactRun(run) {
95
109
  sessionId: run.sessionId,
96
110
  updatedAt: run.updatedAt,
97
111
  children: run.children.map(compactChild),
112
+ ...(running ? { owner: run.owner } : {}),
113
+ ...(run.parentWorkflowRunId ? { parentWorkflowRunId: run.parentWorkflowRunId } : {}),
98
114
  };
99
115
  }
100
- function readIndex(resultsDir) {
101
- const filePath = historyPath(resultsDir);
102
- if (!fs.existsSync(filePath))
103
- return { version: HISTORY_VERSION, runs: [] };
116
+ /** One saved run, or undefined when the file is missing, unreadable or invalid. */
117
+ function readRunFile(filePath) {
104
118
  try {
105
119
  const parsed = JSON.parse(fs.readFileSync(filePath, "utf-8"));
106
- if (!parsed || typeof parsed !== "object" || Array.isArray(parsed))
107
- return { version: HISTORY_VERSION, runs: [] };
108
- const record = parsed;
109
- if (record.version !== HISTORY_VERSION || !Array.isArray(record.runs))
110
- return { version: HISTORY_VERSION, runs: [] };
111
- return { version: HISTORY_VERSION, runs: record.runs.filter(isRestorableRun) };
120
+ return isRestorableRun(parsed) ? parsed : undefined;
121
+ }
122
+ catch {
123
+ return undefined;
124
+ }
125
+ }
126
+ function historyFiles(dir) {
127
+ try {
128
+ return fs.readdirSync(dir).filter((name) => name.endsWith(".json"));
112
129
  }
113
130
  catch {
114
- return { version: HISTORY_VERSION, runs: [] };
131
+ return [];
115
132
  }
116
133
  }
117
134
  function isRestorableRun(value) {
@@ -125,40 +142,125 @@ function isRestorableRun(value) {
125
142
  && typeof run.updatedAt === "number" && Number.isFinite(run.updatedAt)
126
143
  && Array.isArray(run.children)
127
144
  && run.children.length > 0
145
+ && (run.owner === undefined || isRecordedPid(run.owner))
146
+ && (run.parentWorkflowRunId === undefined || typeof run.parentWorkflowRunId === "string")
128
147
  && run.children.every((child) => Boolean(child && typeof child === "object" && !Array.isArray(child)
129
148
  && typeof child.agent === "string"
130
149
  && typeof child.index === "number"
131
150
  && isRestorableResumeContract(child.resumeContract)
132
- && isRestorableForegroundStatus(child.status)));
151
+ && isAdmittedChildStatus(run, child.status)));
133
152
  }
134
- function sortAndBound(runs, limit) {
135
- return [...runs].sort((left, right) => right.updatedAt - left.updatedAt).slice(0, limit);
153
+ /** Deletes the oldest final run files beyond the cap; running records stay, so the cap can be exceeded while runs are live. */
154
+ function pruneHistory(dir, limit) {
155
+ const files = historyFiles(dir);
156
+ if (files.length <= limit)
157
+ return;
158
+ const byAge = files.flatMap((name) => {
159
+ try {
160
+ return [{ name, mtimeMs: fs.statSync(path.join(dir, name)).mtimeMs }];
161
+ }
162
+ catch {
163
+ return [];
164
+ }
165
+ }).sort((left, right) => left.mtimeMs - right.mtimeMs);
166
+ for (const { name, mtimeMs } of byAge.slice(0, byAge.length - limit)) {
167
+ const filePath = path.join(dir, name);
168
+ // A running record is replaced later by its run's final save, possibly in another Pi process;
169
+ // once its owner is confirmed dead nothing can replace it, so it is final.
170
+ const run = readRunFile(filePath);
171
+ if (run && hasRunningChild(run) && !ownerConfirmedDead(run))
172
+ continue;
173
+ // A file rewritten since the snapshot is a fresh record, not the old one judged here.
174
+ try {
175
+ if (fs.statSync(filePath).mtimeMs !== mtimeMs)
176
+ continue;
177
+ }
178
+ catch {
179
+ continue;
180
+ }
181
+ fs.rmSync(filePath, { force: true });
182
+ }
136
183
  }
137
- export function persistForegroundRunHistory(state, options = {}) {
184
+ /**
185
+ * Saves one foreground run to its own history file. A run with a `running` child is a launch-time
186
+ * record; the run's final save replaces it.
187
+ */
188
+ export function persistForegroundRun(run, options = {}) {
189
+ const compact = compactRun(run);
190
+ if (!compact)
191
+ return;
138
192
  const resultsDir = options.resultsDir ?? DIRS.results;
139
- const limit = options.limit ?? MAX_REMEMBERED_FOREGROUND_RUNS;
140
- const existing = readIndex(resultsDir);
141
- const merged = new Map(existing.runs.map((run) => [run.runId, run]));
142
- for (const run of state.foregroundRuns?.values() ?? []) {
143
- const compact = compactRun(run);
144
- if (compact)
145
- merged.set(compact.runId, compact);
193
+ const dir = historyDir(resultsDir);
194
+ if (!fs.existsSync(dir)) {
195
+ fs.mkdirSync(dir, { recursive: true });
196
+ // The single shared file used before per-run files is not read or migrated.
197
+ fs.rmSync(path.join(resultsDir, "foreground-history.json"), { force: true });
146
198
  }
147
- const runs = sortAndBound([...merged.values()], limit);
148
- writePrivateAtomicJson(historyPath(resultsDir), { version: HISTORY_VERSION, runs });
199
+ writePrivateAtomicJson(runHistoryPath(resultsDir, compact.runId), compact);
200
+ pruneHistory(dir, options.limit ?? MAX_REMEMBERED_FOREGROUND_RUNS);
201
+ }
202
+ /** Drops this process's `running` record for a launch that threw, so it never loads as interrupted. */
203
+ export function discardRunningForegroundRun(runId, options = {}) {
204
+ const filePath = runHistoryPath(options.resultsDir ?? DIRS.results, runId);
205
+ const run = readRunFile(filePath);
206
+ if (run && hasRunningChild(run) && run.owner?.pid === process.pid)
207
+ fs.rmSync(filePath, { force: true });
208
+ }
209
+ /** For a record with a `running` child: its owning Pi process is gone. */
210
+ export function ownerConfirmedDead(run) {
211
+ // readRunFile admits a running child only with a valid owner.
212
+ return checkRecordedPidLiveness(run.owner) === "dead";
213
+ }
214
+ /** A run whose owning Pi process is gone loads as stopped and resumable; one whose owner may be alive is not loaded. */
215
+ function restoreInterruptedRun(run) {
216
+ if (!ownerConfirmedDead(run))
217
+ return undefined;
218
+ const { owner: _owner, ...rest } = run;
219
+ return {
220
+ ...rest,
221
+ children: run.children.map((child) => {
222
+ if (child.status !== "running")
223
+ return child;
224
+ const { activityState: _activityState, currentTool: _currentTool, currentToolStartedAt: _currentToolStartedAt, currentPath: _currentPath, sessionFile, ...stopped } = child;
225
+ return {
226
+ ...stopped,
227
+ status: "stopped",
228
+ exitCode: 1,
229
+ error: FOREGROUND_OWNER_EXITED_STOP_ERROR,
230
+ finalOutput: FOREGROUND_OWNER_EXITED_STOP_ERROR,
231
+ ...(sessionFile && fs.existsSync(sessionFile) ? { sessionFile } : {}),
232
+ };
233
+ }),
234
+ };
235
+ }
236
+ /** State text for a remembered child; a child cut off by a Pi exit names the resume target. */
237
+ export function foregroundChildStateLabel(run, child) {
238
+ if (child.status !== "stopped" || child.error !== FOREGROUND_OWNER_EXITED_STOP_ERROR)
239
+ return child.status;
240
+ // Without a transcript there is nothing to resume from.
241
+ return child.sessionFile ? `interrupted when Pi exited · resume ${run.runId}` : "interrupted when Pi exited";
149
242
  }
150
243
  export function restoreForegroundRunHistory(state, options = {}) {
151
244
  const sessionId = options.sessionId ?? state.currentSessionId;
152
245
  if (!sessionId)
153
246
  return 0;
154
- const index = readIndex(options.resultsDir ?? DIRS.results);
155
- const runs = sortAndBound(index.runs.filter((run) => run.sessionId === sessionId), options.limit ?? MAX_REMEMBERED_FOREGROUND_RUNS);
247
+ const dir = historyDir(options.resultsDir ?? DIRS.results);
248
+ const saved = historyFiles(dir).flatMap((name) => readRunFile(path.join(dir, name)) ?? []);
249
+ // Other sessions' records that may have been cut off, for a read-only pointer to them.
250
+ if (options.observeForeign)
251
+ for (const run of saved)
252
+ if (run.sessionId !== sessionId && hasRunningChild(run))
253
+ options.observeForeign(run);
254
+ const runs = saved.filter((run) => run.sessionId === sessionId).sort((left, right) => right.updatedAt - left.updatedAt).slice(0, options.limit ?? MAX_REMEMBERED_FOREGROUND_RUNS);
156
255
  state.foregroundRuns ??= new Map();
157
256
  let restored = 0;
158
257
  for (const run of runs) {
159
258
  if (state.foregroundRuns.has(run.runId))
160
259
  continue;
161
- state.foregroundRuns.set(run.runId, run);
260
+ const loaded = hasRunningChild(run) ? restoreInterruptedRun(run) : run;
261
+ if (!loaded)
262
+ continue;
263
+ state.foregroundRuns.set(run.runId, loaded);
162
264
  restored += 1;
163
265
  }
164
266
  return restored;
@@ -15,6 +15,7 @@ import { steerAsyncRun } from "./async-steering-action.ts";
15
15
  import type { ChildRuntimeConfig } from "../shared/child-runtime-config.ts";
16
16
  import { type MissionLaunchBinding } from "../../missions/lifecycle.ts";
17
17
  import { type WorkflowSteerOptions, type WorkflowSteerResult } from "../../workflows/scripted-workflow.ts";
18
+ import { type PreparedWorkflowResume, type WorkflowResumeRefusalReason } from "../../workflows/workflow-resume.ts";
18
19
  import { type AcceptanceInput, type AgentContract, type ControlConfig, type Details, type ExtensionConfig, type IntercomBridgeConfig, type JsonSchemaObject, type MaxOutputConfig, type ResolvedControlConfig, type RunFanoutBudgetDescriptor, type SteeringRecoveryDescriptor, type SingleResult, type ToolBudgetConfig, type UsageBudgetConfig, type SubagentState, type ScheduleOrigin } from "../../shared/types.ts";
19
20
  export declare function unknownSubagentActionMessage(action: string, disabled?: DisabledFeatureSurface): string;
20
21
  interface TaskParam {
@@ -287,6 +288,14 @@ export declare function resolveRevivalControlConfig(input: {
287
288
  requestedControl?: ControlConfig;
288
289
  recoveryControlConfig?: ResolvedControlConfig;
289
290
  }): ResolvedControlConfig;
291
+ type WorkflowResumeOutcome = {
292
+ result: AgentToolResult<Details>;
293
+ } & ({
294
+ launched: true;
295
+ } | {
296
+ launched: false;
297
+ refusal: WorkflowResumeRefusalReason;
298
+ });
290
299
  export declare function createSubagentExecutor(deps: ExecutorDeps): {
291
300
  execute: (id: string, params: SubagentParamsLike, signal: AbortSignal, onUpdate: ((r: AgentToolResult<Details>) => void) | undefined, ctx: ExtensionContext) => Promise<AgentToolResult<Details>>;
292
301
  /** Public/model-facing execution boundary. Internal direct launch primitives use execute or executeDelegated. */
@@ -299,6 +308,8 @@ export declare function createSubagentExecutor(deps: ExecutorDeps): {
299
308
  executeDelegated: (id: string, params: SubagentParamsLike, signal: AbortSignal, onUpdate: ((r: AgentToolResult<Details>) => void) | undefined, ctx: ExtensionContext) => Promise<AgentToolResult<Details>>;
300
309
  /** Scheduled launches retain their owning context without replacing the live active session. */
301
310
  executeScheduled: (id: string, params: SubagentParamsLike, signal: AbortSignal, ctx: ExtensionContext) => Promise<AgentToolResult<Details>>;
311
+ /** Relaunches a stopped workflow claimed by prepareWorkflowResume (resume by id and auto-resume); a launch that did not start gives up its claim. */
312
+ executeWorkflowResume: (id: string, prepared: PreparedWorkflowResume, signal: AbortSignal, ctx: ExtensionContext) => Promise<WorkflowResumeOutcome>;
302
313
  /** Scheduled state visible to the current runtime supervisor owner only. */
303
314
  getCurrentSupervisorOwnerStates: () => Iterable<SubagentState>;
304
315
  };