pi-subagents 0.80.0 → 0.82.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +66 -0
- package/bundle/agents.js +18 -0
- package/bundle/background-work.js +16 -0
- package/bundle/capability-ceiling.js +22 -0
- package/bundle/child-tool-plan.js +11 -0
- package/bundle/chunk-43SCHIMD.js +57 -0
- package/bundle/chunk-4QFZZV3O.js +57 -0
- package/bundle/chunk-4SDU2BZX.js +1190 -0
- package/bundle/chunk-52UBALJL.js +753 -0
- package/bundle/chunk-5LFLVSTM.js +260 -0
- package/bundle/chunk-5TYMSZX3.js +235 -0
- package/bundle/chunk-62OCY2S4.js +14 -0
- package/bundle/chunk-7F6RKT6W.js +132 -0
- package/bundle/chunk-7IVSUMQQ.js +1337 -0
- package/bundle/chunk-7J4ZXMY7.js +18 -0
- package/bundle/chunk-7PCKZHR5.js +1234 -0
- package/bundle/chunk-7YMXOYEU.js +1373 -0
- package/bundle/chunk-B5EKXDHE.js +16870 -0
- package/bundle/chunk-CIZQXJ5Q.js +8619 -0
- package/bundle/chunk-CTPAE36Y.js +94 -0
- package/bundle/chunk-D4EHT5SJ.js +14 -0
- package/bundle/chunk-GC4DSPMH.js +946 -0
- package/bundle/chunk-GJECHSAR.js +179 -0
- package/bundle/chunk-M2CTFYT3.js +4164 -0
- package/bundle/chunk-MHZ4G23A.js +1296 -0
- package/bundle/chunk-N2BN7BF5.js +159 -0
- package/bundle/chunk-NF7IEKBH.js +490 -0
- package/bundle/chunk-NGGAFDL6.js +742 -0
- package/bundle/chunk-NIXWDZZV.js +9 -0
- package/bundle/chunk-NRQ75MGH.js +8327 -0
- package/bundle/chunk-OYF6AFDY.js +458 -0
- package/bundle/chunk-PLCCBLPY.js +526 -0
- package/bundle/chunk-RMQFMMAT.js +416 -0
- package/bundle/chunk-SIJ4S3EN.js +276 -0
- package/bundle/chunk-UKG4V45O.js +228 -0
- package/bundle/chunk-VH5OAWVG.js +4096 -0
- package/bundle/chunk-W2SZ22EM.js +5462 -0
- package/bundle/chunk-WVD4UEL4.js +9367 -0
- package/bundle/chunk-XSWOTIA3.js +175 -0
- package/bundle/chunk-YFCK7H2S.js +129 -0
- package/bundle/chunk-YKEWIB4M.js +380 -0
- package/bundle/chunk-YYREO77V.js +137 -0
- package/bundle/chunk-Z7KVHQHL.js +173 -0
- package/bundle/chunk-ZLINIYTP.js +8613 -0
- package/bundle/chunk-ZQGSDHL6.js +128 -0
- package/bundle/control-channel.js +11 -0
- package/bundle/delegation.js +14 -0
- package/bundle/external-job-provider.js +20 -0
- package/bundle/external-runs.js +21 -0
- package/bundle/index.js +30 -0
- package/bundle/inspectors.js +8 -0
- package/bundle/intercom-bridge.js +11 -0
- package/bundle/preflight.js +425 -0
- package/bundle/project-panes.js +28 -0
- package/bundle/required-child-extensions.js +6 -0
- package/bundle/shared-types.js +8 -0
- package/bundle/workflow-resources.js +7 -0
- package/docs/agents.md +31 -0
- package/docs/configuration.md +9 -1
- package/docs/extension-api.md +27 -0
- package/docs/observability.md +6 -2
- package/docs/tool-reference.md +3 -1
- package/docs/workflows.md +16 -0
- package/package.json +19 -19
- 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 +4 -0
- package/src/agents/frontmatter.js +24 -3
- package/src/agents/skills.js +2 -1
- package/src/api/delegation.d.ts +20 -0
- package/src/extension/config.js +3 -0
- package/src/extension/herdr-pi-bridge.js +17 -10
- package/src/extension/index.js +109 -9
- 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 +19 -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 +54 -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 +45 -5
- 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/runs/shared/structured-output.d.ts +1 -0
- package/src/runs/shared/structured-output.js +1 -0
- package/src/shared/accessible-dir.js +4 -1
- package/src/shared/completion-owner.d.ts +5 -0
- package/src/shared/completion-owner.js +5 -0
- package/src/shared/provider-diagnostics.d.ts +9 -0
- package/src/shared/provider-diagnostics.js +75 -0
- package/src/shared/session-identity.d.ts +2 -0
- package/src/shared/session-identity.js +26 -0
- package/src/shared/types.d.ts +48 -6
- package/src/slash/delegation-adapters.d.ts +3 -1
- package/src/slash/delegation-adapters.js +7 -0
- package/src/slash/prompt-template-bridge.js +3 -1
- package/src/slash/subagents-admin.d.ts +1 -0
- package/src/slash/subagents-admin.js +1 -1
- package/src/tui/fleet-layout.d.ts +20 -0
- package/src/tui/fleet-layout.js +17 -0
- package/src/tui/fleet-viewport.d.ts +12 -0
- package/src/tui/fleet-viewport.js +35 -0
- package/src/tui/fleet.d.ts +6 -6
- package/src/tui/fleet.js +93 -66
- 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/structured-workflow-scripts.d.ts +1 -0
- package/src/workflows/structured-workflow-scripts.js +5 -4
- 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-resources.js +2 -2
- 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
|
@@ -0,0 +1,1296 @@
|
|
|
1
|
+
import {
|
|
2
|
+
REX_BUNDLE_BIN,
|
|
3
|
+
REX_CALL_TIMEOUT_MS,
|
|
4
|
+
REX_EXIT_GONE,
|
|
5
|
+
REX_EXIT_UNREACHABLE,
|
|
6
|
+
REX_MAX_OUTPUT,
|
|
7
|
+
connectHerdrMachine,
|
|
8
|
+
isRexLocalMachine,
|
|
9
|
+
remoteShellCommand,
|
|
10
|
+
resolveRexBinary,
|
|
11
|
+
rexCommandEnv,
|
|
12
|
+
rexGlobalArgs,
|
|
13
|
+
runBoundedCommand,
|
|
14
|
+
sshMachineAccess
|
|
15
|
+
} from "./chunk-RMQFMMAT.js";
|
|
16
|
+
import {
|
|
17
|
+
package_default
|
|
18
|
+
} from "./chunk-ZQGSDHL6.js";
|
|
19
|
+
import {
|
|
20
|
+
HERDR_PI_MODE_ENV,
|
|
21
|
+
HERDR_PI_PROTOCOL,
|
|
22
|
+
HERDR_PI_RUNTIME_DIR_ENV,
|
|
23
|
+
HERDR_PI_RUN_ENV,
|
|
24
|
+
HerdrPiFrameDecoder,
|
|
25
|
+
encodeHerdrPiFrame,
|
|
26
|
+
validateHerdrPiRunId
|
|
27
|
+
} from "./chunk-4QFZZV3O.js";
|
|
28
|
+
import {
|
|
29
|
+
getAgentDir,
|
|
30
|
+
getPiSpawnCommand
|
|
31
|
+
} from "./chunk-CIZQXJ5Q.js";
|
|
32
|
+
import "./chunk-5LFLVSTM.js";
|
|
33
|
+
import "./chunk-YFCK7H2S.js";
|
|
34
|
+
import "./chunk-Z7KVHQHL.js";
|
|
35
|
+
|
|
36
|
+
// src/runs/shared/herdr-placed-run.ts
|
|
37
|
+
import { randomBytes as randomBytes2 } from "node:crypto";
|
|
38
|
+
import * as fs3 from "node:fs";
|
|
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";
|
|
47
|
+
import * as path from "node:path";
|
|
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);
|
|
67
|
+
}
|
|
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;
|
|
74
|
+
}
|
|
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)}`);
|
|
78
|
+
}
|
|
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;
|
|
174
|
+
try {
|
|
175
|
+
if (closeStartedPane && this.owned) await this.host.closeOwnedPane(this.connection, this.owned, this.terminalId, this.agentName);
|
|
176
|
+
} catch (error) {
|
|
177
|
+
failure = error;
|
|
178
|
+
}
|
|
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;
|
|
188
|
+
}
|
|
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";
|
|
195
|
+
var HERDR_ALLOCATION_LOCK_TIMEOUT_MS = 1e4;
|
|
196
|
+
var HERDR_ALLOCATION_LOCK_POLL_MS = 25;
|
|
197
|
+
function herdrPaneAllocationKey(target, session, cwd) {
|
|
198
|
+
return JSON.stringify([target, session, cwd]);
|
|
199
|
+
}
|
|
200
|
+
function assertPrivateDir(directory) {
|
|
201
|
+
const stat = fs2.lstatSync(directory);
|
|
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.");
|
|
203
|
+
}
|
|
204
|
+
function ensurePrivateChild(parent, name) {
|
|
205
|
+
const directory = path2.join(parent, name);
|
|
206
|
+
try {
|
|
207
|
+
fs2.mkdirSync(directory, { mode: 448 });
|
|
208
|
+
} catch (error) {
|
|
209
|
+
if (error.code !== "EEXIST") throw error;
|
|
210
|
+
}
|
|
211
|
+
assertPrivateDir(directory);
|
|
212
|
+
return directory;
|
|
213
|
+
}
|
|
214
|
+
function allocationLockRoot() {
|
|
215
|
+
const agentDir = getAgentDir();
|
|
216
|
+
fs2.mkdirSync(agentDir, { recursive: true, mode: 448 });
|
|
217
|
+
assertPrivateDir(agentDir);
|
|
218
|
+
const packageDir = ensurePrivateChild(agentDir, "pi-subagents");
|
|
219
|
+
return ensurePrivateChild(packageDir, "herdr-allocation-locks");
|
|
220
|
+
}
|
|
221
|
+
function livePid(pid) {
|
|
222
|
+
try {
|
|
223
|
+
process.kill(pid, 0);
|
|
224
|
+
return true;
|
|
225
|
+
} catch (error) {
|
|
226
|
+
return error.code === "EPERM";
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
function readAllocationOwner(ownerPath) {
|
|
230
|
+
const descriptor = fs2.openSync(ownerPath, fs2.constants.O_RDONLY | (fs2.constants.O_NOFOLLOW ?? 0));
|
|
231
|
+
try {
|
|
232
|
+
const stat = fs2.fstatSync(descriptor);
|
|
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.");
|
|
234
|
+
const value = JSON.parse(fs2.readFileSync(descriptor, "utf8"));
|
|
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.");
|
|
236
|
+
return { pid: value.pid, token: value.token, dev: stat.dev, ino: stat.ino };
|
|
237
|
+
} finally {
|
|
238
|
+
fs2.closeSync(descriptor);
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
function sameOwner(left, right) {
|
|
242
|
+
return left.token === right.token && left.dev === right.dev && left.ino === right.ino;
|
|
243
|
+
}
|
|
244
|
+
async function withHerdrPaneAllocationLock(key, action, options = {}) {
|
|
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);
|
|
246
|
+
let published;
|
|
247
|
+
try {
|
|
248
|
+
fs2.writeFileSync(tempPath, JSON.stringify({ pid: process.pid, token, createdAt: Date.now() }), { mode: 384, flag: "wx" });
|
|
249
|
+
const tempOwner = readAllocationOwner(tempPath);
|
|
250
|
+
for (; ; ) {
|
|
251
|
+
let recovering = false;
|
|
252
|
+
try {
|
|
253
|
+
readAllocationOwner(recoveryPath);
|
|
254
|
+
recovering = true;
|
|
255
|
+
} catch (error) {
|
|
256
|
+
if (error.code !== "ENOENT") throw error;
|
|
257
|
+
}
|
|
258
|
+
if (!recovering) {
|
|
259
|
+
let linked = false;
|
|
260
|
+
try {
|
|
261
|
+
fs2.linkSync(tempPath, lockPath);
|
|
262
|
+
linked = true;
|
|
263
|
+
try {
|
|
264
|
+
readAllocationOwner(recoveryPath);
|
|
265
|
+
const current = readAllocationOwner(lockPath);
|
|
266
|
+
if (!sameOwner(current, tempOwner)) throw new Error("Herdr allocation lock changed during publication.");
|
|
267
|
+
fs2.unlinkSync(lockPath);
|
|
268
|
+
} catch (recoveryError) {
|
|
269
|
+
if (recoveryError.code !== "ENOENT") throw recoveryError;
|
|
270
|
+
published = readAllocationOwner(lockPath);
|
|
271
|
+
if (!sameOwner(published, tempOwner)) throw new Error("Herdr allocation lock publication identity changed.");
|
|
272
|
+
break;
|
|
273
|
+
}
|
|
274
|
+
} catch (publishError) {
|
|
275
|
+
if (linked) {
|
|
276
|
+
try {
|
|
277
|
+
const current = readAllocationOwner(lockPath);
|
|
278
|
+
if (sameOwner(current, tempOwner)) fs2.unlinkSync(lockPath);
|
|
279
|
+
} catch {
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
if (publishError.code !== "EEXIST" && publishError.code !== "ENOENT") throw publishError;
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
if (published) break;
|
|
286
|
+
try {
|
|
287
|
+
const current = readAllocationOwner(lockPath);
|
|
288
|
+
if (!livePid(current.pid)) {
|
|
289
|
+
let claimed = false;
|
|
290
|
+
try {
|
|
291
|
+
fs2.linkSync(lockPath, recoveryPath);
|
|
292
|
+
claimed = true;
|
|
293
|
+
const recovery = readAllocationOwner(recoveryPath), latest = readAllocationOwner(lockPath);
|
|
294
|
+
if (!sameOwner(recovery, current) || !sameOwner(latest, current)) throw new Error("Herdr stale lock identity changed during recovery.");
|
|
295
|
+
fs2.unlinkSync(lockPath);
|
|
296
|
+
} catch (error) {
|
|
297
|
+
if (error.code !== "EEXIST" && error.code !== "ENOENT") throw error;
|
|
298
|
+
} finally {
|
|
299
|
+
if (claimed) fs2.unlinkSync(recoveryPath);
|
|
300
|
+
}
|
|
301
|
+
if (claimed) continue;
|
|
302
|
+
}
|
|
303
|
+
} catch (error) {
|
|
304
|
+
if (error.code !== "ENOENT") throw error;
|
|
305
|
+
}
|
|
306
|
+
if (Date.now() >= deadline) throw new Error("Timed out waiting for the Herdr pane allocation lock.");
|
|
307
|
+
await new Promise((resolve) => setTimeout(resolve, options.pollMs ?? HERDR_ALLOCATION_LOCK_POLL_MS));
|
|
308
|
+
}
|
|
309
|
+
fs2.unlinkSync(tempPath);
|
|
310
|
+
let value, actionError, releaseError, actionFailed = false, releaseFailed = false;
|
|
311
|
+
try {
|
|
312
|
+
value = await action();
|
|
313
|
+
} catch (error) {
|
|
314
|
+
actionFailed = true;
|
|
315
|
+
actionError = error;
|
|
316
|
+
}
|
|
317
|
+
try {
|
|
318
|
+
const current = readAllocationOwner(lockPath);
|
|
319
|
+
if (!published || !sameOwner(current, published)) throw new Error("Herdr pane allocation lock ownership changed before release.");
|
|
320
|
+
fs2.unlinkSync(lockPath);
|
|
321
|
+
} catch (error) {
|
|
322
|
+
releaseFailed = true;
|
|
323
|
+
releaseError = error;
|
|
324
|
+
}
|
|
325
|
+
if (actionFailed && releaseFailed) throw new AggregateError([actionError, releaseError], "Herdr pane allocation action and release both failed.", { cause: actionError });
|
|
326
|
+
if (actionFailed) throw actionError;
|
|
327
|
+
if (releaseFailed) throw releaseError;
|
|
328
|
+
return value;
|
|
329
|
+
} finally {
|
|
330
|
+
try {
|
|
331
|
+
fs2.unlinkSync(tempPath);
|
|
332
|
+
} catch (error) {
|
|
333
|
+
if (error.code !== "ENOENT") throw error;
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
}
|
|
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") };
|
|
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;
|
|
387
|
+
async function provisionHerdrPane(client, cwd, runId, runtimeDir, environment, allocationKey = cwd) {
|
|
388
|
+
return withHerdrPaneAllocationLock(allocationKey, async () => {
|
|
389
|
+
const snapshot = parseHerdrSessionSnapshot(await client.call("session.snapshot"));
|
|
390
|
+
const panes = list(snapshot, "panes").filter((item) => Boolean(item && typeof item === "object")), workspaces = list(snapshot, "workspaces").filter((item) => Boolean(item && typeof item === "object"));
|
|
391
|
+
const ownedLabel = `pi-subagents-${createHash2("sha256").update(cwd).digest("hex").slice(0, 10)}`, owned = workspaces.filter((item) => item.label === ownedLabel);
|
|
392
|
+
if (owned.length > 1) throw new Error("Herdr deterministic owned workspace identity is ambiguous.");
|
|
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));
|
|
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;
|
|
395
|
+
if (actual >= HERDR_MAX_OWNED_PANES) throw new Error(`Herdr workspace pane limit of ${HERDR_MAX_OWNED_PANES} is reached; close an existing pane before launching another placed run.`);
|
|
396
|
+
const env = environment ?? { [HERDR_PI_MODE_ENV]: "1", [HERDR_PI_RUN_ENV]: runId, [HERDR_PI_RUNTIME_DIR_ENV]: runtimeDir };
|
|
397
|
+
if (selectedId) {
|
|
398
|
+
const tab = parseHerdrCreated(await client.call("tab.create", { workspace_id: selectedId, cwd, label: `subagent-${runId.slice(-8)}`, env, focus: false }), "tab_created");
|
|
399
|
+
return { workspaceId: selectedId, tabId: tab.tabId, paneId: tab.paneId };
|
|
400
|
+
}
|
|
401
|
+
const workspace = parseHerdrCreated(await client.call("workspace.create", { cwd, label: ownedLabel, env, focus: false }), "workspace_created");
|
|
402
|
+
return { workspaceId: workspace.workspaceId, tabId: workspace.tabId, paneId: workspace.paneId };
|
|
403
|
+
});
|
|
404
|
+
}
|
|
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()));
|
|
426
|
+
}
|
|
427
|
+
}
|
|
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 };
|
|
431
|
+
}
|
|
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);
|
|
479
|
+
}
|
|
480
|
+
function removeRemoteRuntimeDir(machine, runtimeDir, options = {}) {
|
|
481
|
+
removePlacedRuntimeDir(sshMachineAccess(machine, options), runtimeDir);
|
|
482
|
+
}
|
|
483
|
+
var HerdrPlacedRunOwner = class _HerdrPlacedRunOwner extends PlacedRunOwner {
|
|
484
|
+
constructor(machine, runId, runtimeDir, connection, removeRuntime) {
|
|
485
|
+
super(machine, runId, runtimeDir, connection, herdrPaneHost, sshMachineAccess(machine), removeRuntime);
|
|
486
|
+
}
|
|
487
|
+
static async create(machine, runId, beforePane) {
|
|
488
|
+
const runtimeDir = await createRemoteRuntimeDir(machine, runId);
|
|
489
|
+
let connection;
|
|
490
|
+
try {
|
|
491
|
+
connection = await herdrPaneHost.connect(machine);
|
|
492
|
+
const owner = new _HerdrPlacedRunOwner(machine, runId, runtimeDir, connection);
|
|
493
|
+
await beforePane?.(owner);
|
|
494
|
+
return owner;
|
|
495
|
+
} catch (error) {
|
|
496
|
+
const cleanupErrors = [];
|
|
497
|
+
try {
|
|
498
|
+
await connection?.close();
|
|
499
|
+
} catch (cleanupError) {
|
|
500
|
+
cleanupErrors.push(cleanupError);
|
|
501
|
+
}
|
|
502
|
+
try {
|
|
503
|
+
removeRemoteRuntimeDir(machine, runtimeDir);
|
|
504
|
+
} catch (cleanupError) {
|
|
505
|
+
cleanupErrors.push(cleanupError);
|
|
506
|
+
}
|
|
507
|
+
if (cleanupErrors.length) throw new AggregateError([error, ...cleanupErrors], "Herdr owner creation failed and cleanup also failed.", { cause: error });
|
|
508
|
+
throw error;
|
|
509
|
+
}
|
|
510
|
+
}
|
|
511
|
+
async provision(environment) {
|
|
512
|
+
this.owned = await provisionHerdrOwnedPane(this.connection, this.machine, this.runId, this.runtimeDir, environment);
|
|
513
|
+
return this.owned;
|
|
514
|
+
}
|
|
515
|
+
async start(kind, name, args, options = {}) {
|
|
516
|
+
if (!this.owned) throw new Error("Herdr placement owner has not provisioned its pane.");
|
|
517
|
+
const { terminalId, paneId } = await startHerdrAgent(this.connection, this.owned, kind, name, args, options), argv = [kind, ...args];
|
|
518
|
+
this.agentName = name;
|
|
519
|
+
this.terminalId = terminalId;
|
|
520
|
+
this.startedArgv = argv;
|
|
521
|
+
return { terminalId, paneId, argv };
|
|
522
|
+
}
|
|
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.`);
|
|
599
|
+
}
|
|
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 };
|
|
655
|
+
}
|
|
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));
|
|
676
|
+
}
|
|
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 });
|
|
703
|
+
}
|
|
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)));
|
|
715
|
+
}
|
|
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 });
|
|
791
|
+
}
|
|
792
|
+
throw error;
|
|
793
|
+
}
|
|
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);
|
|
800
|
+
try {
|
|
801
|
+
return new _RexPlacedRunOwner(placed, runId, runtimeDir, await host.connect(placed), access, host);
|
|
802
|
+
} catch (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;
|
|
809
|
+
}
|
|
810
|
+
}
|
|
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
|
+
}
|
|
856
|
+
var BridgeChannel = class {
|
|
857
|
+
socket;
|
|
858
|
+
decoder = new HerdrPiFrameDecoder();
|
|
859
|
+
listeners = /* @__PURE__ */ new Set();
|
|
860
|
+
waiters = /* @__PURE__ */ new Map();
|
|
861
|
+
runId;
|
|
862
|
+
frames = [];
|
|
863
|
+
failure;
|
|
864
|
+
#rejectFailure;
|
|
865
|
+
#failed = false;
|
|
866
|
+
#adopted = false;
|
|
867
|
+
constructor(socketPath, runId) {
|
|
868
|
+
this.runId = runId;
|
|
869
|
+
this.failure = new Promise((_resolve, reject) => {
|
|
870
|
+
this.#rejectFailure = reject;
|
|
871
|
+
});
|
|
872
|
+
this.failure.catch(() => {
|
|
873
|
+
});
|
|
874
|
+
this.socket = net.createConnection(socketPath);
|
|
875
|
+
this.socket.on("data", (chunk) => {
|
|
876
|
+
try {
|
|
877
|
+
for (const frame of this.decoder.push(chunk)) {
|
|
878
|
+
if (frame.runId !== runId) throw new Error("Bridge run identity mismatch.");
|
|
879
|
+
if (!this.#adopted) {
|
|
880
|
+
this.frames.push(frame);
|
|
881
|
+
if (this.frames.length > 1024) throw new Error("Bridge replay queue exceeded its bound.");
|
|
882
|
+
}
|
|
883
|
+
const key = typeof frame.requestId === "string" ? `${frame.type}:${frame.requestId}` : "";
|
|
884
|
+
const waiter = this.waiters.get(key);
|
|
885
|
+
if (waiter) {
|
|
886
|
+
this.waiters.delete(key);
|
|
887
|
+
waiter.resolve(frame);
|
|
888
|
+
}
|
|
889
|
+
for (const listener of this.listeners) listener(frame);
|
|
890
|
+
}
|
|
891
|
+
} catch (error) {
|
|
892
|
+
this.fail(error);
|
|
893
|
+
}
|
|
894
|
+
});
|
|
895
|
+
this.socket.on("error", (error) => this.fail(error));
|
|
896
|
+
this.socket.on("close", () => {
|
|
897
|
+
if (!this.#failed) this.fail(new Error("Remote Pi bridge connection was lost; task state is unknown."));
|
|
898
|
+
});
|
|
899
|
+
}
|
|
900
|
+
fail(error) {
|
|
901
|
+
if (this.#failed) return;
|
|
902
|
+
this.#failed = true;
|
|
903
|
+
this.#rejectFailure(error);
|
|
904
|
+
for (const waiter of this.waiters.values()) waiter.reject(error);
|
|
905
|
+
this.waiters.clear();
|
|
906
|
+
}
|
|
907
|
+
get failed() {
|
|
908
|
+
return this.#failed;
|
|
909
|
+
}
|
|
910
|
+
adopt(listener) {
|
|
911
|
+
this.#adopted = true;
|
|
912
|
+
this.listeners.add(listener);
|
|
913
|
+
return this.frames.splice(0);
|
|
914
|
+
}
|
|
915
|
+
wait(type, requestId) {
|
|
916
|
+
return new Promise((resolve, reject) => this.waiters.set(`${type}:${requestId}`, { resolve, reject }));
|
|
917
|
+
}
|
|
918
|
+
configure(resources) {
|
|
919
|
+
const requestId = `cfg_${randomBytes2(6).toString("hex")}`;
|
|
920
|
+
const waiting = this.wait("configured", requestId);
|
|
921
|
+
this.socket.write(encodeHerdrPiFrame({ protocol: HERDR_PI_PROTOCOL, runId: this.runId, type: "configure", requestId, resources }));
|
|
922
|
+
return waiting;
|
|
923
|
+
}
|
|
924
|
+
supervisorDelivered(requestId) {
|
|
925
|
+
this.socket.write(encodeHerdrPiFrame({ protocol: HERDR_PI_PROTOCOL, runId: this.runId, type: "supervisor-delivered", requestId }));
|
|
926
|
+
}
|
|
927
|
+
prepare(requestId, operation, text, supervisorId, supervisorError) {
|
|
928
|
+
const waiting = this.wait("prepared", requestId);
|
|
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 } : {} }));
|
|
930
|
+
return waiting;
|
|
931
|
+
}
|
|
932
|
+
close() {
|
|
933
|
+
this.socket.destroy();
|
|
934
|
+
}
|
|
935
|
+
};
|
|
936
|
+
function waitForReadyBridgeFrame(bridge, timeoutMs, timeoutMessage, configured = false) {
|
|
937
|
+
return new Promise((resolve, reject) => {
|
|
938
|
+
const timer = setTimeout(() => reject(new Error(timeoutMessage)), timeoutMs);
|
|
939
|
+
const listener = (frame) => {
|
|
940
|
+
if (frame.type !== "ready" || configured && frame.configured !== true) return;
|
|
941
|
+
clearTimeout(timer);
|
|
942
|
+
bridge.listeners.delete(listener);
|
|
943
|
+
resolve(frame);
|
|
944
|
+
};
|
|
945
|
+
bridge.listeners.add(listener);
|
|
946
|
+
});
|
|
947
|
+
}
|
|
948
|
+
var HerdrPiSession = class {
|
|
949
|
+
#listeners = /* @__PURE__ */ new Set();
|
|
950
|
+
#messages = [];
|
|
951
|
+
#sequence = 0;
|
|
952
|
+
#disposed = false;
|
|
953
|
+
#aborted = false;
|
|
954
|
+
#initialGit;
|
|
955
|
+
#finalGit;
|
|
956
|
+
#operations = /* @__PURE__ */ new Map();
|
|
957
|
+
bridgeForward;
|
|
958
|
+
bridge;
|
|
959
|
+
requestedTools;
|
|
960
|
+
model;
|
|
961
|
+
#evidenceCursor = 0;
|
|
962
|
+
#reconnecting;
|
|
963
|
+
#reconnectFactory;
|
|
964
|
+
#reconnectFailure;
|
|
965
|
+
#replyTimer;
|
|
966
|
+
supervisorDir;
|
|
967
|
+
runtime;
|
|
968
|
+
owner;
|
|
969
|
+
constructor(owner, bridgeForward, bridge, requestedTools, model, runtime, initialGit) {
|
|
970
|
+
this.owner = owner;
|
|
971
|
+
this.bridgeForward = bridgeForward;
|
|
972
|
+
this.bridge = bridge;
|
|
973
|
+
this.requestedTools = requestedTools;
|
|
974
|
+
this.model = model;
|
|
975
|
+
this.runtime = runtime;
|
|
976
|
+
this.supervisorDir = runtime.supervisorChannelDir;
|
|
977
|
+
this.#initialGit = initialGit;
|
|
978
|
+
bridge.adopt((frame) => this.#frame(frame));
|
|
979
|
+
if (this.supervisorDir) {
|
|
980
|
+
this.#replyTimer = setInterval(() => {
|
|
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);
|
|
984
|
+
try {
|
|
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 }));
|
|
987
|
+
} catch {
|
|
988
|
+
}
|
|
989
|
+
}
|
|
990
|
+
}, 50);
|
|
991
|
+
this.#replyTimer.unref?.();
|
|
992
|
+
}
|
|
993
|
+
}
|
|
994
|
+
get host() {
|
|
995
|
+
return this.owner.host;
|
|
996
|
+
}
|
|
997
|
+
get identity() {
|
|
998
|
+
return this.owner.identity;
|
|
999
|
+
}
|
|
1000
|
+
set identity(value) {
|
|
1001
|
+
this.owner.identity = value;
|
|
1002
|
+
}
|
|
1003
|
+
get connection() {
|
|
1004
|
+
return this.owner.connection;
|
|
1005
|
+
}
|
|
1006
|
+
set connection(value) {
|
|
1007
|
+
this.owner.connection = value;
|
|
1008
|
+
}
|
|
1009
|
+
armReconnect(factory) {
|
|
1010
|
+
this.#reconnectFactory = factory;
|
|
1011
|
+
const observed = this.bridge;
|
|
1012
|
+
observed.failure.catch(() => {
|
|
1013
|
+
if (this.bridge === observed) void this.reconnect();
|
|
1014
|
+
});
|
|
1015
|
+
}
|
|
1016
|
+
get evidenceCursor() {
|
|
1017
|
+
return this.#evidenceCursor;
|
|
1018
|
+
}
|
|
1019
|
+
async reconnect() {
|
|
1020
|
+
if (this.#disposed || !this.#reconnectFactory || this.#reconnectFailure) return;
|
|
1021
|
+
this.#reconnecting ??= this.#reconnectFactory().catch((error) => {
|
|
1022
|
+
const unknown = this.#reconnectFailure = new Error(`Remote Pi reconciliation failed; task state is unknown: ${error instanceof Error ? error.message : String(error)}`);
|
|
1023
|
+
for (const operation of this.#operations.values()) if (!operation.settled) {
|
|
1024
|
+
operation.settled = true;
|
|
1025
|
+
operation.reject(unknown);
|
|
1026
|
+
}
|
|
1027
|
+
}).finally(() => {
|
|
1028
|
+
this.#reconnecting = void 0;
|
|
1029
|
+
});
|
|
1030
|
+
await this.#reconnecting;
|
|
1031
|
+
}
|
|
1032
|
+
async replaceTransport(input) {
|
|
1033
|
+
if (this.#disposed) {
|
|
1034
|
+
input.unsubscribe();
|
|
1035
|
+
input.bridge.close();
|
|
1036
|
+
await input.bridgeForward.close();
|
|
1037
|
+
await input.connection.close();
|
|
1038
|
+
return;
|
|
1039
|
+
}
|
|
1040
|
+
const old = { bridgeForward: this.bridgeForward, bridge: this.bridge };
|
|
1041
|
+
this.bridgeForward = input.bridgeForward;
|
|
1042
|
+
this.bridge = input.bridge;
|
|
1043
|
+
this.identity = { ...this.identity, paneId: input.paneId };
|
|
1044
|
+
this.#reconnectFailure = void 0;
|
|
1045
|
+
for (const frame of input.bridge.adopt((frame2) => this.#frame(frame2))) if (typeof frame.cursor === "number" && frame.cursor > this.#evidenceCursor) this.#frame(frame);
|
|
1046
|
+
for (const operation of this.#operations.values()) if (!operation.accepted && !operation.settled) {
|
|
1047
|
+
operation.settled = true;
|
|
1048
|
+
operation.reject(new Error("Remote operation was not accepted before transport loss; task state is unknown."));
|
|
1049
|
+
}
|
|
1050
|
+
this.#evidenceCursor = Math.max(this.#evidenceCursor, input.cursor);
|
|
1051
|
+
const ownerReplacement = this.owner.replaceConnection({ connection: input.connection, unsubscribe: input.unsubscribe, paneId: input.paneId, state: input.state });
|
|
1052
|
+
old.bridge.close();
|
|
1053
|
+
await old.bridgeForward.close();
|
|
1054
|
+
await ownerReplacement;
|
|
1055
|
+
if (this.#disposed) {
|
|
1056
|
+
await this.owner.cleanup(this.#aborted);
|
|
1057
|
+
return;
|
|
1058
|
+
}
|
|
1059
|
+
this.armReconnect(this.#reconnectFactory);
|
|
1060
|
+
}
|
|
1061
|
+
#frame(frame) {
|
|
1062
|
+
if (typeof frame.cursor === "number") {
|
|
1063
|
+
if (frame.cursor <= this.#evidenceCursor) return;
|
|
1064
|
+
this.#evidenceCursor = frame.cursor;
|
|
1065
|
+
}
|
|
1066
|
+
if (frame.nativeSessionId !== this.identity.nativeSessionId) {
|
|
1067
|
+
this.bridge.fail(new Error("Remote Pi native session identity changed."));
|
|
1068
|
+
void this.connection.close();
|
|
1069
|
+
return;
|
|
1070
|
+
}
|
|
1071
|
+
if (frame.type === "accepted" && typeof frame.requestId === "string") {
|
|
1072
|
+
const operation = this.#operations.get(frame.requestId);
|
|
1073
|
+
if (operation) operation.accepted = true;
|
|
1074
|
+
}
|
|
1075
|
+
if (frame.type === "supervisor-request" && this.supervisorDir && typeof frame.requestId === "string") this.#relaySupervisor(frame);
|
|
1076
|
+
if (frame.type === "event" && frame.event && typeof frame.event === "object") {
|
|
1077
|
+
const event = frame.event;
|
|
1078
|
+
const message = event.message;
|
|
1079
|
+
if (event.type === "message_end" && message) this.#messages.push(message);
|
|
1080
|
+
for (const listener of this.#listeners) listener(event);
|
|
1081
|
+
}
|
|
1082
|
+
if (frame.type === "settled" && typeof frame.requestId === "string") {
|
|
1083
|
+
if (frame.finalAssistant && typeof frame.finalAssistant === "object") {
|
|
1084
|
+
const message = frame.finalAssistant;
|
|
1085
|
+
if (!this.#messages.includes(message)) this.#messages.push(message);
|
|
1086
|
+
}
|
|
1087
|
+
if (frame.finalGit && typeof frame.finalGit === "object") this.#finalGit = frame.finalGit;
|
|
1088
|
+
const operation = this.#operations.get(frame.requestId);
|
|
1089
|
+
if (operation && !operation.settled) {
|
|
1090
|
+
operation.accepted = true;
|
|
1091
|
+
operation.settled = true;
|
|
1092
|
+
frame.idle === true && frame.pending === false ? operation.resolve() : operation.reject(new Error("Remote Pi did not settle with an empty native queue."));
|
|
1093
|
+
}
|
|
1094
|
+
for (const listener of this.#listeners) listener({ type: "agent_settled" });
|
|
1095
|
+
}
|
|
1096
|
+
}
|
|
1097
|
+
#relaySupervisor(frame) {
|
|
1098
|
+
if (!this.supervisorDir || typeof frame.requestId !== "string") return;
|
|
1099
|
+
const reason = frame.reason;
|
|
1100
|
+
if (reason !== "need_decision" && reason !== "interview_request" && reason !== "progress_update") return;
|
|
1101
|
+
const createdAt = Date.now();
|
|
1102
|
+
const file = path5.join(this.supervisorDir, "requests", `${path5.basename(frame.requestId)}.json`);
|
|
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 };
|
|
1104
|
+
try {
|
|
1105
|
+
fs3.writeFileSync(file, JSON.stringify(request), { mode: 384, flag: "wx" });
|
|
1106
|
+
} catch (error) {
|
|
1107
|
+
if (error.code !== "EEXIST") throw error;
|
|
1108
|
+
const existing = JSON.parse(fs3.readFileSync(file, "utf8"));
|
|
1109
|
+
if (existing.id !== frame.requestId || existing.reason !== reason) throw new Error("Supervisor relay identity changed during replay.");
|
|
1110
|
+
}
|
|
1111
|
+
if (reason === "progress_update") this.bridge.supervisorDelivered(frame.requestId);
|
|
1112
|
+
}
|
|
1113
|
+
async #operation(operation, text, supervisorId, supervisorError) {
|
|
1114
|
+
const requestId = `${++this.#sequence}_${randomBytes2(5).toString("hex")}`;
|
|
1115
|
+
let completion = Promise.resolve();
|
|
1116
|
+
if (operation === "prompt") completion = new Promise((resolve, reject) => this.#operations.set(requestId, { resolve, reject, accepted: false, settled: false }));
|
|
1117
|
+
try {
|
|
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}`);
|
|
1120
|
+
await completion;
|
|
1121
|
+
} catch (error) {
|
|
1122
|
+
if (operation === "prompt" && this.bridge.failed) {
|
|
1123
|
+
await this.reconnect();
|
|
1124
|
+
await completion;
|
|
1125
|
+
return;
|
|
1126
|
+
}
|
|
1127
|
+
const pending = this.#operations.get(requestId);
|
|
1128
|
+
if (pending) pending.settled = true;
|
|
1129
|
+
throw error;
|
|
1130
|
+
} finally {
|
|
1131
|
+
if (operation === "prompt") this.#operations.delete(requestId);
|
|
1132
|
+
}
|
|
1133
|
+
}
|
|
1134
|
+
subscribe(listener) {
|
|
1135
|
+
this.#listeners.add(listener);
|
|
1136
|
+
return () => this.#listeners.delete(listener);
|
|
1137
|
+
}
|
|
1138
|
+
prompt(text) {
|
|
1139
|
+
return this.#operation("prompt", text);
|
|
1140
|
+
}
|
|
1141
|
+
steer(text) {
|
|
1142
|
+
return this.#operation("steer", text);
|
|
1143
|
+
}
|
|
1144
|
+
followUp(text) {
|
|
1145
|
+
return this.#operation("follow-up", text);
|
|
1146
|
+
}
|
|
1147
|
+
async abort() {
|
|
1148
|
+
this.#aborted = true;
|
|
1149
|
+
await this.#operation("abort");
|
|
1150
|
+
}
|
|
1151
|
+
async dispose() {
|
|
1152
|
+
if (this.#disposed) return;
|
|
1153
|
+
this.#disposed = true;
|
|
1154
|
+
if (this.#replyTimer) clearInterval(this.#replyTimer);
|
|
1155
|
+
this.bridge.close();
|
|
1156
|
+
await this.bridgeForward.close();
|
|
1157
|
+
await this.owner.cleanup(this.#aborted);
|
|
1158
|
+
}
|
|
1159
|
+
get messages() {
|
|
1160
|
+
return this.#messages;
|
|
1161
|
+
}
|
|
1162
|
+
get sessionFile() {
|
|
1163
|
+
return void 0;
|
|
1164
|
+
}
|
|
1165
|
+
get sessionId() {
|
|
1166
|
+
return this.identity.nativeSessionId;
|
|
1167
|
+
}
|
|
1168
|
+
get modelId() {
|
|
1169
|
+
return this.model;
|
|
1170
|
+
}
|
|
1171
|
+
get machineEvidence() {
|
|
1172
|
+
return { provider: this.owner.machine.provider, machineId: this.identity.machineId, ...this.#initialGit ? { initial: this.#initialGit } : {}, ...this.#finalGit ? { final: this.#finalGit } : {} };
|
|
1173
|
+
}
|
|
1174
|
+
get placementSnapshot() {
|
|
1175
|
+
return this.owner.snapshot;
|
|
1176
|
+
}
|
|
1177
|
+
};
|
|
1178
|
+
async function reconnectHerdrPiSession(session, launch, dependencies = {}) {
|
|
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));
|
|
1180
|
+
let validated;
|
|
1181
|
+
const result = await boundedHerdrReconnect(session.identity, session.evidenceCursor, async () => {
|
|
1182
|
+
const connection = await connect(launch.machine);
|
|
1183
|
+
let unsubscribe = () => {
|
|
1184
|
+
};
|
|
1185
|
+
let bridgeForward;
|
|
1186
|
+
let bridge;
|
|
1187
|
+
const generation = { adopted: false };
|
|
1188
|
+
try {
|
|
1189
|
+
const panes = await host.panes(connection, session.identity);
|
|
1190
|
+
const manifest = await discoverManifest(session.identity.runId, session.identity.runtimeDir);
|
|
1191
|
+
bridgeForward = await connection.forwardRemoteSocket(manifest.socketPath, "bridge-reconnect");
|
|
1192
|
+
bridge = createBridge(bridgeForward.socketPath, session.identity.runId);
|
|
1193
|
+
const ready = await waitForReadyBridgeFrame(bridge, 8e3, "Reconnected bridge handshake timed out.", true);
|
|
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);
|
|
1196
|
+
if (!check.ok) throw new Error(check.reason);
|
|
1197
|
+
unsubscribe = await host.subscribe(connection, check.paneId, (event) => session.owner.observeEvent(event), (error) => {
|
|
1198
|
+
if (!generation.adopted) {
|
|
1199
|
+
generation.lost ??= error;
|
|
1200
|
+
return;
|
|
1201
|
+
}
|
|
1202
|
+
session.owner.markConnectionUnknown(connection);
|
|
1203
|
+
void session.reconnect();
|
|
1204
|
+
});
|
|
1205
|
+
if (generation.lost) throw generation.lost;
|
|
1206
|
+
validated = { connection, unsubscribe, bridgeForward, bridge, candidate: generation };
|
|
1207
|
+
return candidate;
|
|
1208
|
+
} catch (error) {
|
|
1209
|
+
unsubscribe();
|
|
1210
|
+
bridge?.close();
|
|
1211
|
+
await bridgeForward?.close();
|
|
1212
|
+
await connection.close();
|
|
1213
|
+
throw error;
|
|
1214
|
+
}
|
|
1215
|
+
}, { attempts: 3, deadlineMs: 15e3, disposed: () => session.placementSnapshot.connection === "closed", host });
|
|
1216
|
+
if (!result.ok) throw new Error(`Pane-native reconnect remains unknown: ${result.reason}`);
|
|
1217
|
+
if (!validated) throw new Error("Pane-native reconnect produced no validated transport.");
|
|
1218
|
+
if (validated.candidate.lost) {
|
|
1219
|
+
validated.unsubscribe();
|
|
1220
|
+
validated.bridge.close();
|
|
1221
|
+
await validated.bridgeForward.close();
|
|
1222
|
+
await validated.connection.close();
|
|
1223
|
+
throw validated.candidate.lost;
|
|
1224
|
+
}
|
|
1225
|
+
validated.candidate.adopted = true;
|
|
1226
|
+
await session.replaceTransport({ ...validated, paneId: result.paneId, cursor: result.cursor, state: result.state });
|
|
1227
|
+
}
|
|
1228
|
+
async function createHerdrPiSession(launch) {
|
|
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.");
|
|
1231
|
+
const policy = serializeHerdrPiLaunch(launch);
|
|
1232
|
+
const runId = safeRunId(launch.runtime.runId);
|
|
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;
|
|
1243
|
+
try {
|
|
1244
|
+
let reconnectSession;
|
|
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.");
|
|
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}).`);
|
|
1250
|
+
const bridgeForward = await connection.forwardRemoteSocket(manifest.socketPath, "bridge");
|
|
1251
|
+
const bridge = new BridgeChannel(bridgeForward.socketPath, runId);
|
|
1252
|
+
const ready = await waitForReadyBridgeFrame(bridge, 15e3, "Remote Pi bridge handshake timed out.");
|
|
1253
|
+
if (ready.packageVersion !== package_default.version || ready.nativeSessionId !== manifest.nativeSessionId || ready.cwd !== machine.cwd) throw new Error("Remote Pi bridge handshake identity mismatch.");
|
|
1254
|
+
const configured = await Promise.race([bridge.configure(policy.resources), bridge.failure]);
|
|
1255
|
+
if (configured.nativeSessionId !== manifest.nativeSessionId || configured.agent !== policy.resources.agent) throw new Error("Remote Pi resource resolution identity mismatch.");
|
|
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.");
|
|
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.`);
|
|
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 };
|
|
1259
|
+
owner.journal(identity);
|
|
1260
|
+
const session = new HerdrPiSession(owner, bridgeForward, bridge, configured.tools, typeof ready.model === "string" ? ready.model : void 0, launch.runtime, ready.initialGit);
|
|
1261
|
+
reconnectSession = session;
|
|
1262
|
+
session.armReconnect(() => reconnectHerdrPiSession(session, launch));
|
|
1263
|
+
return session;
|
|
1264
|
+
} catch (error) {
|
|
1265
|
+
await owner.cleanup();
|
|
1266
|
+
throw error;
|
|
1267
|
+
}
|
|
1268
|
+
}
|
|
1269
|
+
function createPlacedPiSession(launch) {
|
|
1270
|
+
return launch.machine?.provider === "rex" ? createRexPiSession(launch) : createHerdrPiSession(launch);
|
|
1271
|
+
}
|
|
1272
|
+
function createPlacementAwareChildSessionFactory(local, createRemote = createPlacedPiSession) {
|
|
1273
|
+
const remote = /* @__PURE__ */ new Set();
|
|
1274
|
+
return { async create(launch) {
|
|
1275
|
+
if (!launch.machine) return local.create(launch);
|
|
1276
|
+
serializeHerdrPiLaunch(launch);
|
|
1277
|
+
const child = await createRemote(launch);
|
|
1278
|
+
remote.add(child);
|
|
1279
|
+
return child;
|
|
1280
|
+
}, async dispose() {
|
|
1281
|
+
await Promise.allSettled([...remote].map((child) => child.dispose()));
|
|
1282
|
+
remote.clear();
|
|
1283
|
+
await local.dispose();
|
|
1284
|
+
} };
|
|
1285
|
+
}
|
|
1286
|
+
export {
|
|
1287
|
+
BridgeChannel,
|
|
1288
|
+
HerdrPiSession,
|
|
1289
|
+
boundedHerdrReconnect,
|
|
1290
|
+
createHerdrPiSession,
|
|
1291
|
+
createPlacementAwareChildSessionFactory,
|
|
1292
|
+
createRexPiSession,
|
|
1293
|
+
reconcileHerdrReconnect,
|
|
1294
|
+
reconnectHerdrPiSession,
|
|
1295
|
+
serializeHerdrPiLaunch
|
|
1296
|
+
};
|