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.
- package/CHANGELOG.md +38 -0
- package/bundle/agents.js +3 -3
- package/bundle/child-tool-plan.js +2 -2
- package/bundle/{chunk-LSRUTHT7.js → chunk-43SCHIMD.js} +1 -1
- package/bundle/{chunk-G5DKFLLI.js → chunk-4SDU2BZX.js} +15 -10
- package/bundle/{chunk-V2JWDB5U.js → chunk-52UBALJL.js} +2 -2
- package/bundle/{chunk-3DC5KBVP.js → chunk-5TYMSZX3.js} +1 -1
- package/bundle/{chunk-CEKS3TFO.js → chunk-7IVSUMQQ.js} +6 -6
- package/bundle/{chunk-KSWQVGDW.js → chunk-7PCKZHR5.js} +225 -18
- package/bundle/{chunk-563BHAZ5.js → chunk-7YMXOYEU.js} +33 -25
- package/bundle/{chunk-XMFLCKMQ.js → chunk-B5EKXDHE.js} +336 -130
- package/bundle/{chunk-ZBC44JJ3.js → chunk-CIZQXJ5Q.js} +22 -9
- package/bundle/{chunk-L5ZHE4L4.js → chunk-GC4DSPMH.js} +4 -4
- package/bundle/{chunk-ARRKBB2B.js → chunk-GJECHSAR.js} +2 -2
- package/bundle/{chunk-2ZSNH26H.js → chunk-M2CTFYT3.js} +7 -7
- package/bundle/{chunk-TUWLXTAM.js → chunk-MHZ4G23A.js} +681 -320
- package/bundle/{chunk-WSBNENFS.js → chunk-NGGAFDL6.js} +14 -4
- package/bundle/{chunk-VJHOELQP.js → chunk-NRQ75MGH.js} +160 -78
- package/bundle/{chunk-BJN5LUQY.js → chunk-OYF6AFDY.js} +1 -1
- package/bundle/{chunk-EYNJ6VL6.js → chunk-PLCCBLPY.js} +2 -2
- package/bundle/{chunk-74RT5YQE.js → chunk-RMQFMMAT.js} +123 -33
- package/bundle/{chunk-V6MR5TOQ.js → chunk-SIJ4S3EN.js} +25 -19
- package/bundle/{chunk-NNLASTQE.js → chunk-VH5OAWVG.js} +19 -5
- package/bundle/{chunk-BPS2AGBZ.js → chunk-W2SZ22EM.js} +93 -52
- package/bundle/{chunk-V3BIUPLU.js → chunk-WVD4UEL4.js} +256 -52
- package/bundle/{chunk-ZP3HODFH.js → chunk-XSWOTIA3.js} +1 -1
- package/bundle/{chunk-3IF2PGW7.js → chunk-YKEWIB4M.js} +1 -1
- package/bundle/{chunk-26C3WLYQ.js → chunk-ZLINIYTP.js} +497 -272
- package/bundle/{chunk-2XYWN7QJ.js → chunk-ZQGSDHL6.js} +2 -2
- package/bundle/control-channel.js +2 -2
- package/bundle/index.js +1 -1
- package/bundle/intercom-bridge.js +2 -2
- package/bundle/preflight.js +8 -8
- package/bundle/project-panes.js +3 -3
- package/docs/agents.md +31 -0
- package/docs/configuration.md +9 -1
- package/docs/tool-reference.md +3 -1
- package/docs/workflows.md +16 -0
- package/package.json +2 -2
- package/skills/pi-subagents/references/execution-controls.md +5 -4
- package/src/agents/agent-management.js +3 -3
- package/src/agents/agent-memory.js +3 -1
- package/src/agents/agents.js +6 -6
- package/src/agents/frontmatter.d.ts +2 -0
- package/src/agents/frontmatter.js +15 -1
- package/src/agents/skills.js +2 -1
- package/src/extension/config.js +3 -0
- package/src/extension/herdr-pi-bridge.js +17 -10
- package/src/extension/index.js +82 -2
- package/src/extension/schemas.js +3 -3
- package/src/intercom/native-supervisor-channel.js +20 -0
- package/src/missions/lifecycle.js +7 -1
- package/src/runs/background/active-async-capacity.d.ts +1 -0
- package/src/runs/background/active-async-capacity.js +9 -3
- package/src/runs/background/async-execution.js +20 -10
- package/src/runs/background/async-job-tracker.d.ts +6 -1
- package/src/runs/background/async-job-tracker.js +9 -4
- package/src/runs/background/async-status.d.ts +2 -0
- package/src/runs/background/async-status.js +21 -2
- package/src/runs/background/await-async-run.js +3 -7
- package/src/runs/background/boot-id.d.ts +12 -0
- package/src/runs/background/boot-id.js +47 -0
- package/src/runs/background/interrupted-elsewhere.d.ts +22 -0
- package/src/runs/background/interrupted-elsewhere.js +68 -0
- package/src/runs/background/notify.d.ts +2 -0
- package/src/runs/background/notify.js +13 -0
- package/src/runs/background/result-delivery-ownership.d.ts +6 -0
- package/src/runs/background/result-delivery-ownership.js +19 -1
- package/src/runs/background/result-watcher.d.ts +3 -1
- package/src/runs/background/result-watcher.js +24 -8
- package/src/runs/background/run-child-session.d.ts +1 -6
- package/src/runs/background/run-child-session.js +1 -1
- package/src/runs/background/run-status.js +2 -1
- package/src/runs/background/stale-run-reconciler.d.ts +25 -0
- package/src/runs/background/stale-run-reconciler.js +52 -13
- package/src/runs/background/subagent-runner-bootstrap.js +3 -0
- package/src/runs/background/subagent-runner.d.ts +4 -0
- package/src/runs/background/subagent-runner.js +13 -5
- package/src/runs/background/subagent-wait.d.ts +1 -1
- package/src/runs/background/subagent-wait.js +6 -1
- package/src/runs/background/terminal-run-index.d.ts +1 -0
- package/src/runs/background/terminal-run-index.js +4 -2
- package/src/runs/foreground/async-stop-action.js +3 -1
- package/src/runs/foreground/execution.js +6 -3
- package/src/runs/foreground/foreground-history.d.ts +16 -2
- package/src/runs/foreground/foreground-history.js +133 -31
- package/src/runs/foreground/subagent-executor.d.ts +11 -0
- package/src/runs/foreground/subagent-executor.js +277 -74
- package/src/runs/shared/child-launch.d.ts +2 -2
- package/src/runs/shared/child-session.d.ts +3 -2
- package/src/runs/shared/child-session.js +51 -8
- package/src/runs/shared/child-tool-plan.d.ts +7 -0
- package/src/runs/shared/child-tool-plan.js +21 -6
- package/src/runs/shared/herdr-connection.d.ts +17 -8
- package/src/runs/shared/herdr-connection.js +19 -7
- package/src/runs/shared/herdr-external-adapters.d.ts +3 -2
- package/src/runs/shared/herdr-external-adapters.js +1 -1
- package/src/runs/shared/herdr-machine.d.ts +12 -2
- package/src/runs/shared/herdr-machine.js +55 -9
- package/src/runs/shared/herdr-pane-host.d.ts +88 -0
- package/src/runs/shared/herdr-pane-host.js +155 -0
- package/src/runs/shared/herdr-placed-run.d.ts +27 -150
- package/src/runs/shared/herdr-placed-run.js +48 -395
- package/src/runs/shared/local-machine-access.d.ts +5 -0
- package/src/runs/shared/local-machine-access.js +28 -0
- package/src/runs/shared/orca-progress-tabs.d.ts +0 -1
- package/src/runs/shared/orca-progress-tabs.js +1 -24
- package/src/runs/shared/parallel-utils.d.ts +1 -1
- package/src/runs/shared/placed-run-owner.d.ts +49 -0
- package/src/runs/shared/placed-run-owner.js +93 -0
- package/src/runs/shared/placement-host.d.ts +99 -0
- package/src/runs/shared/placement-host.js +157 -0
- package/src/runs/shared/rex-machine.d.ts +25 -0
- package/src/runs/shared/rex-machine.js +83 -0
- package/src/runs/shared/rex-pane-host.d.ts +62 -0
- package/src/runs/shared/rex-pane-host.js +327 -0
- package/src/shared/completion-owner.d.ts +5 -0
- package/src/shared/completion-owner.js +5 -0
- package/src/shared/session-identity.d.ts +2 -0
- package/src/shared/session-identity.js +26 -0
- package/src/shared/types.d.ts +45 -6
- package/src/tui/fleet.d.ts +1 -0
- package/src/tui/fleet.js +24 -15
- package/src/tui/render.d.ts +6 -0
- package/src/tui/render.js +3 -3
- package/src/tui/subagent-messages.js +1 -0
- package/src/workflows/scripted-workflow.d.ts +1 -1
- package/src/workflows/workflow-auto-resume.d.ts +32 -0
- package/src/workflows/workflow-auto-resume.js +102 -0
- package/src/workflows/workflow-launch-record.d.ts +27 -0
- package/src/workflows/workflow-launch-record.js +48 -0
- package/src/workflows/workflow-resume-claim.d.ts +82 -0
- package/src/workflows/workflow-resume-claim.js +210 -0
- package/src/workflows/workflow-resume.d.ts +56 -0
- package/src/workflows/workflow-resume.js +106 -0
- package/src/workflows/workflow-reuse.d.ts +15 -9
- package/src/workflows/workflow-reuse.js +77 -14
- package/src/workflows/workflow-revival.d.ts +2 -0
- package/src/workflows/workflow-revival.js +2 -1
- package/src/workflows/workflow-stop-cause.d.ts +6 -0
- 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
|
-
|
|
5
|
-
|
|
6
|
-
|
|
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-
|
|
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
|
-
|
|
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 {
|
|
28
|
-
import * as
|
|
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
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
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
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
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
|
|
46
|
-
|
|
47
|
-
|
|
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
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
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
|
-
|
|
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
|
-
|
|
177
|
+
failure = error;
|
|
64
178
|
}
|
|
65
|
-
|
|
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
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
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 =
|
|
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 =
|
|
205
|
+
const directory = path2.join(parent, name);
|
|
118
206
|
try {
|
|
119
|
-
|
|
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
|
-
|
|
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 =
|
|
230
|
+
const descriptor = fs2.openSync(ownerPath, fs2.constants.O_RDONLY | (fs2.constants.O_NOFOLLOW ?? 0));
|
|
143
231
|
try {
|
|
144
|
-
const stat =
|
|
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(
|
|
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
|
-
|
|
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 =
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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))
|
|
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
|
-
|
|
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
|
-
|
|
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)
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
331
|
+
fs2.unlinkSync(tempPath);
|
|
244
332
|
} catch (error) {
|
|
245
333
|
if (error.code !== "ENOENT") throw error;
|
|
246
334
|
}
|
|
247
335
|
}
|
|
248
336
|
}
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
if (
|
|
259
|
-
|
|
260
|
-
if (
|
|
261
|
-
|
|
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-${
|
|
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
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
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
|
-
|
|
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
|
|
302
|
-
|
|
303
|
-
if (
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
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
|
-
|
|
428
|
-
|
|
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
|
-
|
|
431
|
-
|
|
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
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
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
|
-
|
|
443
|
-
|
|
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
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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_${
|
|
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 =
|
|
606
|
-
for (const name of
|
|
607
|
-
const file =
|
|
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(
|
|
610
|
-
if (typeof value.message === "string" && typeof value.requestId === "string") void this.#operation("supervisor-reply", value.message, value.requestId).then(() =>
|
|
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 =
|
|
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
|
-
|
|
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(
|
|
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}_${
|
|
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.
|
|
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 ??
|
|
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
|
|
819
|
-
const
|
|
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 = {
|
|
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
|
|
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
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
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
|
-
|
|
873
|
-
await owner.
|
|
874
|
-
|
|
875
|
-
const
|
|
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 !==
|
|
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:
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
933
|
-
serializeHerdrPiLaunch,
|
|
934
|
-
withHerdrPaneAllocationLock
|
|
1295
|
+
serializeHerdrPiLaunch
|
|
935
1296
|
};
|