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
@@ -1,12 +1,21 @@
1
1
  import {
2
+ REX_BUNDLE_BIN,
3
+ REX_CALL_TIMEOUT_MS,
4
+ REX_EXIT_GONE,
5
+ REX_EXIT_UNREACHABLE,
6
+ REX_MAX_OUTPUT,
2
7
  connectHerdrMachine,
8
+ isRexLocalMachine,
3
9
  remoteShellCommand,
4
- runHerdrRemoteCommand,
5
- runHerdrRemoteCommandAsync
6
- } from "./chunk-74RT5YQE.js";
10
+ resolveRexBinary,
11
+ rexCommandEnv,
12
+ rexGlobalArgs,
13
+ runBoundedCommand,
14
+ sshMachineAccess
15
+ } from "./chunk-RMQFMMAT.js";
7
16
  import {
8
17
  package_default
9
- } from "./chunk-2XYWN7QJ.js";
18
+ } from "./chunk-ZQGSDHL6.js";
10
19
  import {
11
20
  HERDR_PI_MODE_ENV,
12
21
  HERDR_PI_PROTOCOL,
@@ -17,106 +26,185 @@ import {
17
26
  validateHerdrPiRunId
18
27
  } from "./chunk-4QFZZV3O.js";
19
28
  import {
20
- getAgentDir
21
- } from "./chunk-ZBC44JJ3.js";
29
+ getAgentDir,
30
+ getPiSpawnCommand
31
+ } from "./chunk-CIZQXJ5Q.js";
22
32
  import "./chunk-5LFLVSTM.js";
23
33
  import "./chunk-YFCK7H2S.js";
24
34
  import "./chunk-Z7KVHQHL.js";
25
35
 
26
36
  // src/runs/shared/herdr-placed-run.ts
27
- import { createHash, randomBytes } from "node:crypto";
28
- import * as fs from "node:fs";
37
+ import { randomBytes as randomBytes2 } from "node:crypto";
38
+ import * as fs3 from "node:fs";
29
39
  import * as net from "node:net";
40
+ import * as path5 from "node:path";
41
+
42
+ // src/runs/shared/herdr-pane-host.ts
43
+ import { createHash as createHash2 } from "node:crypto";
44
+
45
+ // src/runs/shared/placed-run-owner.ts
46
+ import * as fs from "node:fs";
30
47
  import * as path from "node:path";
31
- function herdrRunState(status, fallback = "unknown") {
32
- return status === "working" ? "working" : status === "blocked" ? "blocked" : status === "idle" || status === "done" ? "settled" : status === "unknown" ? "unknown" : fallback;
33
- }
34
- function herdrStatusSubscriptions(paneId) {
35
- return [{ type: "pane.agent_status_changed", pane_id: paneId }, { type: "pane.closed" }, { type: "pane.moved" }];
48
+ var PLACED_MANIFEST_READ_SCRIPT = 'p="$1/manifest.json"; test -f "$p" && test ! -L "$p" && cat "$p"';
49
+ async function discoverPlacedBridgeManifest(access, runId, runtimeDir, missing = "The placed Pi's pi-subagents bridge manifest is not in its runtime directory; the Pi may have exited.") {
50
+ for (let attempt = 0; attempt < 100; attempt++) {
51
+ const result = await access.runAsync(access.shellCommand(PLACED_MANIFEST_READ_SCRIPT, [runtimeDir]), { timeout: 5e3, maxBuffer: 64 * 1024 });
52
+ if (result.error) throw new Error(`Remote bridge manifest discovery failed: ${result.error.message}`, { cause: result.error });
53
+ if (result.status === 0 && result.stdout.trim()) {
54
+ let value;
55
+ try {
56
+ value = JSON.parse(result.stdout);
57
+ } catch {
58
+ throw new Error("Remote bridge manifest is malformed.");
59
+ }
60
+ const p = value;
61
+ if (p.runId !== runId || typeof p.socketPath !== "string" || path.posix.dirname(p.socketPath) !== runtimeDir || typeof p.nativeSessionId !== "string" || typeof p.packageVersion !== "string" || typeof p.protocol !== "number") throw new Error("Remote bridge manifest identity is incomplete or outside its owned runtime directory.");
62
+ return { socketPath: p.socketPath, nativeSessionId: p.nativeSessionId, packageVersion: p.packageVersion, protocol: p.protocol };
63
+ }
64
+ await new Promise((resolve) => setTimeout(resolve, 50));
65
+ }
66
+ throw new Error(missing);
36
67
  }
37
- function reconcileHerdrPlacedRun(identity, candidate, proveEvidence) {
38
- if (candidate.endpoint.session !== identity.session) return { ok: false, reason: "Herdr session changed during reconnect." };
39
- const matches = candidate.agents.filter((agent) => agent.terminal_id === identity.terminalId);
40
- if (matches.length !== 1 || typeof matches[0].pane_id !== "string") return { ok: false, reason: matches.length ? "Terminal identity is ambiguous." : "Owned terminal is missing." };
41
- const proof = proveEvidence();
42
- if (!proof.ok) return proof;
43
- return { ok: true, state: herdrRunState(matches[0].agent_status), paneId: matches[0].pane_id, cursor: proof.cursor };
68
+ async function createPlacedRuntimeDir(access, runId) {
69
+ const result = await access.runAsync(access.shellCommand('umask 077; mktemp -d "${TMPDIR:-/tmp}/pi-subagents-herdr-$1-XXXXXXXX"', [runId]), { timeout: 1e4, maxBuffer: 4096 });
70
+ if (result.error) throw new Error(`Could not provision a run-private remote bridge runtime directory: ${result.error.message}`, { cause: result.error });
71
+ const value = result.stdout.trim();
72
+ if (result.status !== 0 || !path.posix.isAbsolute(value) || value.includes("\n")) throw new Error("Could not provision a run-private remote bridge runtime directory.");
73
+ return value;
44
74
  }
45
- function reconcileHerdrReconnect(identity, candidate, lastEvidenceCursor) {
46
- return reconcileHerdrPlacedRun(identity, candidate, () => {
47
- const bridge = candidate.bridge;
48
- if (bridge.runId !== identity.runId || bridge.nativeSessionId !== identity.nativeSessionId || bridge.packageVersion !== package_default.version || bridge.protocol !== HERDR_PI_PROTOCOL) return { ok: false, reason: "Bridge run, package, protocol, or native session identity changed." };
49
- if (typeof bridge.evidenceCursor !== "number" || typeof bridge.evidenceFloor !== "number" || bridge.evidenceCursor < lastEvidenceCursor || bridge.evidenceFloor > lastEvidenceCursor + 1) return { ok: false, reason: "Bridge evidence has a gap after reconnect." };
50
- return { ok: true, cursor: bridge.evidenceCursor };
51
- });
75
+ function removePlacedRuntimeDir(access, runtimeDir) {
76
+ const result = access.run(access.shellCommand('p=$1; case "${p##*/}" in pi-subagents-herdr-*) test -d "$p" && test ! -L "$p" && rm -rf -- "$p";; *) exit 64;; esac', [runtimeDir]), { timeout: 1e4, maxBuffer: 4096 });
77
+ if (result.status !== 0 || result.stdout) throw new Error(`Could not remove the exact owned remote runtime directory: ${String(result.stderr).slice(0, 512)}`);
52
78
  }
53
- async function boundedHerdrReconnect(identity, lastEvidenceCursor, attempt, options = {}) {
54
- const deadline = Date.now() + (options.deadlineMs ?? 15e3);
55
- let last = "Reconnect did not run.";
56
- for (let number = 1; number <= (options.attempts ?? 3) && Date.now() < deadline; number++) {
57
- if (options.disposed?.()) return { ok: false, reason: "Reconnect was disposed." };
79
+ var PlacedRunOwner = class {
80
+ machine;
81
+ runId;
82
+ runtimeDir;
83
+ connection;
84
+ host;
85
+ access;
86
+ owned;
87
+ terminalId;
88
+ agentName;
89
+ startedArgv;
90
+ identity;
91
+ #unsubscribe = () => {
92
+ };
93
+ #snapshot = { connection: "connected", state: "ready", revision: 0 };
94
+ #cleanupPromise;
95
+ #removeRuntime;
96
+ constructor(machine, runId, runtimeDir, connection, host, access, removeRuntime = () => removePlacedRuntimeDir(access, runtimeDir)) {
97
+ this.machine = machine;
98
+ this.runId = runId;
99
+ this.runtimeDir = runtimeDir;
100
+ this.connection = connection;
101
+ this.#removeRuntime = removeRuntime;
102
+ this.host = host;
103
+ this.access = access;
104
+ }
105
+ observeEvent(event) {
106
+ const data = this.host.paneEvent(event, this.#snapshot.state);
107
+ if (data && (!this.terminalId || data.terminalId === this.terminalId || data.paneId === this.owned?.paneId)) this.#snapshot = { ...this.#snapshot, revision: this.#snapshot.revision + 1, state: data.state };
108
+ }
109
+ markConnectionUnknown(observed) {
110
+ if (this.connection !== observed) return false;
111
+ this.#snapshot = { ...this.#snapshot, connection: "unknown", state: "unknown", revision: this.#snapshot.revision + 1 };
112
+ return true;
113
+ }
114
+ async subscribe(listener, disconnected) {
115
+ const observed = this.connection, paneId = this.owned?.paneId;
116
+ let adopted = false, candidateLost;
117
+ if (!paneId) throw new Error("Herdr status subscription requires a provisioned pane.");
118
+ const unsubscribe = await this.host.subscribe(observed, paneId, (event) => {
119
+ this.observeEvent(event);
120
+ listener(event);
121
+ }, (error) => {
122
+ if (!adopted) {
123
+ candidateLost ??= error;
124
+ return;
125
+ }
126
+ if (!this.markConnectionUnknown(observed)) return;
127
+ disconnected(error);
128
+ });
129
+ if (candidateLost) {
130
+ unsubscribe();
131
+ throw candidateLost;
132
+ }
133
+ this.#unsubscribe = unsubscribe;
134
+ adopted = true;
135
+ }
136
+ /** Create the owned pane and start the agent in one host step; onPane runs once the pane exists and before the agent starts. */
137
+ async launch(request, onPane, options = {}) {
138
+ const { owned, terminalId } = await this.host.launch(this.connection, this.machine, this.runId, this.runtimeDir, request, async (pane) => {
139
+ this.owned = pane;
140
+ await onPane(pane);
141
+ }, options), argv = [request.kind, ...request.args];
142
+ this.agentName = request.name;
143
+ this.terminalId = terminalId;
144
+ this.startedArgv = argv;
145
+ return { owned, terminalId, argv };
146
+ }
147
+ journal(identity) {
148
+ this.identity = identity;
149
+ this.#snapshot = { ...this.#snapshot, identity };
150
+ const journalDir = path.join(getAgentDir(), "pi-subagents", "herdr-run-journal");
151
+ fs.mkdirSync(journalDir, { recursive: true, mode: 448 });
152
+ fs.writeFileSync(path.join(journalDir, `${this.runId}.json`), JSON.stringify({ version: 1, identity, state: "ready", updatedAt: Date.now() }), { mode: 384, flag: "wx" });
153
+ }
154
+ get snapshot() {
155
+ return this.#snapshot;
156
+ }
157
+ runRemote(command, options = {}) {
158
+ return this.access.run(command, options);
159
+ }
160
+ async replaceConnection(input) {
161
+ const oldConnection = this.connection, oldUnsubscribe = this.#unsubscribe;
162
+ this.connection = input.connection;
163
+ this.#unsubscribe = input.unsubscribe;
164
+ if (this.identity) this.identity = { ...this.identity, paneId: input.paneId };
165
+ this.#snapshot = { connection: "connected", state: input.state, identity: this.identity, revision: this.#snapshot.revision + 1 };
166
+ oldUnsubscribe();
167
+ await oldConnection.close();
168
+ }
169
+ cleanup(closeStartedPane = true) {
170
+ return this.#cleanupPromise ??= this.#performCleanup(closeStartedPane);
171
+ }
172
+ async #performCleanup(closeStartedPane) {
173
+ let failure;
58
174
  try {
59
- const result = reconcileHerdrReconnect(identity, await attempt(number), lastEvidenceCursor);
60
- if (result.ok) return result;
61
- last = result.reason;
175
+ if (closeStartedPane && this.owned) await this.host.closeOwnedPane(this.connection, this.owned, this.terminalId, this.agentName);
62
176
  } catch (error) {
63
- last = error instanceof Error ? error.message : String(error);
177
+ failure = error;
64
178
  }
65
- await new Promise((resolve) => setTimeout(resolve, Math.min(number * 100, Math.max(0, deadline - Date.now()))));
179
+ try {
180
+ this.#unsubscribe();
181
+ await this.connection.close();
182
+ this.#removeRuntime();
183
+ } catch (error) {
184
+ failure ??= error;
185
+ }
186
+ this.#snapshot = { ...this.#snapshot, connection: "closed", revision: this.#snapshot.revision + 1 };
187
+ if (failure) throw failure;
66
188
  }
67
- return { ok: false, reason: last };
68
- }
69
- function safeRunId(value) {
70
- const prefix = value?.replace(/[^A-Za-z0-9_-]/gu, "_").slice(0, 48) ?? "run";
71
- return validateHerdrPiRunId(`${prefix}_${randomBytes(8).toString("hex")}`);
72
- }
73
- function idOf(value, key) {
74
- const found = value && typeof value === "object" ? value[key] : void 0;
75
- if (typeof found !== "string" || !found) throw new Error(`Herdr did not return ${key}.`);
76
- return found;
77
- }
78
- function list(value, key) {
79
- if (Array.isArray(value)) return value;
80
- const found = value && typeof value === "object" ? value[key] : void 0;
81
- return Array.isArray(found) ? found : [];
82
- }
83
- function tagged(value, type, key) {
84
- if (!value || typeof value !== "object" || value.type !== type || !(key in value) || typeof value[key] !== "object") throw new Error(`Herdr returned an invalid ${type} envelope.`);
85
- return value[key];
86
- }
87
- function parseHerdrCreated(value, type) {
88
- const envelope = value;
89
- if (envelope?.type !== type) throw new Error(`Herdr returned an invalid ${type} envelope.`);
90
- const tab = envelope.tab;
91
- const pane = envelope.root_pane;
92
- return { ...type === "workspace_created" ? { workspaceId: idOf(envelope.workspace, "workspace_id") } : {}, tabId: idOf(tab, "tab_id"), paneId: idOf(pane, "pane_id") };
93
- }
94
- function parseHerdrAgent(value, type) {
95
- return tagged(value, type, "agent");
96
- }
97
- function parseHerdrSessionSnapshot(value) {
98
- return tagged(value, "session_snapshot", "snapshot");
99
- }
100
- function ownsHerdrPane(agent, terminalId, paneId) {
101
- return agent.terminal_id === terminalId && agent.pane_id === paneId;
102
- }
103
- function snapshotAgents(snapshot) {
104
- return list(snapshot, "agents").filter((agent) => Boolean(agent && typeof agent === "object")).map((agent) => ({ terminal_id: typeof agent.terminal_id === "string" ? agent.terminal_id : void 0, pane_id: typeof agent.pane_id === "string" ? agent.pane_id : void 0, agent_status: typeof agent.agent_status === "string" ? agent.agent_status : void 0 }));
105
- }
106
- var HERDR_MAX_OWNED_PANES = 20;
189
+ };
190
+
191
+ // src/runs/shared/placement-host.ts
192
+ import { createHash, randomBytes } from "node:crypto";
193
+ import * as fs2 from "node:fs";
194
+ import * as path2 from "node:path";
107
195
  var HERDR_ALLOCATION_LOCK_TIMEOUT_MS = 1e4;
108
196
  var HERDR_ALLOCATION_LOCK_POLL_MS = 25;
109
197
  function herdrPaneAllocationKey(target, session, cwd) {
110
198
  return JSON.stringify([target, session, cwd]);
111
199
  }
112
200
  function assertPrivateDir(directory) {
113
- const stat = fs.lstatSync(directory);
201
+ const stat = fs2.lstatSync(directory);
114
202
  if (!stat.isDirectory() || stat.isSymbolicLink() || (stat.mode & 511) !== 448 || typeof process.getuid === "function" && stat.uid !== process.getuid()) throw new Error("Herdr allocation path is not a mode-0700 agent-private owned directory.");
115
203
  }
116
204
  function ensurePrivateChild(parent, name) {
117
- const directory = path.join(parent, name);
205
+ const directory = path2.join(parent, name);
118
206
  try {
119
- fs.mkdirSync(directory, { mode: 448 });
207
+ fs2.mkdirSync(directory, { mode: 448 });
120
208
  } catch (error) {
121
209
  if (error.code !== "EEXIST") throw error;
122
210
  }
@@ -125,7 +213,7 @@ function ensurePrivateChild(parent, name) {
125
213
  }
126
214
  function allocationLockRoot() {
127
215
  const agentDir = getAgentDir();
128
- fs.mkdirSync(agentDir, { recursive: true, mode: 448 });
216
+ fs2.mkdirSync(agentDir, { recursive: true, mode: 448 });
129
217
  assertPrivateDir(agentDir);
130
218
  const packageDir = ensurePrivateChild(agentDir, "pi-subagents");
131
219
  return ensurePrivateChild(packageDir, "herdr-allocation-locks");
@@ -139,25 +227,25 @@ function livePid(pid) {
139
227
  }
140
228
  }
141
229
  function readAllocationOwner(ownerPath) {
142
- const descriptor = fs.openSync(ownerPath, fs.constants.O_RDONLY | (fs.constants.O_NOFOLLOW ?? 0));
230
+ const descriptor = fs2.openSync(ownerPath, fs2.constants.O_RDONLY | (fs2.constants.O_NOFOLLOW ?? 0));
143
231
  try {
144
- const stat = fs.fstatSync(descriptor);
232
+ const stat = fs2.fstatSync(descriptor);
145
233
  if (!stat.isFile() || (stat.mode & 511) !== 384 || typeof process.getuid === "function" && stat.uid !== process.getuid()) throw new Error("Herdr allocation lock owner record is unsafe.");
146
- const value = JSON.parse(fs.readFileSync(descriptor, "utf8"));
234
+ const value = JSON.parse(fs2.readFileSync(descriptor, "utf8"));
147
235
  if (typeof value.pid !== "number" || !Number.isSafeInteger(value.pid) || value.pid <= 0 || typeof value.token !== "string" || !/^[a-f0-9]{32}$/u.test(value.token)) throw new Error("Herdr allocation lock owner record is malformed.");
148
236
  return { pid: value.pid, token: value.token, dev: stat.dev, ino: stat.ino };
149
237
  } finally {
150
- fs.closeSync(descriptor);
238
+ fs2.closeSync(descriptor);
151
239
  }
152
240
  }
153
241
  function sameOwner(left, right) {
154
242
  return left.token === right.token && left.dev === right.dev && left.ino === right.ino;
155
243
  }
156
244
  async function withHerdrPaneAllocationLock(key, action, options = {}) {
157
- const root = allocationLockRoot(), digest = createHash("sha256").update(key).digest("hex"), lockPath = path.join(root, `${digest}.lock`), recoveryPath = path.join(root, `${digest}.recovery`), token = randomBytes(16).toString("hex"), tempPath = path.join(root, `.${digest}.${token}.tmp`), deadline = Date.now() + (options.timeoutMs ?? HERDR_ALLOCATION_LOCK_TIMEOUT_MS);
245
+ const root = allocationLockRoot(), digest = createHash("sha256").update(key).digest("hex"), lockPath = path2.join(root, `${digest}.lock`), recoveryPath = path2.join(root, `${digest}.recovery`), token = randomBytes(16).toString("hex"), tempPath = path2.join(root, `.${digest}.${token}.tmp`), deadline = Date.now() + (options.timeoutMs ?? HERDR_ALLOCATION_LOCK_TIMEOUT_MS);
158
246
  let published;
159
247
  try {
160
- fs.writeFileSync(tempPath, JSON.stringify({ pid: process.pid, token, createdAt: Date.now() }), { mode: 384, flag: "wx" });
248
+ fs2.writeFileSync(tempPath, JSON.stringify({ pid: process.pid, token, createdAt: Date.now() }), { mode: 384, flag: "wx" });
161
249
  const tempOwner = readAllocationOwner(tempPath);
162
250
  for (; ; ) {
163
251
  let recovering = false;
@@ -170,13 +258,13 @@ async function withHerdrPaneAllocationLock(key, action, options = {}) {
170
258
  if (!recovering) {
171
259
  let linked = false;
172
260
  try {
173
- fs.linkSync(tempPath, lockPath);
261
+ fs2.linkSync(tempPath, lockPath);
174
262
  linked = true;
175
263
  try {
176
264
  readAllocationOwner(recoveryPath);
177
265
  const current = readAllocationOwner(lockPath);
178
266
  if (!sameOwner(current, tempOwner)) throw new Error("Herdr allocation lock changed during publication.");
179
- fs.unlinkSync(lockPath);
267
+ fs2.unlinkSync(lockPath);
180
268
  } catch (recoveryError) {
181
269
  if (recoveryError.code !== "ENOENT") throw recoveryError;
182
270
  published = readAllocationOwner(lockPath);
@@ -187,7 +275,7 @@ async function withHerdrPaneAllocationLock(key, action, options = {}) {
187
275
  if (linked) {
188
276
  try {
189
277
  const current = readAllocationOwner(lockPath);
190
- if (sameOwner(current, tempOwner)) fs.unlinkSync(lockPath);
278
+ if (sameOwner(current, tempOwner)) fs2.unlinkSync(lockPath);
191
279
  } catch {
192
280
  }
193
281
  }
@@ -200,15 +288,15 @@ async function withHerdrPaneAllocationLock(key, action, options = {}) {
200
288
  if (!livePid(current.pid)) {
201
289
  let claimed = false;
202
290
  try {
203
- fs.linkSync(lockPath, recoveryPath);
291
+ fs2.linkSync(lockPath, recoveryPath);
204
292
  claimed = true;
205
293
  const recovery = readAllocationOwner(recoveryPath), latest = readAllocationOwner(lockPath);
206
294
  if (!sameOwner(recovery, current) || !sameOwner(latest, current)) throw new Error("Herdr stale lock identity changed during recovery.");
207
- fs.unlinkSync(lockPath);
295
+ fs2.unlinkSync(lockPath);
208
296
  } catch (error) {
209
297
  if (error.code !== "EEXIST" && error.code !== "ENOENT") throw error;
210
298
  } finally {
211
- if (claimed) fs.unlinkSync(recoveryPath);
299
+ if (claimed) fs2.unlinkSync(recoveryPath);
212
300
  }
213
301
  if (claimed) continue;
214
302
  }
@@ -218,7 +306,7 @@ async function withHerdrPaneAllocationLock(key, action, options = {}) {
218
306
  if (Date.now() >= deadline) throw new Error("Timed out waiting for the Herdr pane allocation lock.");
219
307
  await new Promise((resolve) => setTimeout(resolve, options.pollMs ?? HERDR_ALLOCATION_LOCK_POLL_MS));
220
308
  }
221
- fs.unlinkSync(tempPath);
309
+ fs2.unlinkSync(tempPath);
222
310
  let value, actionError, releaseError, actionFailed = false, releaseFailed = false;
223
311
  try {
224
312
  value = await action();
@@ -229,7 +317,7 @@ async function withHerdrPaneAllocationLock(key, action, options = {}) {
229
317
  try {
230
318
  const current = readAllocationOwner(lockPath);
231
319
  if (!published || !sameOwner(current, published)) throw new Error("Herdr pane allocation lock ownership changed before release.");
232
- fs.unlinkSync(lockPath);
320
+ fs2.unlinkSync(lockPath);
233
321
  } catch (error) {
234
322
  releaseFailed = true;
235
323
  releaseError = error;
@@ -240,31 +328,67 @@ async function withHerdrPaneAllocationLock(key, action, options = {}) {
240
328
  return value;
241
329
  } finally {
242
330
  try {
243
- fs.unlinkSync(tempPath);
331
+ fs2.unlinkSync(tempPath);
244
332
  } catch (error) {
245
333
  if (error.code !== "ENOENT") throw error;
246
334
  }
247
335
  }
248
336
  }
249
- function serializeHerdrPiLaunch(launch) {
250
- if (launch.storage.kind === "file") throw new Error("Pane-native remote Pi does not support fork, resume, or revival; use fresh context.");
251
- if (launch.extensionPaths.length || launch.requiredExtensions?.length) throw new Error("Pane-native remote Pi cannot transfer local extension paths; install/configure pi-subagents on the remote machine.");
252
- if (launch.processEnv && Object.values(launch.processEnv).some((value) => value !== void 0 && value !== "__none__")) throw new Error("Pane-native remote Pi cannot transfer local bindings, MCP selections, or process environment.");
253
- if (launch.runtime.nestedRoute || launch.runtime.fanoutChild || launch.runtime.depth > 1) throw new Error("Nested delegation from a pane-native remote Pi is not supported.");
254
- if (launch.excludeTools?.length || launch.runtime.permissions || launch.runtime.toolBudget || launch.runtime.requiredExtensions || launch.runtime.requiredTools?.length || launch.runtime.mcpDirectTools?.length) 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.");
255
- const resources = launch.remoteResources;
256
- 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 !== void 0 && resources.reads !== false && resources.reads.some((read) => typeof read !== "string" || Buffer.byteLength(read) > 4096)) throw new Error("Pane-native remote resources require bounded logical agent, skill, tool, and read names.");
257
- const args = [];
258
- if (launch.noSkills) args.push("--no-skills", "--no-prompt-templates");
259
- if (launch.noContextFiles) args.push("--no-context-files");
260
- if (launch.model) args.push("--model", launch.model);
261
- return { args, resources: { agent: resources.agent, ...resources.skills ? { skills: [...resources.skills] } : {}, ...resources.toolCeiling ? { toolCeiling: [...resources.toolCeiling] } : {}, ...resources.reads !== void 0 ? { reads: resources.reads === false ? false : [...resources.reads] } : {} } };
337
+
338
+ // src/runs/shared/herdr-pane-host.ts
339
+ function herdrRunState(status, fallback = "unknown") {
340
+ return status === "working" ? "working" : status === "blocked" ? "blocked" : status === "idle" || status === "done" ? "settled" : status === "unknown" ? "unknown" : fallback;
341
+ }
342
+ function herdrStatusSubscriptions(paneId) {
343
+ return [{ type: "pane.agent_status_changed", pane_id: paneId }, { type: "pane.closed" }, { type: "pane.moved" }];
344
+ }
345
+ function reconcileHerdrPlacedRun(identity, candidate, proveEvidence) {
346
+ if (candidate.endpoint.session !== identity.session) return { ok: false, reason: "Herdr session changed during reconnect." };
347
+ const matches = candidate.agents.filter((agent) => agent.terminal_id === identity.terminalId);
348
+ if (matches.length !== 1 || typeof matches[0].pane_id !== "string") return { ok: false, reason: matches.length ? "Terminal identity is ambiguous." : "Owned terminal is missing." };
349
+ const proof = proveEvidence();
350
+ if (!proof.ok) return proof;
351
+ return { ok: true, state: herdrRunState(matches[0].agent_status), paneId: matches[0].pane_id, cursor: proof.cursor };
352
+ }
353
+ function idOf(value, key) {
354
+ const found = value && typeof value === "object" ? value[key] : void 0;
355
+ if (typeof found !== "string" || !found) throw new Error(`Herdr did not return ${key}.`);
356
+ return found;
357
+ }
358
+ function list(value, key) {
359
+ if (Array.isArray(value)) return value;
360
+ const found = value && typeof value === "object" ? value[key] : void 0;
361
+ return Array.isArray(found) ? found : [];
362
+ }
363
+ function tagged(value, type, key) {
364
+ if (!value || typeof value !== "object" || value.type !== type || !(key in value) || typeof value[key] !== "object") throw new Error(`Herdr returned an invalid ${type} envelope.`);
365
+ return value[key];
366
+ }
367
+ function parseHerdrCreated(value, type) {
368
+ const envelope = value;
369
+ if (envelope?.type !== type) throw new Error(`Herdr returned an invalid ${type} envelope.`);
370
+ const tab = envelope.tab;
371
+ const pane = envelope.root_pane;
372
+ return { ...type === "workspace_created" ? { workspaceId: idOf(envelope.workspace, "workspace_id") } : {}, tabId: idOf(tab, "tab_id"), paneId: idOf(pane, "pane_id") };
262
373
  }
374
+ function parseHerdrAgent(value, type) {
375
+ return tagged(value, type, "agent");
376
+ }
377
+ function parseHerdrSessionSnapshot(value) {
378
+ return tagged(value, "session_snapshot", "snapshot");
379
+ }
380
+ function ownsHerdrPane(agent, terminalId, paneId) {
381
+ return agent.terminal_id === terminalId && agent.pane_id === paneId;
382
+ }
383
+ function snapshotAgents(snapshot) {
384
+ return list(snapshot, "agents").filter((agent) => Boolean(agent && typeof agent === "object")).map((agent) => ({ terminal_id: typeof agent.terminal_id === "string" ? agent.terminal_id : void 0, pane_id: typeof agent.pane_id === "string" ? agent.pane_id : void 0, agent_status: typeof agent.agent_status === "string" ? agent.agent_status : void 0 }));
385
+ }
386
+ var HERDR_MAX_OWNED_PANES = 20;
263
387
  async function provisionHerdrPane(client, cwd, runId, runtimeDir, environment, allocationKey = cwd) {
264
388
  return withHerdrPaneAllocationLock(allocationKey, async () => {
265
389
  const snapshot = parseHerdrSessionSnapshot(await client.call("session.snapshot"));
266
390
  const panes = list(snapshot, "panes").filter((item) => Boolean(item && typeof item === "object")), workspaces = list(snapshot, "workspaces").filter((item) => Boolean(item && typeof item === "object"));
267
- const ownedLabel = `pi-subagents-${createHash("sha256").update(cwd).digest("hex").slice(0, 10)}`, owned = workspaces.filter((item) => item.label === ownedLabel);
391
+ const ownedLabel = `pi-subagents-${createHash2("sha256").update(cwd).digest("hex").slice(0, 10)}`, owned = workspaces.filter((item) => item.label === ownedLabel);
268
392
  if (owned.length > 1) throw new Error("Herdr deterministic owned workspace identity is ambiguous.");
269
393
  const cwdWorkspaceIds = new Set(panes.filter((item) => item.cwd === cwd || item.foreground_cwd === cwd).map((item) => item.workspace_id).filter((id) => typeof id === "string")), cwdWorkspaces = workspaces.filter((item) => typeof item.workspace_id === "string" && cwdWorkspaceIds.has(item.workspace_id));
270
394
  const selected = owned[0] ?? (cwdWorkspaces.length === 1 ? cwdWorkspaces[0] : void 0), selectedId = selected ? idOf(selected, "workspace_id") : void 0, actual = selectedId ? panes.filter((pane) => pane.workspace_id === selectedId).length : 0;
@@ -278,64 +402,93 @@ async function provisionHerdrPane(client, cwd, runId, runtimeDir, environment, a
278
402
  return { workspaceId: workspace.workspaceId, tabId: workspace.tabId, paneId: workspace.paneId };
279
403
  });
280
404
  }
281
- async function discoverBridgeManifest(machine, runId, runtimeDir, options = {}) {
282
- const script = `p="$1/manifest.json"; test -f "$p" && test ! -L "$p" && cat "$p"`;
283
- for (let attempt = 0; attempt < 100; attempt++) {
284
- const result = await runHerdrRemoteCommandAsync(machine, remoteShellCommand(script, [runtimeDir]), { ...options, timeout: 5e3, maxBuffer: 64 * 1024 });
285
- if (result.error) throw new Error(`Remote bridge manifest discovery failed: ${result.error.message}`, { cause: result.error });
286
- if (result.status === 0 && result.stdout.trim()) {
287
- let value;
288
- try {
289
- value = JSON.parse(result.stdout);
290
- } catch {
291
- throw new Error("Remote bridge manifest is malformed.");
292
- }
293
- const p = value;
294
- if (p.runId !== runId || typeof p.socketPath !== "string" || path.posix.dirname(p.socketPath) !== runtimeDir || typeof p.nativeSessionId !== "string" || typeof p.packageVersion !== "string" || typeof p.protocol !== "number") throw new Error("Remote bridge manifest identity is incomplete or outside its owned runtime directory.");
295
- return p;
405
+ function provisionHerdrOwnedPane(connection, machine, runId, runtimeDir, environment) {
406
+ return provisionHerdrPane(connection.client, machine.cwd, runId, runtimeDir, environment, herdrPaneAllocationKey(machine.target, connection.endpoint.session, machine.cwd));
407
+ }
408
+ function asHerdrMachine(machine) {
409
+ if (machine.provider !== "herdr") throw new Error("The Herdr pane host only places runs on Herdr saved machines.");
410
+ return machine;
411
+ }
412
+ async function startHerdrAgent(connection, owned, kind, name, args, options = {}) {
413
+ const clock = options.clock ?? { now: () => Date.now(), wait: (ms) => new Promise((resolve) => setTimeout(resolve, ms)) }, deadline = clock.now() + (options.timeoutMs ?? 5e3), pollMs = options.pollMs ?? 50;
414
+ let response;
415
+ for (; ; ) {
416
+ try {
417
+ response = await connection.client.call("agent.start", { name, kind, pane_id: owned.paneId, args, timeout_ms: 45e3 }, 6e4);
418
+ break;
419
+ } catch (error) {
420
+ if (!(error instanceof Error) || !error.message.includes("is not an available shell")) throw error;
421
+ const pane = tagged(await connection.client.call("pane.get", { pane_id: owned.paneId }), "pane_info", "pane");
422
+ if (pane.pane_id !== owned.paneId || pane.workspace_id !== owned.workspaceId || pane.tab_id !== owned.tabId) throw new Error("Herdr startup pane identity changed after a transient start rejection.", { cause: error });
423
+ if (pane.agent != null || pane.agent_session != null) throw new Error("Herdr startup pane became occupied after a transient start rejection.", { cause: error });
424
+ if (clock.now() >= deadline) throw new Error("Herdr owned pane remained unavailable until the startup deadline.", { cause: error });
425
+ await clock.wait(Math.min(pollMs, deadline - clock.now()));
296
426
  }
297
- await new Promise((resolve) => setTimeout(resolve, 50));
298
427
  }
299
- throw new Error("The remote Pi did not expose the packaged pi-subagents bridge. Install/configure the same package version as an ambient Pi extension on the saved machine.");
428
+ const started = parseHerdrAgent(response, "agent_started"), terminalId = idOf(started, "terminal_id"), paneId = idOf(started, "pane_id");
429
+ if (paneId !== owned.paneId) throw new Error("Herdr started the native agent in an unexpected pane.");
430
+ return { terminalId, paneId };
300
431
  }
301
- async function createRemoteRuntimeDir(machine, runId, options = {}) {
302
- const result = await runHerdrRemoteCommandAsync(machine, remoteShellCommand('umask 077; mktemp -d "${TMPDIR:-/tmp}/pi-subagents-herdr-$1-XXXXXXXX"', [runId]), { ...options, timeout: 1e4, maxBuffer: 4096 });
303
- if (result.error) throw new Error(`Could not provision a run-private remote bridge runtime directory: ${result.error.message}`, { cause: result.error });
304
- const value = result.stdout.trim();
305
- if (result.status !== 0 || !path.posix.isAbsolute(value) || value.includes("\n")) throw new Error("Could not provision a run-private remote bridge runtime directory.");
306
- return value;
432
+ async function closeOwnedHerdrPane(connection, owned, terminalId, agentName) {
433
+ let closed = false;
434
+ if (terminalId && agentName) {
435
+ const current = parseHerdrAgent(await connection.client.call("agent.get", { target: agentName }), "agent_info");
436
+ if (ownsHerdrPane(current, terminalId, owned.paneId)) {
437
+ await connection.client.call("pane.close", { pane_id: owned.paneId });
438
+ closed = true;
439
+ }
440
+ }
441
+ if (!closed) {
442
+ const snapshot = parseHerdrSessionSnapshot(await connection.client.call("session.snapshot"));
443
+ const occupants = snapshotAgents(snapshot).filter((agent) => agent.pane_id === owned.paneId);
444
+ if (occupants.length !== 0) throw new Error("Refusing cleanup because startup pane ownership is uncertain.");
445
+ await connection.client.call("pane.close", { pane_id: owned.paneId });
446
+ }
447
+ }
448
+ var herdrPaneHost = {
449
+ connect(machine) {
450
+ return connectHerdrMachine(asHerdrMachine(machine));
451
+ },
452
+ sessionOf(connection) {
453
+ return connection.endpoint.session;
454
+ },
455
+ async launch(connection, machine, runId, runtimeDir, request, onPane, options) {
456
+ const owned = await provisionHerdrOwnedPane(connection, asHerdrMachine(machine), runId, runtimeDir, request.environment);
457
+ await onPane(owned);
458
+ const { terminalId } = await startHerdrAgent(connection, owned, request.kind, request.name, request.args, options);
459
+ return { owned, terminalId };
460
+ },
461
+ async prompt(connection, identity, text) {
462
+ await connection.client.call("agent.prompt", { target: identity.agentName, text });
463
+ },
464
+ subscribe(connection, paneId, listener, onDisconnect) {
465
+ return connection.client.subscribe(herdrStatusSubscriptions(paneId), listener, onDisconnect);
466
+ },
467
+ paneEvent(event, fallback) {
468
+ const data = event && typeof event === "object" ? event.data : void 0;
469
+ return data ? { terminalId: data.terminal_id, paneId: data.pane_id, state: herdrRunState(data.agent_status, fallback) } : void 0;
470
+ },
471
+ async panes(connection) {
472
+ return { endpoint: connection.endpoint, agents: snapshotAgents(parseHerdrSessionSnapshot(await connection.client.call("session.snapshot"))) };
473
+ },
474
+ reconcile: reconcileHerdrPlacedRun,
475
+ closeOwnedPane: closeOwnedHerdrPane
476
+ };
477
+ function createRemoteRuntimeDir(machine, runId, options = {}) {
478
+ return createPlacedRuntimeDir(sshMachineAccess(machine, options), runId);
307
479
  }
308
480
  function removeRemoteRuntimeDir(machine, runtimeDir, options = {}) {
309
- const result = runHerdrRemoteCommand(machine, remoteShellCommand('p=$1; case "${p##*/}" in pi-subagents-herdr-*) test -d "$p" && test ! -L "$p" && rm -rf -- "$p";; *) exit 64;; esac', [runtimeDir]), { ...options, timeout: 1e4, maxBuffer: 4096 });
310
- if (result.status !== 0 || result.stdout) throw new Error(`Could not remove the exact owned remote runtime directory: ${String(result.stderr).slice(0, 512)}`);
481
+ removePlacedRuntimeDir(sshMachineAccess(machine, options), runtimeDir);
311
482
  }
312
- var HerdrPlacedRunOwner = class _HerdrPlacedRunOwner {
313
- machine;
314
- runId;
315
- runtimeDir;
316
- connection;
317
- owned;
318
- terminalId;
319
- agentName;
320
- startedArgv;
321
- identity;
322
- #unsubscribe = () => {
323
- };
324
- #snapshot = { connection: "connected", state: "ready", revision: 0 };
325
- #cleanupPromise;
326
- #removeRuntime;
327
- constructor(machine, runId, runtimeDir, connection, removeRuntime = () => removeRemoteRuntimeDir(machine, runtimeDir)) {
328
- this.machine = machine;
329
- this.runId = runId;
330
- this.runtimeDir = runtimeDir;
331
- this.connection = connection;
332
- this.#removeRuntime = removeRuntime;
483
+ var HerdrPlacedRunOwner = class _HerdrPlacedRunOwner extends PlacedRunOwner {
484
+ constructor(machine, runId, runtimeDir, connection, removeRuntime) {
485
+ super(machine, runId, runtimeDir, connection, herdrPaneHost, sshMachineAccess(machine), removeRuntime);
333
486
  }
334
487
  static async create(machine, runId, beforePane) {
335
488
  const runtimeDir = await createRemoteRuntimeDir(machine, runId);
336
489
  let connection;
337
490
  try {
338
- connection = await connectHerdrMachine(machine);
491
+ connection = await herdrPaneHost.connect(machine);
339
492
  const owner = new _HerdrPlacedRunOwner(machine, runId, runtimeDir, connection);
340
493
  await beforePane?.(owner);
341
494
  return owner;
@@ -355,126 +508,351 @@ var HerdrPlacedRunOwner = class _HerdrPlacedRunOwner {
355
508
  throw error;
356
509
  }
357
510
  }
358
- observeEvent(event) {
359
- const data = event && typeof event === "object" ? event.data : void 0;
360
- if (data && (!this.terminalId || data.terminal_id === this.terminalId || data.pane_id === this.owned?.paneId)) this.#snapshot = { ...this.#snapshot, revision: this.#snapshot.revision + 1, state: herdrRunState(data.agent_status, this.#snapshot.state) };
361
- }
362
- markConnectionUnknown(observed) {
363
- if (this.connection !== observed) return false;
364
- this.#snapshot = { ...this.#snapshot, connection: "unknown", state: "unknown", revision: this.#snapshot.revision + 1 };
365
- return true;
366
- }
367
- statusSubscriptions(paneId = this.owned?.paneId) {
368
- if (!paneId) throw new Error("Herdr status subscription requires a provisioned pane.");
369
- return herdrStatusSubscriptions(paneId);
370
- }
371
- async subscribe(listener, disconnected) {
372
- const observed = this.connection;
373
- let adopted = false, candidateLost;
374
- const unsubscribe = await observed.client.subscribe(this.statusSubscriptions(), (event) => {
375
- this.observeEvent(event);
376
- listener(event);
377
- }, (error) => {
378
- if (!adopted) {
379
- candidateLost ??= error;
380
- return;
381
- }
382
- if (!this.markConnectionUnknown(observed)) return;
383
- disconnected(error);
384
- });
385
- if (candidateLost) {
386
- unsubscribe();
387
- throw candidateLost;
388
- }
389
- this.#unsubscribe = unsubscribe;
390
- adopted = true;
391
- }
392
511
  async provision(environment) {
393
- this.owned = await provisionHerdrPane(this.connection.client, this.machine.cwd, this.runId, this.runtimeDir, environment, herdrPaneAllocationKey(this.machine.target, this.connection.endpoint.session, this.machine.cwd));
512
+ this.owned = await provisionHerdrOwnedPane(this.connection, this.machine, this.runId, this.runtimeDir, environment);
394
513
  return this.owned;
395
514
  }
396
515
  async start(kind, name, args, options = {}) {
397
516
  if (!this.owned) throw new Error("Herdr placement owner has not provisioned its pane.");
398
- const clock = options.clock ?? { now: () => Date.now(), wait: (ms) => new Promise((resolve) => setTimeout(resolve, ms)) }, deadline = clock.now() + (options.timeoutMs ?? 5e3), pollMs = options.pollMs ?? 50;
399
- let response;
400
- for (; ; ) {
401
- try {
402
- response = await this.connection.client.call("agent.start", { name, kind, pane_id: this.owned.paneId, args, timeout_ms: 45e3 }, 6e4);
403
- break;
404
- } catch (error) {
405
- if (!(error instanceof Error) || !error.message.includes("is not an available shell")) throw error;
406
- const pane = tagged(await this.connection.client.call("pane.get", { pane_id: this.owned.paneId }), "pane_info", "pane");
407
- if (pane.pane_id !== this.owned.paneId || pane.workspace_id !== this.owned.workspaceId || pane.tab_id !== this.owned.tabId) throw new Error("Herdr startup pane identity changed after a transient start rejection.", { cause: error });
408
- if (pane.agent != null || pane.agent_session != null) throw new Error("Herdr startup pane became occupied after a transient start rejection.", { cause: error });
409
- if (clock.now() >= deadline) throw new Error("Herdr owned pane remained unavailable until the startup deadline.", { cause: error });
410
- await clock.wait(Math.min(pollMs, deadline - clock.now()));
411
- }
412
- }
413
- const started = parseHerdrAgent(response, "agent_started"), argv = [kind, ...args], terminalId = idOf(started, "terminal_id"), paneId = idOf(started, "pane_id");
414
- if (paneId !== this.owned.paneId) throw new Error("Herdr started the native agent in an unexpected pane.");
517
+ const { terminalId, paneId } = await startHerdrAgent(this.connection, this.owned, kind, name, args, options), argv = [kind, ...args];
415
518
  this.agentName = name;
416
519
  this.terminalId = terminalId;
417
520
  this.startedArgv = argv;
418
521
  return { terminalId, paneId, argv };
419
522
  }
420
- journal(identity) {
421
- this.identity = identity;
422
- this.#snapshot = { ...this.#snapshot, identity };
423
- const journalDir = path.join(getAgentDir(), "pi-subagents", "herdr-run-journal");
424
- fs.mkdirSync(journalDir, { recursive: true, mode: 448 });
425
- fs.writeFileSync(path.join(journalDir, `${this.runId}.json`), JSON.stringify({ version: 1, identity, state: "ready", updatedAt: Date.now() }), { mode: 384, flag: "wx" });
523
+ };
524
+
525
+ // src/runs/shared/rex-pane-host.ts
526
+ import { createHash as createHash3 } from "node:crypto";
527
+ import * as path4 from "node:path";
528
+ import { Type } from "typebox";
529
+ import { Compile } from "typebox/compile";
530
+
531
+ // src/runs/shared/local-machine-access.ts
532
+ import { spawnSync } from "node:child_process";
533
+ import * as path3 from "node:path";
534
+ var DEFAULT_TIMEOUT_MS = 15e3;
535
+ var DEFAULT_MAX_BUFFER = 256 * 1024;
536
+ function localSockets() {
537
+ return {
538
+ async forwardRemoteSocket(socketPath) {
539
+ if (!path3.isAbsolute(socketPath)) throw new Error("Local socket path must be absolute.");
540
+ return { socketPath, async close() {
541
+ } };
542
+ },
543
+ async close() {
544
+ }
545
+ };
546
+ }
547
+ function localMachineAccess(options = {}) {
548
+ const env = options.env ?? process.env;
549
+ return {
550
+ shellCommand: remoteShellCommand,
551
+ run(command, limits = {}) {
552
+ const spawned = spawnSync("/bin/sh", ["-c", command], { encoding: "utf8", env, timeout: limits.timeout ?? DEFAULT_TIMEOUT_MS, maxBuffer: limits.maxBuffer ?? DEFAULT_MAX_BUFFER, windowsHide: true });
553
+ const result = { status: spawned.status, stdout: spawned.stdout ?? "", stderr: spawned.stderr ?? "" };
554
+ if (spawned.error) result.error = spawned.error;
555
+ return result;
556
+ },
557
+ runAsync: (command, limits = {}) => runBoundedCommand("/bin/sh", ["-c", command], { env, ...limits }),
558
+ openSockets: localSockets
559
+ };
560
+ }
561
+
562
+ // src/runs/shared/rex-pane-host.ts
563
+ var REX_CLOSE_POLLS = 20;
564
+ var REX_CLOSE_POLL_MS = 50;
565
+ var REX_SSH_SESSION_LABEL = "subagents";
566
+ var sessionInspection = Compile(Type.Object({ session_id: Type.String() }));
567
+ var sessionList = Compile(Type.Object({ sessions: Type.Array(Type.Object({ session_id: Type.String(), label: Type.Optional(Type.String()) })) }));
568
+ var createdWindow = Compile(Type.Object({ window_id: Type.String(), block_ids: Type.Tuple([Type.String()]) }));
569
+ var 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() })) })) })) }));
570
+ var blockProcess = Compile(Type.Object({ foreground: Type.Object({ pid: Type.Integer() }) }));
571
+ var bridgeManifestPid = Compile(Type.Object({ pid: Type.Integer() }));
572
+ function rexAccess() {
573
+ return localMachineAccess({ env: { ...rexCommandEnv(), TMPDIR: "/tmp" } });
574
+ }
575
+ function rexSshAccess(machine, access = sshMachineAccess(machine)) {
576
+ return { ...access, shellCommand: (script, args) => remoteShellCommand(`TMPDIR=/tmp; export TMPDIR; ${script}`, args) };
577
+ }
578
+ function asRexMachine(machine) {
579
+ if (machine.provider !== "rex") throw new Error("The Rex pane host only places runs on Rex machines.");
580
+ return machine;
581
+ }
582
+ async function runRex(endpoint, args, what) {
583
+ 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 });
584
+ if (result.error) throw new Error(`Rex ${what} failed: ${result.error.message}`, { cause: result.error });
585
+ if (result.status === REX_EXIT_UNREACHABLE) throw new Error(`The Rex server is unreachable (rex ${what} exited 4); the placed pane's state is unknown.`);
586
+ if (result.status === null) throw new Error(`Rex ${what} did not finish; the placed pane's state is unknown.`);
587
+ return { ...result, status: result.status };
588
+ }
589
+ function requireRexSuccess(result, what) {
590
+ if (result.status === REX_EXIT_GONE) throw new Error(`Rex ${what} failed because its target no longer exists (exit 3): ${result.stderr.trim().slice(0, 512)}`);
591
+ if (result.status !== 0) throw new Error(`Rex ${what} exited with code ${result.status}: ${result.stderr.trim().slice(0, 512)}`);
592
+ }
593
+ function parseRexJson(text, validator, what) {
594
+ let value;
595
+ try {
596
+ value = JSON.parse(text);
597
+ } catch {
598
+ throw new Error(`Rex ${what} returned malformed JSON.`);
426
599
  }
427
- get snapshot() {
428
- return this.#snapshot;
600
+ return validator.Check(value) ? value : void 0;
601
+ }
602
+ async function resolveRemoteRexBinary(access, machine) {
603
+ 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`;
604
+ const found = await access.runAsync(access.shellCommand(script), { timeout: REX_CALL_TIMEOUT_MS, maxBuffer: 64 * 1024 });
605
+ if (found.error) throw new Error(`Could not look for the Rex CLI on ${machine.id}: ${found.error.message}`, { cause: found.error });
606
+ const bin = found.stdout.trim();
607
+ if (found.status !== 0 || !path4.posix.isAbsolute(bin) || bin.includes("\n")) throw new Error(`The Rex CLI was not found on ${machine.id}. Install Rex there or put rex on its PATH.`);
608
+ return bin;
609
+ }
610
+ async function resolveRemoteCwd(access, machine) {
611
+ const script = `case "$1" in "~") d=$HOME;; "~/"*) d=$HOME/\${1#"~/"};; *) d=$1;; esac; cd -P -- "$d" 2>/dev/null && pwd -P`;
612
+ const resolved = await access.runAsync(access.shellCommand(script, [machine.cwd]), { timeout: REX_CALL_TIMEOUT_MS, maxBuffer: 64 * 1024 });
613
+ if (resolved.error) throw new Error(`Could not check ${machine.cwd} on ${machine.id}: ${resolved.error.message}`, { cause: resolved.error });
614
+ const cwd = resolved.stdout.trim();
615
+ if (resolved.status !== 0 || !path4.posix.isAbsolute(cwd) || cwd.includes("\n")) 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.`);
616
+ return cwd;
617
+ }
618
+ async function resolveRemotePi(access, machine) {
619
+ const found = await access.runAsync(access.shellCommand(`exec "\${SHELL:-/bin/sh}" -lc 'command -v pi'`), { timeout: REX_CALL_TIMEOUT_MS, maxBuffer: 64 * 1024 });
620
+ if (found.error) throw new Error(`Could not look for Pi on ${machine.id}: ${found.error.message}`, { cause: found.error });
621
+ const pi = found.stdout.trim().split("\n").at(-1)?.trim() ?? "";
622
+ if (found.status !== 0 || !path4.posix.isAbsolute(pi)) throw new Error(`Pi was not found on ${machine.id}: its login shell has no pi on PATH. Install Pi there.`);
623
+ return pi;
624
+ }
625
+ async function labeledSessions(endpoint) {
626
+ const listed = await runRex(endpoint, ["ls", "--json"], "ls");
627
+ requireRexSuccess(listed, "ls");
628
+ const parsed = parseRexJson(listed.stdout, sessionList, "ls");
629
+ if (!parsed) throw new Error("Rex ls did not list the server's sessions.");
630
+ return parsed.sessions.filter((session) => session.label === REX_SSH_SESSION_LABEL).map((session) => session.session_id);
631
+ }
632
+ async function subagentsSession(endpoint, machine) {
633
+ return withHerdrPaneAllocationLock(JSON.stringify(["rex-session", machine.target, REX_SSH_SESSION_LABEL]), async () => {
634
+ let sessions = await labeledSessions(endpoint);
635
+ if (sessions.length === 0) {
636
+ requireRexSuccess(await runRex(endpoint, ["new", REX_SSH_SESSION_LABEL, "--json"], "new"), "new");
637
+ sessions = await labeledSessions(endpoint);
638
+ }
639
+ const [session] = sessions;
640
+ if (sessions.length > 1) 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.`);
641
+ if (!session) throw new Error(`Rex on ${machine.id} did not list the ${REX_SSH_SESSION_LABEL} session it just created.`);
642
+ return session;
643
+ });
644
+ }
645
+ async function connectRex(machine, options = {}) {
646
+ const rex = asRexMachine(machine);
647
+ let endpoint;
648
+ if (isRexLocalMachine(rex)) {
649
+ endpoint = { access: options.access ?? rexAccess(), bin: options.bin ?? resolveRexBinary(), session: rex.session };
650
+ if (rex.server) endpoint.server = rex.server;
651
+ } else {
652
+ if (!options.session) throw new Error(`Rex machine '${rex.id}' has no recorded session; a placed run never picks one by label after launch.`);
653
+ const access = options.access ?? rexSshAccess(rex);
654
+ endpoint = { access, bin: options.bin ?? await resolveRemoteRexBinary(access, rex), session: options.session };
429
655
  }
430
- runRemote(command, options = {}) {
431
- return runHerdrRemoteCommand(this.machine, command, options);
656
+ const inspected = await runRex(endpoint, ["session", "inspect", endpoint.session, "--json"], "session inspect");
657
+ if (inspected.status === REX_EXIT_GONE) throw new Error(`Rex session ${endpoint.session} no longer exists on the Rex server.`);
658
+ requireRexSuccess(inspected, "session inspect");
659
+ if (parseRexJson(inspected.stdout, sessionInspection, "session inspect")?.session_id !== endpoint.session) throw new Error("Rex session identity changed: the server reported a different session id.");
660
+ return { ...endpoint, ...endpoint.access.openSockets() };
661
+ }
662
+ async function inspectBlock(connection, blockId) {
663
+ const inspected = await runRex(connection, ["block", "inspect", "-s", connection.session, blockId], "block inspect");
664
+ if (inspected.status === REX_EXIT_GONE) return "gone";
665
+ requireRexSuccess(inspected, "block inspect");
666
+ return "present";
667
+ }
668
+ async function closeOwnedRexPane(connection, owned) {
669
+ if (owned.workspaceId !== connection.session) throw new Error("Refusing Rex cleanup: the owned pane belongs to a different Rex session.");
670
+ if (await inspectBlock(connection, owned.paneId) === "gone") return;
671
+ const closed = await runRex(connection, ["block", "close", "-s", connection.session, owned.paneId], "block close");
672
+ if (closed.status !== REX_EXIT_GONE) requireRexSuccess(closed, "block close");
673
+ for (let poll = 0; poll < REX_CLOSE_POLLS; poll++) {
674
+ if (await inspectBlock(connection, owned.paneId) === "gone") return;
675
+ await new Promise((resolve) => setTimeout(resolve, REX_CLOSE_POLL_MS));
432
676
  }
433
- async replaceConnection(input) {
434
- const oldConnection = this.connection, oldUnsubscribe = this.#unsubscribe;
435
- this.connection = input.connection;
436
- this.#unsubscribe = input.unsubscribe;
437
- if (this.identity) this.identity = { ...this.identity, paneId: input.paneId };
438
- this.#snapshot = { connection: "connected", state: input.state, identity: this.identity, revision: this.#snapshot.revision + 1 };
439
- oldUnsubscribe();
440
- await oldConnection.close();
677
+ throw new Error(`Rex block ${owned.paneId} still exists after close; refusing to report the placed pane closed.`);
678
+ }
679
+ function reconcileRexPlacedRun(identity, candidate, proveEvidence) {
680
+ if (!candidate.present) return { ok: false, reason: "Owned Rex block is gone." };
681
+ if (candidate.foregroundPid === void 0 || candidate.foregroundPid !== candidate.bridgePid) return { ok: false, reason: "The owned Rex block's foreground process is not the placed Pi." };
682
+ const proof = proveEvidence();
683
+ if (!proof.ok) return proof;
684
+ return { ok: true, state: "unknown", paneId: identity.paneId, cursor: proof.cursor };
685
+ }
686
+ async function readBridgePid(connection, runtimeDir) {
687
+ const manifest = await connection.access.runAsync(connection.access.shellCommand(PLACED_MANIFEST_READ_SCRIPT, [runtimeDir]), { timeout: 5e3, maxBuffer: 64 * 1024 });
688
+ if (manifest.status !== 0) return void 0;
689
+ return parseRexJson(manifest.stdout, bridgeManifestPid, "bridge manifest")?.pid;
690
+ }
691
+ async function closeUntrackedRexWindow(connection, label, failure) {
692
+ try {
693
+ const inspected = await runRex(connection, ["session", "inspect", connection.session, "--json"], "session inspect");
694
+ if (inspected.status === REX_EXIT_GONE) throw failure;
695
+ requireRexSuccess(inspected, "session inspect");
696
+ const windows = parseRexJson(inspected.stdout, sessionWindows, "session inspect")?.windows.filter((window) => window.label === label);
697
+ if (!windows) throw new Error("Rex session inspect did not list the session's windows.");
698
+ if (windows.length > 1) throw new Error(`The Rex session has ${windows.length} windows labeled ${label}; refusing to guess which one this run opened.`);
699
+ for (const window of windows) for (const layer of window.layers) for (const block of layer.blocks) await closeOwnedRexPane(connection, { workspaceId: connection.session, tabId: window.window_id, paneId: block.block_id });
700
+ } catch (cleanupError) {
701
+ if (cleanupError === failure) throw failure;
702
+ 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 });
441
703
  }
442
- cleanup(closeStartedPane = true) {
443
- return this.#cleanupPromise ??= this.#performCleanup(closeStartedPane);
704
+ throw failure;
705
+ }
706
+ async function openRexWindow(connection, label, cwd, argv) {
707
+ try {
708
+ const created = await runRex(connection, ["window", "new", "-s", connection.session, label, "-c", cwd, "--focus=false", "--json", "--", ...argv], "window new");
709
+ requireRexSuccess(created, "window new");
710
+ const window = parseRexJson(created.stdout, createdWindow, "window new");
711
+ if (!window) throw new Error(`Rex window new did not return exactly one block: ${created.stdout.trim().slice(0, 512)}`);
712
+ return window;
713
+ } catch (error) {
714
+ return closeUntrackedRexWindow(connection, label, error instanceof Error ? error : new Error(String(error)));
444
715
  }
445
- async #performCleanup(closeStartedPane) {
446
- let failure;
447
- try {
448
- if (closeStartedPane && this.owned) {
449
- let closed = false;
450
- if (this.terminalId && this.agentName) {
451
- const current = parseHerdrAgent(await this.connection.client.call("agent.get", { target: this.agentName }), "agent_info");
452
- if (ownsHerdrPane(current, this.terminalId, this.owned.paneId)) {
453
- await this.connection.client.call("pane.close", { pane_id: this.owned.paneId });
454
- closed = true;
455
- }
456
- }
457
- if (!closed) {
458
- const snapshot = parseHerdrSessionSnapshot(await this.connection.client.call("session.snapshot"));
459
- const occupants = snapshotAgents(snapshot).filter((agent) => agent.pane_id === this.owned.paneId);
460
- if (occupants.length !== 0) throw new Error("Refusing cleanup because startup pane ownership is uncertain.");
461
- await this.connection.client.call("pane.close", { pane_id: this.owned.paneId });
716
+ }
717
+ var rexPaneHost = {
718
+ connect: connectRex,
719
+ sessionOf(connection) {
720
+ return connection.session;
721
+ },
722
+ async launch(connection, machine, runId, _runtimeDir, request, onPane) {
723
+ if (request.kind !== "pi") throw new Error("Rex supports native Pi children only.");
724
+ const environment = Object.entries(request.environment ?? {});
725
+ if (environment.some(([name]) => !/^[A-Za-z_][A-Za-z0-9_]*$/u.test(name))) throw new Error("Rex launch environment names must be shell variable names.");
726
+ const rex = asRexMachine(machine);
727
+ let pi;
728
+ if (isRexLocalMachine(rex)) {
729
+ pi = getPiSpawnCommand(request.args);
730
+ if (!path4.isAbsolute(pi.command)) throw new Error("Rex placement needs an absolute Pi executable; set PI_SUBAGENT_PI_BINARY to the Pi binary path.");
731
+ } else pi = { command: await resolveRemotePi(connection.access, rex), args: request.args };
732
+ const window = await openRexWindow(connection, `subagent-${runId.slice(-8)}`, rex.cwd, ["/usr/bin/env", ...environment.map(([name, value]) => `${name}=${value}`), pi.command, ...pi.args]);
733
+ const owned = { workspaceId: connection.session, tabId: window.window_id, paneId: window.block_ids[0] };
734
+ await onPane(owned);
735
+ return { owned, terminalId: owned.paneId };
736
+ },
737
+ async prompt(connection, identity, text) {
738
+ requireRexSuccess(await runRex(connection, ["send", "-s", connection.session, "-b", identity.paneId, "--", text], "send"), "send");
739
+ requireRexSuccess(await runRex(connection, ["send-key", "-s", connection.session, "-b", identity.paneId, "enter"], "send-key"), "send-key");
740
+ },
741
+ async subscribe() {
742
+ return () => {
743
+ };
744
+ },
745
+ paneEvent() {
746
+ return void 0;
747
+ },
748
+ async panes(connection, identity) {
749
+ const processInfo = await runRex(connection, ["block", "call", "-s", connection.session, "-b", identity.paneId, "process"], "block process");
750
+ if (processInfo.status === REX_EXIT_GONE) return { present: false };
751
+ requireRexSuccess(processInfo, "block process");
752
+ const candidate = { present: true };
753
+ const foregroundPid = parseRexJson(processInfo.stdout, blockProcess, "block process")?.foreground.pid;
754
+ if (foregroundPid !== void 0) candidate.foregroundPid = foregroundPid;
755
+ const bridgePid = await readBridgePid(connection, identity.runtimeDir);
756
+ if (bridgePid !== void 0) candidate.bridgePid = bridgePid;
757
+ return candidate;
758
+ },
759
+ reconcile: reconcileRexPlacedRun,
760
+ closeOwnedPane: closeOwnedRexPane
761
+ };
762
+ function runtimeDirPresent(access, runtimeDir) {
763
+ const result = access.run(access.shellCommand('test -e "$1" || test -L "$1"', [runtimeDir]), { timeout: 1e4, maxBuffer: 4096 });
764
+ if (result.error) throw new Error(`Could not check the owned runtime directory: ${result.error.message}`, { cause: result.error });
765
+ return result.status === 0;
766
+ }
767
+ function removeRexRuntimeDir(access, runtimeDir) {
768
+ if (!runtimeDirPresent(access, runtimeDir)) return;
769
+ try {
770
+ removePlacedRuntimeDir(access, runtimeDir);
771
+ } catch (error) {
772
+ if (runtimeDirPresent(access, runtimeDir)) throw error;
773
+ }
774
+ }
775
+ var RexPlacedRunOwner = class _RexPlacedRunOwner extends PlacedRunOwner {
776
+ constructor(machine, runId, runtimeDir, connection, access, host) {
777
+ super(machine, runId, runtimeDir, connection, host, access, () => removeRexRuntimeDir(access, runtimeDir));
778
+ }
779
+ /** A Rex SSH owner's machine carries the resolved remote cwd, and its host reconnects only to the session recorded here. */
780
+ static async create(machine, runId, options = {}) {
781
+ const hash = createHash3("sha256").update(runId).digest("hex").slice(0, 12);
782
+ if (isRexLocalMachine(machine)) {
783
+ const access2 = rexAccess(), runtimeDir2 = await createPlacedRuntimeDir(access2, hash);
784
+ try {
785
+ return new _RexPlacedRunOwner(machine, runId, runtimeDir2, await connectRex(machine, { access: access2 }), access2, rexPaneHost);
786
+ } catch (error) {
787
+ try {
788
+ removeRexRuntimeDir(access2, runtimeDir2);
789
+ } catch (cleanupError) {
790
+ throw new AggregateError([error, cleanupError], "Rex owner creation failed and runtime cleanup also failed.", { cause: error });
462
791
  }
792
+ throw error;
463
793
  }
464
- } catch (error) {
465
- failure = error;
466
794
  }
795
+ const access = rexSshAccess(machine, options.sshAccess), bin = await resolveRemoteRexBinary(access, machine);
796
+ const placed = { ...machine, cwd: await resolveRemoteCwd(access, machine) };
797
+ const session = await subagentsSession({ access, bin }, placed);
798
+ const host = { ...rexPaneHost, connect: (target) => connectRex(target, { access, bin, session }) };
799
+ const runtimeDir = await createPlacedRuntimeDir(access, hash);
467
800
  try {
468
- this.#unsubscribe();
469
- await this.connection.close();
470
- this.#removeRuntime();
801
+ return new _RexPlacedRunOwner(placed, runId, runtimeDir, await host.connect(placed), access, host);
471
802
  } catch (error) {
472
- failure ??= error;
803
+ try {
804
+ removeRexRuntimeDir(access, runtimeDir);
805
+ } catch (cleanupError) {
806
+ throw new AggregateError([error, cleanupError], "Rex owner creation failed and runtime cleanup also failed.", { cause: error });
807
+ }
808
+ throw error;
473
809
  }
474
- this.#snapshot = { ...this.#snapshot, connection: "closed", revision: this.#snapshot.revision + 1 };
475
- if (failure) throw failure;
476
810
  }
477
811
  };
812
+
813
+ // src/runs/shared/herdr-placed-run.ts
814
+ function reconcileHerdrReconnect(identity, candidate, lastEvidenceCursor, host = herdrPaneHost) {
815
+ return host.reconcile(identity, candidate, () => {
816
+ const bridge = candidate.bridge;
817
+ if (bridge.runId !== identity.runId || bridge.nativeSessionId !== identity.nativeSessionId || bridge.packageVersion !== package_default.version || bridge.protocol !== HERDR_PI_PROTOCOL) return { ok: false, reason: "Bridge run, package, protocol, or native session identity changed." };
818
+ if (typeof bridge.evidenceCursor !== "number" || typeof bridge.evidenceFloor !== "number" || bridge.evidenceCursor < lastEvidenceCursor || bridge.evidenceFloor > lastEvidenceCursor + 1) return { ok: false, reason: "Bridge evidence has a gap after reconnect." };
819
+ return { ok: true, cursor: bridge.evidenceCursor };
820
+ });
821
+ }
822
+ async function boundedHerdrReconnect(identity, lastEvidenceCursor, attempt, options = {}) {
823
+ const deadline = Date.now() + (options.deadlineMs ?? 15e3);
824
+ let last = "Reconnect did not run.";
825
+ for (let number = 1; number <= (options.attempts ?? 3) && Date.now() < deadline; number++) {
826
+ if (options.disposed?.()) return { ok: false, reason: "Reconnect was disposed." };
827
+ try {
828
+ const result = reconcileHerdrReconnect(identity, await attempt(number), lastEvidenceCursor, options.host);
829
+ if (result.ok) return result;
830
+ last = result.reason;
831
+ } catch (error) {
832
+ last = error instanceof Error ? error.message : String(error);
833
+ }
834
+ await new Promise((resolve) => setTimeout(resolve, Math.min(number * 100, Math.max(0, deadline - Date.now()))));
835
+ }
836
+ return { ok: false, reason: last };
837
+ }
838
+ function safeRunId(value) {
839
+ const prefix = value?.replace(/[^A-Za-z0-9_-]/gu, "_").slice(0, 48) ?? "run";
840
+ return validateHerdrPiRunId(`${prefix}_${randomBytes2(8).toString("hex")}`);
841
+ }
842
+ function serializeHerdrPiLaunch(launch) {
843
+ if (launch.storage.kind === "file") throw new Error("Pane-native remote Pi does not support fork, resume, or revival; use fresh context.");
844
+ if (launch.extensionPaths.length || launch.requiredExtensions?.length) throw new Error("Pane-native remote Pi cannot transfer local extension paths; install/configure pi-subagents on the remote machine.");
845
+ if (launch.processEnv && Object.values(launch.processEnv).some((value) => value !== void 0 && value !== "__none__")) throw new Error("Pane-native remote Pi cannot transfer local bindings, MCP selections, or process environment.");
846
+ if (launch.runtime.nestedRoute || launch.runtime.fanoutChild || launch.runtime.depth > 1) throw new Error("Nested delegation from a pane-native remote Pi is not supported.");
847
+ if (launch.excludeTools?.length || launch.runtime.permissions || launch.runtime.toolBudget || launch.runtime.requiredExtensions || launch.runtime.requiredTools?.length || launch.runtime.mcpDirectTools?.length) 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.");
848
+ const resources = launch.remoteResources;
849
+ 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 !== void 0 && resources.reads !== false && resources.reads.some((read) => typeof read !== "string" || Buffer.byteLength(read) > 4096)) throw new Error("Pane-native remote resources require bounded logical agent, skill, tool, and read names.");
850
+ const args = [];
851
+ if (launch.noSkills) args.push("--no-skills", "--no-prompt-templates");
852
+ if (launch.noContextFiles) args.push("--no-context-files");
853
+ if (launch.model) args.push("--model", launch.model);
854
+ return { args, resources: { agent: resources.agent, ...resources.skills ? { skills: [...resources.skills] } : {}, ...resources.toolCeiling ? { toolCeiling: [...resources.toolCeiling] } : {}, ...resources.reads !== void 0 ? { reads: resources.reads === false ? false : [...resources.reads] } : {} } };
855
+ }
478
856
  var BridgeChannel = class {
479
857
  socket;
480
858
  decoder = new HerdrPiFrameDecoder();
@@ -538,7 +916,7 @@ var BridgeChannel = class {
538
916
  return new Promise((resolve, reject) => this.waiters.set(`${type}:${requestId}`, { resolve, reject }));
539
917
  }
540
918
  configure(resources) {
541
- const requestId = `cfg_${randomBytes(6).toString("hex")}`;
919
+ const requestId = `cfg_${randomBytes2(6).toString("hex")}`;
542
920
  const waiting = this.wait("configured", requestId);
543
921
  this.socket.write(encodeHerdrPiFrame({ protocol: HERDR_PI_PROTOCOL, runId: this.runId, type: "configure", requestId, resources }));
544
922
  return waiting;
@@ -546,9 +924,9 @@ var BridgeChannel = class {
546
924
  supervisorDelivered(requestId) {
547
925
  this.socket.write(encodeHerdrPiFrame({ protocol: HERDR_PI_PROTOCOL, runId: this.runId, type: "supervisor-delivered", requestId }));
548
926
  }
549
- prepare(requestId, operation, text, supervisorId) {
927
+ prepare(requestId, operation, text, supervisorId, supervisorError) {
550
928
  const waiting = this.wait("prepared", requestId);
551
- this.socket.write(encodeHerdrPiFrame({ protocol: HERDR_PI_PROTOCOL, runId: this.runId, type: "prepare", requestId, operation, ...text !== void 0 ? { text } : {}, ...supervisorId ? { supervisorId } : {} }));
929
+ this.socket.write(encodeHerdrPiFrame({ protocol: HERDR_PI_PROTOCOL, runId: this.runId, type: "prepare", requestId, operation, ...text !== void 0 ? { text } : {}, ...supervisorId ? { supervisorId } : {}, ...supervisorError !== void 0 ? { error: supervisorError } : {} }));
552
930
  return waiting;
553
931
  }
554
932
  close() {
@@ -576,7 +954,6 @@ var HerdrPiSession = class {
576
954
  #initialGit;
577
955
  #finalGit;
578
956
  #operations = /* @__PURE__ */ new Map();
579
- #snapshot;
580
957
  bridgeForward;
581
958
  bridge;
582
959
  requestedTools;
@@ -597,17 +974,16 @@ var HerdrPiSession = class {
597
974
  this.model = model;
598
975
  this.runtime = runtime;
599
976
  this.supervisorDir = runtime.supervisorChannelDir;
600
- this.#snapshot = { connection: "connected", state: "ready", identity: owner.identity, revision: 0 };
601
977
  this.#initialGit = initialGit;
602
978
  bridge.adopt((frame) => this.#frame(frame));
603
979
  if (this.supervisorDir) {
604
980
  this.#replyTimer = setInterval(() => {
605
- const replies = path.join(this.supervisorDir, "replies");
606
- for (const name of fs.existsSync(replies) ? fs.readdirSync(replies) : []) {
607
- const file = path.join(replies, name);
981
+ const replies = path5.join(this.supervisorDir, "replies");
982
+ for (const name of fs3.existsSync(replies) ? fs3.readdirSync(replies) : []) {
983
+ const file = path5.join(replies, name);
608
984
  try {
609
- const value = JSON.parse(fs.readFileSync(file, "utf8"));
610
- if (typeof value.message === "string" && typeof value.requestId === "string") void this.#operation("supervisor-reply", value.message, value.requestId).then(() => fs.rmSync(file, { force: true }));
985
+ const value = JSON.parse(fs3.readFileSync(file, "utf8"));
986
+ if (typeof value.message === "string" && typeof value.requestId === "string") void this.#operation("supervisor-reply", value.message, value.requestId, typeof value.error === "string" ? value.error : void 0).then(() => fs3.rmSync(file, { force: true }));
611
987
  } catch {
612
988
  }
613
989
  }
@@ -615,6 +991,9 @@ var HerdrPiSession = class {
615
991
  this.#replyTimer.unref?.();
616
992
  }
617
993
  }
994
+ get host() {
995
+ return this.owner.host;
996
+ }
618
997
  get identity() {
619
998
  return this.owner.identity;
620
999
  }
@@ -639,7 +1018,6 @@ var HerdrPiSession = class {
639
1018
  }
640
1019
  async reconnect() {
641
1020
  if (this.#disposed || !this.#reconnectFactory || this.#reconnectFailure) return;
642
- this.#snapshot = { ...this.#snapshot, connection: "unknown", state: "unknown", revision: this.#snapshot.revision + 1 };
643
1021
  this.#reconnecting ??= this.#reconnectFactory().catch((error) => {
644
1022
  const unknown = this.#reconnectFailure = new Error(`Remote Pi reconciliation failed; task state is unknown: ${error instanceof Error ? error.message : String(error)}`);
645
1023
  for (const operation of this.#operations.values()) if (!operation.settled) {
@@ -670,7 +1048,6 @@ var HerdrPiSession = class {
670
1048
  operation.reject(new Error("Remote operation was not accepted before transport loss; task state is unknown."));
671
1049
  }
672
1050
  this.#evidenceCursor = Math.max(this.#evidenceCursor, input.cursor);
673
- this.#snapshot = { connection: "connected", state: input.state, identity: this.identity, revision: this.#snapshot.revision + 1 };
674
1051
  const ownerReplacement = this.owner.replaceConnection({ connection: input.connection, unsubscribe: input.unsubscribe, paneId: input.paneId, state: input.state });
675
1052
  old.bridge.close();
676
1053
  await old.bridgeForward.close();
@@ -681,11 +1058,6 @@ var HerdrPiSession = class {
681
1058
  }
682
1059
  this.armReconnect(this.#reconnectFactory);
683
1060
  }
684
- observeHerdr(value) {
685
- const data = value && typeof value === "object" ? value.data : void 0;
686
- if (!data || data.terminal_id !== this.identity.terminalId && data.pane_id !== this.identity.paneId) return;
687
- this.#snapshot = { ...this.#snapshot, revision: this.#snapshot.revision + 1, state: herdrRunState(data.agent_status, this.#snapshot.state) };
688
- }
689
1061
  #frame(frame) {
690
1062
  if (typeof frame.cursor === "number") {
691
1063
  if (frame.cursor <= this.#evidenceCursor) return;
@@ -727,24 +1099,24 @@ var HerdrPiSession = class {
727
1099
  const reason = frame.reason;
728
1100
  if (reason !== "need_decision" && reason !== "interview_request" && reason !== "progress_update") return;
729
1101
  const createdAt = Date.now();
730
- const file = path.join(this.supervisorDir, "requests", `${path.basename(frame.requestId)}.json`);
1102
+ const file = path5.join(this.supervisorDir, "requests", `${path5.basename(frame.requestId)}.json`);
731
1103
  const request = { type: "subagent.supervisor.request", id: frame.requestId, createdAt, ...reason !== "progress_update" ? { expiresAt: createdAt + 10 * 6e4 } : {}, reason, message: typeof frame.message === "string" ? frame.message : "", ...frame.interview && typeof frame.interview === "object" ? { interview: frame.interview } : {}, expectsReply: reason !== "progress_update", runId: this.runtime.runId ?? this.identity.runId, agent: this.runtime.agent ?? "remote", childIndex: this.runtime.childIndex ?? 0, orchestratorSessionId: this.runtime.orchestratorSessionId };
732
1104
  try {
733
- fs.writeFileSync(file, JSON.stringify(request), { mode: 384, flag: "wx" });
1105
+ fs3.writeFileSync(file, JSON.stringify(request), { mode: 384, flag: "wx" });
734
1106
  } catch (error) {
735
1107
  if (error.code !== "EEXIST") throw error;
736
- const existing = JSON.parse(fs.readFileSync(file, "utf8"));
1108
+ const existing = JSON.parse(fs3.readFileSync(file, "utf8"));
737
1109
  if (existing.id !== frame.requestId || existing.reason !== reason) throw new Error("Supervisor relay identity changed during replay.");
738
1110
  }
739
1111
  if (reason === "progress_update") this.bridge.supervisorDelivered(frame.requestId);
740
1112
  }
741
- async #operation(operation, text, supervisorId) {
742
- const requestId = `${++this.#sequence}_${randomBytes(5).toString("hex")}`;
1113
+ async #operation(operation, text, supervisorId, supervisorError) {
1114
+ const requestId = `${++this.#sequence}_${randomBytes2(5).toString("hex")}`;
743
1115
  let completion = Promise.resolve();
744
1116
  if (operation === "prompt") completion = new Promise((resolve, reject) => this.#operations.set(requestId, { resolve, reject, accepted: false, settled: false }));
745
1117
  try {
746
- await Promise.race([this.bridge.prepare(requestId, operation, text, supervisorId), this.bridge.failure]);
747
- await this.connection.client.call("agent.prompt", { target: this.identity.agentName, text: `/pi-subagents-bridge ${requestId}` });
1118
+ await Promise.race([this.bridge.prepare(requestId, operation, text, supervisorId, supervisorError), this.bridge.failure]);
1119
+ await this.host.prompt(this.connection, this.identity, `/pi-subagents-bridge ${requestId}`);
748
1120
  await completion;
749
1121
  } catch (error) {
750
1122
  if (operation === "prompt" && this.bridge.failed) {
@@ -783,7 +1155,6 @@ var HerdrPiSession = class {
783
1155
  this.bridge.close();
784
1156
  await this.bridgeForward.close();
785
1157
  await this.owner.cleanup(this.#aborted);
786
- this.#snapshot = { ...this.#snapshot, connection: "closed" };
787
1158
  }
788
1159
  get messages() {
789
1160
  return this.#messages;
@@ -798,14 +1169,14 @@ var HerdrPiSession = class {
798
1169
  return this.model;
799
1170
  }
800
1171
  get machineEvidence() {
801
- return { machineId: this.identity.machineId, ...this.#initialGit ? { initial: this.#initialGit } : {}, ...this.#finalGit ? { final: this.#finalGit } : {} };
1172
+ return { provider: this.owner.machine.provider, machineId: this.identity.machineId, ...this.#initialGit ? { initial: this.#initialGit } : {}, ...this.#finalGit ? { final: this.#finalGit } : {} };
802
1173
  }
803
1174
  get placementSnapshot() {
804
1175
  return this.owner.snapshot;
805
1176
  }
806
1177
  };
807
1178
  async function reconnectHerdrPiSession(session, launch, dependencies = {}) {
808
- const connect = dependencies.connect ?? connectHerdrMachine, discoverManifest = dependencies.discoverManifest ?? discoverBridgeManifest, createBridge = dependencies.createBridge ?? ((socketPath, runId) => new BridgeChannel(socketPath, runId));
1179
+ const host = session.host, connect = dependencies.connect ?? ((machine) => host.connect(machine)), discoverManifest = dependencies.discoverManifest ?? ((runId, runtimeDir) => discoverPlacedBridgeManifest(session.owner.access, runId, runtimeDir)), createBridge = dependencies.createBridge ?? ((socketPath, runId) => new BridgeChannel(socketPath, runId));
809
1180
  let validated;
810
1181
  const result = await boundedHerdrReconnect(session.identity, session.evidenceCursor, async () => {
811
1182
  const connection = await connect(launch.machine);
@@ -815,19 +1186,15 @@ async function reconnectHerdrPiSession(session, launch, dependencies = {}) {
815
1186
  let bridge;
816
1187
  const generation = { adopted: false };
817
1188
  try {
818
- const snapshot = parseHerdrSessionSnapshot(await connection.client.call("session.snapshot"));
819
- const agents = snapshotAgents(snapshot);
820
- const manifest = await discoverManifest(launch.machine, session.identity.runId, session.identity.runtimeDir);
1189
+ const panes = await host.panes(connection, session.identity);
1190
+ const manifest = await discoverManifest(session.identity.runId, session.identity.runtimeDir);
821
1191
  bridgeForward = await connection.forwardRemoteSocket(manifest.socketPath, "bridge-reconnect");
822
1192
  bridge = createBridge(bridgeForward.socketPath, session.identity.runId);
823
1193
  const ready = await waitForReadyBridgeFrame(bridge, 8e3, "Reconnected bridge handshake timed out.", true);
824
- const candidate = { endpoint: connection.endpoint, agents, bridge: { runId: ready.runId, nativeSessionId: ready.nativeSessionId, packageVersion: ready.packageVersion, protocol: ready.protocol, evidenceCursor: ready.evidenceCursor, evidenceFloor: ready.evidenceFloor } };
825
- const check = reconcileHerdrReconnect(session.identity, candidate, session.evidenceCursor);
1194
+ const candidate = { ...panes, bridge: { runId: ready.runId, nativeSessionId: ready.nativeSessionId, packageVersion: ready.packageVersion, protocol: ready.protocol, evidenceCursor: ready.evidenceCursor, evidenceFloor: ready.evidenceFloor } };
1195
+ const check = reconcileHerdrReconnect(session.identity, candidate, session.evidenceCursor, host);
826
1196
  if (!check.ok) throw new Error(check.reason);
827
- unsubscribe = await connection.client.subscribe(herdrStatusSubscriptions(check.paneId), (event) => {
828
- session.owner.observeEvent(event);
829
- session.observeHerdr(event);
830
- }, (error) => {
1197
+ unsubscribe = await host.subscribe(connection, check.paneId, (event) => session.owner.observeEvent(event), (error) => {
831
1198
  if (!generation.adopted) {
832
1199
  generation.lost ??= error;
833
1200
  return;
@@ -845,7 +1212,7 @@ async function reconnectHerdrPiSession(session, launch, dependencies = {}) {
845
1212
  await connection.close();
846
1213
  throw error;
847
1214
  }
848
- }, { attempts: 3, deadlineMs: 15e3, disposed: () => session.placementSnapshot.connection === "closed" });
1215
+ }, { attempts: 3, deadlineMs: 15e3, disposed: () => session.placementSnapshot.connection === "closed", host });
849
1216
  if (!result.ok) throw new Error(`Pane-native reconnect remains unknown: ${result.reason}`);
850
1217
  if (!validated) throw new Error("Pane-native reconnect produced no validated transport.");
851
1218
  if (validated.candidate.lost) {
@@ -860,34 +1227,38 @@ async function reconnectHerdrPiSession(session, launch, dependencies = {}) {
860
1227
  }
861
1228
  async function createHerdrPiSession(launch) {
862
1229
  if (!launch.machine) throw new Error("Pane-native Pi launch requires a saved machine.");
1230
+ if (launch.machine.provider !== "herdr") throw new Error("Herdr pane-native Pi launch requires a Herdr saved machine.");
863
1231
  const policy = serializeHerdrPiLaunch(launch);
864
1232
  const runId = safeRunId(launch.runtime.runId);
865
- const owner = await HerdrPlacedRunOwner.create(launch.machine, runId);
866
- const runtimeDir = owner.runtimeDir, connection = owner.connection;
867
- let owned;
868
- let agentName;
869
- let observeHerdr;
1233
+ return startPlacedPiSession(launch, await HerdrPlacedRunOwner.create(launch.machine, runId), policy);
1234
+ }
1235
+ async function createRexPiSession(launch) {
1236
+ if (launch.machine?.provider !== "rex") throw new Error("Rex pane-native Pi launch requires a Rex machine.");
1237
+ const policy = serializeHerdrPiLaunch(launch);
1238
+ const runId = safeRunId(launch.runtime.runId);
1239
+ return startPlacedPiSession(launch, await RexPlacedRunOwner.create(launch.machine, runId), policy);
1240
+ }
1241
+ async function startPlacedPiSession(launch, owner, policy) {
1242
+ const machine = owner.machine, runId = owner.runId, runtimeDir = owner.runtimeDir, connection = owner.connection;
870
1243
  try {
871
1244
  let reconnectSession;
872
- owned = await owner.provision();
873
- await owner.subscribe((event) => observeHerdr?.(event), () => void reconnectSession?.reconnect());
874
- agentName = `pi-${runId.slice(-20)}`;
875
- const { terminalId } = await owner.start("pi", agentName, policy.args);
876
- const manifest = await discoverBridgeManifest(launch.machine, runId, runtimeDir);
1245
+ const agentName = `pi-${runId.slice(-20)}`;
1246
+ const { owned, terminalId } = await owner.launch({ kind: "pi", name: agentName, args: policy.args, environment: { [HERDR_PI_MODE_ENV]: "1", [HERDR_PI_RUN_ENV]: runId, [HERDR_PI_RUNTIME_DIR_ENV]: runtimeDir } }, () => owner.subscribe(() => {
1247
+ }, () => void reconnectSession?.reconnect()));
1248
+ const manifest = await discoverPlacedBridgeManifest(owner.access, runId, runtimeDir, "The placed Pi did not start the pi-subagents bridge, so its pane was closed. Start pi in a new pane on that machine to see why: it may wait on a prompt (such as a macOS folder-access prompt for the terminal app), exit at startup, or not load the same pi-subagents version as an extension.");
877
1249
  if (manifest.protocol !== HERDR_PI_PROTOCOL || manifest.packageVersion !== package_default.version) throw new Error(`Remote pi-subagents bridge version mismatch (remote ${manifest.packageVersion}, local ${package_default.version}).`);
878
1250
  const bridgeForward = await connection.forwardRemoteSocket(manifest.socketPath, "bridge");
879
1251
  const bridge = new BridgeChannel(bridgeForward.socketPath, runId);
880
1252
  const ready = await waitForReadyBridgeFrame(bridge, 15e3, "Remote Pi bridge handshake timed out.");
881
- if (ready.packageVersion !== package_default.version || ready.nativeSessionId !== manifest.nativeSessionId || ready.cwd !== launch.machine.cwd) throw new Error("Remote Pi bridge handshake identity mismatch.");
1253
+ if (ready.packageVersion !== package_default.version || ready.nativeSessionId !== manifest.nativeSessionId || ready.cwd !== machine.cwd) throw new Error("Remote Pi bridge handshake identity mismatch.");
882
1254
  const configured = await Promise.race([bridge.configure(policy.resources), bridge.failure]);
883
1255
  if (configured.nativeSessionId !== manifest.nativeSessionId || configured.agent !== policy.resources.agent) throw new Error("Remote Pi resource resolution identity mismatch.");
884
1256
  if (!Array.isArray(configured.tools) || policy.resources.toolCeiling && configured.tools.some((tool) => !policy.resources.toolCeiling.includes(String(tool)))) throw new Error("Remote Pi active-tool ceiling acknowledgement mismatch.");
885
1257
  if (launch.model && configured.model !== launch.model.split(":")[0]) throw new Error(`Remote Pi model '${launch.model}' did not resolve to the requested provider/model.`);
886
- const identity = { runId, machineId: launch.machine.id, target: launch.machine.target, session: connection.endpoint.session, workspaceId: owned.workspaceId, tabId: owned.tabId, paneId: owned.paneId, terminalId, agentName, nativeSessionId: manifest.nativeSessionId, cwd: launch.machine.cwd, runtimeDir };
1258
+ 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 };
887
1259
  owner.journal(identity);
888
1260
  const session = new HerdrPiSession(owner, bridgeForward, bridge, configured.tools, typeof ready.model === "string" ? ready.model : void 0, launch.runtime, ready.initialGit);
889
1261
  reconnectSession = session;
890
- observeHerdr = (event) => session.observeHerdr(event);
891
1262
  session.armReconnect(() => reconnectHerdrPiSession(session, launch));
892
1263
  return session;
893
1264
  } catch (error) {
@@ -895,7 +1266,10 @@ async function createHerdrPiSession(launch) {
895
1266
  throw error;
896
1267
  }
897
1268
  }
898
- function createPlacementAwareChildSessionFactory(local, createRemote = createHerdrPiSession) {
1269
+ function createPlacedPiSession(launch) {
1270
+ return launch.machine?.provider === "rex" ? createRexPiSession(launch) : createHerdrPiSession(launch);
1271
+ }
1272
+ function createPlacementAwareChildSessionFactory(local, createRemote = createPlacedPiSession) {
899
1273
  const remote = /* @__PURE__ */ new Set();
900
1274
  return { async create(launch) {
901
1275
  if (!launch.machine) return local.create(launch);
@@ -911,25 +1285,12 @@ function createPlacementAwareChildSessionFactory(local, createRemote = createHer
911
1285
  }
912
1286
  export {
913
1287
  BridgeChannel,
914
- HERDR_MAX_OWNED_PANES,
915
1288
  HerdrPiSession,
916
- HerdrPlacedRunOwner,
917
1289
  boundedHerdrReconnect,
918
1290
  createHerdrPiSession,
919
1291
  createPlacementAwareChildSessionFactory,
920
- createRemoteRuntimeDir,
921
- discoverBridgeManifest,
922
- herdrPaneAllocationKey,
923
- herdrStatusSubscriptions,
924
- ownsHerdrPane,
925
- parseHerdrAgent,
926
- parseHerdrCreated,
927
- parseHerdrSessionSnapshot,
928
- provisionHerdrPane,
929
- reconcileHerdrPlacedRun,
1292
+ createRexPiSession,
930
1293
  reconcileHerdrReconnect,
931
1294
  reconnectHerdrPiSession,
932
- removeRemoteRuntimeDir,
933
- serializeHerdrPiLaunch,
934
- withHerdrPaneAllocationLock
1295
+ serializeHerdrPiLaunch
935
1296
  };