pi-subagents 0.81.0 → 0.82.1
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 +55 -0
- package/README.md +1 -1
- package/bundle/agents.js +3 -3
- package/bundle/child-tool-plan.js +2 -2
- package/bundle/{chunk-L5ZHE4L4.js → chunk-3JCDWF4V.js} +4 -4
- package/bundle/{chunk-LSRUTHT7.js → chunk-43SCHIMD.js} +1 -1
- package/bundle/{chunk-VJHOELQP.js → chunk-46XKCPJR.js} +193 -81
- package/bundle/{chunk-G5DKFLLI.js → chunk-4SDU2BZX.js} +15 -10
- package/bundle/{chunk-2ZSNH26H.js → chunk-4Y2ROR7L.js} +7 -7
- package/bundle/{chunk-V2JWDB5U.js → chunk-52UBALJL.js} +2 -2
- package/bundle/{chunk-3DC5KBVP.js → chunk-5TYMSZX3.js} +1 -1
- package/bundle/{chunk-NNLASTQE.js → chunk-6XJB7PIX.js} +20 -6
- package/bundle/{chunk-ZBC44JJ3.js → chunk-CIZQXJ5Q.js} +22 -9
- package/bundle/{chunk-KSWQVGDW.js → chunk-FXUXCX2K.js} +225 -18
- package/bundle/{chunk-563BHAZ5.js → chunk-G4GZD3OJ.js} +35 -26
- package/bundle/{chunk-ARRKBB2B.js → chunk-GJECHSAR.js} +2 -2
- package/bundle/{chunk-V3BIUPLU.js → chunk-IVAE5Z6Q.js} +256 -52
- package/bundle/{chunk-V6MR5TOQ.js → chunk-JYUKW2SY.js} +25 -19
- package/bundle/{chunk-BJN5LUQY.js → chunk-OYF6AFDY.js} +1 -1
- package/bundle/{chunk-CEKS3TFO.js → chunk-PBK2I7WY.js} +6 -6
- package/bundle/{chunk-TUWLXTAM.js → chunk-PDZWJCHD.js} +681 -320
- package/bundle/{chunk-EYNJ6VL6.js → chunk-PLCCBLPY.js} +2 -2
- package/bundle/{chunk-XMFLCKMQ.js → chunk-QE4AGIMG.js} +339 -132
- package/bundle/{chunk-74RT5YQE.js → chunk-RMQFMMAT.js} +123 -33
- package/bundle/{chunk-WSBNENFS.js → chunk-UCPP4GNU.js} +14 -4
- package/bundle/{chunk-2XYWN7QJ.js → chunk-UTT254JE.js} +2 -2
- package/bundle/{chunk-BPS2AGBZ.js → chunk-VSUSLDNW.js} +93 -52
- package/bundle/{chunk-ZP3HODFH.js → chunk-XSWOTIA3.js} +1 -1
- package/bundle/{chunk-3IF2PGW7.js → chunk-YKEWIB4M.js} +1 -1
- package/bundle/{chunk-26C3WLYQ.js → chunk-ZIV4J4HG.js} +498 -274
- package/bundle/control-channel.js +2 -2
- package/bundle/index.js +1 -1
- package/bundle/intercom-bridge.js +2 -2
- package/bundle/preflight.js +8 -8
- package/bundle/project-panes.js +3 -3
- package/docs/agents.md +37 -7
- package/docs/configuration.md +9 -1
- package/docs/tool-reference.md +3 -1
- package/docs/workflows.md +16 -0
- package/package.json +2 -2
- package/skills/council-mode/SKILL.md +1 -4
- package/skills/pi-subagents/SKILL.md +130 -107
- package/skills/pi-subagents/references/orchestration.md +124 -0
- package/skills/pi-subagents/references/prompting-and-roles.md +96 -295
- package/src/agents/agent-management.js +3 -3
- package/src/agents/agent-memory.js +3 -1
- package/src/agents/agents.js +6 -6
- package/src/agents/frontmatter.d.ts +2 -0
- package/src/agents/frontmatter.js +15 -1
- package/src/agents/skills.js +2 -1
- package/src/extension/config.js +3 -0
- package/src/extension/herdr-pi-bridge.js +17 -10
- package/src/extension/index.js +82 -2
- package/src/extension/reply-workflow-script.js +5 -2
- package/src/extension/schemas.js +3 -3
- package/src/extension/subagent-guide.js +1 -2
- 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 +4 -0
- package/src/runs/background/async-status.js +22 -3
- package/src/runs/background/await-async-run.js +3 -7
- package/src/runs/background/boot-id.d.ts +12 -0
- package/src/runs/background/boot-id.js +47 -0
- package/src/runs/background/interrupted-elsewhere.d.ts +22 -0
- package/src/runs/background/interrupted-elsewhere.js +68 -0
- package/src/runs/background/notify.d.ts +2 -0
- package/src/runs/background/notify.js +13 -0
- package/src/runs/background/result-delivery-ownership.d.ts +6 -0
- package/src/runs/background/result-delivery-ownership.js +19 -1
- package/src/runs/background/result-watcher.d.ts +3 -1
- package/src/runs/background/result-watcher.js +24 -8
- package/src/runs/background/run-child-session.d.ts +1 -6
- package/src/runs/background/run-child-session.js +1 -1
- package/src/runs/background/run-status.js +2 -1
- package/src/runs/background/stale-run-reconciler.d.ts +25 -0
- package/src/runs/background/stale-run-reconciler.js +52 -13
- package/src/runs/background/subagent-runner-bootstrap.js +3 -0
- package/src/runs/background/subagent-runner.d.ts +4 -0
- package/src/runs/background/subagent-runner.js +13 -5
- package/src/runs/background/subagent-wait.d.ts +1 -1
- package/src/runs/background/subagent-wait.js +6 -1
- package/src/runs/background/terminal-run-index.d.ts +2 -0
- package/src/runs/background/terminal-run-index.js +44 -3
- package/src/runs/foreground/async-stop-action.js +3 -1
- package/src/runs/foreground/execution.js +6 -3
- package/src/runs/foreground/foreground-history.d.ts +16 -2
- package/src/runs/foreground/foreground-history.js +133 -31
- package/src/runs/foreground/subagent-executor.d.ts +11 -0
- package/src/runs/foreground/subagent-executor.js +277 -74
- package/src/runs/shared/child-launch.d.ts +2 -2
- package/src/runs/shared/child-session.d.ts +3 -2
- package/src/runs/shared/child-session.js +51 -8
- package/src/runs/shared/child-tool-plan.d.ts +7 -0
- package/src/runs/shared/child-tool-plan.js +21 -6
- package/src/runs/shared/herdr-connection.d.ts +17 -8
- package/src/runs/shared/herdr-connection.js +19 -7
- package/src/runs/shared/herdr-external-adapters.d.ts +3 -2
- package/src/runs/shared/herdr-external-adapters.js +1 -1
- package/src/runs/shared/herdr-machine.d.ts +12 -2
- package/src/runs/shared/herdr-machine.js +55 -9
- package/src/runs/shared/herdr-pane-host.d.ts +88 -0
- package/src/runs/shared/herdr-pane-host.js +155 -0
- package/src/runs/shared/herdr-placed-run.d.ts +27 -150
- package/src/runs/shared/herdr-placed-run.js +48 -395
- package/src/runs/shared/local-machine-access.d.ts +5 -0
- package/src/runs/shared/local-machine-access.js +28 -0
- package/src/runs/shared/orca-progress-tabs.d.ts +0 -1
- package/src/runs/shared/orca-progress-tabs.js +1 -24
- package/src/runs/shared/parallel-utils.d.ts +1 -1
- package/src/runs/shared/placed-run-owner.d.ts +49 -0
- package/src/runs/shared/placed-run-owner.js +93 -0
- package/src/runs/shared/placement-host.d.ts +99 -0
- package/src/runs/shared/placement-host.js +157 -0
- package/src/runs/shared/rex-machine.d.ts +25 -0
- package/src/runs/shared/rex-machine.js +83 -0
- package/src/runs/shared/rex-pane-host.d.ts +62 -0
- package/src/runs/shared/rex-pane-host.js +327 -0
- package/src/shared/completion-owner.d.ts +5 -0
- package/src/shared/completion-owner.js +5 -0
- package/src/shared/session-identity.d.ts +2 -0
- package/src/shared/session-identity.js +26 -0
- package/src/shared/types.d.ts +45 -6
- package/src/tui/fleet.d.ts +1 -0
- package/src/tui/fleet.js +25 -15
- package/src/tui/render.d.ts +6 -0
- package/src/tui/render.js +3 -3
- package/src/tui/subagent-messages.js +1 -0
- package/src/workflows/scripted-workflow.d.ts +1 -1
- package/src/workflows/workflow-auto-resume.d.ts +32 -0
- package/src/workflows/workflow-auto-resume.js +102 -0
- package/src/workflows/workflow-launch-record.d.ts +27 -0
- package/src/workflows/workflow-launch-record.js +48 -0
- package/src/workflows/workflow-resume-claim.d.ts +82 -0
- package/src/workflows/workflow-resume-claim.js +210 -0
- package/src/workflows/workflow-resume.d.ts +56 -0
- package/src/workflows/workflow-resume.js +106 -0
- package/src/workflows/workflow-reuse.d.ts +15 -9
- package/src/workflows/workflow-reuse.js +77 -14
- package/src/workflows/workflow-revival.d.ts +2 -0
- package/src/workflows/workflow-revival.js +2 -1
- package/src/workflows/workflow-stop-cause.d.ts +6 -0
- package/src/workflows/workflow-stop-cause.js +8 -0
- package/skills/pi-subagents/references/constraints-and-recipes.md +0 -74
- package/skills/pi-subagents/references/execution-controls.md +0 -563
- package/skills/pi-subagents/references/management-authoring-rpc.md +0 -165
- package/skills/pi-subagents/references/multi-lane-orchestration.md +0 -61
- package/skills/pi-subagents/references/review-and-validation.md +0 -73
|
@@ -19,7 +19,6 @@ export interface OrcaProgressTab {
|
|
|
19
19
|
finish(status: "completed" | "failed" | "stopped", sessionFile?: string): Promise<void>;
|
|
20
20
|
}
|
|
21
21
|
export declare function resolveOrcaCommand(env?: NodeJS.ProcessEnv): string | undefined;
|
|
22
|
-
export declare function resolvePiSessionId(sessionFile: string | undefined): string | undefined;
|
|
23
22
|
export declare function createOrcaProgressTab(input: {
|
|
24
23
|
cwd: string;
|
|
25
24
|
runId: string;
|
|
@@ -4,6 +4,7 @@ import * as path from "node:path";
|
|
|
4
4
|
import { createHash, randomUUID } from "node:crypto";
|
|
5
5
|
import { resolveNodeExecutable } from "../../shared/node-executable.js";
|
|
6
6
|
import { getProjectSubagentsDir } from "../../shared/artifacts.js";
|
|
7
|
+
import { resolvePiSessionId } from "../../shared/session-identity.js";
|
|
7
8
|
import { TEMP_ROOT_DIR } from "../../shared/types.js";
|
|
8
9
|
import { extractTextFromContent, extractToolArgsPreview, getAgentDir } from "../../shared/utils.js";
|
|
9
10
|
const ORCA_CREATE_TIMEOUT_MS = 20_000;
|
|
@@ -257,30 +258,6 @@ function reserveTabSequence(root, cwd) {
|
|
|
257
258
|
}
|
|
258
259
|
});
|
|
259
260
|
}
|
|
260
|
-
export function resolvePiSessionId(sessionFile) {
|
|
261
|
-
if (!sessionFile)
|
|
262
|
-
return undefined;
|
|
263
|
-
try {
|
|
264
|
-
const fd = fs.openSync(sessionFile, "r");
|
|
265
|
-
try {
|
|
266
|
-
const buffer = Buffer.alloc(16 * 1024);
|
|
267
|
-
const bytes = fs.readSync(fd, buffer, 0, buffer.length, 0);
|
|
268
|
-
const line = buffer.toString("utf-8", 0, bytes).split("\n", 1)[0];
|
|
269
|
-
if (!line)
|
|
270
|
-
return undefined;
|
|
271
|
-
const header = JSON.parse(line);
|
|
272
|
-
if (header.type === "session" && typeof header.id === "string" && /^[A-Za-z0-9-]{8,}$/.test(header.id))
|
|
273
|
-
return header.id;
|
|
274
|
-
return undefined;
|
|
275
|
-
}
|
|
276
|
-
finally {
|
|
277
|
-
fs.closeSync(fd);
|
|
278
|
-
}
|
|
279
|
-
}
|
|
280
|
-
catch {
|
|
281
|
-
return undefined;
|
|
282
|
-
}
|
|
283
|
-
}
|
|
284
261
|
function scheduleCleanup(nodeExecutable, paths) {
|
|
285
262
|
try {
|
|
286
263
|
const watchdog = spawn(nodeExecutable, ["-e", ORCA_CLEANUP_WATCHDOG_SCRIPT, String(CLEANUP_DELAY_MS), ...paths], {
|
|
@@ -12,7 +12,7 @@ export interface RunnerSubagentStep {
|
|
|
12
12
|
task: string;
|
|
13
13
|
runner?: ResolvedRunnerConfig;
|
|
14
14
|
/** Herdr saved machine this external-cli step runs on; `cwd` is then the directory on that machine. */
|
|
15
|
-
machine?: import("../../shared/types.ts").
|
|
15
|
+
machine?: import("../../shared/types.ts").PlacementMachineReference;
|
|
16
16
|
remoteReads?: string[] | false;
|
|
17
17
|
machineEnv?: Record<string, string>;
|
|
18
18
|
externalJobFollowUp?: {
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import type { PlacementMachineReference } from "../../shared/types.ts";
|
|
2
|
+
import type { HerdrRunIdentity, HerdrRunSnapshot } from "./herdr-placed-run.ts";
|
|
3
|
+
import type { PlacementCommandOptions, PlacementLaunchRequest, PlacementMachineAccess, PlacementOwnedPane, PlacementPaneHost, PlacementSockets, PlacementStartOptions } from "./placement-host.ts";
|
|
4
|
+
export interface PlacedBridgeManifest {
|
|
5
|
+
socketPath: string;
|
|
6
|
+
nativeSessionId: string;
|
|
7
|
+
packageVersion: string;
|
|
8
|
+
protocol: number;
|
|
9
|
+
}
|
|
10
|
+
/** Prints `$1/manifest.json` when it is a regular file, never through a symlink. */
|
|
11
|
+
export declare const PLACED_MANIFEST_READ_SCRIPT = "p=\"$1/manifest.json\"; test -f \"$p\" && test ! -L \"$p\" && cat \"$p\"";
|
|
12
|
+
export declare function discoverPlacedBridgeManifest(access: PlacementMachineAccess, runId: string, runtimeDir: string, missing?: string): Promise<PlacedBridgeManifest>;
|
|
13
|
+
export declare function createPlacedRuntimeDir(access: PlacementMachineAccess, runId: string): Promise<string>;
|
|
14
|
+
export declare function removePlacedRuntimeDir(access: PlacementMachineAccess, runtimeDir: string): void;
|
|
15
|
+
/** The single private owner for allocation shared by every pane-native backend projection. */
|
|
16
|
+
export declare class PlacedRunOwner<C extends PlacementSockets, M extends PlacementMachineReference = PlacementMachineReference> {
|
|
17
|
+
#private;
|
|
18
|
+
readonly machine: M;
|
|
19
|
+
readonly runId: string;
|
|
20
|
+
readonly runtimeDir: string;
|
|
21
|
+
connection: C;
|
|
22
|
+
readonly host: PlacementPaneHost<C>;
|
|
23
|
+
readonly access: PlacementMachineAccess;
|
|
24
|
+
owned?: PlacementOwnedPane;
|
|
25
|
+
terminalId?: string;
|
|
26
|
+
agentName?: string;
|
|
27
|
+
startedArgv?: string[];
|
|
28
|
+
identity?: HerdrRunIdentity;
|
|
29
|
+
protected constructor(machine: M, runId: string, runtimeDir: string, connection: C, host: PlacementPaneHost<C>, access: PlacementMachineAccess, removeRuntime?: () => void);
|
|
30
|
+
observeEvent(event: unknown): void;
|
|
31
|
+
markConnectionUnknown(observed: C): boolean;
|
|
32
|
+
subscribe(listener: (event: unknown) => void, disconnected: (error: Error) => void): Promise<void>;
|
|
33
|
+
/** Create the owned pane and start the agent in one host step; onPane runs once the pane exists and before the agent starts. */
|
|
34
|
+
launch(request: PlacementLaunchRequest, onPane: (owned: PlacementOwnedPane) => Promise<void>, options?: PlacementStartOptions): Promise<{
|
|
35
|
+
owned: PlacementOwnedPane;
|
|
36
|
+
terminalId: string;
|
|
37
|
+
argv: string[];
|
|
38
|
+
}>;
|
|
39
|
+
journal(identity: HerdrRunIdentity): void;
|
|
40
|
+
get snapshot(): HerdrRunSnapshot;
|
|
41
|
+
runRemote(command: string, options?: PlacementCommandOptions): import("./placement-host.ts").PlacementCommandResult;
|
|
42
|
+
replaceConnection(input: {
|
|
43
|
+
connection: C;
|
|
44
|
+
unsubscribe: () => void;
|
|
45
|
+
paneId: string;
|
|
46
|
+
state: HerdrRunSnapshot["state"];
|
|
47
|
+
}): Promise<void>;
|
|
48
|
+
cleanup(closeStartedPane?: boolean): Promise<void>;
|
|
49
|
+
}
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import * as fs from "node:fs";
|
|
2
|
+
import * as path from "node:path";
|
|
3
|
+
import { getAgentDir } from "../../shared/utils.js";
|
|
4
|
+
/** Prints `$1/manifest.json` when it is a regular file, never through a symlink. */
|
|
5
|
+
export const PLACED_MANIFEST_READ_SCRIPT = 'p="$1/manifest.json"; test -f "$p" && test ! -L "$p" && cat "$p"';
|
|
6
|
+
export 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.") {
|
|
7
|
+
for (let attempt = 0; attempt < 100; attempt++) {
|
|
8
|
+
const result = await access.runAsync(access.shellCommand(PLACED_MANIFEST_READ_SCRIPT, [runtimeDir]), { timeout: 5_000, maxBuffer: 64 * 1024 });
|
|
9
|
+
if (result.error)
|
|
10
|
+
throw new Error(`Remote bridge manifest discovery failed: ${result.error.message}`, { cause: result.error });
|
|
11
|
+
if (result.status === 0 && result.stdout.trim()) {
|
|
12
|
+
let value;
|
|
13
|
+
try {
|
|
14
|
+
value = JSON.parse(result.stdout);
|
|
15
|
+
}
|
|
16
|
+
catch {
|
|
17
|
+
throw new Error("Remote bridge manifest is malformed.");
|
|
18
|
+
}
|
|
19
|
+
const p = value;
|
|
20
|
+
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")
|
|
21
|
+
throw new Error("Remote bridge manifest identity is incomplete or outside its owned runtime directory.");
|
|
22
|
+
return { socketPath: p.socketPath, nativeSessionId: p.nativeSessionId, packageVersion: p.packageVersion, protocol: p.protocol };
|
|
23
|
+
}
|
|
24
|
+
await new Promise((resolve) => setTimeout(resolve, 50));
|
|
25
|
+
}
|
|
26
|
+
throw new Error(missing);
|
|
27
|
+
}
|
|
28
|
+
export async function createPlacedRuntimeDir(access, runId) { const result = await access.runAsync(access.shellCommand("umask 077; mktemp -d \"${TMPDIR:-/tmp}/pi-subagents-herdr-$1-XXXXXXXX\"", [runId]), { timeout: 10_000, maxBuffer: 4096 }); if (result.error)
|
|
29
|
+
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"))
|
|
30
|
+
throw new Error("Could not provision a run-private remote bridge runtime directory."); return value; }
|
|
31
|
+
export function removePlacedRuntimeDir(access, runtimeDir) { 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: 10_000, maxBuffer: 4096 }); if (result.status !== 0 || result.stdout)
|
|
32
|
+
throw new Error(`Could not remove the exact owned remote runtime directory: ${String(result.stderr).slice(0, 512)}`); }
|
|
33
|
+
/** The single private owner for allocation shared by every pane-native backend projection. */
|
|
34
|
+
export class PlacedRunOwner {
|
|
35
|
+
machine;
|
|
36
|
+
runId;
|
|
37
|
+
runtimeDir;
|
|
38
|
+
connection;
|
|
39
|
+
host;
|
|
40
|
+
access;
|
|
41
|
+
owned;
|
|
42
|
+
terminalId;
|
|
43
|
+
agentName;
|
|
44
|
+
startedArgv;
|
|
45
|
+
identity;
|
|
46
|
+
#unsubscribe = () => { };
|
|
47
|
+
#snapshot = { connection: "connected", state: "ready", revision: 0 };
|
|
48
|
+
#cleanupPromise;
|
|
49
|
+
#removeRuntime;
|
|
50
|
+
constructor(machine, runId, runtimeDir, connection, host, access, removeRuntime = () => removePlacedRuntimeDir(access, runtimeDir)) { this.machine = machine; this.runId = runId; this.runtimeDir = runtimeDir; this.connection = connection; this.#removeRuntime = removeRuntime; this.host = host; this.access = access; }
|
|
51
|
+
observeEvent(event) { const data = this.host.paneEvent(event, this.#snapshot.state); if (data && (!this.terminalId || data.terminalId === this.terminalId || data.paneId === this.owned?.paneId))
|
|
52
|
+
this.#snapshot = { ...this.#snapshot, revision: this.#snapshot.revision + 1, state: data.state }; }
|
|
53
|
+
markConnectionUnknown(observed) { if (this.connection !== observed)
|
|
54
|
+
return false; this.#snapshot = { ...this.#snapshot, connection: "unknown", state: "unknown", revision: this.#snapshot.revision + 1 }; return true; }
|
|
55
|
+
async subscribe(listener, disconnected) { const observed = this.connection, paneId = this.owned?.paneId; let adopted = false, candidateLost; if (!paneId)
|
|
56
|
+
throw new Error("Herdr status subscription requires a provisioned pane."); const unsubscribe = await this.host.subscribe(observed, paneId, (event) => { this.observeEvent(event); listener(event); }, (error) => { if (!adopted) {
|
|
57
|
+
candidateLost ??= error;
|
|
58
|
+
return;
|
|
59
|
+
} if (!this.markConnectionUnknown(observed))
|
|
60
|
+
return; disconnected(error); }); if (candidateLost) {
|
|
61
|
+
unsubscribe();
|
|
62
|
+
throw candidateLost;
|
|
63
|
+
} this.#unsubscribe = unsubscribe; adopted = true; }
|
|
64
|
+
/** Create the owned pane and start the agent in one host step; onPane runs once the pane exists and before the agent starts. */
|
|
65
|
+
async launch(request, onPane, options = {}) {
|
|
66
|
+
const { owned, terminalId } = await this.host.launch(this.connection, this.machine, this.runId, this.runtimeDir, request, async (pane) => { this.owned = pane; await onPane(pane); }, options), argv = [request.kind, ...request.args];
|
|
67
|
+
this.agentName = request.name;
|
|
68
|
+
this.terminalId = terminalId;
|
|
69
|
+
this.startedArgv = argv;
|
|
70
|
+
return { owned, terminalId, argv };
|
|
71
|
+
}
|
|
72
|
+
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" }); }
|
|
73
|
+
get snapshot() { return this.#snapshot; }
|
|
74
|
+
runRemote(command, options = {}) { return this.access.run(command, options); }
|
|
75
|
+
async replaceConnection(input) { const oldConnection = this.connection, oldUnsubscribe = this.#unsubscribe; this.connection = input.connection; this.#unsubscribe = input.unsubscribe; if (this.identity)
|
|
76
|
+
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(); }
|
|
77
|
+
cleanup(closeStartedPane = true) { return this.#cleanupPromise ??= this.#performCleanup(closeStartedPane); }
|
|
78
|
+
async #performCleanup(closeStartedPane) { let failure; try {
|
|
79
|
+
if (closeStartedPane && this.owned)
|
|
80
|
+
await this.host.closeOwnedPane(this.connection, this.owned, this.terminalId, this.agentName);
|
|
81
|
+
}
|
|
82
|
+
catch (error) {
|
|
83
|
+
failure = error;
|
|
84
|
+
} try {
|
|
85
|
+
this.#unsubscribe();
|
|
86
|
+
await this.connection.close();
|
|
87
|
+
this.#removeRuntime();
|
|
88
|
+
}
|
|
89
|
+
catch (error) {
|
|
90
|
+
failure ??= error;
|
|
91
|
+
} this.#snapshot = { ...this.#snapshot, connection: "closed", revision: this.#snapshot.revision + 1 }; if (failure)
|
|
92
|
+
throw failure; }
|
|
93
|
+
}
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import type { PlacementMachineReference } from "../../shared/types.ts";
|
|
2
|
+
import type { HerdrRunIdentity } from "./herdr-placed-run.ts";
|
|
3
|
+
export type PlacementRunState = "ready" | "working" | "blocked" | "settled" | "unknown";
|
|
4
|
+
export type PlacementAgentKind = "pi" | "claude" | "cursor" | "codex";
|
|
5
|
+
export interface PlacementOwnedPane {
|
|
6
|
+
workspaceId: string;
|
|
7
|
+
tabId: string;
|
|
8
|
+
paneId: string;
|
|
9
|
+
}
|
|
10
|
+
export interface PlacementBridgeIdentity {
|
|
11
|
+
runId?: string;
|
|
12
|
+
nativeSessionId?: string;
|
|
13
|
+
packageVersion?: string;
|
|
14
|
+
protocol?: number;
|
|
15
|
+
evidenceCursor?: number;
|
|
16
|
+
evidenceFloor?: number;
|
|
17
|
+
}
|
|
18
|
+
export type PlacementEvidenceProof = {
|
|
19
|
+
ok: true;
|
|
20
|
+
cursor: number;
|
|
21
|
+
} | {
|
|
22
|
+
ok: false;
|
|
23
|
+
reason: string;
|
|
24
|
+
};
|
|
25
|
+
export type PlacementReconnectCheck = {
|
|
26
|
+
ok: true;
|
|
27
|
+
state: PlacementRunState;
|
|
28
|
+
paneId: string;
|
|
29
|
+
cursor: number;
|
|
30
|
+
} | {
|
|
31
|
+
ok: false;
|
|
32
|
+
reason: string;
|
|
33
|
+
};
|
|
34
|
+
export interface PlacementStartOptions {
|
|
35
|
+
timeoutMs?: number;
|
|
36
|
+
pollMs?: number;
|
|
37
|
+
clock?: {
|
|
38
|
+
now(): number;
|
|
39
|
+
wait(ms: number): Promise<void>;
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
/** What to start in a new pane; the pane is created in the machine's cwd with this environment. */
|
|
43
|
+
export interface PlacementLaunchRequest {
|
|
44
|
+
kind: PlacementAgentKind;
|
|
45
|
+
name: string;
|
|
46
|
+
args: string[];
|
|
47
|
+
environment?: Record<string, string>;
|
|
48
|
+
}
|
|
49
|
+
export interface PlacementCommandOptions {
|
|
50
|
+
timeout?: number;
|
|
51
|
+
maxBuffer?: number;
|
|
52
|
+
}
|
|
53
|
+
export interface PlacementCommandResult {
|
|
54
|
+
status: number | null;
|
|
55
|
+
stdout: string;
|
|
56
|
+
stderr: string;
|
|
57
|
+
error?: Error;
|
|
58
|
+
}
|
|
59
|
+
export interface PlacementSocketForward {
|
|
60
|
+
socketPath: string;
|
|
61
|
+
close(): Promise<void>;
|
|
62
|
+
}
|
|
63
|
+
/** Unix sockets on the pane machine made reachable from the parent; close() ends every forward opened through it. */
|
|
64
|
+
export interface PlacementSockets {
|
|
65
|
+
forwardRemoteSocket(remotePath: string, name: string): Promise<PlacementSocketForward>;
|
|
66
|
+
close(): Promise<void>;
|
|
67
|
+
}
|
|
68
|
+
/** Runs commands on, and reaches unix sockets on, the machine that hosts the pane: over SSH (`sshMachineAccess`) or directly (`localMachineAccess`). */
|
|
69
|
+
export interface PlacementMachineAccess {
|
|
70
|
+
shellCommand(script: string, args?: string[]): string;
|
|
71
|
+
run(command: string, options?: PlacementCommandOptions): PlacementCommandResult;
|
|
72
|
+
runAsync(command: string, options?: PlacementCommandOptions): Promise<PlacementCommandResult>;
|
|
73
|
+
openSockets(): PlacementSockets;
|
|
74
|
+
}
|
|
75
|
+
/** Every pane operation a placed native run and its owner need. The connection carries the host's socket forwarding; P is the host's reconnect view of its panes. */
|
|
76
|
+
export interface PlacementPaneHost<C extends PlacementSockets = PlacementSockets, P extends object = object> {
|
|
77
|
+
connect(machine: PlacementMachineReference): Promise<C>;
|
|
78
|
+
sessionOf(connection: C): string | null;
|
|
79
|
+
/** Create the pane and start the agent in it; call onPane after the pane exists and before the agent starts. */
|
|
80
|
+
launch(connection: C, machine: PlacementMachineReference, runId: string, runtimeDir: string, request: PlacementLaunchRequest, onPane: (owned: PlacementOwnedPane) => Promise<void>, options?: PlacementStartOptions): Promise<{
|
|
81
|
+
owned: PlacementOwnedPane;
|
|
82
|
+
terminalId: string;
|
|
83
|
+
}>;
|
|
84
|
+
prompt(connection: C, identity: HerdrRunIdentity, text: string): Promise<void>;
|
|
85
|
+
subscribe(connection: C, paneId: string, listener: (event: unknown) => void, onDisconnect: (error: Error) => void): Promise<() => void>;
|
|
86
|
+
paneEvent(event: unknown, fallback: PlacementRunState): {
|
|
87
|
+
terminalId: unknown;
|
|
88
|
+
paneId: unknown;
|
|
89
|
+
state: PlacementRunState;
|
|
90
|
+
} | undefined;
|
|
91
|
+
panes(connection: C, identity: HerdrRunIdentity): Promise<P>;
|
|
92
|
+
reconcile(identity: HerdrRunIdentity, panes: P, proveEvidence: () => PlacementEvidenceProof): PlacementReconnectCheck;
|
|
93
|
+
closeOwnedPane(connection: C, owned: PlacementOwnedPane, terminalId: string | undefined, agentName: string | undefined): Promise<void>;
|
|
94
|
+
}
|
|
95
|
+
export declare function herdrPaneAllocationKey(target: string, session: string | null, cwd: string): string;
|
|
96
|
+
export declare function withHerdrPaneAllocationLock<T>(key: string, action: () => Promise<T>, options?: {
|
|
97
|
+
timeoutMs?: number;
|
|
98
|
+
pollMs?: number;
|
|
99
|
+
}): Promise<T>;
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
import { createHash, randomBytes } from "node:crypto";
|
|
2
|
+
import * as fs from "node:fs";
|
|
3
|
+
import * as path from "node:path";
|
|
4
|
+
import { getAgentDir } from "../../shared/utils.js";
|
|
5
|
+
const HERDR_ALLOCATION_LOCK_TIMEOUT_MS = 10_000, HERDR_ALLOCATION_LOCK_POLL_MS = 25;
|
|
6
|
+
export function herdrPaneAllocationKey(target, session, cwd) { return JSON.stringify([target, session, cwd]); }
|
|
7
|
+
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()))
|
|
8
|
+
throw new Error("Herdr allocation path is not a mode-0700 agent-private owned directory."); }
|
|
9
|
+
function ensurePrivateChild(parent, name) { const directory = path.join(parent, name); try {
|
|
10
|
+
fs.mkdirSync(directory, { mode: 0o700 });
|
|
11
|
+
}
|
|
12
|
+
catch (error) {
|
|
13
|
+
if (error.code !== "EEXIST")
|
|
14
|
+
throw error;
|
|
15
|
+
} assertPrivateDir(directory); return directory; }
|
|
16
|
+
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"); }
|
|
17
|
+
function livePid(pid) { try {
|
|
18
|
+
process.kill(pid, 0);
|
|
19
|
+
return true;
|
|
20
|
+
}
|
|
21
|
+
catch (error) {
|
|
22
|
+
return error.code === "EPERM";
|
|
23
|
+
} }
|
|
24
|
+
function readAllocationOwner(ownerPath) { const descriptor = fs.openSync(ownerPath, fs.constants.O_RDONLY | (fs.constants.O_NOFOLLOW ?? 0)); try {
|
|
25
|
+
const stat = fs.fstatSync(descriptor);
|
|
26
|
+
if (!stat.isFile() || (stat.mode & 0o777) !== 0o600 || (typeof process.getuid === "function" && stat.uid !== process.getuid()))
|
|
27
|
+
throw new Error("Herdr allocation lock owner record is unsafe.");
|
|
28
|
+
const value = JSON.parse(fs.readFileSync(descriptor, "utf8"));
|
|
29
|
+
if (typeof value.pid !== "number" || !Number.isSafeInteger(value.pid) || value.pid <= 0 || typeof value.token !== "string" || !/^[a-f0-9]{32}$/u.test(value.token))
|
|
30
|
+
throw new Error("Herdr allocation lock owner record is malformed.");
|
|
31
|
+
return { pid: value.pid, token: value.token, dev: stat.dev, ino: stat.ino };
|
|
32
|
+
}
|
|
33
|
+
finally {
|
|
34
|
+
fs.closeSync(descriptor);
|
|
35
|
+
} }
|
|
36
|
+
function sameOwner(left, right) { return left.token === right.token && left.dev === right.dev && left.ino === right.ino; }
|
|
37
|
+
export async function withHerdrPaneAllocationLock(key, action, options = {}) {
|
|
38
|
+
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);
|
|
39
|
+
let published;
|
|
40
|
+
try {
|
|
41
|
+
fs.writeFileSync(tempPath, JSON.stringify({ pid: process.pid, token, createdAt: Date.now() }), { mode: 0o600, flag: "wx" });
|
|
42
|
+
const tempOwner = readAllocationOwner(tempPath);
|
|
43
|
+
for (;;) {
|
|
44
|
+
let recovering = false;
|
|
45
|
+
try {
|
|
46
|
+
readAllocationOwner(recoveryPath);
|
|
47
|
+
recovering = true;
|
|
48
|
+
}
|
|
49
|
+
catch (error) {
|
|
50
|
+
if (error.code !== "ENOENT")
|
|
51
|
+
throw error;
|
|
52
|
+
}
|
|
53
|
+
if (!recovering) {
|
|
54
|
+
let linked = false;
|
|
55
|
+
try {
|
|
56
|
+
fs.linkSync(tempPath, lockPath);
|
|
57
|
+
linked = true;
|
|
58
|
+
try {
|
|
59
|
+
readAllocationOwner(recoveryPath);
|
|
60
|
+
const current = readAllocationOwner(lockPath);
|
|
61
|
+
if (!sameOwner(current, tempOwner))
|
|
62
|
+
throw new Error("Herdr allocation lock changed during publication.");
|
|
63
|
+
fs.unlinkSync(lockPath);
|
|
64
|
+
}
|
|
65
|
+
catch (recoveryError) {
|
|
66
|
+
if (recoveryError.code !== "ENOENT")
|
|
67
|
+
throw recoveryError;
|
|
68
|
+
published = readAllocationOwner(lockPath);
|
|
69
|
+
if (!sameOwner(published, tempOwner))
|
|
70
|
+
throw new Error("Herdr allocation lock publication identity changed.");
|
|
71
|
+
break;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
catch (publishError) {
|
|
75
|
+
if (linked) {
|
|
76
|
+
try {
|
|
77
|
+
const current = readAllocationOwner(lockPath);
|
|
78
|
+
if (sameOwner(current, tempOwner))
|
|
79
|
+
fs.unlinkSync(lockPath);
|
|
80
|
+
}
|
|
81
|
+
catch { }
|
|
82
|
+
}
|
|
83
|
+
if (publishError.code !== "EEXIST" && publishError.code !== "ENOENT")
|
|
84
|
+
throw publishError;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
if (published)
|
|
88
|
+
break;
|
|
89
|
+
try {
|
|
90
|
+
const current = readAllocationOwner(lockPath);
|
|
91
|
+
if (!livePid(current.pid)) {
|
|
92
|
+
let claimed = false;
|
|
93
|
+
try {
|
|
94
|
+
fs.linkSync(lockPath, recoveryPath);
|
|
95
|
+
claimed = true;
|
|
96
|
+
const recovery = readAllocationOwner(recoveryPath), latest = readAllocationOwner(lockPath);
|
|
97
|
+
if (!sameOwner(recovery, current) || !sameOwner(latest, current))
|
|
98
|
+
throw new Error("Herdr stale lock identity changed during recovery.");
|
|
99
|
+
fs.unlinkSync(lockPath);
|
|
100
|
+
}
|
|
101
|
+
catch (error) {
|
|
102
|
+
if (error.code !== "EEXIST" && error.code !== "ENOENT")
|
|
103
|
+
throw error;
|
|
104
|
+
}
|
|
105
|
+
finally {
|
|
106
|
+
if (claimed)
|
|
107
|
+
fs.unlinkSync(recoveryPath);
|
|
108
|
+
}
|
|
109
|
+
if (claimed)
|
|
110
|
+
continue;
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
catch (error) {
|
|
114
|
+
if (error.code !== "ENOENT")
|
|
115
|
+
throw error;
|
|
116
|
+
}
|
|
117
|
+
if (Date.now() >= deadline)
|
|
118
|
+
throw new Error("Timed out waiting for the Herdr pane allocation lock.");
|
|
119
|
+
await new Promise((resolve) => setTimeout(resolve, options.pollMs ?? HERDR_ALLOCATION_LOCK_POLL_MS));
|
|
120
|
+
}
|
|
121
|
+
fs.unlinkSync(tempPath);
|
|
122
|
+
let value, actionError, releaseError, actionFailed = false, releaseFailed = false;
|
|
123
|
+
try {
|
|
124
|
+
value = await action();
|
|
125
|
+
}
|
|
126
|
+
catch (error) {
|
|
127
|
+
actionFailed = true;
|
|
128
|
+
actionError = error;
|
|
129
|
+
}
|
|
130
|
+
try {
|
|
131
|
+
const current = readAllocationOwner(lockPath);
|
|
132
|
+
if (!published || !sameOwner(current, published))
|
|
133
|
+
throw new Error("Herdr pane allocation lock ownership changed before release.");
|
|
134
|
+
fs.unlinkSync(lockPath);
|
|
135
|
+
}
|
|
136
|
+
catch (error) {
|
|
137
|
+
releaseFailed = true;
|
|
138
|
+
releaseError = error;
|
|
139
|
+
}
|
|
140
|
+
if (actionFailed && releaseFailed)
|
|
141
|
+
throw new AggregateError([actionError, releaseError], "Herdr pane allocation action and release both failed.", { cause: actionError });
|
|
142
|
+
if (actionFailed)
|
|
143
|
+
throw actionError;
|
|
144
|
+
if (releaseFailed)
|
|
145
|
+
throw releaseError;
|
|
146
|
+
return value;
|
|
147
|
+
}
|
|
148
|
+
finally {
|
|
149
|
+
try {
|
|
150
|
+
fs.unlinkSync(tempPath);
|
|
151
|
+
}
|
|
152
|
+
catch (error) {
|
|
153
|
+
if (error.code !== "ENOENT")
|
|
154
|
+
throw error;
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import type { PlacementMachineReference, RexLocalMachineReference } from "../../shared/types.ts";
|
|
2
|
+
/** `machine: "local"` is reserved: this machine, in a Rex pane of the parent's own Rex session. It never consults the Herdr catalog. */
|
|
3
|
+
export declare const REX_LOCAL_MACHINE = "local";
|
|
4
|
+
/** Rex CLI exit codes: 3 means the target is gone, 4 means the server is unreachable (state unknown, never "gone"). */
|
|
5
|
+
export declare const REX_EXIT_GONE = 3, REX_EXIT_UNREACHABLE = 4;
|
|
6
|
+
/** A `local` child edits this machine's disk; every other placed child edits the remote one. */
|
|
7
|
+
export declare function isRexLocalMachine(machine: PlacementMachineReference | undefined): machine is RexLocalMachineReference;
|
|
8
|
+
/** The Rex server bundle's CLI, relative to the home directory. */
|
|
9
|
+
export declare const REX_BUNDLE_BIN = "Library/Application Support/com.superlogical.rex/RexServer/Versions/Current/Rex Server.app/Contents/MacOS/rex";
|
|
10
|
+
export declare const REX_CALL_TIMEOUT_MS = 15000, REX_MAX_OUTPUT: number;
|
|
11
|
+
/** Global rex flags: never start a server, and use the recorded one when there is one. */
|
|
12
|
+
export declare function rexGlobalArgs(server: string | undefined): string[];
|
|
13
|
+
/** `$REX_BIN_DIR/rex`, then `rex` on PATH, then the Rex server bundle. */
|
|
14
|
+
export declare function resolveRexBinary(env?: NodeJS.ProcessEnv): string;
|
|
15
|
+
/** Every Rex selector comes from the recorded machine reference, never from the calling process's own Rex pane variables. */
|
|
16
|
+
export declare function rexCommandEnv(source?: NodeJS.ProcessEnv): NodeJS.ProcessEnv;
|
|
17
|
+
/**
|
|
18
|
+
* Resolves `machine: "local"` in the parent: records its Rex session (confirmed by the server) and server address, and the real
|
|
19
|
+
* launch directory. `stepCwd` is absolute, `~`, or relative to the parent's launch directory `cwd`.
|
|
20
|
+
*/
|
|
21
|
+
export declare function resolveRexLocalPlacement(input: {
|
|
22
|
+
cwd: string;
|
|
23
|
+
stepCwd?: string;
|
|
24
|
+
env?: NodeJS.ProcessEnv;
|
|
25
|
+
}): RexLocalMachineReference;
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import { spawnSync } from "node:child_process";
|
|
2
|
+
import * as fs from "node:fs";
|
|
3
|
+
import * as os from "node:os";
|
|
4
|
+
import * as path from "node:path";
|
|
5
|
+
/** `machine: "local"` is reserved: this machine, in a Rex pane of the parent's own Rex session. It never consults the Herdr catalog. */
|
|
6
|
+
export const REX_LOCAL_MACHINE = "local";
|
|
7
|
+
/** Rex CLI exit codes: 3 means the target is gone, 4 means the server is unreachable (state unknown, never "gone"). */
|
|
8
|
+
export const REX_EXIT_GONE = 3, REX_EXIT_UNREACHABLE = 4;
|
|
9
|
+
/** A `local` child edits this machine's disk; every other placed child edits the remote one. */
|
|
10
|
+
export function isRexLocalMachine(machine) { return machine?.provider === "rex" && !("target" in machine); }
|
|
11
|
+
/** The Rex server bundle's CLI, relative to the home directory. */
|
|
12
|
+
export const REX_BUNDLE_BIN = "Library/Application Support/com.superlogical.rex/RexServer/Versions/Current/Rex Server.app/Contents/MacOS/rex";
|
|
13
|
+
export const REX_CALL_TIMEOUT_MS = 15_000, REX_MAX_OUTPUT = 1024 * 1024;
|
|
14
|
+
/** Global rex flags: never start a server, and use the recorded one when there is one. */
|
|
15
|
+
export function rexGlobalArgs(server) { return server ? ["--autostart=false", "-S", server] : ["--autostart=false"]; }
|
|
16
|
+
function executable(file) { try {
|
|
17
|
+
fs.accessSync(file, fs.constants.X_OK);
|
|
18
|
+
return fs.statSync(file).isFile();
|
|
19
|
+
}
|
|
20
|
+
catch {
|
|
21
|
+
return false;
|
|
22
|
+
} }
|
|
23
|
+
/** `$REX_BIN_DIR/rex`, then `rex` on PATH, then the Rex server bundle. */
|
|
24
|
+
export function resolveRexBinary(env = process.env) {
|
|
25
|
+
const fromDir = env.REX_BIN_DIR ? [path.join(env.REX_BIN_DIR, "rex")] : [];
|
|
26
|
+
const fromPath = (env.PATH ?? "").split(path.delimiter).filter((dir) => path.isAbsolute(dir)).map((dir) => path.join(dir, "rex"));
|
|
27
|
+
const found = [...fromDir, ...fromPath, path.join(os.homedir(), REX_BUNDLE_BIN)].find(executable);
|
|
28
|
+
if (!found)
|
|
29
|
+
throw new Error("The Rex CLI was not found: set REX_BIN_DIR, put rex on PATH, or install Rex.");
|
|
30
|
+
return found;
|
|
31
|
+
}
|
|
32
|
+
/** Every Rex selector comes from the recorded machine reference, never from the calling process's own Rex pane variables. */
|
|
33
|
+
export function rexCommandEnv(source = process.env) {
|
|
34
|
+
const { REX_SESSION: _session, REX_BLOCK: _block, REX_SERVER: _server, ...env } = source;
|
|
35
|
+
return env;
|
|
36
|
+
}
|
|
37
|
+
function sessionConfirmed(stdout, session) {
|
|
38
|
+
try {
|
|
39
|
+
const parsed = JSON.parse(stdout);
|
|
40
|
+
return parsed?.session_id === session;
|
|
41
|
+
}
|
|
42
|
+
catch {
|
|
43
|
+
return false;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Resolves `machine: "local"` in the parent: records its Rex session (confirmed by the server) and server address, and the real
|
|
48
|
+
* launch directory. `stepCwd` is absolute, `~`, or relative to the parent's launch directory `cwd`.
|
|
49
|
+
*/
|
|
50
|
+
export function resolveRexLocalPlacement(input) {
|
|
51
|
+
if (process.platform === "win32")
|
|
52
|
+
throw new Error("Machine 'local' runs the child in a Rex pane, which is not supported on Windows.");
|
|
53
|
+
const env = input.env ?? process.env, session = env.REX_SESSION?.trim(), server = env.REX_SERVER?.trim();
|
|
54
|
+
if (!session)
|
|
55
|
+
throw new Error("Machine 'local' opens the child in a new tab of this Rex session, but this Pi is not running inside Rex (REX_SESSION is not set).");
|
|
56
|
+
const result = spawnSync(resolveRexBinary(env), [...rexGlobalArgs(server), "session", "inspect", session, "--json"], { encoding: "utf8", env: rexCommandEnv(env), timeout: REX_CALL_TIMEOUT_MS, maxBuffer: REX_MAX_OUTPUT, windowsHide: true });
|
|
57
|
+
if (result.error)
|
|
58
|
+
throw new Error(`Could not ask Rex about this session: ${result.error.message}`, { cause: result.error });
|
|
59
|
+
if (result.status === REX_EXIT_GONE)
|
|
60
|
+
throw new Error(`Machine 'local' needs this Pi's Rex session, but the Rex server does not know session ${session}.`);
|
|
61
|
+
if (result.status === REX_EXIT_UNREACHABLE)
|
|
62
|
+
throw new Error("Machine 'local' needs the Rex server, but it is unreachable.");
|
|
63
|
+
if (result.status !== 0)
|
|
64
|
+
throw new Error(`rex session inspect exited with code ${result.status}: ${(result.stderr || result.stdout).trim().slice(0, 512)}`);
|
|
65
|
+
if (!sessionConfirmed(result.stdout, session))
|
|
66
|
+
throw new Error(`The Rex server did not confirm session ${session}.`);
|
|
67
|
+
const stepCwd = input.stepCwd?.trim();
|
|
68
|
+
const home = stepCwd === "~" || stepCwd?.startsWith("~/") ? path.join(os.homedir(), stepCwd.slice(1)) : undefined;
|
|
69
|
+
const requested = home ?? (stepCwd ? path.resolve(input.cwd, stepCwd) : input.cwd);
|
|
70
|
+
let cwd;
|
|
71
|
+
try {
|
|
72
|
+
cwd = fs.realpathSync(requested);
|
|
73
|
+
}
|
|
74
|
+
catch (error) {
|
|
75
|
+
throw new Error(`Machine 'local' cwd does not exist: ${requested}.`, { cause: error });
|
|
76
|
+
}
|
|
77
|
+
if (!fs.statSync(cwd).isDirectory())
|
|
78
|
+
throw new Error(`Machine 'local' cwd is not a directory: ${requested}.`);
|
|
79
|
+
const machine = { provider: "rex", id: REX_LOCAL_MACHINE, session, cwd };
|
|
80
|
+
if (server)
|
|
81
|
+
machine.server = server;
|
|
82
|
+
return machine;
|
|
83
|
+
}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import type { PlacementMachineReference, RexMachineReference } from "../../shared/types.ts";
|
|
2
|
+
import type { HerdrRunIdentity } from "./herdr-placed-run.ts";
|
|
3
|
+
import { PlacedRunOwner } from "./placed-run-owner.ts";
|
|
4
|
+
import { type PlacementEvidenceProof, type PlacementMachineAccess, type PlacementOwnedPane, type PlacementReconnectCheck, type PlacementSockets } from "./placement-host.ts";
|
|
5
|
+
/** On a Rex SSH machine, placed tabs open in the one session with this label. */
|
|
6
|
+
export declare const REX_SSH_SESSION_LABEL = "subagents";
|
|
7
|
+
interface RexEndpoint {
|
|
8
|
+
access: PlacementMachineAccess;
|
|
9
|
+
bin: string;
|
|
10
|
+
session: string;
|
|
11
|
+
server?: string;
|
|
12
|
+
}
|
|
13
|
+
export interface RexConnection extends RexEndpoint, PlacementSockets {
|
|
14
|
+
}
|
|
15
|
+
export interface RexPaneCandidate {
|
|
16
|
+
present: boolean;
|
|
17
|
+
foregroundPid?: number;
|
|
18
|
+
bridgePid?: number;
|
|
19
|
+
}
|
|
20
|
+
/** The server must confirm the recorded session; a missing session fails closed. A Rex SSH machine's session is the one recorded at launch. */
|
|
21
|
+
export declare function connectRex(machine: PlacementMachineReference, options?: {
|
|
22
|
+
bin?: string;
|
|
23
|
+
access?: PlacementMachineAccess;
|
|
24
|
+
session?: string;
|
|
25
|
+
}): Promise<RexConnection>;
|
|
26
|
+
/** Ownership is the never-reused block id in the recorded session. The block counts as closed only once `block inspect` exits 3. */
|
|
27
|
+
export declare function closeOwnedRexPane(connection: RexConnection, owned: PlacementOwnedPane): Promise<void>;
|
|
28
|
+
declare function reconcileRexPlacedRun(identity: HerdrRunIdentity, candidate: RexPaneCandidate, proveEvidence: () => PlacementEvidenceProof): PlacementReconnectCheck;
|
|
29
|
+
/**
|
|
30
|
+
* Rex pane host: rex CLI calls on this machine, always with an explicit session. A local child opens as a background
|
|
31
|
+
* window in the parent's session; a Rex SSH child in the remote `subagents` session, with every rex call run over SSH.
|
|
32
|
+
* Pi starts through the login shell's environment (as Rex starts any window) with the bridge variables set by
|
|
33
|
+
* /usr/bin/env. There is no status subscription: a dead Pi closes the bridge socket (directly, or through the SSH
|
|
34
|
+
* forward), and the reconnect that follows checks the block with `rex block call process` and fails closed.
|
|
35
|
+
*/
|
|
36
|
+
export declare const rexPaneHost: {
|
|
37
|
+
connect: typeof connectRex;
|
|
38
|
+
sessionOf(connection: RexConnection): string;
|
|
39
|
+
launch(connection: RexConnection, machine: PlacementMachineReference, runId: string, _runtimeDir: string, request: import("./placement-host.ts").PlacementLaunchRequest, onPane: (owned: PlacementOwnedPane) => Promise<void>): Promise<{
|
|
40
|
+
owned: {
|
|
41
|
+
workspaceId: string;
|
|
42
|
+
tabId: string;
|
|
43
|
+
paneId: string;
|
|
44
|
+
};
|
|
45
|
+
terminalId: string;
|
|
46
|
+
}>;
|
|
47
|
+
prompt(connection: RexConnection, identity: HerdrRunIdentity, text: string): Promise<void>;
|
|
48
|
+
subscribe(): Promise<() => void>;
|
|
49
|
+
paneEvent(): undefined;
|
|
50
|
+
panes(connection: RexConnection, identity: HerdrRunIdentity): Promise<RexPaneCandidate>;
|
|
51
|
+
reconcile: typeof reconcileRexPlacedRun;
|
|
52
|
+
closeOwnedPane: typeof closeOwnedRexPane;
|
|
53
|
+
};
|
|
54
|
+
/** The placed-run owner bound to Rex and the pane machine's access. */
|
|
55
|
+
export declare class RexPlacedRunOwner extends PlacedRunOwner<RexConnection, RexMachineReference> {
|
|
56
|
+
private constructor();
|
|
57
|
+
/** A Rex SSH owner's machine carries the resolved remote cwd, and its host reconnects only to the session recorded here. */
|
|
58
|
+
static create(machine: RexMachineReference, runId: string, options?: {
|
|
59
|
+
sshAccess?: PlacementMachineAccess;
|
|
60
|
+
}): Promise<RexPlacedRunOwner>;
|
|
61
|
+
}
|
|
62
|
+
export {};
|