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
|
@@ -0,0 +1,327 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import * as path from "node:path";
|
|
3
|
+
import { Type } from "typebox";
|
|
4
|
+
import { Compile } from "typebox/compile";
|
|
5
|
+
import { remoteShellCommand, sshMachineAccess } from "./herdr-connection.js";
|
|
6
|
+
import { localMachineAccess } from "./local-machine-access.js";
|
|
7
|
+
import { getPiSpawnCommand } from "./pi-spawn.js";
|
|
8
|
+
import { createPlacedRuntimeDir, PLACED_MANIFEST_READ_SCRIPT, PlacedRunOwner, removePlacedRuntimeDir } from "./placed-run-owner.js";
|
|
9
|
+
import { withHerdrPaneAllocationLock } from "./placement-host.js";
|
|
10
|
+
import { isRexLocalMachine, REX_BUNDLE_BIN, REX_CALL_TIMEOUT_MS, REX_EXIT_GONE, REX_EXIT_UNREACHABLE, REX_MAX_OUTPUT, resolveRexBinary, rexCommandEnv, rexGlobalArgs } from "./rex-machine.js";
|
|
11
|
+
const REX_CLOSE_POLLS = 20, REX_CLOSE_POLL_MS = 50;
|
|
12
|
+
/** On a Rex SSH machine, placed tabs open in the one session with this label. */
|
|
13
|
+
export const REX_SSH_SESSION_LABEL = "subagents";
|
|
14
|
+
const sessionInspection = Compile(Type.Object({ session_id: Type.String() }));
|
|
15
|
+
const sessionList = Compile(Type.Object({ sessions: Type.Array(Type.Object({ session_id: Type.String(), label: Type.Optional(Type.String()) })) }));
|
|
16
|
+
const createdWindow = Compile(Type.Object({ window_id: Type.String(), block_ids: Type.Tuple([Type.String()]) }));
|
|
17
|
+
const sessionWindows = Compile(Type.Object({ windows: Type.Array(Type.Object({ window_id: Type.String(), label: Type.Optional(Type.String()), layers: Type.Array(Type.Object({ blocks: Type.Array(Type.Object({ block_id: Type.String() })) })) })) }));
|
|
18
|
+
const blockProcess = Compile(Type.Object({ foreground: Type.Object({ pid: Type.Integer() }) }));
|
|
19
|
+
const bridgeManifestPid = Compile(Type.Object({ pid: Type.Integer() }));
|
|
20
|
+
/** Rex runtime directories go under /tmp: the bridge socket path inside a macOS TMPDIR exceeds the unix socket path limit. */
|
|
21
|
+
function rexAccess() { return localMachineAccess({ env: { ...rexCommandEnv(), TMPDIR: "/tmp" } }); }
|
|
22
|
+
/** The same /tmp rule on the remote, whatever TMPDIR its SSH session has. */
|
|
23
|
+
function rexSshAccess(machine, access = sshMachineAccess(machine)) { return { ...access, shellCommand: (script, args) => remoteShellCommand(`TMPDIR=/tmp; export TMPDIR; ${script}`, args) }; }
|
|
24
|
+
function asRexMachine(machine) { if (machine.provider !== "rex")
|
|
25
|
+
throw new Error("The Rex pane host only places runs on Rex machines."); return machine; }
|
|
26
|
+
/** Runs one rex command on the pane machine with --autostart=false, the recorded server, and no inherited Rex selectors; exit 4 always throws. */
|
|
27
|
+
async function runRex(endpoint, args, what) {
|
|
28
|
+
const result = await endpoint.access.runAsync(endpoint.access.shellCommand('exec /usr/bin/env -u REX_SESSION -u REX_BLOCK -u REX_SERVER "$@"', [endpoint.bin, ...rexGlobalArgs(endpoint.server), ...args]), { timeout: REX_CALL_TIMEOUT_MS, maxBuffer: REX_MAX_OUTPUT });
|
|
29
|
+
if (result.error)
|
|
30
|
+
throw new Error(`Rex ${what} failed: ${result.error.message}`, { cause: result.error });
|
|
31
|
+
if (result.status === REX_EXIT_UNREACHABLE)
|
|
32
|
+
throw new Error(`The Rex server is unreachable (rex ${what} exited 4); the placed pane's state is unknown.`);
|
|
33
|
+
if (result.status === null)
|
|
34
|
+
throw new Error(`Rex ${what} did not finish; the placed pane's state is unknown.`);
|
|
35
|
+
return { ...result, status: result.status };
|
|
36
|
+
}
|
|
37
|
+
function requireRexSuccess(result, what) {
|
|
38
|
+
if (result.status === REX_EXIT_GONE)
|
|
39
|
+
throw new Error(`Rex ${what} failed because its target no longer exists (exit 3): ${result.stderr.trim().slice(0, 512)}`);
|
|
40
|
+
if (result.status !== 0)
|
|
41
|
+
throw new Error(`Rex ${what} exited with code ${result.status}: ${result.stderr.trim().slice(0, 512)}`);
|
|
42
|
+
}
|
|
43
|
+
/** Parses rex's JSON output against its schema; undefined when the shape does not match. */
|
|
44
|
+
function parseRexJson(text, validator, what) {
|
|
45
|
+
let value;
|
|
46
|
+
try {
|
|
47
|
+
value = JSON.parse(text);
|
|
48
|
+
}
|
|
49
|
+
catch {
|
|
50
|
+
throw new Error(`Rex ${what} returned malformed JSON.`);
|
|
51
|
+
}
|
|
52
|
+
return validator.Check(value) ? value : undefined;
|
|
53
|
+
}
|
|
54
|
+
/** One remote lookup, matching resolveRexBinary: `$REX_BIN_DIR/rex`, then `rex` on PATH, then the Rex server bundle under the remote home. */
|
|
55
|
+
async function resolveRemoteRexBinary(access, machine) {
|
|
56
|
+
const script = `for c in "\${REX_BIN_DIR:+$REX_BIN_DIR/rex}" "$(command -v rex 2>/dev/null)" "$HOME/${REX_BUNDLE_BIN}"; do if [ -n "$c" ] && [ -f "$c" ] && [ -x "$c" ]; then printf '%s\\n' "$c"; exit 0; fi; done; exit 1`;
|
|
57
|
+
const found = await access.runAsync(access.shellCommand(script), { timeout: REX_CALL_TIMEOUT_MS, maxBuffer: 64 * 1024 });
|
|
58
|
+
if (found.error)
|
|
59
|
+
throw new Error(`Could not look for the Rex CLI on ${machine.id}: ${found.error.message}`, { cause: found.error });
|
|
60
|
+
const bin = found.stdout.trim();
|
|
61
|
+
if (found.status !== 0 || !path.posix.isAbsolute(bin) || bin.includes("\n"))
|
|
62
|
+
throw new Error(`The Rex CLI was not found on ${machine.id}. Install Rex there or put rex on its PATH.`);
|
|
63
|
+
return bin;
|
|
64
|
+
}
|
|
65
|
+
/** The pane's cwd as the remote Pi will report it: `~` expanded and symlinks resolved. Rex does not expand `~` itself. */
|
|
66
|
+
async function resolveRemoteCwd(access, machine) {
|
|
67
|
+
const script = `case "$1" in "~") d=$HOME;; "~/"*) d=$HOME/\${1#"~/"};; *) d=$1;; esac; cd -P -- "$d" 2>/dev/null && pwd -P`;
|
|
68
|
+
const resolved = await access.runAsync(access.shellCommand(script, [machine.cwd]), { timeout: REX_CALL_TIMEOUT_MS, maxBuffer: 64 * 1024 });
|
|
69
|
+
if (resolved.error)
|
|
70
|
+
throw new Error(`Could not check ${machine.cwd} on ${machine.id}: ${resolved.error.message}`, { cause: resolved.error });
|
|
71
|
+
const cwd = resolved.stdout.trim();
|
|
72
|
+
if (resolved.status !== 0 || !path.posix.isAbsolute(cwd) || cwd.includes("\n"))
|
|
73
|
+
throw new Error(`Nothing at ${machine.cwd} on ${machine.id}. Clone the repo there first; pi-subagents never clones, pulls, or checks out on a machine.`);
|
|
74
|
+
return cwd;
|
|
75
|
+
}
|
|
76
|
+
/** The remote Pi as a Rex tab's login environment finds it. */
|
|
77
|
+
async function resolveRemotePi(access, machine) {
|
|
78
|
+
const found = await access.runAsync(access.shellCommand(`exec "\${SHELL:-/bin/sh}" -lc 'command -v pi'`), { timeout: REX_CALL_TIMEOUT_MS, maxBuffer: 64 * 1024 });
|
|
79
|
+
if (found.error)
|
|
80
|
+
throw new Error(`Could not look for Pi on ${machine.id}: ${found.error.message}`, { cause: found.error });
|
|
81
|
+
const pi = found.stdout.trim().split("\n").at(-1)?.trim() ?? "";
|
|
82
|
+
if (found.status !== 0 || !path.posix.isAbsolute(pi))
|
|
83
|
+
throw new Error(`Pi was not found on ${machine.id}: its login shell has no pi on PATH. Install Pi there.`);
|
|
84
|
+
return pi;
|
|
85
|
+
}
|
|
86
|
+
async function labeledSessions(endpoint) {
|
|
87
|
+
const listed = await runRex(endpoint, ["ls", "--json"], "ls");
|
|
88
|
+
requireRexSuccess(listed, "ls");
|
|
89
|
+
const parsed = parseRexJson(listed.stdout, sessionList, "ls");
|
|
90
|
+
if (!parsed)
|
|
91
|
+
throw new Error("Rex ls did not list the server's sessions.");
|
|
92
|
+
return parsed.sessions.filter((session) => session.label === REX_SSH_SESSION_LABEL).map((session) => session.session_id);
|
|
93
|
+
}
|
|
94
|
+
/**
|
|
95
|
+
* The session labeled `subagents` on the SSH machine, created when there is none. Rex allows duplicate labels, so two or
|
|
96
|
+
* more fail closed, also after creating one. Only launch calls this; reconnect and cleanup use the recorded session id.
|
|
97
|
+
*/
|
|
98
|
+
async function subagentsSession(endpoint, machine) {
|
|
99
|
+
return withHerdrPaneAllocationLock(JSON.stringify(["rex-session", machine.target, REX_SSH_SESSION_LABEL]), async () => {
|
|
100
|
+
let sessions = await labeledSessions(endpoint);
|
|
101
|
+
if (sessions.length === 0) {
|
|
102
|
+
requireRexSuccess(await runRex(endpoint, ["new", REX_SSH_SESSION_LABEL, "--json"], "new"), "new");
|
|
103
|
+
sessions = await labeledSessions(endpoint);
|
|
104
|
+
}
|
|
105
|
+
const [session] = sessions;
|
|
106
|
+
if (sessions.length > 1)
|
|
107
|
+
throw new Error(`Rex on ${machine.id} has ${sessions.length} sessions labeled ${REX_SSH_SESSION_LABEL}; close or rename the extras so placed runs have one session to open in.`);
|
|
108
|
+
if (!session)
|
|
109
|
+
throw new Error(`Rex on ${machine.id} did not list the ${REX_SSH_SESSION_LABEL} session it just created.`);
|
|
110
|
+
return session;
|
|
111
|
+
});
|
|
112
|
+
}
|
|
113
|
+
/** The server must confirm the recorded session; a missing session fails closed. A Rex SSH machine's session is the one recorded at launch. */
|
|
114
|
+
export async function connectRex(machine, options = {}) {
|
|
115
|
+
const rex = asRexMachine(machine);
|
|
116
|
+
let endpoint;
|
|
117
|
+
if (isRexLocalMachine(rex)) {
|
|
118
|
+
endpoint = { access: options.access ?? rexAccess(), bin: options.bin ?? resolveRexBinary(), session: rex.session };
|
|
119
|
+
if (rex.server)
|
|
120
|
+
endpoint.server = rex.server;
|
|
121
|
+
}
|
|
122
|
+
else {
|
|
123
|
+
if (!options.session)
|
|
124
|
+
throw new Error(`Rex machine '${rex.id}' has no recorded session; a placed run never picks one by label after launch.`);
|
|
125
|
+
const access = options.access ?? rexSshAccess(rex);
|
|
126
|
+
endpoint = { access, bin: options.bin ?? await resolveRemoteRexBinary(access, rex), session: options.session };
|
|
127
|
+
}
|
|
128
|
+
const inspected = await runRex(endpoint, ["session", "inspect", endpoint.session, "--json"], "session inspect");
|
|
129
|
+
if (inspected.status === REX_EXIT_GONE)
|
|
130
|
+
throw new Error(`Rex session ${endpoint.session} no longer exists on the Rex server.`);
|
|
131
|
+
requireRexSuccess(inspected, "session inspect");
|
|
132
|
+
if (parseRexJson(inspected.stdout, sessionInspection, "session inspect")?.session_id !== endpoint.session)
|
|
133
|
+
throw new Error("Rex session identity changed: the server reported a different session id.");
|
|
134
|
+
return { ...endpoint, ...endpoint.access.openSockets() };
|
|
135
|
+
}
|
|
136
|
+
async function inspectBlock(connection, blockId) {
|
|
137
|
+
const inspected = await runRex(connection, ["block", "inspect", "-s", connection.session, blockId], "block inspect");
|
|
138
|
+
if (inspected.status === REX_EXIT_GONE)
|
|
139
|
+
return "gone";
|
|
140
|
+
requireRexSuccess(inspected, "block inspect");
|
|
141
|
+
return "present";
|
|
142
|
+
}
|
|
143
|
+
/** Ownership is the never-reused block id in the recorded session. The block counts as closed only once `block inspect` exits 3. */
|
|
144
|
+
export async function closeOwnedRexPane(connection, owned) {
|
|
145
|
+
if (owned.workspaceId !== connection.session)
|
|
146
|
+
throw new Error("Refusing Rex cleanup: the owned pane belongs to a different Rex session.");
|
|
147
|
+
if (await inspectBlock(connection, owned.paneId) === "gone")
|
|
148
|
+
return;
|
|
149
|
+
const closed = await runRex(connection, ["block", "close", "-s", connection.session, owned.paneId], "block close");
|
|
150
|
+
if (closed.status !== REX_EXIT_GONE)
|
|
151
|
+
requireRexSuccess(closed, "block close");
|
|
152
|
+
for (let poll = 0; poll < REX_CLOSE_POLLS; poll++) {
|
|
153
|
+
if (await inspectBlock(connection, owned.paneId) === "gone")
|
|
154
|
+
return;
|
|
155
|
+
await new Promise((resolve) => setTimeout(resolve, REX_CLOSE_POLL_MS));
|
|
156
|
+
}
|
|
157
|
+
throw new Error(`Rex block ${owned.paneId} still exists after close; refusing to report the placed pane closed.`);
|
|
158
|
+
}
|
|
159
|
+
function reconcileRexPlacedRun(identity, candidate, proveEvidence) {
|
|
160
|
+
if (!candidate.present)
|
|
161
|
+
return { ok: false, reason: "Owned Rex block is gone." };
|
|
162
|
+
if (candidate.foregroundPid === undefined || candidate.foregroundPid !== candidate.bridgePid)
|
|
163
|
+
return { ok: false, reason: "The owned Rex block's foreground process is not the placed Pi." };
|
|
164
|
+
const proof = proveEvidence();
|
|
165
|
+
if (!proof.ok)
|
|
166
|
+
return proof;
|
|
167
|
+
return { ok: true, state: "unknown", paneId: identity.paneId, cursor: proof.cursor };
|
|
168
|
+
}
|
|
169
|
+
async function readBridgePid(connection, runtimeDir) {
|
|
170
|
+
const manifest = await connection.access.runAsync(connection.access.shellCommand(PLACED_MANIFEST_READ_SCRIPT, [runtimeDir]), { timeout: 5_000, maxBuffer: 64 * 1024 });
|
|
171
|
+
if (manifest.status !== 0)
|
|
172
|
+
return undefined;
|
|
173
|
+
return parseRexJson(manifest.stdout, bridgeManifestPid, "bridge manifest")?.pid;
|
|
174
|
+
}
|
|
175
|
+
/**
|
|
176
|
+
* `window new` failed, timed out, or printed something unreadable, so the server may still have opened this run's window.
|
|
177
|
+
* Find it by the run's tab label in the recorded session and close its blocks with the same close-then-prove-gone rule.
|
|
178
|
+
*/
|
|
179
|
+
async function closeUntrackedRexWindow(connection, label, failure) {
|
|
180
|
+
try {
|
|
181
|
+
const inspected = await runRex(connection, ["session", "inspect", connection.session, "--json"], "session inspect");
|
|
182
|
+
if (inspected.status === REX_EXIT_GONE)
|
|
183
|
+
throw failure;
|
|
184
|
+
requireRexSuccess(inspected, "session inspect");
|
|
185
|
+
const windows = parseRexJson(inspected.stdout, sessionWindows, "session inspect")?.windows.filter((window) => window.label === label);
|
|
186
|
+
if (!windows)
|
|
187
|
+
throw new Error("Rex session inspect did not list the session's windows.");
|
|
188
|
+
if (windows.length > 1)
|
|
189
|
+
throw new Error(`The Rex session has ${windows.length} windows labeled ${label}; refusing to guess which one this run opened.`);
|
|
190
|
+
for (const window of windows)
|
|
191
|
+
for (const layer of window.layers)
|
|
192
|
+
for (const block of layer.blocks)
|
|
193
|
+
await closeOwnedRexPane(connection, { workspaceId: connection.session, tabId: window.window_id, paneId: block.block_id });
|
|
194
|
+
}
|
|
195
|
+
catch (cleanupError) {
|
|
196
|
+
if (cleanupError === failure)
|
|
197
|
+
throw failure;
|
|
198
|
+
throw new AggregateError([failure, cleanupError], `Rex window new failed, and the window labeled ${label} that it may have opened could not be closed.`, { cause: failure });
|
|
199
|
+
}
|
|
200
|
+
throw failure;
|
|
201
|
+
}
|
|
202
|
+
/** Opens the run's background window; on any failure, closes the window the server may have opened before rethrowing. */
|
|
203
|
+
async function openRexWindow(connection, label, cwd, argv) {
|
|
204
|
+
try {
|
|
205
|
+
const created = await runRex(connection, ["window", "new", "-s", connection.session, label, "-c", cwd, "--focus=false", "--json", "--", ...argv], "window new");
|
|
206
|
+
requireRexSuccess(created, "window new");
|
|
207
|
+
const window = parseRexJson(created.stdout, createdWindow, "window new");
|
|
208
|
+
if (!window)
|
|
209
|
+
throw new Error(`Rex window new did not return exactly one block: ${created.stdout.trim().slice(0, 512)}`);
|
|
210
|
+
return window;
|
|
211
|
+
}
|
|
212
|
+
catch (error) {
|
|
213
|
+
return closeUntrackedRexWindow(connection, label, error instanceof Error ? error : new Error(String(error)));
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
/**
|
|
217
|
+
* Rex pane host: rex CLI calls on this machine, always with an explicit session. A local child opens as a background
|
|
218
|
+
* window in the parent's session; a Rex SSH child in the remote `subagents` session, with every rex call run over SSH.
|
|
219
|
+
* Pi starts through the login shell's environment (as Rex starts any window) with the bridge variables set by
|
|
220
|
+
* /usr/bin/env. There is no status subscription: a dead Pi closes the bridge socket (directly, or through the SSH
|
|
221
|
+
* forward), and the reconnect that follows checks the block with `rex block call process` and fails closed.
|
|
222
|
+
*/
|
|
223
|
+
export const rexPaneHost = {
|
|
224
|
+
connect: connectRex,
|
|
225
|
+
sessionOf(connection) { return connection.session; },
|
|
226
|
+
async launch(connection, machine, runId, _runtimeDir, request, onPane) {
|
|
227
|
+
if (request.kind !== "pi")
|
|
228
|
+
throw new Error("Rex supports native Pi children only.");
|
|
229
|
+
const environment = Object.entries(request.environment ?? {});
|
|
230
|
+
if (environment.some(([name]) => !/^[A-Za-z_][A-Za-z0-9_]*$/u.test(name)))
|
|
231
|
+
throw new Error("Rex launch environment names must be shell variable names.");
|
|
232
|
+
const rex = asRexMachine(machine);
|
|
233
|
+
let pi;
|
|
234
|
+
if (isRexLocalMachine(rex)) {
|
|
235
|
+
pi = getPiSpawnCommand(request.args);
|
|
236
|
+
if (!path.isAbsolute(pi.command))
|
|
237
|
+
throw new Error("Rex placement needs an absolute Pi executable; set PI_SUBAGENT_PI_BINARY to the Pi binary path.");
|
|
238
|
+
}
|
|
239
|
+
else
|
|
240
|
+
pi = { command: await resolveRemotePi(connection.access, rex), args: request.args };
|
|
241
|
+
const window = await openRexWindow(connection, `subagent-${runId.slice(-8)}`, rex.cwd, ["/usr/bin/env", ...environment.map(([name, value]) => `${name}=${value}`), pi.command, ...pi.args]);
|
|
242
|
+
const owned = { workspaceId: connection.session, tabId: window.window_id, paneId: window.block_ids[0] };
|
|
243
|
+
await onPane(owned);
|
|
244
|
+
return { owned, terminalId: owned.paneId };
|
|
245
|
+
},
|
|
246
|
+
async prompt(connection, identity, text) {
|
|
247
|
+
requireRexSuccess(await runRex(connection, ["send", "-s", connection.session, "-b", identity.paneId, "--", text], "send"), "send");
|
|
248
|
+
requireRexSuccess(await runRex(connection, ["send-key", "-s", connection.session, "-b", identity.paneId, "enter"], "send-key"), "send-key");
|
|
249
|
+
},
|
|
250
|
+
async subscribe() { return () => { }; },
|
|
251
|
+
paneEvent() { return undefined; },
|
|
252
|
+
async panes(connection, identity) {
|
|
253
|
+
const processInfo = await runRex(connection, ["block", "call", "-s", connection.session, "-b", identity.paneId, "process"], "block process");
|
|
254
|
+
if (processInfo.status === REX_EXIT_GONE)
|
|
255
|
+
return { present: false };
|
|
256
|
+
requireRexSuccess(processInfo, "block process");
|
|
257
|
+
const candidate = { present: true };
|
|
258
|
+
const foregroundPid = parseRexJson(processInfo.stdout, blockProcess, "block process")?.foreground.pid;
|
|
259
|
+
if (foregroundPid !== undefined)
|
|
260
|
+
candidate.foregroundPid = foregroundPid;
|
|
261
|
+
const bridgePid = await readBridgePid(connection, identity.runtimeDir);
|
|
262
|
+
if (bridgePid !== undefined)
|
|
263
|
+
candidate.bridgePid = bridgePid;
|
|
264
|
+
return candidate;
|
|
265
|
+
},
|
|
266
|
+
reconcile: reconcileRexPlacedRun,
|
|
267
|
+
closeOwnedPane: closeOwnedRexPane,
|
|
268
|
+
};
|
|
269
|
+
function runtimeDirPresent(access, runtimeDir) {
|
|
270
|
+
const result = access.run(access.shellCommand('test -e "$1" || test -L "$1"', [runtimeDir]), { timeout: 10_000, maxBuffer: 4096 });
|
|
271
|
+
if (result.error)
|
|
272
|
+
throw new Error(`Could not check the owned runtime directory: ${result.error.message}`, { cause: result.error });
|
|
273
|
+
return result.status === 0;
|
|
274
|
+
}
|
|
275
|
+
/** A closed local Pi removes its own runtime directory on shutdown, so already-removed counts as removed. */
|
|
276
|
+
function removeRexRuntimeDir(access, runtimeDir) {
|
|
277
|
+
if (!runtimeDirPresent(access, runtimeDir))
|
|
278
|
+
return;
|
|
279
|
+
try {
|
|
280
|
+
removePlacedRuntimeDir(access, runtimeDir);
|
|
281
|
+
}
|
|
282
|
+
catch (error) {
|
|
283
|
+
if (runtimeDirPresent(access, runtimeDir))
|
|
284
|
+
throw error;
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
/** The placed-run owner bound to Rex and the pane machine's access. */
|
|
288
|
+
export class RexPlacedRunOwner extends PlacedRunOwner {
|
|
289
|
+
constructor(machine, runId, runtimeDir, connection, access, host) { super(machine, runId, runtimeDir, connection, host, access, () => removeRexRuntimeDir(access, runtimeDir)); }
|
|
290
|
+
/** A Rex SSH owner's machine carries the resolved remote cwd, and its host reconnects only to the session recorded here. */
|
|
291
|
+
static async create(machine, runId, options = {}) {
|
|
292
|
+
// A short hash names the directory, so /tmp/pi-subagents-herdr-<hash>-XXXXXXXX/bridge.sock (57 bytes) fits the 103-byte macOS socket path limit for any run id.
|
|
293
|
+
const hash = createHash("sha256").update(runId).digest("hex").slice(0, 12);
|
|
294
|
+
if (isRexLocalMachine(machine)) {
|
|
295
|
+
const access = rexAccess(), runtimeDir = await createPlacedRuntimeDir(access, hash);
|
|
296
|
+
try {
|
|
297
|
+
return new RexPlacedRunOwner(machine, runId, runtimeDir, await connectRex(machine, { access }), access, rexPaneHost);
|
|
298
|
+
}
|
|
299
|
+
catch (error) {
|
|
300
|
+
try {
|
|
301
|
+
removeRexRuntimeDir(access, runtimeDir);
|
|
302
|
+
}
|
|
303
|
+
catch (cleanupError) {
|
|
304
|
+
throw new AggregateError([error, cleanupError], "Rex owner creation failed and runtime cleanup also failed.", { cause: error });
|
|
305
|
+
}
|
|
306
|
+
throw error;
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
const access = rexSshAccess(machine, options.sshAccess), bin = await resolveRemoteRexBinary(access, machine);
|
|
310
|
+
const placed = { ...machine, cwd: await resolveRemoteCwd(access, machine) };
|
|
311
|
+
const session = await subagentsSession({ access, bin }, placed);
|
|
312
|
+
const host = { ...rexPaneHost, connect: (target) => connectRex(target, { access, bin, session }) };
|
|
313
|
+
const runtimeDir = await createPlacedRuntimeDir(access, hash);
|
|
314
|
+
try {
|
|
315
|
+
return new RexPlacedRunOwner(placed, runId, runtimeDir, await host.connect(placed), access, host);
|
|
316
|
+
}
|
|
317
|
+
catch (error) {
|
|
318
|
+
try {
|
|
319
|
+
removeRexRuntimeDir(access, runtimeDir);
|
|
320
|
+
}
|
|
321
|
+
catch (cleanupError) {
|
|
322
|
+
throw new AggregateError([error, cleanupError], "Rex owner creation failed and runtime cleanup also failed.", { cause: error });
|
|
323
|
+
}
|
|
324
|
+
throw error;
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
}
|
|
@@ -1,2 +1,7 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The completion owner of a result whose own owner process is dead: whichever session owns the run
|
|
3
|
+
* delivers it. Delivery consumes the result.
|
|
4
|
+
*/
|
|
5
|
+
export declare const OWNING_SESSION_COMPLETION_OWNER = "owning-session";
|
|
1
6
|
/** Stable for one parent Pi process across extension reloads. */
|
|
2
7
|
export declare function currentCompletionOwnerId(): string;
|
|
@@ -1,5 +1,10 @@
|
|
|
1
1
|
import { randomUUID } from "node:crypto";
|
|
2
2
|
const COMPLETION_OWNER_KEY = Symbol.for("pi-subagents.completion-owner-id");
|
|
3
|
+
/**
|
|
4
|
+
* The completion owner of a result whose own owner process is dead: whichever session owns the run
|
|
5
|
+
* delivers it. Delivery consumes the result.
|
|
6
|
+
*/
|
|
7
|
+
export const OWNING_SESSION_COMPLETION_OWNER = "owning-session";
|
|
3
8
|
/** Stable for one parent Pi process across extension reloads. */
|
|
4
9
|
export function currentCompletionOwnerId() {
|
|
5
10
|
const runtime = globalThis;
|
|
@@ -3,4 +3,6 @@ interface SessionIdentityManager {
|
|
|
3
3
|
getSessionId(): string | null | undefined;
|
|
4
4
|
}
|
|
5
5
|
export declare function resolveCurrentSessionId(sessionManager: SessionIdentityManager): string;
|
|
6
|
+
/** The id in a Pi session file's header, which `pi --session <id>` resolves; only ids that need no shell quoting. */
|
|
7
|
+
export declare function resolvePiSessionId(sessionFile: string | undefined): string | undefined;
|
|
6
8
|
export {};
|
|
@@ -1,6 +1,32 @@
|
|
|
1
|
+
import * as fs from "node:fs";
|
|
1
2
|
export function resolveCurrentSessionId(sessionManager) {
|
|
2
3
|
const sessionId = sessionManager.getSessionFile() ?? sessionManager.getSessionId();
|
|
3
4
|
if (!sessionId)
|
|
4
5
|
throw new Error("Current session identity is unavailable.");
|
|
5
6
|
return sessionId;
|
|
6
7
|
}
|
|
8
|
+
/** The id in a Pi session file's header, which `pi --session <id>` resolves; only ids that need no shell quoting. */
|
|
9
|
+
export function resolvePiSessionId(sessionFile) {
|
|
10
|
+
if (!sessionFile)
|
|
11
|
+
return undefined;
|
|
12
|
+
try {
|
|
13
|
+
const fd = fs.openSync(sessionFile, "r");
|
|
14
|
+
try {
|
|
15
|
+
const buffer = Buffer.alloc(16 * 1024);
|
|
16
|
+
const bytes = fs.readSync(fd, buffer, 0, buffer.length, 0);
|
|
17
|
+
const line = buffer.toString("utf-8", 0, bytes).split("\n", 1)[0];
|
|
18
|
+
if (!line)
|
|
19
|
+
return undefined;
|
|
20
|
+
const header = JSON.parse(line);
|
|
21
|
+
if (header.type === "session" && typeof header.id === "string" && /^[A-Za-z0-9-]{8,}$/.test(header.id))
|
|
22
|
+
return header.id;
|
|
23
|
+
return undefined;
|
|
24
|
+
}
|
|
25
|
+
finally {
|
|
26
|
+
fs.closeSync(fd);
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
catch {
|
|
30
|
+
return undefined;
|
|
31
|
+
}
|
|
32
|
+
}
|
package/src/shared/types.d.ts
CHANGED
|
@@ -16,6 +16,8 @@ import type { ExtensionBindings } from "../runs/shared/extension-bindings.ts";
|
|
|
16
16
|
import type { WorkflowChildPermitContext } from "./workflow-child-permit.ts";
|
|
17
17
|
import type { WatchdogWarningDetails } from "../watchdog/types.ts";
|
|
18
18
|
import type { RequiredChildExtensionSnapshot } from "./required-child-extensions.ts";
|
|
19
|
+
import type { RecordedPid } from "../runs/background/stale-run-reconciler.ts";
|
|
20
|
+
import type { WorkflowStopCause } from "../workflows/workflow-stop-cause.ts";
|
|
19
21
|
export interface MaxOutputConfig {
|
|
20
22
|
bytes?: number;
|
|
21
23
|
lines?: number;
|
|
@@ -1166,7 +1168,7 @@ export interface SingleResult {
|
|
|
1166
1168
|
model?: string;
|
|
1167
1169
|
/** Authoritative before/after Git evidence captured by a pane-native remote machine. */
|
|
1168
1170
|
nativeMachine?: {
|
|
1169
|
-
provider: "
|
|
1171
|
+
provider: PlacementMachineReference["provider"];
|
|
1170
1172
|
machineId: string;
|
|
1171
1173
|
initialGit?: HerdrRemoteGitStatus;
|
|
1172
1174
|
finalGit?: HerdrRemoteGitStatus;
|
|
@@ -1431,10 +1433,17 @@ export interface Details {
|
|
|
1431
1433
|
argsDigest?: string;
|
|
1432
1434
|
/** SHA-256 of the workflow script source (async workflows). */
|
|
1433
1435
|
scriptDigest?: string;
|
|
1434
|
-
/** Structured stop cause
|
|
1435
|
-
stopCause?:
|
|
1436
|
-
/**
|
|
1436
|
+
/** Structured stop cause: `runtime-replaced` when the owning extension runtime was replaced; `owner-exited` when stale-run reconciliation found the owning Pi process dead. */
|
|
1437
|
+
stopCause?: WorkflowStopCause;
|
|
1438
|
+
/** Interrupted workflow run whose finished children this run reused. */
|
|
1437
1439
|
reusedFrom?: string;
|
|
1440
|
+
/** Owner-exited runs: the last time the dead Pi process updated the run (epoch ms), not the repair time. */
|
|
1441
|
+
ownerExitedAt?: number;
|
|
1442
|
+
/** Set on a run that resumes an interrupted one; `attempt` counts resumes that followed Pi exits. */
|
|
1443
|
+
resumeOrigin?: {
|
|
1444
|
+
sourceRunId: string;
|
|
1445
|
+
attempt: number;
|
|
1446
|
+
};
|
|
1438
1447
|
resource?: WorkflowResourceProvenance;
|
|
1439
1448
|
preflightWarnings?: string[];
|
|
1440
1449
|
trace: Array<{
|
|
@@ -1450,7 +1459,7 @@ export interface Details {
|
|
|
1450
1459
|
generatedLaneKey?: string;
|
|
1451
1460
|
warning?: string;
|
|
1452
1461
|
error?: string;
|
|
1453
|
-
/** Came from a previous runtime-replaced run of the same script (and args, unless named by options.reuseFrom); this run launched nothing. */
|
|
1462
|
+
/** Came from a previous runtime-replaced or owner-exited run of the same script (and args, unless named by options.reuseFrom); this run launched nothing. */
|
|
1454
1463
|
reused?: boolean;
|
|
1455
1464
|
}>;
|
|
1456
1465
|
emits: unknown[];
|
|
@@ -1673,6 +1682,24 @@ export interface HerdrMachineReference {
|
|
|
1673
1682
|
session?: string;
|
|
1674
1683
|
cwd: string;
|
|
1675
1684
|
}
|
|
1685
|
+
/** `machine: "local"`: a Rex pane in the parent's own Rex session on this machine. `session` is the Rex session id and `server` the Rex server address, both recorded in the parent at resolution time; `cwd` is the local launch directory. */
|
|
1686
|
+
export interface RexLocalMachineReference {
|
|
1687
|
+
provider: "rex";
|
|
1688
|
+
id: "local";
|
|
1689
|
+
session: string;
|
|
1690
|
+
server?: string;
|
|
1691
|
+
cwd: string;
|
|
1692
|
+
}
|
|
1693
|
+
/** A Rex machine reached over SSH, named in user settings (`subagents.machines.<id>.rex.ssh`). The pane opens in that machine's Rex session labeled `subagents`, found or created at launch and recorded with the run; `cwd` is the directory on that machine. */
|
|
1694
|
+
export interface RexSshMachineReference {
|
|
1695
|
+
provider: "rex";
|
|
1696
|
+
id: string;
|
|
1697
|
+
target: string;
|
|
1698
|
+
cwd: string;
|
|
1699
|
+
}
|
|
1700
|
+
export type RexMachineReference = RexLocalMachineReference | RexSshMachineReference;
|
|
1701
|
+
/** Where a placed native Pi child runs. */
|
|
1702
|
+
export type PlacementMachineReference = HerdrMachineReference | RexMachineReference;
|
|
1676
1703
|
export interface ExternalCliMachineStatus extends HerdrMachineReference {
|
|
1677
1704
|
remoteGit?: HerdrRemoteGitStatus;
|
|
1678
1705
|
}
|
|
@@ -1824,6 +1851,8 @@ export interface AsyncStatus {
|
|
|
1824
1851
|
runId: string;
|
|
1825
1852
|
/** Parent Pi process/window that owns local completion delivery. */
|
|
1826
1853
|
completionOwnerId?: string;
|
|
1854
|
+
/** The Pi process behind `completionOwnerId`; once it is dead, the session that owns the run delivers it. */
|
|
1855
|
+
completionOwnerProcess?: RecordedPid;
|
|
1827
1856
|
/** Host tool-call id retained when it differs from the internal run id. */
|
|
1828
1857
|
toolCallId?: string;
|
|
1829
1858
|
sessionId?: string;
|
|
@@ -1860,6 +1889,8 @@ export interface AsyncStatus {
|
|
|
1860
1889
|
pid?: number;
|
|
1861
1890
|
/** Linux PID namespace identity used to scope liveness probes. */
|
|
1862
1891
|
pidNamespaceScope?: string;
|
|
1892
|
+
/** Boot id of the machine when `pid` was recorded; a different current boot id proves the pid dead. */
|
|
1893
|
+
bootId?: string;
|
|
1863
1894
|
cwd?: string;
|
|
1864
1895
|
/** User-configured launcher that wrapped the background runner (argv only, never environment). */
|
|
1865
1896
|
launcher?: RunnerLauncher;
|
|
@@ -2135,6 +2166,10 @@ export interface ForegroundResumeRun {
|
|
|
2135
2166
|
sessionId?: string;
|
|
2136
2167
|
updatedAt: number;
|
|
2137
2168
|
children: ForegroundResumeChild[];
|
|
2169
|
+
/** Pi process running this run; only on records whose child is `running`. */
|
|
2170
|
+
owner?: RecordedPid;
|
|
2171
|
+
/** Workflow that launched this foreground child. */
|
|
2172
|
+
parentWorkflowRunId?: string;
|
|
2138
2173
|
}
|
|
2139
2174
|
export interface ForegroundChildControl {
|
|
2140
2175
|
index: number;
|
|
@@ -2318,6 +2353,8 @@ export interface SubagentState {
|
|
|
2318
2353
|
workflowControllers?: Map<string, AbortController>;
|
|
2319
2354
|
/** Live in-process workflow child stoppers keyed by parent workflow run id. */
|
|
2320
2355
|
workflowChildStops?: Map<string, (childId: string, message?: string) => boolean>;
|
|
2356
|
+
/** Background children awaited by a blocking workflow call: their parent cannot answer supervisor asks. */
|
|
2357
|
+
blockingWorkflowChildren?: Set<string>;
|
|
2321
2358
|
}
|
|
2322
2359
|
export interface HerdrProjectPaneSnapshot {
|
|
2323
2360
|
projectRoot: string;
|
|
@@ -2422,7 +2459,7 @@ export interface RunSyncOptions {
|
|
|
2422
2459
|
context?: "fresh" | "fork";
|
|
2423
2460
|
cwd?: string;
|
|
2424
2461
|
/** Resolved pane-native saved-machine placement. */
|
|
2425
|
-
machine?:
|
|
2462
|
+
machine?: PlacementMachineReference;
|
|
2426
2463
|
/** Explicit read override resolved by the remote ambient agent profile. */
|
|
2427
2464
|
remoteReads?: string[] | false;
|
|
2428
2465
|
/** Original cwd input retained for launch diagnostics. */
|
|
@@ -2583,6 +2620,8 @@ export interface ActiveAsyncCapacityConfig {
|
|
|
2583
2620
|
}
|
|
2584
2621
|
export interface ExtensionConfig {
|
|
2585
2622
|
asyncByDefault?: boolean;
|
|
2623
|
+
/** Relaunch this session's interrupted async workflows when the session is reopened or reloaded (default true). */
|
|
2624
|
+
autoResume?: boolean;
|
|
2586
2625
|
/** Set the context for launches that omit an explicit context. */
|
|
2587
2626
|
defaultSubagentContext?: "fresh" | "fork";
|
|
2588
2627
|
/** Configure how every resolved fork session is prepared before child spawn. */
|
package/src/tui/fleet.d.ts
CHANGED
package/src/tui/fleet.js
CHANGED
|
@@ -16,7 +16,9 @@ import { steerAsyncRun } from "../runs/foreground/async-steering-action.js";
|
|
|
16
16
|
import { stopAsyncRun } from "../runs/foreground/async-stop-action.js";
|
|
17
17
|
import { resolveWorkflowForegroundSteeringTarget, steerWorkflowForegroundTarget } from "../runs/foreground/workflow-foreground-steering.js";
|
|
18
18
|
import { contextModeBadge, contextModeLabel } from "../runs/shared/context-mode.js";
|
|
19
|
+
import { foregroundChildStateLabel } from "../runs/foreground/foreground-history.js";
|
|
19
20
|
import { FLEET_STATUS_WIDGET_KEY, reportFleetWarning } from "./fleet-status.js";
|
|
21
|
+
import { stepTaskSummary } from "./render.js";
|
|
20
22
|
import { readFleetTranscript, renderFleetTranscript } from "./fleet-transcript.js";
|
|
21
23
|
import { runningTone } from "./running-tone.js";
|
|
22
24
|
import { childThinkingLevel } from "../shared/model-info.js";
|
|
@@ -110,18 +112,22 @@ function asyncItems(run, description) {
|
|
|
110
112
|
if (run.steps.length === 0 || run.mode === "workflow") {
|
|
111
113
|
return [{ key: `async:${run.id}`, kind: "async", runId: run.id, agent: run.mode, state: run.state, updatedAt, run, ...(description ? { description } : {}) }];
|
|
112
114
|
}
|
|
113
|
-
return run.steps.map((step) =>
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
115
|
+
return run.steps.map((step) => {
|
|
116
|
+
const task = stepTaskSummary(step, step.agent);
|
|
117
|
+
return {
|
|
118
|
+
key: `async:${run.id}:${step.index}`,
|
|
119
|
+
kind: "async",
|
|
120
|
+
runId: run.id,
|
|
121
|
+
index: step.index,
|
|
122
|
+
agent: step.label ? `${step.label} (${step.agent})` : step.agent,
|
|
123
|
+
state: step.status,
|
|
124
|
+
updatedAt: step.lastActivityAt ?? updatedAt,
|
|
125
|
+
run,
|
|
126
|
+
step,
|
|
127
|
+
...(task ? { task } : {}),
|
|
128
|
+
...(description ? { description } : {}),
|
|
129
|
+
};
|
|
130
|
+
});
|
|
125
131
|
}
|
|
126
132
|
function orderFleetAsyncRuns(runs, terminalLimit, trackedJobs) {
|
|
127
133
|
const updatedAt = (run) => run.lastUpdate ?? run.endedAt ?? run.startedAt;
|
|
@@ -216,6 +222,7 @@ export function collectFleetSnapshot(state, options = {}) {
|
|
|
216
222
|
entryLimit: MAX_FLEET_HISTORY_CANDIDATES,
|
|
217
223
|
resultsDir: options.resultsDir ?? DIRS.results,
|
|
218
224
|
reconcile: false,
|
|
225
|
+
onCorruptStatus: (warning) => reportFleetWarning(state, warning),
|
|
219
226
|
}).filter((run) => !trackedIds.has(run.id));
|
|
220
227
|
runs = [...trackedRuns, ...history];
|
|
221
228
|
}
|
|
@@ -445,7 +452,7 @@ function foregroundRecentDetail(item, state) {
|
|
|
445
452
|
const lines = [
|
|
446
453
|
`Run: ${item.runId}`,
|
|
447
454
|
"Source: foreground",
|
|
448
|
-
`State: ${child
|
|
455
|
+
`State: ${foregroundChildStateLabel(run, child)}`,
|
|
449
456
|
`Mode: ${run.mode}`,
|
|
450
457
|
`Child: ${child.index} (${child.agent})${contextModeLabel(child.context) ? ` ${contextModeLabel(child.context)}` : ""}`,
|
|
451
458
|
modelThinking ? `Model: ${modelThinking}` : undefined,
|
|
@@ -1226,8 +1233,11 @@ export class SubagentFleetComponent {
|
|
|
1226
1233
|
const item = this.snapshot.items[index];
|
|
1227
1234
|
const marker = index === this.selected ? this.theme.fg("accent", "›") : " ";
|
|
1228
1235
|
const context = item.kind === "async" ? contextModeBadge(this.theme, item.step?.context ?? item.run.context) : item.kind === "foreground-recent" ? contextModeBadge(this.theme, item.child.context) : "";
|
|
1229
|
-
|
|
1230
|
-
const
|
|
1236
|
+
// A child's task tells rows of one run apart; the run id stays in the detail header.
|
|
1237
|
+
const task = item.kind === "async" ? item.task : undefined;
|
|
1238
|
+
const name = task && item.kind === "async" && item.step ? item.step.agent : item.agent;
|
|
1239
|
+
const agent = index === this.selected ? this.theme.bold(name) : name;
|
|
1240
|
+
const identity = task ?? item.runId.slice(0, 8);
|
|
1231
1241
|
const left = `${marker} ${statusGlyph(item, this.theme)} ${agent}${context} ${this.theme.fg("dim", `· ${identity}`)}`;
|
|
1232
1242
|
return rightAligned(left, this.theme.fg("dim", item.state), layout.roster.width);
|
|
1233
1243
|
});
|
package/src/tui/render.d.ts
CHANGED
|
@@ -39,6 +39,12 @@ export interface AsyncLaneProjection {
|
|
|
39
39
|
/** Project already-loaded async status facts into one bounded, render-only lane row. */
|
|
40
40
|
export declare function projectAsyncLane(job: AsyncJobState, ...args: [selectedStep?: AsyncJobStep]): AsyncLaneProjection | undefined;
|
|
41
41
|
export declare function widgetRenderKey(job: AsyncJobState, expanded?: boolean): string;
|
|
42
|
+
/** A child's task in one line: its label and task, else its session name without the repeated agent name. */
|
|
43
|
+
export declare function stepTaskSummary(step: {
|
|
44
|
+
description?: string;
|
|
45
|
+
label?: string;
|
|
46
|
+
sessionName?: string;
|
|
47
|
+
}, agentName: string | undefined): string | undefined;
|
|
42
48
|
/** Structural identity of the workflow rows that Fleet can cover. */
|
|
43
49
|
export declare function inlineWorkflowRenderKey(job: AsyncJobState, children: AsyncJobState[]): string;
|
|
44
50
|
/** Presentation-only coverage from the mounted inline Fleet roster, never configuration. */
|
package/src/tui/render.js
CHANGED
|
@@ -1104,14 +1104,14 @@ function shouldCollapseSingleChildDetails(job, step) {
|
|
|
1104
1104
|
return isCompletedWidgetStepStatus(step.status);
|
|
1105
1105
|
return false;
|
|
1106
1106
|
}
|
|
1107
|
-
|
|
1107
|
+
/** A child's task in one line: its label and task, else its session name without the repeated agent name. */
|
|
1108
|
+
export function stepTaskSummary(step, agentName) {
|
|
1108
1109
|
const task = compactTaskText(step.description, step.label);
|
|
1109
1110
|
if (task)
|
|
1110
1111
|
return task;
|
|
1111
1112
|
const sessionName = step.sessionName?.trim();
|
|
1112
1113
|
if (!sessionName)
|
|
1113
1114
|
return undefined;
|
|
1114
|
-
const agentName = singleChildAgentName(job, step);
|
|
1115
1115
|
const sessionTask = stripRepeatedAgentPrefix(sessionName, agentName);
|
|
1116
1116
|
return sessionTask === agentName ? undefined : compactTaskText(sessionTask);
|
|
1117
1117
|
}
|
|
@@ -2223,7 +2223,7 @@ function foregroundStyleWidgetStepLines(job, theme, step, itemTitle, index, tota
|
|
|
2223
2223
|
const lane = projectAsyncLane(job, step);
|
|
2224
2224
|
if (lane)
|
|
2225
2225
|
lines.push(...formatLaneProjectionLines(lane, theme, detailIndent));
|
|
2226
|
-
const task = collapseDetails ?
|
|
2226
|
+
const task = collapseDetails ? stepTaskSummary(step, displayName) : compactTaskText(step.description, step.label);
|
|
2227
2227
|
if (task)
|
|
2228
2228
|
lines.push(`${detailIndent}${theme.fg("dim", `task: ${task}`)}`);
|
|
2229
2229
|
const activity = widgetStepActivityLine(step, width, expanded, job.updatedAt);
|
|
@@ -183,6 +183,7 @@ const MESSAGE_VIEWS = new Map([
|
|
|
183
183
|
["subagent-wait-subscription", readsContent("result", (message) => waitSubscriptionHeadline(message.details))],
|
|
184
184
|
["subagent-incremental-child-notify", readsContent("result", workflowChildHeadline)],
|
|
185
185
|
["subagent-workflow-result-write-failed", readsContent("result", () => ["workflow result ", { status: "save failed" }])],
|
|
186
|
+
["subagent-auto-resume", readsContent("result", () => ["workflow auto-resume ", { status: "failed" }])],
|
|
186
187
|
[SUBAGENT_CONTROL_MESSAGE_TYPE, readsContent("attention", (message) => controlNoticeHeadline(message.details))],
|
|
187
188
|
[SUBAGENT_STEERING_MESSAGE_TYPE, readsContent("attention", (message) => steeringNoticeHeadline(message.details))],
|
|
188
189
|
[SUBAGENT_WATCHDOG_WARNING_TYPE, (message) => watchdogWarningView(message.details, contentText(message.content))],
|