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
|
@@ -1,30 +1,16 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { randomBytes } from "node:crypto";
|
|
2
2
|
import * as fs from "node:fs";
|
|
3
3
|
import * as net from "node:net";
|
|
4
4
|
import * as path from "node:path";
|
|
5
5
|
import packageJson from "../../../package.json" with { type: "json" };
|
|
6
|
-
import {
|
|
6
|
+
import { herdrPaneHost, HerdrPlacedRunOwner } from "./herdr-pane-host.js";
|
|
7
7
|
import { encodeHerdrPiFrame, HERDR_PI_MODE_ENV, HERDR_PI_PROTOCOL, HERDR_PI_RUN_ENV, HERDR_PI_RUNTIME_DIR_ENV, HerdrPiFrameDecoder, validateHerdrPiRunId } from "./herdr-pi-protocol.js";
|
|
8
|
-
import {
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
export function
|
|
13
|
-
return
|
|
14
|
-
}
|
|
15
|
-
export function reconcileHerdrPlacedRun(identity, candidate, proveEvidence) {
|
|
16
|
-
if (candidate.endpoint.session !== identity.session)
|
|
17
|
-
return { ok: false, reason: "Herdr session changed during reconnect." };
|
|
18
|
-
const matches = candidate.agents.filter((agent) => agent.terminal_id === identity.terminalId);
|
|
19
|
-
if (matches.length !== 1 || typeof matches[0].pane_id !== "string")
|
|
20
|
-
return { ok: false, reason: matches.length ? "Terminal identity is ambiguous." : "Owned terminal is missing." };
|
|
21
|
-
const proof = proveEvidence();
|
|
22
|
-
if (!proof.ok)
|
|
23
|
-
return proof;
|
|
24
|
-
return { ok: true, state: herdrRunState(matches[0].agent_status), paneId: matches[0].pane_id, cursor: proof.cursor };
|
|
25
|
-
}
|
|
26
|
-
export function reconcileHerdrReconnect(identity, candidate, lastEvidenceCursor) {
|
|
27
|
-
return reconcileHerdrPlacedRun(identity, candidate, () => { const bridge = candidate.bridge; if (bridge.runId !== identity.runId || bridge.nativeSessionId !== identity.nativeSessionId || bridge.packageVersion !== packageJson.version || bridge.protocol !== HERDR_PI_PROTOCOL)
|
|
8
|
+
import { discoverPlacedBridgeManifest } from "./placed-run-owner.js";
|
|
9
|
+
import { RexPlacedRunOwner } from "./rex-pane-host.js";
|
|
10
|
+
import { isRexLocalMachine } from "./rex-machine.js";
|
|
11
|
+
/** The pane half comes from the host; the bridge half (run, package, protocol, native session, evidence cursor) is host-neutral. */
|
|
12
|
+
export function reconcileHerdrReconnect(identity, candidate, lastEvidenceCursor, host = herdrPaneHost) {
|
|
13
|
+
return host.reconcile(identity, candidate, () => { const bridge = candidate.bridge; if (bridge.runId !== identity.runId || bridge.nativeSessionId !== identity.nativeSessionId || bridge.packageVersion !== packageJson.version || bridge.protocol !== HERDR_PI_PROTOCOL)
|
|
28
14
|
return { ok: false, reason: "Bridge run, package, protocol, or native session identity changed." }; if (typeof bridge.evidenceCursor !== "number" || typeof bridge.evidenceFloor !== "number" || bridge.evidenceCursor < lastEvidenceCursor || bridge.evidenceFloor > lastEvidenceCursor + 1)
|
|
29
15
|
return { ok: false, reason: "Bridge evidence has a gap after reconnect." }; return { ok: true, cursor: bridge.evidenceCursor }; });
|
|
30
16
|
}
|
|
@@ -35,7 +21,7 @@ export async function boundedHerdrReconnect(identity, lastEvidenceCursor, attemp
|
|
|
35
21
|
if (options.disposed?.())
|
|
36
22
|
return { ok: false, reason: "Reconnect was disposed." };
|
|
37
23
|
try {
|
|
38
|
-
const result = reconcileHerdrReconnect(identity, await attempt(number), lastEvidenceCursor);
|
|
24
|
+
const result = reconcileHerdrReconnect(identity, await attempt(number), lastEvidenceCursor, options.host);
|
|
39
25
|
if (result.ok)
|
|
40
26
|
return result;
|
|
41
27
|
last = result.reason;
|
|
@@ -48,172 +34,6 @@ export async function boundedHerdrReconnect(identity, lastEvidenceCursor, attemp
|
|
|
48
34
|
return { ok: false, reason: last };
|
|
49
35
|
}
|
|
50
36
|
function safeRunId(value) { const prefix = value?.replace(/[^A-Za-z0-9_-]/gu, "_").slice(0, 48) ?? "run"; return validateHerdrPiRunId(`${prefix}_${randomBytes(8).toString("hex")}`); }
|
|
51
|
-
function idOf(value, key) { const found = value && typeof value === "object" ? value[key] : undefined; if (typeof found !== "string" || !found)
|
|
52
|
-
throw new Error(`Herdr did not return ${key}.`); return found; }
|
|
53
|
-
function list(value, key) { if (Array.isArray(value))
|
|
54
|
-
return value; const found = value && typeof value === "object" ? value[key] : undefined; return Array.isArray(found) ? found : []; }
|
|
55
|
-
function tagged(value, type, key) { if (!value || typeof value !== "object" || value.type !== type || !(key in value) || typeof value[key] !== "object")
|
|
56
|
-
throw new Error(`Herdr returned an invalid ${type} envelope.`); return value[key]; }
|
|
57
|
-
export function parseHerdrCreated(value, type) { const envelope = value; if (envelope?.type !== type)
|
|
58
|
-
throw new Error(`Herdr returned an invalid ${type} envelope.`); const tab = envelope.tab; const pane = envelope.root_pane; return { ...(type === "workspace_created" ? { workspaceId: idOf(envelope.workspace, "workspace_id") } : {}), tabId: idOf(tab, "tab_id"), paneId: idOf(pane, "pane_id") }; }
|
|
59
|
-
export function parseHerdrAgent(value, type) { return tagged(value, type, "agent"); }
|
|
60
|
-
export function parseHerdrSessionSnapshot(value) { return tagged(value, "session_snapshot", "snapshot"); }
|
|
61
|
-
export function ownsHerdrPane(agent, terminalId, paneId) { return agent.terminal_id === terminalId && agent.pane_id === paneId; }
|
|
62
|
-
function snapshotAgents(snapshot) { return list(snapshot, "agents").filter((agent) => Boolean(agent && typeof agent === "object")).map((agent) => ({ terminal_id: typeof agent.terminal_id === "string" ? agent.terminal_id : undefined, pane_id: typeof agent.pane_id === "string" ? agent.pane_id : undefined, agent_status: typeof agent.agent_status === "string" ? agent.agent_status : undefined })); }
|
|
63
|
-
export const HERDR_MAX_OWNED_PANES = 20;
|
|
64
|
-
const HERDR_ALLOCATION_LOCK_TIMEOUT_MS = 10_000, HERDR_ALLOCATION_LOCK_POLL_MS = 25;
|
|
65
|
-
export function herdrPaneAllocationKey(target, session, cwd) { return JSON.stringify([target, session, cwd]); }
|
|
66
|
-
function assertPrivateDir(directory) { const stat = fs.lstatSync(directory); if (!stat.isDirectory() || stat.isSymbolicLink() || (stat.mode & 0o777) !== 0o700 || (typeof process.getuid === "function" && stat.uid !== process.getuid()))
|
|
67
|
-
throw new Error("Herdr allocation path is not a mode-0700 agent-private owned directory."); }
|
|
68
|
-
function ensurePrivateChild(parent, name) { const directory = path.join(parent, name); try {
|
|
69
|
-
fs.mkdirSync(directory, { mode: 0o700 });
|
|
70
|
-
}
|
|
71
|
-
catch (error) {
|
|
72
|
-
if (error.code !== "EEXIST")
|
|
73
|
-
throw error;
|
|
74
|
-
} assertPrivateDir(directory); return directory; }
|
|
75
|
-
function allocationLockRoot() { const agentDir = getAgentDir(); fs.mkdirSync(agentDir, { recursive: true, mode: 0o700 }); assertPrivateDir(agentDir); const packageDir = ensurePrivateChild(agentDir, "pi-subagents"); return ensurePrivateChild(packageDir, "herdr-allocation-locks"); }
|
|
76
|
-
function livePid(pid) { try {
|
|
77
|
-
process.kill(pid, 0);
|
|
78
|
-
return true;
|
|
79
|
-
}
|
|
80
|
-
catch (error) {
|
|
81
|
-
return error.code === "EPERM";
|
|
82
|
-
} }
|
|
83
|
-
function readAllocationOwner(ownerPath) { const descriptor = fs.openSync(ownerPath, fs.constants.O_RDONLY | (fs.constants.O_NOFOLLOW ?? 0)); try {
|
|
84
|
-
const stat = fs.fstatSync(descriptor);
|
|
85
|
-
if (!stat.isFile() || (stat.mode & 0o777) !== 0o600 || (typeof process.getuid === "function" && stat.uid !== process.getuid()))
|
|
86
|
-
throw new Error("Herdr allocation lock owner record is unsafe.");
|
|
87
|
-
const value = JSON.parse(fs.readFileSync(descriptor, "utf8"));
|
|
88
|
-
if (typeof value.pid !== "number" || !Number.isSafeInteger(value.pid) || value.pid <= 0 || typeof value.token !== "string" || !/^[a-f0-9]{32}$/u.test(value.token))
|
|
89
|
-
throw new Error("Herdr allocation lock owner record is malformed.");
|
|
90
|
-
return { pid: value.pid, token: value.token, dev: stat.dev, ino: stat.ino };
|
|
91
|
-
}
|
|
92
|
-
finally {
|
|
93
|
-
fs.closeSync(descriptor);
|
|
94
|
-
} }
|
|
95
|
-
function sameOwner(left, right) { return left.token === right.token && left.dev === right.dev && left.ino === right.ino; }
|
|
96
|
-
export async function withHerdrPaneAllocationLock(key, action, options = {}) {
|
|
97
|
-
const root = allocationLockRoot(), digest = createHash("sha256").update(key).digest("hex"), lockPath = path.join(root, `${digest}.lock`), recoveryPath = path.join(root, `${digest}.recovery`), token = randomBytes(16).toString("hex"), tempPath = path.join(root, `.${digest}.${token}.tmp`), deadline = Date.now() + (options.timeoutMs ?? HERDR_ALLOCATION_LOCK_TIMEOUT_MS);
|
|
98
|
-
let published;
|
|
99
|
-
try {
|
|
100
|
-
fs.writeFileSync(tempPath, JSON.stringify({ pid: process.pid, token, createdAt: Date.now() }), { mode: 0o600, flag: "wx" });
|
|
101
|
-
const tempOwner = readAllocationOwner(tempPath);
|
|
102
|
-
for (;;) {
|
|
103
|
-
let recovering = false;
|
|
104
|
-
try {
|
|
105
|
-
readAllocationOwner(recoveryPath);
|
|
106
|
-
recovering = true;
|
|
107
|
-
}
|
|
108
|
-
catch (error) {
|
|
109
|
-
if (error.code !== "ENOENT")
|
|
110
|
-
throw error;
|
|
111
|
-
}
|
|
112
|
-
if (!recovering) {
|
|
113
|
-
let linked = false;
|
|
114
|
-
try {
|
|
115
|
-
fs.linkSync(tempPath, lockPath);
|
|
116
|
-
linked = true;
|
|
117
|
-
try {
|
|
118
|
-
readAllocationOwner(recoveryPath);
|
|
119
|
-
const current = readAllocationOwner(lockPath);
|
|
120
|
-
if (!sameOwner(current, tempOwner))
|
|
121
|
-
throw new Error("Herdr allocation lock changed during publication.");
|
|
122
|
-
fs.unlinkSync(lockPath);
|
|
123
|
-
}
|
|
124
|
-
catch (recoveryError) {
|
|
125
|
-
if (recoveryError.code !== "ENOENT")
|
|
126
|
-
throw recoveryError;
|
|
127
|
-
published = readAllocationOwner(lockPath);
|
|
128
|
-
if (!sameOwner(published, tempOwner))
|
|
129
|
-
throw new Error("Herdr allocation lock publication identity changed.");
|
|
130
|
-
break;
|
|
131
|
-
}
|
|
132
|
-
}
|
|
133
|
-
catch (publishError) {
|
|
134
|
-
if (linked) {
|
|
135
|
-
try {
|
|
136
|
-
const current = readAllocationOwner(lockPath);
|
|
137
|
-
if (sameOwner(current, tempOwner))
|
|
138
|
-
fs.unlinkSync(lockPath);
|
|
139
|
-
}
|
|
140
|
-
catch { }
|
|
141
|
-
}
|
|
142
|
-
if (publishError.code !== "EEXIST" && publishError.code !== "ENOENT")
|
|
143
|
-
throw publishError;
|
|
144
|
-
}
|
|
145
|
-
}
|
|
146
|
-
if (published)
|
|
147
|
-
break;
|
|
148
|
-
try {
|
|
149
|
-
const current = readAllocationOwner(lockPath);
|
|
150
|
-
if (!livePid(current.pid)) {
|
|
151
|
-
let claimed = false;
|
|
152
|
-
try {
|
|
153
|
-
fs.linkSync(lockPath, recoveryPath);
|
|
154
|
-
claimed = true;
|
|
155
|
-
const recovery = readAllocationOwner(recoveryPath), latest = readAllocationOwner(lockPath);
|
|
156
|
-
if (!sameOwner(recovery, current) || !sameOwner(latest, current))
|
|
157
|
-
throw new Error("Herdr stale lock identity changed during recovery.");
|
|
158
|
-
fs.unlinkSync(lockPath);
|
|
159
|
-
}
|
|
160
|
-
catch (error) {
|
|
161
|
-
if (error.code !== "EEXIST" && error.code !== "ENOENT")
|
|
162
|
-
throw error;
|
|
163
|
-
}
|
|
164
|
-
finally {
|
|
165
|
-
if (claimed)
|
|
166
|
-
fs.unlinkSync(recoveryPath);
|
|
167
|
-
}
|
|
168
|
-
if (claimed)
|
|
169
|
-
continue;
|
|
170
|
-
}
|
|
171
|
-
}
|
|
172
|
-
catch (error) {
|
|
173
|
-
if (error.code !== "ENOENT")
|
|
174
|
-
throw error;
|
|
175
|
-
}
|
|
176
|
-
if (Date.now() >= deadline)
|
|
177
|
-
throw new Error("Timed out waiting for the Herdr pane allocation lock.");
|
|
178
|
-
await new Promise((resolve) => setTimeout(resolve, options.pollMs ?? HERDR_ALLOCATION_LOCK_POLL_MS));
|
|
179
|
-
}
|
|
180
|
-
fs.unlinkSync(tempPath);
|
|
181
|
-
let value, actionError, releaseError, actionFailed = false, releaseFailed = false;
|
|
182
|
-
try {
|
|
183
|
-
value = await action();
|
|
184
|
-
}
|
|
185
|
-
catch (error) {
|
|
186
|
-
actionFailed = true;
|
|
187
|
-
actionError = error;
|
|
188
|
-
}
|
|
189
|
-
try {
|
|
190
|
-
const current = readAllocationOwner(lockPath);
|
|
191
|
-
if (!published || !sameOwner(current, published))
|
|
192
|
-
throw new Error("Herdr pane allocation lock ownership changed before release.");
|
|
193
|
-
fs.unlinkSync(lockPath);
|
|
194
|
-
}
|
|
195
|
-
catch (error) {
|
|
196
|
-
releaseFailed = true;
|
|
197
|
-
releaseError = error;
|
|
198
|
-
}
|
|
199
|
-
if (actionFailed && releaseFailed)
|
|
200
|
-
throw new AggregateError([actionError, releaseError], "Herdr pane allocation action and release both failed.", { cause: actionError });
|
|
201
|
-
if (actionFailed)
|
|
202
|
-
throw actionError;
|
|
203
|
-
if (releaseFailed)
|
|
204
|
-
throw releaseError;
|
|
205
|
-
return value;
|
|
206
|
-
}
|
|
207
|
-
finally {
|
|
208
|
-
try {
|
|
209
|
-
fs.unlinkSync(tempPath);
|
|
210
|
-
}
|
|
211
|
-
catch (error) {
|
|
212
|
-
if (error.code !== "ENOENT")
|
|
213
|
-
throw error;
|
|
214
|
-
}
|
|
215
|
-
}
|
|
216
|
-
}
|
|
217
37
|
export function serializeHerdrPiLaunch(launch) {
|
|
218
38
|
if (launch.storage.kind === "file")
|
|
219
39
|
throw new Error("Pane-native remote Pi does not support fork, resume, or revival; use fresh context.");
|
|
@@ -237,177 +57,6 @@ export function serializeHerdrPiLaunch(launch) {
|
|
|
237
57
|
args.push("--model", launch.model);
|
|
238
58
|
return { args, resources: { agent: resources.agent, ...(resources.skills ? { skills: [...resources.skills] } : {}), ...(resources.toolCeiling ? { toolCeiling: [...resources.toolCeiling] } : {}), ...(resources.reads !== undefined ? { reads: resources.reads === false ? false : [...resources.reads] } : {}) } };
|
|
239
59
|
}
|
|
240
|
-
export async function provisionHerdrPane(client, cwd, runId, runtimeDir, environment, allocationKey = cwd) {
|
|
241
|
-
return withHerdrPaneAllocationLock(allocationKey, async () => {
|
|
242
|
-
const snapshot = parseHerdrSessionSnapshot(await client.call("session.snapshot"));
|
|
243
|
-
const panes = list(snapshot, "panes").filter((item) => Boolean(item && typeof item === "object")), workspaces = list(snapshot, "workspaces").filter((item) => Boolean(item && typeof item === "object"));
|
|
244
|
-
const ownedLabel = `pi-subagents-${createHash("sha256").update(cwd).digest("hex").slice(0, 10)}`, owned = workspaces.filter((item) => item.label === ownedLabel);
|
|
245
|
-
if (owned.length > 1)
|
|
246
|
-
throw new Error("Herdr deterministic owned workspace identity is ambiguous.");
|
|
247
|
-
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));
|
|
248
|
-
const selected = owned[0] ?? (cwdWorkspaces.length === 1 ? cwdWorkspaces[0] : undefined), selectedId = selected ? idOf(selected, "workspace_id") : undefined, actual = selectedId ? panes.filter((pane) => pane.workspace_id === selectedId).length : 0;
|
|
249
|
-
if (actual >= HERDR_MAX_OWNED_PANES)
|
|
250
|
-
throw new Error(`Herdr workspace pane limit of ${HERDR_MAX_OWNED_PANES} is reached; close an existing pane before launching another placed run.`);
|
|
251
|
-
const env = environment ?? { [HERDR_PI_MODE_ENV]: "1", [HERDR_PI_RUN_ENV]: runId, [HERDR_PI_RUNTIME_DIR_ENV]: runtimeDir };
|
|
252
|
-
if (selectedId) {
|
|
253
|
-
const tab = parseHerdrCreated(await client.call("tab.create", { workspace_id: selectedId, cwd, label: `subagent-${runId.slice(-8)}`, env, focus: false }), "tab_created");
|
|
254
|
-
return { workspaceId: selectedId, tabId: tab.tabId, paneId: tab.paneId };
|
|
255
|
-
}
|
|
256
|
-
const workspace = parseHerdrCreated(await client.call("workspace.create", { cwd, label: ownedLabel, env, focus: false }), "workspace_created");
|
|
257
|
-
return { workspaceId: workspace.workspaceId, tabId: workspace.tabId, paneId: workspace.paneId };
|
|
258
|
-
});
|
|
259
|
-
}
|
|
260
|
-
export async function discoverBridgeManifest(machine, runId, runtimeDir, options = {}) {
|
|
261
|
-
const script = `p="$1/manifest.json"; test -f "$p" && test ! -L "$p" && cat "$p"`;
|
|
262
|
-
for (let attempt = 0; attempt < 100; attempt++) {
|
|
263
|
-
const result = await runHerdrRemoteCommandAsync(machine, remoteShellCommand(script, [runtimeDir]), { ...options, timeout: 5_000, maxBuffer: 64 * 1024 });
|
|
264
|
-
if (result.error)
|
|
265
|
-
throw new Error(`Remote bridge manifest discovery failed: ${result.error.message}`, { cause: result.error });
|
|
266
|
-
if (result.status === 0 && result.stdout.trim()) {
|
|
267
|
-
let value;
|
|
268
|
-
try {
|
|
269
|
-
value = JSON.parse(result.stdout);
|
|
270
|
-
}
|
|
271
|
-
catch {
|
|
272
|
-
throw new Error("Remote bridge manifest is malformed.");
|
|
273
|
-
}
|
|
274
|
-
const p = value;
|
|
275
|
-
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")
|
|
276
|
-
throw new Error("Remote bridge manifest identity is incomplete or outside its owned runtime directory.");
|
|
277
|
-
return p;
|
|
278
|
-
}
|
|
279
|
-
await new Promise((resolve) => setTimeout(resolve, 50));
|
|
280
|
-
}
|
|
281
|
-
throw new Error("The remote Pi did not expose the packaged pi-subagents bridge. Install/configure the same package version as an ambient Pi extension on the saved machine.");
|
|
282
|
-
}
|
|
283
|
-
export async function createRemoteRuntimeDir(machine, runId, options = {}) { const result = await runHerdrRemoteCommandAsync(machine, remoteShellCommand("umask 077; mktemp -d \"${TMPDIR:-/tmp}/pi-subagents-herdr-$1-XXXXXXXX\"", [runId]), { ...options, timeout: 10_000, maxBuffer: 4096 }); if (result.error)
|
|
284
|
-
throw new Error(`Could not provision a run-private remote bridge runtime directory: ${result.error.message}`, { cause: result.error }); const value = result.stdout.trim(); if (result.status !== 0 || !path.posix.isAbsolute(value) || value.includes("\n"))
|
|
285
|
-
throw new Error("Could not provision a run-private remote bridge runtime directory."); return value; }
|
|
286
|
-
export function removeRemoteRuntimeDir(machine, runtimeDir, options = {}) { const result = runHerdrRemoteCommand(machine, remoteShellCommand("p=$1; case \"${p##*/}\" in pi-subagents-herdr-*) test -d \"$p\" && test ! -L \"$p\" && rm -rf -- \"$p\";; *) exit 64;; esac", [runtimeDir]), { ...options, timeout: 10_000, maxBuffer: 4096 }); if (result.status !== 0 || result.stdout)
|
|
287
|
-
throw new Error(`Could not remove the exact owned remote runtime directory: ${String(result.stderr).slice(0, 512)}`); }
|
|
288
|
-
/** The single private owner for allocation shared by every pane-native backend projection. */
|
|
289
|
-
export class HerdrPlacedRunOwner {
|
|
290
|
-
machine;
|
|
291
|
-
runId;
|
|
292
|
-
runtimeDir;
|
|
293
|
-
connection;
|
|
294
|
-
owned;
|
|
295
|
-
terminalId;
|
|
296
|
-
agentName;
|
|
297
|
-
startedArgv;
|
|
298
|
-
identity;
|
|
299
|
-
#unsubscribe = () => { };
|
|
300
|
-
#snapshot = { connection: "connected", state: "ready", revision: 0 };
|
|
301
|
-
#cleanupPromise;
|
|
302
|
-
#removeRuntime;
|
|
303
|
-
constructor(machine, runId, runtimeDir, connection, removeRuntime = () => removeRemoteRuntimeDir(machine, runtimeDir)) { this.machine = machine; this.runId = runId; this.runtimeDir = runtimeDir; this.connection = connection; this.#removeRuntime = removeRuntime; }
|
|
304
|
-
static async create(machine, runId, beforePane) { const runtimeDir = await createRemoteRuntimeDir(machine, runId); let connection; try {
|
|
305
|
-
connection = await connectHerdrMachine(machine);
|
|
306
|
-
const owner = new HerdrPlacedRunOwner(machine, runId, runtimeDir, connection);
|
|
307
|
-
await beforePane?.(owner);
|
|
308
|
-
return owner;
|
|
309
|
-
}
|
|
310
|
-
catch (error) {
|
|
311
|
-
const cleanupErrors = [];
|
|
312
|
-
try {
|
|
313
|
-
await connection?.close();
|
|
314
|
-
}
|
|
315
|
-
catch (cleanupError) {
|
|
316
|
-
cleanupErrors.push(cleanupError);
|
|
317
|
-
}
|
|
318
|
-
try {
|
|
319
|
-
removeRemoteRuntimeDir(machine, runtimeDir);
|
|
320
|
-
}
|
|
321
|
-
catch (cleanupError) {
|
|
322
|
-
cleanupErrors.push(cleanupError);
|
|
323
|
-
}
|
|
324
|
-
if (cleanupErrors.length)
|
|
325
|
-
throw new AggregateError([error, ...cleanupErrors], "Herdr owner creation failed and cleanup also failed.", { cause: error });
|
|
326
|
-
throw error;
|
|
327
|
-
} }
|
|
328
|
-
observeEvent(event) { const data = event && typeof event === "object" ? event.data : undefined; if (data && (!this.terminalId || data.terminal_id === this.terminalId || data.pane_id === this.owned?.paneId))
|
|
329
|
-
this.#snapshot = { ...this.#snapshot, revision: this.#snapshot.revision + 1, state: herdrRunState(data.agent_status, this.#snapshot.state) }; }
|
|
330
|
-
markConnectionUnknown(observed) { if (this.connection !== observed)
|
|
331
|
-
return false; this.#snapshot = { ...this.#snapshot, connection: "unknown", state: "unknown", revision: this.#snapshot.revision + 1 }; return true; }
|
|
332
|
-
statusSubscriptions(paneId = this.owned?.paneId) { if (!paneId)
|
|
333
|
-
throw new Error("Herdr status subscription requires a provisioned pane."); return herdrStatusSubscriptions(paneId); }
|
|
334
|
-
async subscribe(listener, disconnected) { const observed = this.connection; let adopted = false, candidateLost; const unsubscribe = await observed.client.subscribe(this.statusSubscriptions(), (event) => { this.observeEvent(event); listener(event); }, (error) => { if (!adopted) {
|
|
335
|
-
candidateLost ??= error;
|
|
336
|
-
return;
|
|
337
|
-
} if (!this.markConnectionUnknown(observed))
|
|
338
|
-
return; disconnected(error); }); if (candidateLost) {
|
|
339
|
-
unsubscribe();
|
|
340
|
-
throw candidateLost;
|
|
341
|
-
} this.#unsubscribe = unsubscribe; adopted = true; }
|
|
342
|
-
async provision(environment) { this.owned = await provisionHerdrPane(this.connection.client, this.machine.cwd, this.runId, this.runtimeDir, environment, herdrPaneAllocationKey(this.machine.target, this.connection.endpoint.session, this.machine.cwd)); return this.owned; }
|
|
343
|
-
async start(kind, name, args, options = {}) {
|
|
344
|
-
if (!this.owned)
|
|
345
|
-
throw new Error("Herdr placement owner has not provisioned its pane.");
|
|
346
|
-
const clock = options.clock ?? { now: () => Date.now(), wait: (ms) => new Promise((resolve) => setTimeout(resolve, ms)) }, deadline = clock.now() + (options.timeoutMs ?? 5_000), pollMs = options.pollMs ?? 50;
|
|
347
|
-
let response;
|
|
348
|
-
for (;;) {
|
|
349
|
-
try {
|
|
350
|
-
response = await this.connection.client.call("agent.start", { name, kind, pane_id: this.owned.paneId, args, timeout_ms: 45_000 }, 60_000);
|
|
351
|
-
break;
|
|
352
|
-
}
|
|
353
|
-
catch (error) {
|
|
354
|
-
if (!(error instanceof Error) || !error.message.includes("is not an available shell"))
|
|
355
|
-
throw error;
|
|
356
|
-
const pane = tagged(await this.connection.client.call("pane.get", { pane_id: this.owned.paneId }), "pane_info", "pane");
|
|
357
|
-
if (pane.pane_id !== this.owned.paneId || pane.workspace_id !== this.owned.workspaceId || pane.tab_id !== this.owned.tabId)
|
|
358
|
-
throw new Error("Herdr startup pane identity changed after a transient start rejection.", { cause: error });
|
|
359
|
-
if (pane.agent != null || pane.agent_session != null)
|
|
360
|
-
throw new Error("Herdr startup pane became occupied after a transient start rejection.", { cause: error });
|
|
361
|
-
if (clock.now() >= deadline)
|
|
362
|
-
throw new Error("Herdr owned pane remained unavailable until the startup deadline.", { cause: error });
|
|
363
|
-
await clock.wait(Math.min(pollMs, deadline - clock.now()));
|
|
364
|
-
}
|
|
365
|
-
}
|
|
366
|
-
const started = parseHerdrAgent(response, "agent_started"), argv = [kind, ...args], terminalId = idOf(started, "terminal_id"), paneId = idOf(started, "pane_id");
|
|
367
|
-
if (paneId !== this.owned.paneId)
|
|
368
|
-
throw new Error("Herdr started the native agent in an unexpected pane.");
|
|
369
|
-
this.agentName = name;
|
|
370
|
-
this.terminalId = terminalId;
|
|
371
|
-
this.startedArgv = argv;
|
|
372
|
-
return { terminalId, paneId, argv };
|
|
373
|
-
}
|
|
374
|
-
journal(identity) { this.identity = identity; this.#snapshot = { ...this.#snapshot, identity }; const journalDir = path.join(getAgentDir(), "pi-subagents", "herdr-run-journal"); fs.mkdirSync(journalDir, { recursive: true, mode: 0o700 }); fs.writeFileSync(path.join(journalDir, `${this.runId}.json`), JSON.stringify({ version: 1, identity, state: "ready", updatedAt: Date.now() }), { mode: 0o600, flag: "wx" }); }
|
|
375
|
-
get snapshot() { return this.#snapshot; }
|
|
376
|
-
runRemote(command, options = {}) { return runHerdrRemoteCommand(this.machine, command, options); }
|
|
377
|
-
async replaceConnection(input) { const oldConnection = this.connection, oldUnsubscribe = this.#unsubscribe; this.connection = input.connection; this.#unsubscribe = input.unsubscribe; if (this.identity)
|
|
378
|
-
this.identity = { ...this.identity, paneId: input.paneId }; this.#snapshot = { connection: "connected", state: input.state, identity: this.identity, revision: this.#snapshot.revision + 1 }; oldUnsubscribe(); await oldConnection.close(); }
|
|
379
|
-
cleanup(closeStartedPane = true) { return this.#cleanupPromise ??= this.#performCleanup(closeStartedPane); }
|
|
380
|
-
async #performCleanup(closeStartedPane) { let failure; try {
|
|
381
|
-
if (closeStartedPane && this.owned) {
|
|
382
|
-
let closed = false;
|
|
383
|
-
if (this.terminalId && this.agentName) {
|
|
384
|
-
const current = parseHerdrAgent(await this.connection.client.call("agent.get", { target: this.agentName }), "agent_info");
|
|
385
|
-
if (ownsHerdrPane(current, this.terminalId, this.owned.paneId)) {
|
|
386
|
-
await this.connection.client.call("pane.close", { pane_id: this.owned.paneId });
|
|
387
|
-
closed = true;
|
|
388
|
-
}
|
|
389
|
-
}
|
|
390
|
-
if (!closed) {
|
|
391
|
-
const snapshot = parseHerdrSessionSnapshot(await this.connection.client.call("session.snapshot"));
|
|
392
|
-
const occupants = snapshotAgents(snapshot).filter((agent) => agent.pane_id === this.owned.paneId);
|
|
393
|
-
if (occupants.length !== 0)
|
|
394
|
-
throw new Error("Refusing cleanup because startup pane ownership is uncertain.");
|
|
395
|
-
await this.connection.client.call("pane.close", { pane_id: this.owned.paneId });
|
|
396
|
-
}
|
|
397
|
-
}
|
|
398
|
-
}
|
|
399
|
-
catch (error) {
|
|
400
|
-
failure = error;
|
|
401
|
-
} try {
|
|
402
|
-
this.#unsubscribe();
|
|
403
|
-
await this.connection.close();
|
|
404
|
-
this.#removeRuntime();
|
|
405
|
-
}
|
|
406
|
-
catch (error) {
|
|
407
|
-
failure ??= error;
|
|
408
|
-
} this.#snapshot = { ...this.#snapshot, connection: "closed", revision: this.#snapshot.revision + 1 }; if (failure)
|
|
409
|
-
throw failure; }
|
|
410
|
-
}
|
|
411
60
|
export class BridgeChannel {
|
|
412
61
|
socket;
|
|
413
62
|
decoder = new HerdrPiFrameDecoder();
|
|
@@ -450,7 +99,7 @@ export class BridgeChannel {
|
|
|
450
99
|
wait(type, requestId) { return new Promise((resolve, reject) => this.waiters.set(`${type}:${requestId}`, { resolve, reject })); }
|
|
451
100
|
configure(resources) { const requestId = `cfg_${randomBytes(6).toString("hex")}`; const waiting = this.wait("configured", requestId); this.socket.write(encodeHerdrPiFrame({ protocol: HERDR_PI_PROTOCOL, runId: this.runId, type: "configure", requestId, resources })); return waiting; }
|
|
452
101
|
supervisorDelivered(requestId) { this.socket.write(encodeHerdrPiFrame({ protocol: HERDR_PI_PROTOCOL, runId: this.runId, type: "supervisor-delivered", requestId })); }
|
|
453
|
-
prepare(requestId, operation, text, supervisorId) { const waiting = this.wait("prepared", requestId); this.socket.write(encodeHerdrPiFrame({ protocol: HERDR_PI_PROTOCOL, runId: this.runId, type: "prepare", requestId, operation, ...(text !== undefined ? { text } : {}), ...(supervisorId ? { supervisorId } : {}) })); return waiting; }
|
|
102
|
+
prepare(requestId, operation, text, supervisorId, supervisorError) { const waiting = this.wait("prepared", requestId); this.socket.write(encodeHerdrPiFrame({ protocol: HERDR_PI_PROTOCOL, runId: this.runId, type: "prepare", requestId, operation, ...(text !== undefined ? { text } : {}), ...(supervisorId ? { supervisorId } : {}), ...(supervisorError !== undefined ? { error: supervisorError } : {}) })); return waiting; }
|
|
454
103
|
close() { this.socket.destroy(); }
|
|
455
104
|
}
|
|
456
105
|
function waitForReadyBridgeFrame(bridge, timeoutMs, timeoutMessage, configured = false) {
|
|
@@ -475,7 +124,6 @@ export class HerdrPiSession {
|
|
|
475
124
|
#initialGit;
|
|
476
125
|
#finalGit;
|
|
477
126
|
#operations = new Map();
|
|
478
|
-
#snapshot;
|
|
479
127
|
bridgeForward;
|
|
480
128
|
bridge;
|
|
481
129
|
requestedTools;
|
|
@@ -488,18 +136,19 @@ export class HerdrPiSession {
|
|
|
488
136
|
supervisorDir;
|
|
489
137
|
runtime;
|
|
490
138
|
owner;
|
|
491
|
-
constructor(owner, bridgeForward, bridge, requestedTools, model, runtime, initialGit) { this.owner = owner; this.bridgeForward = bridgeForward; this.bridge = bridge; this.requestedTools = requestedTools; this.model = model; this.runtime = runtime; this.supervisorDir = runtime.supervisorChannelDir; this.#
|
|
139
|
+
constructor(owner, bridgeForward, bridge, requestedTools, model, runtime, initialGit) { this.owner = owner; this.bridgeForward = bridgeForward; this.bridge = bridge; this.requestedTools = requestedTools; this.model = model; this.runtime = runtime; this.supervisorDir = runtime.supervisorChannelDir; this.#initialGit = initialGit; bridge.adopt((frame) => this.#frame(frame)); if (this.supervisorDir) {
|
|
492
140
|
this.#replyTimer = setInterval(() => { const replies = path.join(this.supervisorDir, "replies"); for (const name of fs.existsSync(replies) ? fs.readdirSync(replies) : []) {
|
|
493
141
|
const file = path.join(replies, name);
|
|
494
142
|
try {
|
|
495
143
|
const value = JSON.parse(fs.readFileSync(file, "utf8"));
|
|
496
144
|
if (typeof value.message === "string" && typeof value.requestId === "string")
|
|
497
|
-
void this.#operation("supervisor-reply", value.message, value.requestId).then(() => fs.rmSync(file, { force: true }));
|
|
145
|
+
void this.#operation("supervisor-reply", value.message, value.requestId, typeof value.error === "string" ? value.error : undefined).then(() => fs.rmSync(file, { force: true }));
|
|
498
146
|
}
|
|
499
147
|
catch { }
|
|
500
148
|
} }, 50);
|
|
501
149
|
this.#replyTimer.unref?.();
|
|
502
150
|
} }
|
|
151
|
+
get host() { return this.owner.host; }
|
|
503
152
|
get identity() { return this.owner.identity; }
|
|
504
153
|
set identity(value) { this.owner.identity = value; }
|
|
505
154
|
get connection() { return this.owner.connection; }
|
|
@@ -508,7 +157,7 @@ export class HerdrPiSession {
|
|
|
508
157
|
void this.reconnect(); }); }
|
|
509
158
|
get evidenceCursor() { return this.#evidenceCursor; }
|
|
510
159
|
async reconnect() { if (this.#disposed || !this.#reconnectFactory || this.#reconnectFailure)
|
|
511
|
-
return; this.#
|
|
160
|
+
return; this.#reconnecting ??= this.#reconnectFactory().catch((error) => { const unknown = this.#reconnectFailure = new Error(`Remote Pi reconciliation failed; task state is unknown: ${error instanceof Error ? error.message : String(error)}`); for (const operation of this.#operations.values())
|
|
512
161
|
if (!operation.settled) {
|
|
513
162
|
operation.settled = true;
|
|
514
163
|
operation.reject(unknown);
|
|
@@ -525,12 +174,10 @@ export class HerdrPiSession {
|
|
|
525
174
|
if (!operation.accepted && !operation.settled) {
|
|
526
175
|
operation.settled = true;
|
|
527
176
|
operation.reject(new Error("Remote operation was not accepted before transport loss; task state is unknown."));
|
|
528
|
-
} this.#evidenceCursor = Math.max(this.#evidenceCursor, input.cursor);
|
|
177
|
+
} this.#evidenceCursor = Math.max(this.#evidenceCursor, input.cursor); const ownerReplacement = this.owner.replaceConnection({ connection: input.connection, unsubscribe: input.unsubscribe, paneId: input.paneId, state: input.state }); old.bridge.close(); await old.bridgeForward.close(); await ownerReplacement; if (this.#disposed) {
|
|
529
178
|
await this.owner.cleanup(this.#aborted);
|
|
530
179
|
return;
|
|
531
180
|
} this.armReconnect(this.#reconnectFactory); }
|
|
532
|
-
observeHerdr(value) { const data = value && typeof value === "object" ? value.data : undefined; if (!data || (data.terminal_id !== this.identity.terminalId && data.pane_id !== this.identity.paneId))
|
|
533
|
-
return; this.#snapshot = { ...this.#snapshot, revision: this.#snapshot.revision + 1, state: herdrRunState(data.agent_status, this.#snapshot.state) }; }
|
|
534
181
|
#frame(frame) { if (typeof frame.cursor === "number") {
|
|
535
182
|
if (frame.cursor <= this.#evidenceCursor)
|
|
536
183
|
return;
|
|
@@ -581,10 +228,10 @@ export class HerdrPiSession {
|
|
|
581
228
|
throw new Error("Supervisor relay identity changed during replay.");
|
|
582
229
|
} if (reason === "progress_update")
|
|
583
230
|
this.bridge.supervisorDelivered(frame.requestId); }
|
|
584
|
-
async #operation(operation, text, supervisorId) { const requestId = `${++this.#sequence}_${randomBytes(5).toString("hex")}`; let completion = Promise.resolve(); if (operation === "prompt")
|
|
231
|
+
async #operation(operation, text, supervisorId, supervisorError) { const requestId = `${++this.#sequence}_${randomBytes(5).toString("hex")}`; let completion = Promise.resolve(); if (operation === "prompt")
|
|
585
232
|
completion = new Promise((resolve, reject) => this.#operations.set(requestId, { resolve, reject, accepted: false, settled: false })); try {
|
|
586
|
-
await Promise.race([this.bridge.prepare(requestId, operation, text, supervisorId), this.bridge.failure]);
|
|
587
|
-
await this.
|
|
233
|
+
await Promise.race([this.bridge.prepare(requestId, operation, text, supervisorId, supervisorError), this.bridge.failure]);
|
|
234
|
+
await this.host.prompt(this.connection, this.identity, `/pi-subagents-bridge ${requestId}`);
|
|
588
235
|
await completion;
|
|
589
236
|
}
|
|
590
237
|
catch (error) {
|
|
@@ -609,16 +256,16 @@ export class HerdrPiSession {
|
|
|
609
256
|
async abort() { this.#aborted = true; await this.#operation("abort"); }
|
|
610
257
|
async dispose() { if (this.#disposed)
|
|
611
258
|
return; this.#disposed = true; if (this.#replyTimer)
|
|
612
|
-
clearInterval(this.#replyTimer); this.bridge.close(); await this.bridgeForward.close(); await this.owner.cleanup(this.#aborted);
|
|
259
|
+
clearInterval(this.#replyTimer); this.bridge.close(); await this.bridgeForward.close(); await this.owner.cleanup(this.#aborted); }
|
|
613
260
|
get messages() { return this.#messages; }
|
|
614
261
|
get sessionFile() { return undefined; }
|
|
615
262
|
get sessionId() { return this.identity.nativeSessionId; }
|
|
616
263
|
get modelId() { return this.model; }
|
|
617
|
-
get machineEvidence() { return { machineId: this.identity.machineId, ...(this.#initialGit ? { initial: this.#initialGit } : {}), ...(this.#finalGit ? { final: this.#finalGit } : {}) }; }
|
|
264
|
+
get machineEvidence() { return { provider: this.owner.machine.provider, machineId: this.identity.machineId, ...(this.#initialGit ? { initial: this.#initialGit } : {}), ...(this.#finalGit ? { final: this.#finalGit } : {}) }; }
|
|
618
265
|
get placementSnapshot() { return this.owner.snapshot; }
|
|
619
266
|
}
|
|
620
267
|
export async function reconnectHerdrPiSession(session, launch, dependencies = {}) {
|
|
621
|
-
const connect = dependencies.connect ??
|
|
268
|
+
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));
|
|
622
269
|
let validated;
|
|
623
270
|
const result = await boundedHerdrReconnect(session.identity, session.evidenceCursor, async () => {
|
|
624
271
|
const connection = await connect(launch.machine);
|
|
@@ -627,17 +274,16 @@ export async function reconnectHerdrPiSession(session, launch, dependencies = {}
|
|
|
627
274
|
let bridge;
|
|
628
275
|
const generation = { adopted: false };
|
|
629
276
|
try {
|
|
630
|
-
const
|
|
631
|
-
const
|
|
632
|
-
const manifest = await discoverManifest(launch.machine, session.identity.runId, session.identity.runtimeDir);
|
|
277
|
+
const panes = await host.panes(connection, session.identity);
|
|
278
|
+
const manifest = await discoverManifest(session.identity.runId, session.identity.runtimeDir);
|
|
633
279
|
bridgeForward = await connection.forwardRemoteSocket(manifest.socketPath, "bridge-reconnect");
|
|
634
280
|
bridge = createBridge(bridgeForward.socketPath, session.identity.runId);
|
|
635
281
|
const ready = await waitForReadyBridgeFrame(bridge, 8_000, "Reconnected bridge handshake timed out.", true);
|
|
636
|
-
const candidate = {
|
|
637
|
-
const check = reconcileHerdrReconnect(session.identity, candidate, session.evidenceCursor);
|
|
282
|
+
const candidate = { ...panes, bridge: { runId: ready.runId, nativeSessionId: ready.nativeSessionId, packageVersion: ready.packageVersion, protocol: ready.protocol, evidenceCursor: ready.evidenceCursor, evidenceFloor: ready.evidenceFloor } };
|
|
283
|
+
const check = reconcileHerdrReconnect(session.identity, candidate, session.evidenceCursor, host);
|
|
638
284
|
if (!check.ok)
|
|
639
285
|
throw new Error(check.reason);
|
|
640
|
-
unsubscribe = await
|
|
286
|
+
unsubscribe = await host.subscribe(connection, check.paneId, (event) => session.owner.observeEvent(event), (error) => { if (!generation.adopted) {
|
|
641
287
|
generation.lost ??= error;
|
|
642
288
|
return;
|
|
643
289
|
} session.owner.markConnectionUnknown(connection); void session.reconnect(); });
|
|
@@ -653,7 +299,7 @@ export async function reconnectHerdrPiSession(session, launch, dependencies = {}
|
|
|
653
299
|
await connection.close();
|
|
654
300
|
throw error;
|
|
655
301
|
}
|
|
656
|
-
}, { attempts: 3, deadlineMs: 15_000, disposed: () => session.placementSnapshot.connection === "closed" });
|
|
302
|
+
}, { attempts: 3, deadlineMs: 15_000, disposed: () => session.placementSnapshot.connection === "closed", host });
|
|
657
303
|
if (!result.ok)
|
|
658
304
|
throw new Error(`Pane-native reconnect remains unknown: ${result.reason}`);
|
|
659
305
|
if (!validated)
|
|
@@ -671,26 +317,33 @@ export async function reconnectHerdrPiSession(session, launch, dependencies = {}
|
|
|
671
317
|
export async function createHerdrPiSession(launch) {
|
|
672
318
|
if (!launch.machine)
|
|
673
319
|
throw new Error("Pane-native Pi launch requires a saved machine.");
|
|
320
|
+
if (launch.machine.provider !== "herdr")
|
|
321
|
+
throw new Error("Herdr pane-native Pi launch requires a Herdr saved machine.");
|
|
674
322
|
const policy = serializeHerdrPiLaunch(launch);
|
|
675
323
|
const runId = safeRunId(launch.runtime.runId);
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
324
|
+
return startPlacedPiSession(launch, await HerdrPlacedRunOwner.create(launch.machine, runId), policy);
|
|
325
|
+
}
|
|
326
|
+
/** A Rex machine: the same bridge session, with the Rex pane host and local (`machine: "local"`) or SSH machine access. */
|
|
327
|
+
export async function createRexPiSession(launch) {
|
|
328
|
+
if (launch.machine?.provider !== "rex")
|
|
329
|
+
throw new Error("Rex pane-native Pi launch requires a Rex machine.");
|
|
330
|
+
const policy = serializeHerdrPiLaunch(launch);
|
|
331
|
+
const runId = safeRunId(launch.runtime.runId);
|
|
332
|
+
return startPlacedPiSession(launch, await RexPlacedRunOwner.create(launch.machine, runId), policy);
|
|
333
|
+
}
|
|
334
|
+
async function startPlacedPiSession(launch, owner, policy) {
|
|
335
|
+
const machine = owner.machine, runId = owner.runId, runtimeDir = owner.runtimeDir, connection = owner.connection;
|
|
681
336
|
try {
|
|
682
337
|
let reconnectSession;
|
|
683
|
-
|
|
684
|
-
await owner.
|
|
685
|
-
|
|
686
|
-
const { terminalId } = await owner.start("pi", agentName, policy.args);
|
|
687
|
-
const manifest = await discoverBridgeManifest(launch.machine, runId, runtimeDir);
|
|
338
|
+
const agentName = `pi-${runId.slice(-20)}`;
|
|
339
|
+
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(() => { }, () => void reconnectSession?.reconnect()));
|
|
340
|
+
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.");
|
|
688
341
|
if (manifest.protocol !== HERDR_PI_PROTOCOL || manifest.packageVersion !== packageJson.version)
|
|
689
342
|
throw new Error(`Remote pi-subagents bridge version mismatch (remote ${manifest.packageVersion}, local ${packageJson.version}).`);
|
|
690
343
|
const bridgeForward = await connection.forwardRemoteSocket(manifest.socketPath, "bridge");
|
|
691
344
|
const bridge = new BridgeChannel(bridgeForward.socketPath, runId);
|
|
692
345
|
const ready = await waitForReadyBridgeFrame(bridge, 15_000, "Remote Pi bridge handshake timed out.");
|
|
693
|
-
if (ready.packageVersion !== packageJson.version || ready.nativeSessionId !== manifest.nativeSessionId || ready.cwd !==
|
|
346
|
+
if (ready.packageVersion !== packageJson.version || ready.nativeSessionId !== manifest.nativeSessionId || ready.cwd !== machine.cwd)
|
|
694
347
|
throw new Error("Remote Pi bridge handshake identity mismatch.");
|
|
695
348
|
const configured = await Promise.race([bridge.configure(policy.resources), bridge.failure]);
|
|
696
349
|
if (configured.nativeSessionId !== manifest.nativeSessionId || configured.agent !== policy.resources.agent)
|
|
@@ -699,11 +352,10 @@ export async function createHerdrPiSession(launch) {
|
|
|
699
352
|
throw new Error("Remote Pi active-tool ceiling acknowledgement mismatch.");
|
|
700
353
|
if (launch.model && configured.model !== launch.model.split(":")[0])
|
|
701
354
|
throw new Error(`Remote Pi model '${launch.model}' did not resolve to the requested provider/model.`);
|
|
702
|
-
const identity = { runId, machineId:
|
|
355
|
+
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 };
|
|
703
356
|
owner.journal(identity);
|
|
704
357
|
const session = new HerdrPiSession(owner, bridgeForward, bridge, configured.tools, typeof ready.model === "string" ? ready.model : undefined, launch.runtime, ready.initialGit);
|
|
705
358
|
reconnectSession = session;
|
|
706
|
-
observeHerdr = (event) => session.observeHerdr(event);
|
|
707
359
|
session.armReconnect(() => reconnectHerdrPiSession(session, launch));
|
|
708
360
|
return session;
|
|
709
361
|
}
|
|
@@ -712,7 +364,8 @@ export async function createHerdrPiSession(launch) {
|
|
|
712
364
|
throw error;
|
|
713
365
|
}
|
|
714
366
|
}
|
|
715
|
-
|
|
367
|
+
function createPlacedPiSession(launch) { return launch.machine?.provider === "rex" ? createRexPiSession(launch) : createHerdrPiSession(launch); }
|
|
368
|
+
export function createPlacementAwareChildSessionFactory(local, createRemote = createPlacedPiSession) {
|
|
716
369
|
const remote = new Set();
|
|
717
370
|
return { async create(launch) { if (!launch.machine)
|
|
718
371
|
return local.create(launch); serializeHerdrPiLaunch(launch); const child = await createRemote(launch); remote.add(child); return child; }, async dispose() { await Promise.allSettled([...remote].map((child) => child.dispose())); remote.clear(); await local.dispose(); } };
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import type { PlacementMachineAccess } from "./placement-host.ts";
|
|
2
|
+
/** Machine access for the parent's own machine: the same quoted `sh -c` scripts the SSH access sends, run directly through /bin/sh. */
|
|
3
|
+
export declare function localMachineAccess(options?: {
|
|
4
|
+
env?: NodeJS.ProcessEnv;
|
|
5
|
+
}): PlacementMachineAccess;
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { spawnSync } from "node:child_process";
|
|
2
|
+
import * as path from "node:path";
|
|
3
|
+
import { remoteShellCommand, runBoundedCommand } from "./herdr-connection.js";
|
|
4
|
+
const DEFAULT_TIMEOUT_MS = 15_000, DEFAULT_MAX_BUFFER = 256 * 1024;
|
|
5
|
+
/** Sockets on this machine are already reachable: forwarding returns the path unchanged and closing is a no-op. */
|
|
6
|
+
function localSockets() {
|
|
7
|
+
return {
|
|
8
|
+
async forwardRemoteSocket(socketPath) { if (!path.isAbsolute(socketPath))
|
|
9
|
+
throw new Error("Local socket path must be absolute."); return { socketPath, async close() { } }; },
|
|
10
|
+
async close() { },
|
|
11
|
+
};
|
|
12
|
+
}
|
|
13
|
+
/** Machine access for the parent's own machine: the same quoted `sh -c` scripts the SSH access sends, run directly through /bin/sh. */
|
|
14
|
+
export function localMachineAccess(options = {}) {
|
|
15
|
+
const env = options.env ?? process.env;
|
|
16
|
+
return {
|
|
17
|
+
shellCommand: remoteShellCommand,
|
|
18
|
+
run(command, limits = {}) {
|
|
19
|
+
const spawned = spawnSync("/bin/sh", ["-c", command], { encoding: "utf8", env, timeout: limits.timeout ?? DEFAULT_TIMEOUT_MS, maxBuffer: limits.maxBuffer ?? DEFAULT_MAX_BUFFER, windowsHide: true });
|
|
20
|
+
const result = { status: spawned.status, stdout: spawned.stdout ?? "", stderr: spawned.stderr ?? "" };
|
|
21
|
+
if (spawned.error)
|
|
22
|
+
result.error = spawned.error;
|
|
23
|
+
return result;
|
|
24
|
+
},
|
|
25
|
+
runAsync: (command, limits = {}) => runBoundedCommand("/bin/sh", ["-c", command], { env, ...limits }),
|
|
26
|
+
openSockets: localSockets,
|
|
27
|
+
};
|
|
28
|
+
}
|