pi-subagents 0.80.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 (184) hide show
  1. package/CHANGELOG.md +66 -0
  2. package/bundle/agents.js +18 -0
  3. package/bundle/background-work.js +16 -0
  4. package/bundle/capability-ceiling.js +22 -0
  5. package/bundle/child-tool-plan.js +11 -0
  6. package/bundle/chunk-43SCHIMD.js +57 -0
  7. package/bundle/chunk-4QFZZV3O.js +57 -0
  8. package/bundle/chunk-4SDU2BZX.js +1190 -0
  9. package/bundle/chunk-52UBALJL.js +753 -0
  10. package/bundle/chunk-5LFLVSTM.js +260 -0
  11. package/bundle/chunk-5TYMSZX3.js +235 -0
  12. package/bundle/chunk-62OCY2S4.js +14 -0
  13. package/bundle/chunk-7F6RKT6W.js +132 -0
  14. package/bundle/chunk-7IVSUMQQ.js +1337 -0
  15. package/bundle/chunk-7J4ZXMY7.js +18 -0
  16. package/bundle/chunk-7PCKZHR5.js +1234 -0
  17. package/bundle/chunk-7YMXOYEU.js +1373 -0
  18. package/bundle/chunk-B5EKXDHE.js +16870 -0
  19. package/bundle/chunk-CIZQXJ5Q.js +8619 -0
  20. package/bundle/chunk-CTPAE36Y.js +94 -0
  21. package/bundle/chunk-D4EHT5SJ.js +14 -0
  22. package/bundle/chunk-GC4DSPMH.js +946 -0
  23. package/bundle/chunk-GJECHSAR.js +179 -0
  24. package/bundle/chunk-M2CTFYT3.js +4164 -0
  25. package/bundle/chunk-MHZ4G23A.js +1296 -0
  26. package/bundle/chunk-N2BN7BF5.js +159 -0
  27. package/bundle/chunk-NF7IEKBH.js +490 -0
  28. package/bundle/chunk-NGGAFDL6.js +742 -0
  29. package/bundle/chunk-NIXWDZZV.js +9 -0
  30. package/bundle/chunk-NRQ75MGH.js +8327 -0
  31. package/bundle/chunk-OYF6AFDY.js +458 -0
  32. package/bundle/chunk-PLCCBLPY.js +526 -0
  33. package/bundle/chunk-RMQFMMAT.js +416 -0
  34. package/bundle/chunk-SIJ4S3EN.js +276 -0
  35. package/bundle/chunk-UKG4V45O.js +228 -0
  36. package/bundle/chunk-VH5OAWVG.js +4096 -0
  37. package/bundle/chunk-W2SZ22EM.js +5462 -0
  38. package/bundle/chunk-WVD4UEL4.js +9367 -0
  39. package/bundle/chunk-XSWOTIA3.js +175 -0
  40. package/bundle/chunk-YFCK7H2S.js +129 -0
  41. package/bundle/chunk-YKEWIB4M.js +380 -0
  42. package/bundle/chunk-YYREO77V.js +137 -0
  43. package/bundle/chunk-Z7KVHQHL.js +173 -0
  44. package/bundle/chunk-ZLINIYTP.js +8613 -0
  45. package/bundle/chunk-ZQGSDHL6.js +128 -0
  46. package/bundle/control-channel.js +11 -0
  47. package/bundle/delegation.js +14 -0
  48. package/bundle/external-job-provider.js +20 -0
  49. package/bundle/external-runs.js +21 -0
  50. package/bundle/index.js +30 -0
  51. package/bundle/inspectors.js +8 -0
  52. package/bundle/intercom-bridge.js +11 -0
  53. package/bundle/preflight.js +425 -0
  54. package/bundle/project-panes.js +28 -0
  55. package/bundle/required-child-extensions.js +6 -0
  56. package/bundle/shared-types.js +8 -0
  57. package/bundle/workflow-resources.js +7 -0
  58. package/docs/agents.md +31 -0
  59. package/docs/configuration.md +9 -1
  60. package/docs/extension-api.md +27 -0
  61. package/docs/observability.md +6 -2
  62. package/docs/tool-reference.md +3 -1
  63. package/docs/workflows.md +16 -0
  64. package/package.json +19 -19
  65. package/skills/pi-subagents/references/execution-controls.md +5 -4
  66. package/src/agents/agent-management.js +3 -3
  67. package/src/agents/agent-memory.js +3 -1
  68. package/src/agents/agents.js +6 -6
  69. package/src/agents/frontmatter.d.ts +4 -0
  70. package/src/agents/frontmatter.js +24 -3
  71. package/src/agents/skills.js +2 -1
  72. package/src/api/delegation.d.ts +20 -0
  73. package/src/extension/config.js +3 -0
  74. package/src/extension/herdr-pi-bridge.js +17 -10
  75. package/src/extension/index.js +109 -9
  76. package/src/extension/schemas.js +3 -3
  77. package/src/intercom/native-supervisor-channel.js +20 -0
  78. package/src/missions/lifecycle.js +7 -1
  79. package/src/runs/background/active-async-capacity.d.ts +1 -0
  80. package/src/runs/background/active-async-capacity.js +9 -3
  81. package/src/runs/background/async-execution.js +20 -10
  82. package/src/runs/background/async-job-tracker.d.ts +6 -1
  83. package/src/runs/background/async-job-tracker.js +9 -4
  84. package/src/runs/background/async-status.d.ts +2 -0
  85. package/src/runs/background/async-status.js +21 -2
  86. package/src/runs/background/await-async-run.js +3 -7
  87. package/src/runs/background/boot-id.d.ts +12 -0
  88. package/src/runs/background/boot-id.js +47 -0
  89. package/src/runs/background/interrupted-elsewhere.d.ts +22 -0
  90. package/src/runs/background/interrupted-elsewhere.js +68 -0
  91. package/src/runs/background/notify.d.ts +2 -0
  92. package/src/runs/background/notify.js +13 -0
  93. package/src/runs/background/result-delivery-ownership.d.ts +6 -0
  94. package/src/runs/background/result-delivery-ownership.js +19 -1
  95. package/src/runs/background/result-watcher.d.ts +3 -1
  96. package/src/runs/background/result-watcher.js +24 -8
  97. package/src/runs/background/run-child-session.d.ts +1 -6
  98. package/src/runs/background/run-child-session.js +19 -1
  99. package/src/runs/background/run-status.js +2 -1
  100. package/src/runs/background/stale-run-reconciler.d.ts +25 -0
  101. package/src/runs/background/stale-run-reconciler.js +54 -13
  102. package/src/runs/background/subagent-runner-bootstrap.js +3 -0
  103. package/src/runs/background/subagent-runner.d.ts +4 -0
  104. package/src/runs/background/subagent-runner.js +13 -5
  105. package/src/runs/background/subagent-wait.d.ts +1 -1
  106. package/src/runs/background/subagent-wait.js +6 -1
  107. package/src/runs/background/terminal-run-index.d.ts +1 -0
  108. package/src/runs/background/terminal-run-index.js +4 -2
  109. package/src/runs/foreground/async-stop-action.js +3 -1
  110. package/src/runs/foreground/execution.js +45 -5
  111. package/src/runs/foreground/foreground-history.d.ts +16 -2
  112. package/src/runs/foreground/foreground-history.js +133 -31
  113. package/src/runs/foreground/subagent-executor.d.ts +11 -0
  114. package/src/runs/foreground/subagent-executor.js +277 -74
  115. package/src/runs/shared/child-launch.d.ts +2 -2
  116. package/src/runs/shared/child-session.d.ts +3 -2
  117. package/src/runs/shared/child-session.js +51 -8
  118. package/src/runs/shared/child-tool-plan.d.ts +7 -0
  119. package/src/runs/shared/child-tool-plan.js +21 -6
  120. package/src/runs/shared/herdr-connection.d.ts +17 -8
  121. package/src/runs/shared/herdr-connection.js +19 -7
  122. package/src/runs/shared/herdr-external-adapters.d.ts +3 -2
  123. package/src/runs/shared/herdr-external-adapters.js +1 -1
  124. package/src/runs/shared/herdr-machine.d.ts +12 -2
  125. package/src/runs/shared/herdr-machine.js +55 -9
  126. package/src/runs/shared/herdr-pane-host.d.ts +88 -0
  127. package/src/runs/shared/herdr-pane-host.js +155 -0
  128. package/src/runs/shared/herdr-placed-run.d.ts +27 -150
  129. package/src/runs/shared/herdr-placed-run.js +48 -395
  130. package/src/runs/shared/local-machine-access.d.ts +5 -0
  131. package/src/runs/shared/local-machine-access.js +28 -0
  132. package/src/runs/shared/orca-progress-tabs.d.ts +0 -1
  133. package/src/runs/shared/orca-progress-tabs.js +1 -24
  134. package/src/runs/shared/parallel-utils.d.ts +1 -1
  135. package/src/runs/shared/placed-run-owner.d.ts +49 -0
  136. package/src/runs/shared/placed-run-owner.js +93 -0
  137. package/src/runs/shared/placement-host.d.ts +99 -0
  138. package/src/runs/shared/placement-host.js +157 -0
  139. package/src/runs/shared/rex-machine.d.ts +25 -0
  140. package/src/runs/shared/rex-machine.js +83 -0
  141. package/src/runs/shared/rex-pane-host.d.ts +62 -0
  142. package/src/runs/shared/rex-pane-host.js +327 -0
  143. package/src/runs/shared/structured-output.d.ts +1 -0
  144. package/src/runs/shared/structured-output.js +1 -0
  145. package/src/shared/accessible-dir.js +4 -1
  146. package/src/shared/completion-owner.d.ts +5 -0
  147. package/src/shared/completion-owner.js +5 -0
  148. package/src/shared/provider-diagnostics.d.ts +9 -0
  149. package/src/shared/provider-diagnostics.js +75 -0
  150. package/src/shared/session-identity.d.ts +2 -0
  151. package/src/shared/session-identity.js +26 -0
  152. package/src/shared/types.d.ts +48 -6
  153. package/src/slash/delegation-adapters.d.ts +3 -1
  154. package/src/slash/delegation-adapters.js +7 -0
  155. package/src/slash/prompt-template-bridge.js +3 -1
  156. package/src/slash/subagents-admin.d.ts +1 -0
  157. package/src/slash/subagents-admin.js +1 -1
  158. package/src/tui/fleet-layout.d.ts +20 -0
  159. package/src/tui/fleet-layout.js +17 -0
  160. package/src/tui/fleet-viewport.d.ts +12 -0
  161. package/src/tui/fleet-viewport.js +35 -0
  162. package/src/tui/fleet.d.ts +6 -6
  163. package/src/tui/fleet.js +93 -66
  164. package/src/tui/render.d.ts +6 -0
  165. package/src/tui/render.js +3 -3
  166. package/src/tui/subagent-messages.js +1 -0
  167. package/src/workflows/scripted-workflow.d.ts +1 -1
  168. package/src/workflows/structured-workflow-scripts.d.ts +1 -0
  169. package/src/workflows/structured-workflow-scripts.js +5 -4
  170. package/src/workflows/workflow-auto-resume.d.ts +32 -0
  171. package/src/workflows/workflow-auto-resume.js +102 -0
  172. package/src/workflows/workflow-launch-record.d.ts +27 -0
  173. package/src/workflows/workflow-launch-record.js +48 -0
  174. package/src/workflows/workflow-resources.js +2 -2
  175. package/src/workflows/workflow-resume-claim.d.ts +82 -0
  176. package/src/workflows/workflow-resume-claim.js +210 -0
  177. package/src/workflows/workflow-resume.d.ts +56 -0
  178. package/src/workflows/workflow-resume.js +106 -0
  179. package/src/workflows/workflow-reuse.d.ts +15 -9
  180. package/src/workflows/workflow-reuse.js +77 -14
  181. package/src/workflows/workflow-revival.d.ts +2 -0
  182. package/src/workflows/workflow-revival.js +2 -1
  183. package/src/workflows/workflow-stop-cause.d.ts +6 -0
  184. package/src/workflows/workflow-stop-cause.js +8 -0
@@ -0,0 +1,327 @@
1
+ import { createHash } from "node:crypto";
2
+ import * as path from "node:path";
3
+ import { Type } from "typebox";
4
+ import { Compile } from "typebox/compile";
5
+ import { remoteShellCommand, sshMachineAccess } from "./herdr-connection.js";
6
+ import { localMachineAccess } from "./local-machine-access.js";
7
+ import { getPiSpawnCommand } from "./pi-spawn.js";
8
+ import { createPlacedRuntimeDir, PLACED_MANIFEST_READ_SCRIPT, PlacedRunOwner, removePlacedRuntimeDir } from "./placed-run-owner.js";
9
+ import { withHerdrPaneAllocationLock } from "./placement-host.js";
10
+ import { isRexLocalMachine, REX_BUNDLE_BIN, REX_CALL_TIMEOUT_MS, REX_EXIT_GONE, REX_EXIT_UNREACHABLE, REX_MAX_OUTPUT, resolveRexBinary, rexCommandEnv, rexGlobalArgs } from "./rex-machine.js";
11
+ const REX_CLOSE_POLLS = 20, REX_CLOSE_POLL_MS = 50;
12
+ /** On a Rex SSH machine, placed tabs open in the one session with this label. */
13
+ export const REX_SSH_SESSION_LABEL = "subagents";
14
+ const sessionInspection = Compile(Type.Object({ session_id: Type.String() }));
15
+ const sessionList = Compile(Type.Object({ sessions: Type.Array(Type.Object({ session_id: Type.String(), label: Type.Optional(Type.String()) })) }));
16
+ const createdWindow = Compile(Type.Object({ window_id: Type.String(), block_ids: Type.Tuple([Type.String()]) }));
17
+ const sessionWindows = Compile(Type.Object({ windows: Type.Array(Type.Object({ window_id: Type.String(), label: Type.Optional(Type.String()), layers: Type.Array(Type.Object({ blocks: Type.Array(Type.Object({ block_id: Type.String() })) })) })) }));
18
+ const blockProcess = Compile(Type.Object({ foreground: Type.Object({ pid: Type.Integer() }) }));
19
+ const bridgeManifestPid = Compile(Type.Object({ pid: Type.Integer() }));
20
+ /** Rex runtime directories go under /tmp: the bridge socket path inside a macOS TMPDIR exceeds the unix socket path limit. */
21
+ function rexAccess() { return localMachineAccess({ env: { ...rexCommandEnv(), TMPDIR: "/tmp" } }); }
22
+ /** The same /tmp rule on the remote, whatever TMPDIR its SSH session has. */
23
+ function rexSshAccess(machine, access = sshMachineAccess(machine)) { return { ...access, shellCommand: (script, args) => remoteShellCommand(`TMPDIR=/tmp; export TMPDIR; ${script}`, args) }; }
24
+ function asRexMachine(machine) { if (machine.provider !== "rex")
25
+ throw new Error("The Rex pane host only places runs on Rex machines."); return machine; }
26
+ /** Runs one rex command on the pane machine with --autostart=false, the recorded server, and no inherited Rex selectors; exit 4 always throws. */
27
+ async function runRex(endpoint, args, what) {
28
+ const result = await endpoint.access.runAsync(endpoint.access.shellCommand('exec /usr/bin/env -u REX_SESSION -u REX_BLOCK -u REX_SERVER "$@"', [endpoint.bin, ...rexGlobalArgs(endpoint.server), ...args]), { timeout: REX_CALL_TIMEOUT_MS, maxBuffer: REX_MAX_OUTPUT });
29
+ if (result.error)
30
+ throw new Error(`Rex ${what} failed: ${result.error.message}`, { cause: result.error });
31
+ if (result.status === REX_EXIT_UNREACHABLE)
32
+ throw new Error(`The Rex server is unreachable (rex ${what} exited 4); the placed pane's state is unknown.`);
33
+ if (result.status === null)
34
+ throw new Error(`Rex ${what} did not finish; the placed pane's state is unknown.`);
35
+ return { ...result, status: result.status };
36
+ }
37
+ function requireRexSuccess(result, what) {
38
+ if (result.status === REX_EXIT_GONE)
39
+ throw new Error(`Rex ${what} failed because its target no longer exists (exit 3): ${result.stderr.trim().slice(0, 512)}`);
40
+ if (result.status !== 0)
41
+ throw new Error(`Rex ${what} exited with code ${result.status}: ${result.stderr.trim().slice(0, 512)}`);
42
+ }
43
+ /** Parses rex's JSON output against its schema; undefined when the shape does not match. */
44
+ function parseRexJson(text, validator, what) {
45
+ let value;
46
+ try {
47
+ value = JSON.parse(text);
48
+ }
49
+ catch {
50
+ throw new Error(`Rex ${what} returned malformed JSON.`);
51
+ }
52
+ return validator.Check(value) ? value : undefined;
53
+ }
54
+ /** One remote lookup, matching resolveRexBinary: `$REX_BIN_DIR/rex`, then `rex` on PATH, then the Rex server bundle under the remote home. */
55
+ async function resolveRemoteRexBinary(access, machine) {
56
+ const script = `for c in "\${REX_BIN_DIR:+$REX_BIN_DIR/rex}" "$(command -v rex 2>/dev/null)" "$HOME/${REX_BUNDLE_BIN}"; do if [ -n "$c" ] && [ -f "$c" ] && [ -x "$c" ]; then printf '%s\\n' "$c"; exit 0; fi; done; exit 1`;
57
+ const found = await access.runAsync(access.shellCommand(script), { timeout: REX_CALL_TIMEOUT_MS, maxBuffer: 64 * 1024 });
58
+ if (found.error)
59
+ throw new Error(`Could not look for the Rex CLI on ${machine.id}: ${found.error.message}`, { cause: found.error });
60
+ const bin = found.stdout.trim();
61
+ if (found.status !== 0 || !path.posix.isAbsolute(bin) || bin.includes("\n"))
62
+ throw new Error(`The Rex CLI was not found on ${machine.id}. Install Rex there or put rex on its PATH.`);
63
+ return bin;
64
+ }
65
+ /** The pane's cwd as the remote Pi will report it: `~` expanded and symlinks resolved. Rex does not expand `~` itself. */
66
+ async function resolveRemoteCwd(access, machine) {
67
+ const script = `case "$1" in "~") d=$HOME;; "~/"*) d=$HOME/\${1#"~/"};; *) d=$1;; esac; cd -P -- "$d" 2>/dev/null && pwd -P`;
68
+ const resolved = await access.runAsync(access.shellCommand(script, [machine.cwd]), { timeout: REX_CALL_TIMEOUT_MS, maxBuffer: 64 * 1024 });
69
+ if (resolved.error)
70
+ throw new Error(`Could not check ${machine.cwd} on ${machine.id}: ${resolved.error.message}`, { cause: resolved.error });
71
+ const cwd = resolved.stdout.trim();
72
+ if (resolved.status !== 0 || !path.posix.isAbsolute(cwd) || cwd.includes("\n"))
73
+ throw new Error(`Nothing at ${machine.cwd} on ${machine.id}. Clone the repo there first; pi-subagents never clones, pulls, or checks out on a machine.`);
74
+ return cwd;
75
+ }
76
+ /** The remote Pi as a Rex tab's login environment finds it. */
77
+ async function resolveRemotePi(access, machine) {
78
+ const found = await access.runAsync(access.shellCommand(`exec "\${SHELL:-/bin/sh}" -lc 'command -v pi'`), { timeout: REX_CALL_TIMEOUT_MS, maxBuffer: 64 * 1024 });
79
+ if (found.error)
80
+ throw new Error(`Could not look for Pi on ${machine.id}: ${found.error.message}`, { cause: found.error });
81
+ const pi = found.stdout.trim().split("\n").at(-1)?.trim() ?? "";
82
+ if (found.status !== 0 || !path.posix.isAbsolute(pi))
83
+ throw new Error(`Pi was not found on ${machine.id}: its login shell has no pi on PATH. Install Pi there.`);
84
+ return pi;
85
+ }
86
+ async function labeledSessions(endpoint) {
87
+ const listed = await runRex(endpoint, ["ls", "--json"], "ls");
88
+ requireRexSuccess(listed, "ls");
89
+ const parsed = parseRexJson(listed.stdout, sessionList, "ls");
90
+ if (!parsed)
91
+ throw new Error("Rex ls did not list the server's sessions.");
92
+ return parsed.sessions.filter((session) => session.label === REX_SSH_SESSION_LABEL).map((session) => session.session_id);
93
+ }
94
+ /**
95
+ * The session labeled `subagents` on the SSH machine, created when there is none. Rex allows duplicate labels, so two or
96
+ * more fail closed, also after creating one. Only launch calls this; reconnect and cleanup use the recorded session id.
97
+ */
98
+ async function subagentsSession(endpoint, machine) {
99
+ return withHerdrPaneAllocationLock(JSON.stringify(["rex-session", machine.target, REX_SSH_SESSION_LABEL]), async () => {
100
+ let sessions = await labeledSessions(endpoint);
101
+ if (sessions.length === 0) {
102
+ requireRexSuccess(await runRex(endpoint, ["new", REX_SSH_SESSION_LABEL, "--json"], "new"), "new");
103
+ sessions = await labeledSessions(endpoint);
104
+ }
105
+ const [session] = sessions;
106
+ if (sessions.length > 1)
107
+ throw new Error(`Rex on ${machine.id} has ${sessions.length} sessions labeled ${REX_SSH_SESSION_LABEL}; close or rename the extras so placed runs have one session to open in.`);
108
+ if (!session)
109
+ throw new Error(`Rex on ${machine.id} did not list the ${REX_SSH_SESSION_LABEL} session it just created.`);
110
+ return session;
111
+ });
112
+ }
113
+ /** The server must confirm the recorded session; a missing session fails closed. A Rex SSH machine's session is the one recorded at launch. */
114
+ export async function connectRex(machine, options = {}) {
115
+ const rex = asRexMachine(machine);
116
+ let endpoint;
117
+ if (isRexLocalMachine(rex)) {
118
+ endpoint = { access: options.access ?? rexAccess(), bin: options.bin ?? resolveRexBinary(), session: rex.session };
119
+ if (rex.server)
120
+ endpoint.server = rex.server;
121
+ }
122
+ else {
123
+ if (!options.session)
124
+ throw new Error(`Rex machine '${rex.id}' has no recorded session; a placed run never picks one by label after launch.`);
125
+ const access = options.access ?? rexSshAccess(rex);
126
+ endpoint = { access, bin: options.bin ?? await resolveRemoteRexBinary(access, rex), session: options.session };
127
+ }
128
+ const inspected = await runRex(endpoint, ["session", "inspect", endpoint.session, "--json"], "session inspect");
129
+ if (inspected.status === REX_EXIT_GONE)
130
+ throw new Error(`Rex session ${endpoint.session} no longer exists on the Rex server.`);
131
+ requireRexSuccess(inspected, "session inspect");
132
+ if (parseRexJson(inspected.stdout, sessionInspection, "session inspect")?.session_id !== endpoint.session)
133
+ throw new Error("Rex session identity changed: the server reported a different session id.");
134
+ return { ...endpoint, ...endpoint.access.openSockets() };
135
+ }
136
+ async function inspectBlock(connection, blockId) {
137
+ const inspected = await runRex(connection, ["block", "inspect", "-s", connection.session, blockId], "block inspect");
138
+ if (inspected.status === REX_EXIT_GONE)
139
+ return "gone";
140
+ requireRexSuccess(inspected, "block inspect");
141
+ return "present";
142
+ }
143
+ /** Ownership is the never-reused block id in the recorded session. The block counts as closed only once `block inspect` exits 3. */
144
+ export async function closeOwnedRexPane(connection, owned) {
145
+ if (owned.workspaceId !== connection.session)
146
+ throw new Error("Refusing Rex cleanup: the owned pane belongs to a different Rex session.");
147
+ if (await inspectBlock(connection, owned.paneId) === "gone")
148
+ return;
149
+ const closed = await runRex(connection, ["block", "close", "-s", connection.session, owned.paneId], "block close");
150
+ if (closed.status !== REX_EXIT_GONE)
151
+ requireRexSuccess(closed, "block close");
152
+ for (let poll = 0; poll < REX_CLOSE_POLLS; poll++) {
153
+ if (await inspectBlock(connection, owned.paneId) === "gone")
154
+ return;
155
+ await new Promise((resolve) => setTimeout(resolve, REX_CLOSE_POLL_MS));
156
+ }
157
+ throw new Error(`Rex block ${owned.paneId} still exists after close; refusing to report the placed pane closed.`);
158
+ }
159
+ function reconcileRexPlacedRun(identity, candidate, proveEvidence) {
160
+ if (!candidate.present)
161
+ return { ok: false, reason: "Owned Rex block is gone." };
162
+ if (candidate.foregroundPid === undefined || candidate.foregroundPid !== candidate.bridgePid)
163
+ return { ok: false, reason: "The owned Rex block's foreground process is not the placed Pi." };
164
+ const proof = proveEvidence();
165
+ if (!proof.ok)
166
+ return proof;
167
+ return { ok: true, state: "unknown", paneId: identity.paneId, cursor: proof.cursor };
168
+ }
169
+ async function readBridgePid(connection, runtimeDir) {
170
+ const manifest = await connection.access.runAsync(connection.access.shellCommand(PLACED_MANIFEST_READ_SCRIPT, [runtimeDir]), { timeout: 5_000, maxBuffer: 64 * 1024 });
171
+ if (manifest.status !== 0)
172
+ return undefined;
173
+ return parseRexJson(manifest.stdout, bridgeManifestPid, "bridge manifest")?.pid;
174
+ }
175
+ /**
176
+ * `window new` failed, timed out, or printed something unreadable, so the server may still have opened this run's window.
177
+ * Find it by the run's tab label in the recorded session and close its blocks with the same close-then-prove-gone rule.
178
+ */
179
+ async function closeUntrackedRexWindow(connection, label, failure) {
180
+ try {
181
+ const inspected = await runRex(connection, ["session", "inspect", connection.session, "--json"], "session inspect");
182
+ if (inspected.status === REX_EXIT_GONE)
183
+ throw failure;
184
+ requireRexSuccess(inspected, "session inspect");
185
+ const windows = parseRexJson(inspected.stdout, sessionWindows, "session inspect")?.windows.filter((window) => window.label === label);
186
+ if (!windows)
187
+ throw new Error("Rex session inspect did not list the session's windows.");
188
+ if (windows.length > 1)
189
+ throw new Error(`The Rex session has ${windows.length} windows labeled ${label}; refusing to guess which one this run opened.`);
190
+ for (const window of windows)
191
+ for (const layer of window.layers)
192
+ for (const block of layer.blocks)
193
+ await closeOwnedRexPane(connection, { workspaceId: connection.session, tabId: window.window_id, paneId: block.block_id });
194
+ }
195
+ catch (cleanupError) {
196
+ if (cleanupError === failure)
197
+ throw failure;
198
+ throw new AggregateError([failure, cleanupError], `Rex window new failed, and the window labeled ${label} that it may have opened could not be closed.`, { cause: failure });
199
+ }
200
+ throw failure;
201
+ }
202
+ /** Opens the run's background window; on any failure, closes the window the server may have opened before rethrowing. */
203
+ async function openRexWindow(connection, label, cwd, argv) {
204
+ try {
205
+ const created = await runRex(connection, ["window", "new", "-s", connection.session, label, "-c", cwd, "--focus=false", "--json", "--", ...argv], "window new");
206
+ requireRexSuccess(created, "window new");
207
+ const window = parseRexJson(created.stdout, createdWindow, "window new");
208
+ if (!window)
209
+ throw new Error(`Rex window new did not return exactly one block: ${created.stdout.trim().slice(0, 512)}`);
210
+ return window;
211
+ }
212
+ catch (error) {
213
+ return closeUntrackedRexWindow(connection, label, error instanceof Error ? error : new Error(String(error)));
214
+ }
215
+ }
216
+ /**
217
+ * Rex pane host: rex CLI calls on this machine, always with an explicit session. A local child opens as a background
218
+ * window in the parent's session; a Rex SSH child in the remote `subagents` session, with every rex call run over SSH.
219
+ * Pi starts through the login shell's environment (as Rex starts any window) with the bridge variables set by
220
+ * /usr/bin/env. There is no status subscription: a dead Pi closes the bridge socket (directly, or through the SSH
221
+ * forward), and the reconnect that follows checks the block with `rex block call process` and fails closed.
222
+ */
223
+ export const rexPaneHost = {
224
+ connect: connectRex,
225
+ sessionOf(connection) { return connection.session; },
226
+ async launch(connection, machine, runId, _runtimeDir, request, onPane) {
227
+ if (request.kind !== "pi")
228
+ throw new Error("Rex supports native Pi children only.");
229
+ const environment = Object.entries(request.environment ?? {});
230
+ if (environment.some(([name]) => !/^[A-Za-z_][A-Za-z0-9_]*$/u.test(name)))
231
+ throw new Error("Rex launch environment names must be shell variable names.");
232
+ const rex = asRexMachine(machine);
233
+ let pi;
234
+ if (isRexLocalMachine(rex)) {
235
+ pi = getPiSpawnCommand(request.args);
236
+ if (!path.isAbsolute(pi.command))
237
+ throw new Error("Rex placement needs an absolute Pi executable; set PI_SUBAGENT_PI_BINARY to the Pi binary path.");
238
+ }
239
+ else
240
+ pi = { command: await resolveRemotePi(connection.access, rex), args: request.args };
241
+ const window = await openRexWindow(connection, `subagent-${runId.slice(-8)}`, rex.cwd, ["/usr/bin/env", ...environment.map(([name, value]) => `${name}=${value}`), pi.command, ...pi.args]);
242
+ const owned = { workspaceId: connection.session, tabId: window.window_id, paneId: window.block_ids[0] };
243
+ await onPane(owned);
244
+ return { owned, terminalId: owned.paneId };
245
+ },
246
+ async prompt(connection, identity, text) {
247
+ requireRexSuccess(await runRex(connection, ["send", "-s", connection.session, "-b", identity.paneId, "--", text], "send"), "send");
248
+ requireRexSuccess(await runRex(connection, ["send-key", "-s", connection.session, "-b", identity.paneId, "enter"], "send-key"), "send-key");
249
+ },
250
+ async subscribe() { return () => { }; },
251
+ paneEvent() { return undefined; },
252
+ async panes(connection, identity) {
253
+ const processInfo = await runRex(connection, ["block", "call", "-s", connection.session, "-b", identity.paneId, "process"], "block process");
254
+ if (processInfo.status === REX_EXIT_GONE)
255
+ return { present: false };
256
+ requireRexSuccess(processInfo, "block process");
257
+ const candidate = { present: true };
258
+ const foregroundPid = parseRexJson(processInfo.stdout, blockProcess, "block process")?.foreground.pid;
259
+ if (foregroundPid !== undefined)
260
+ candidate.foregroundPid = foregroundPid;
261
+ const bridgePid = await readBridgePid(connection, identity.runtimeDir);
262
+ if (bridgePid !== undefined)
263
+ candidate.bridgePid = bridgePid;
264
+ return candidate;
265
+ },
266
+ reconcile: reconcileRexPlacedRun,
267
+ closeOwnedPane: closeOwnedRexPane,
268
+ };
269
+ function runtimeDirPresent(access, runtimeDir) {
270
+ const result = access.run(access.shellCommand('test -e "$1" || test -L "$1"', [runtimeDir]), { timeout: 10_000, maxBuffer: 4096 });
271
+ if (result.error)
272
+ throw new Error(`Could not check the owned runtime directory: ${result.error.message}`, { cause: result.error });
273
+ return result.status === 0;
274
+ }
275
+ /** A closed local Pi removes its own runtime directory on shutdown, so already-removed counts as removed. */
276
+ function removeRexRuntimeDir(access, runtimeDir) {
277
+ if (!runtimeDirPresent(access, runtimeDir))
278
+ return;
279
+ try {
280
+ removePlacedRuntimeDir(access, runtimeDir);
281
+ }
282
+ catch (error) {
283
+ if (runtimeDirPresent(access, runtimeDir))
284
+ throw error;
285
+ }
286
+ }
287
+ /** The placed-run owner bound to Rex and the pane machine's access. */
288
+ export class RexPlacedRunOwner extends PlacedRunOwner {
289
+ constructor(machine, runId, runtimeDir, connection, access, host) { super(machine, runId, runtimeDir, connection, host, access, () => removeRexRuntimeDir(access, runtimeDir)); }
290
+ /** A Rex SSH owner's machine carries the resolved remote cwd, and its host reconnects only to the session recorded here. */
291
+ static async create(machine, runId, options = {}) {
292
+ // A short hash names the directory, so /tmp/pi-subagents-herdr-<hash>-XXXXXXXX/bridge.sock (57 bytes) fits the 103-byte macOS socket path limit for any run id.
293
+ const hash = createHash("sha256").update(runId).digest("hex").slice(0, 12);
294
+ if (isRexLocalMachine(machine)) {
295
+ const access = rexAccess(), runtimeDir = await createPlacedRuntimeDir(access, hash);
296
+ try {
297
+ return new RexPlacedRunOwner(machine, runId, runtimeDir, await connectRex(machine, { access }), access, rexPaneHost);
298
+ }
299
+ catch (error) {
300
+ try {
301
+ removeRexRuntimeDir(access, runtimeDir);
302
+ }
303
+ catch (cleanupError) {
304
+ throw new AggregateError([error, cleanupError], "Rex owner creation failed and runtime cleanup also failed.", { cause: error });
305
+ }
306
+ throw error;
307
+ }
308
+ }
309
+ const access = rexSshAccess(machine, options.sshAccess), bin = await resolveRemoteRexBinary(access, machine);
310
+ const placed = { ...machine, cwd: await resolveRemoteCwd(access, machine) };
311
+ const session = await subagentsSession({ access, bin }, placed);
312
+ const host = { ...rexPaneHost, connect: (target) => connectRex(target, { access, bin, session }) };
313
+ const runtimeDir = await createPlacedRuntimeDir(access, hash);
314
+ try {
315
+ return new RexPlacedRunOwner(placed, runId, runtimeDir, await host.connect(placed), access, host);
316
+ }
317
+ catch (error) {
318
+ try {
319
+ removeRexRuntimeDir(access, runtimeDir);
320
+ }
321
+ catch (cleanupError) {
322
+ throw new AggregateError([error, cleanupError], "Rex owner creation failed and runtime cleanup also failed.", { cause: error });
323
+ }
324
+ throw error;
325
+ }
326
+ }
327
+ }
@@ -2,6 +2,7 @@ import type { Message } from "@earendil-works/pi-ai";
2
2
  import type { JsonSchemaObject } from "../../shared/types.ts";
3
3
  import type { ResolvedAcceptanceReportMode } from "./acceptance.ts";
4
4
  export declare const MISSING_STRUCTURED_OUTPUT_CALL_ERROR = "Missing structured_output call; this step has outputSchema and must finish by calling structured_output.";
5
+ export declare const STRUCTURED_OUTPUT_FOLLOW_UP = "You ended without calling `structured_output`. Call it now with your result. Do not repeat the work.";
5
6
  export declare const MISSING_STRUCTURED_ACCEPTANCE_REPORT_ERROR = "Missing acceptanceReport in structured_output call; acceptance.report is \"on\".";
6
7
  export declare const STRUCTURED_OUTPUT_REJECTION_ERROR = "structured_output was invoked but no valid output was captured.";
7
8
  export declare const INVALID_STRUCTURED_OUTPUT_SCHEMA_ERROR = "Structured output invocation was rejected: invalid outputSchema.";
@@ -14,6 +14,7 @@ import { pathToFileURL } from "node:url";
14
14
  import { PI_CODING_AGENT_PACKAGE_ROOT_ENV } from "../../shared/utils.js";
15
15
  import { resolveInstalledPiPackageRoot, resolvePiPackageRoot } from "./pi-spawn.js";
16
16
  export const MISSING_STRUCTURED_OUTPUT_CALL_ERROR = "Missing structured_output call; this step has outputSchema and must finish by calling structured_output.";
17
+ export const STRUCTURED_OUTPUT_FOLLOW_UP = "You ended without calling `structured_output`. Call it now with your result. Do not repeat the work.";
17
18
  export const MISSING_STRUCTURED_ACCEPTANCE_REPORT_ERROR = "Missing acceptanceReport in structured_output call; acceptance.report is \"on\".";
18
19
  export const STRUCTURED_OUTPUT_REJECTION_ERROR = "structured_output was invoked but no valid output was captured.";
19
20
  export const INVALID_STRUCTURED_OUTPUT_SCHEMA_ERROR = "Structured output invocation was rejected: invalid outputSchema.";
@@ -4,7 +4,10 @@ export function ensureAccessibleDir(dirPath, options = {}) {
4
4
  const fsImpl = options.fs ?? fs;
5
5
  const pid = options.pid ?? process.pid;
6
6
  const retryDirectoryErrors = options.retryDirectoryErrors ?? process.platform === "win32";
7
- const retryDelaysMs = retryDirectoryErrors ? options.retryDelaysMs ?? DEFAULT_FILE_SYSTEM_RETRY_DELAYS_MS : [];
7
+ // Runs synchronously at extension registration. The full ladder would block
8
+ // every Pi start for ~7.9 s on a directory with broken ACLs before reaching
9
+ // the pid fallback, so only its first steps (85 ms per operation) are used here.
10
+ const retryDelaysMs = retryDirectoryErrors ? options.retryDelaysMs ?? DEFAULT_FILE_SYSTEM_RETRY_DELAYS_MS.slice(0, 3) : [];
8
11
  const wait = options.wait ?? waitForFileSystemRetry;
9
12
  const mkdirWithRetry = (target) => {
10
13
  runFileSystemOperationWithRetry(() => {
@@ -1,2 +1,7 @@
1
+ /**
2
+ * The completion owner of a result whose own owner process is dead: whichever session owns the run
3
+ * delivers it. Delivery consumes the result.
4
+ */
5
+ export declare const OWNING_SESSION_COMPLETION_OWNER = "owning-session";
1
6
  /** Stable for one parent Pi process across extension reloads. */
2
7
  export declare function currentCompletionOwnerId(): string;
@@ -1,5 +1,10 @@
1
1
  import { randomUUID } from "node:crypto";
2
2
  const COMPLETION_OWNER_KEY = Symbol.for("pi-subagents.completion-owner-id");
3
+ /**
4
+ * The completion owner of a result whose own owner process is dead: whichever session owns the run
5
+ * delivers it. Delivery consumes the result.
6
+ */
7
+ export const OWNING_SESSION_COMPLETION_OWNER = "owning-session";
3
8
  /** Stable for one parent Pi process across extension reloads. */
4
9
  export function currentCompletionOwnerId() {
5
10
  const runtime = globalThis;
@@ -0,0 +1,9 @@
1
+ import type { SubagentDelegationProviderDiagnostics } from "../api/delegation.ts";
2
+ import type { ChildSessionEvent } from "../runs/shared/child-session.ts";
3
+ type Diagnostics = SubagentDelegationProviderDiagnostics;
4
+ /** Immutable, bounded observations only. This never decides whether to retry. */
5
+ export declare function observeProviderDiagnostics(current: Diagnostics | undefined, event: ChildSessionEvent, at: number): Diagnostics | undefined;
6
+ export declare function clearProviderRetry(current: Diagnostics | undefined): Diagnostics | undefined;
7
+ export declare function snapshotProviderDiagnostics(current: Diagnostics | undefined): Diagnostics | undefined;
8
+ export declare function sameProviderDiagnostics(left: Diagnostics | undefined, right: Diagnostics | undefined): boolean;
9
+ export {};
@@ -0,0 +1,75 @@
1
+ import { isRetryableAssistantError } from "@earendil-works/pi-ai";
2
+ // Account exhaustion can also carry 429. Do not present it as transient throttling.
3
+ // Pi's classifier supplies its own provider-specific quota/billing exclusions too.
4
+ const ACCOUNT_LIMIT = /quota|billing|out of budget|available balance|(?:account|subscription|credits?|balance|usage)[\s_-]*(?:(?:is|are|has been|have been)[\s_-]*)?(?:limit|exhaust|deplet|exceed)|(?:insufficient|exhausted|depleted|low|no(?:[\s_-]+remaining)?)[\s_-]*(?:credits?|balance|fund)/i;
5
+ const RATE_LIMIT = /\brate[\s_-]*limit(?:ed|ing|s|_(?:exceeded|error))?\b|\btoo[\s_-]+many[\s_-]+requests\b|\b(?:HTTP(?:\/\d(?:\.\d)?)?\s*[:=]?\s*429|(?:status|statusCode|status_code)["']?\s*[:=]?\s*429)\b|^\s*429\b/i;
6
+ function classifyError(message) {
7
+ const text = message.errorMessage;
8
+ if (!text || ACCOUNT_LIMIT.test(text) || !isRetryableAssistantError(message))
9
+ return "unknown";
10
+ return RATE_LIMIT.test(text) ? "rate_limited" : "transient_other";
11
+ }
12
+ const SAFE_ERROR_MESSAGES = {
13
+ rate_limited: "Assistant request was rate limited.",
14
+ transient_other: "Assistant request encountered a transient provider or transport error.",
15
+ unknown: "Assistant request failed.",
16
+ };
17
+ /** Immutable, bounded observations only. This never decides whether to retry. */
18
+ export function observeProviderDiagnostics(current, event, at) {
19
+ if (event.type === "message_end") {
20
+ const message = event.message;
21
+ if (message?.role === "assistant" && message.stopReason === "error") {
22
+ const kind = classifyError(message);
23
+ return {
24
+ ...current,
25
+ errorCount: (current?.errorCount ?? 0) + 1,
26
+ lastError: { kind, message: SAFE_ERROR_MESSAGES[kind], at },
27
+ };
28
+ }
29
+ }
30
+ else if (event.type === "auto_retry_start") {
31
+ const { attempt, maxAttempts, delayMs } = event;
32
+ if (typeof attempt === "number" && Number.isSafeInteger(attempt) && attempt > 0
33
+ && typeof maxAttempts === "number" && Number.isSafeInteger(maxAttempts) && maxAttempts >= attempt
34
+ && typeof delayMs === "number" && Number.isFinite(delayMs) && delayMs >= 0) {
35
+ return { ...current, errorCount: current?.errorCount ?? 0, retry: { phase: "backoff", attempt, maxAttempts, delayMs, scheduledAt: at } };
36
+ }
37
+ }
38
+ else if ((event.type === "agent_start" || event.type === "turn_start") && current?.retry?.phase === "backoff") {
39
+ return { ...current, retry: { ...current.retry, phase: "in_flight" } };
40
+ }
41
+ else if (event.type === "auto_retry_end" || event.type === "agent_settled") {
42
+ return clearProviderRetry(current);
43
+ }
44
+ return current;
45
+ }
46
+ export function clearProviderRetry(current) {
47
+ if (!current?.retry)
48
+ return current;
49
+ const { retry: _retry, ...history } = current;
50
+ return history;
51
+ }
52
+ export function snapshotProviderDiagnostics(current) {
53
+ if (!current)
54
+ return undefined;
55
+ return {
56
+ errorCount: current.errorCount,
57
+ ...(current.lastError ? { lastError: { ...current.lastError } } : {}),
58
+ ...(current.retry ? { retry: { ...current.retry } } : {}),
59
+ };
60
+ }
61
+ export function sameProviderDiagnostics(left, right) {
62
+ if (left === right)
63
+ return true;
64
+ if (!left || !right)
65
+ return false;
66
+ return left.errorCount === right.errorCount
67
+ && left.lastError?.kind === right.lastError?.kind
68
+ && left.lastError?.message === right.lastError?.message
69
+ && left.lastError?.at === right.lastError?.at
70
+ && left.retry?.phase === right.retry?.phase
71
+ && left.retry?.attempt === right.retry?.attempt
72
+ && left.retry?.maxAttempts === right.retry?.maxAttempts
73
+ && left.retry?.delayMs === right.retry?.delayMs
74
+ && left.retry?.scheduledAt === right.retry?.scheduledAt;
75
+ }
@@ -3,4 +3,6 @@ interface SessionIdentityManager {
3
3
  getSessionId(): string | null | undefined;
4
4
  }
5
5
  export declare function resolveCurrentSessionId(sessionManager: SessionIdentityManager): string;
6
+ /** The id in a Pi session file's header, which `pi --session <id>` resolves; only ids that need no shell quoting. */
7
+ export declare function resolvePiSessionId(sessionFile: string | undefined): string | undefined;
6
8
  export {};
@@ -1,6 +1,32 @@
1
+ import * as fs from "node:fs";
1
2
  export function resolveCurrentSessionId(sessionManager) {
2
3
  const sessionId = sessionManager.getSessionFile() ?? sessionManager.getSessionId();
3
4
  if (!sessionId)
4
5
  throw new Error("Current session identity is unavailable.");
5
6
  return sessionId;
6
7
  }
8
+ /** The id in a Pi session file's header, which `pi --session <id>` resolves; only ids that need no shell quoting. */
9
+ export function resolvePiSessionId(sessionFile) {
10
+ if (!sessionFile)
11
+ return undefined;
12
+ try {
13
+ const fd = fs.openSync(sessionFile, "r");
14
+ try {
15
+ const buffer = Buffer.alloc(16 * 1024);
16
+ const bytes = fs.readSync(fd, buffer, 0, buffer.length, 0);
17
+ const line = buffer.toString("utf-8", 0, bytes).split("\n", 1)[0];
18
+ if (!line)
19
+ return undefined;
20
+ const header = JSON.parse(line);
21
+ if (header.type === "session" && typeof header.id === "string" && /^[A-Za-z0-9-]{8,}$/.test(header.id))
22
+ return header.id;
23
+ return undefined;
24
+ }
25
+ finally {
26
+ fs.closeSync(fd);
27
+ }
28
+ }
29
+ catch {
30
+ return undefined;
31
+ }
32
+ }