@henryqw/pi-subagent 18.0.1 → 22.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CONTEXT.md +76 -24
- package/README.md +116 -142
- package/dist/composition.d.ts +11 -0
- package/dist/composition.js +107 -0
- package/dist/direct-herdr.d.ts +17 -0
- package/dist/direct-herdr.js +219 -0
- package/dist/ephemeral.d.ts +2 -2
- package/dist/ephemeral.js +7 -7
- package/dist/git-runtime.d.ts +110 -0
- package/dist/git-runtime.js +472 -0
- package/dist/herdr-runtime.d.ts +137 -0
- package/dist/herdr-runtime.js +1228 -0
- package/dist/index.d.ts +11 -5
- package/dist/index.js +20 -16
- package/dist/integration-git.d.ts +59 -0
- package/dist/integration-git.js +484 -0
- package/dist/launch-runtime.d.ts +54 -0
- package/dist/launch-runtime.js +195 -0
- package/dist/process.d.ts +9 -0
- package/dist/process.js +11 -0
- package/dist/runner.d.ts +356 -0
- package/dist/runner.js +2710 -0
- package/dist/schema.d.ts +2234 -0
- package/dist/schema.js +1236 -0
- package/dist/store.d.ts +50 -0
- package/dist/store.js +288 -0
- package/dist/working-evidence.d.ts +25 -0
- package/dist/working-evidence.js +261 -0
- package/docs/adr/001-composable-ephemeral-execution.md +3 -3
- package/docs/delegation-routing.html +89 -0
- package/docs/delegation-routing.svg +78 -0
- package/docs/orchestration-architecture.html +238 -0
- package/docs/orchestration-architecture.svg +189 -0
- package/docs/orchestration.md +122 -331
- package/example.png +0 -0
- package/examples/roles/implementer.md +2 -3
- package/extensions/admission.ts +66 -0
- package/extensions/config.ts +51 -37
- package/extensions/isolated.ts +638 -0
- package/extensions/result-transport.ts +13 -103
- package/extensions/role-tools.ts +11 -11
- package/extensions/subagent-command.ts +167 -0
- package/extensions/subagent.ts +264 -540
- package/extensions/task-name.ts +1 -2
- package/extensions/workflow.ts +60 -96
- package/package.json +14 -6
- package/skills/pi-subagent/SKILL.md +35 -0
|
@@ -0,0 +1,1228 @@
|
|
|
1
|
+
import { createHash, randomBytes } from "node:crypto";
|
|
2
|
+
import { constants } from "node:fs";
|
|
3
|
+
import { lstat, mkdir, open, realpath, rmdir, unlink } from "node:fs/promises";
|
|
4
|
+
import { basename, dirname, isAbsolute, join, resolve, sep } from "node:path";
|
|
5
|
+
import { setTimeout as sleep } from "node:timers/promises";
|
|
6
|
+
import { extensionConfigDir } from "@henryqw/pi-config-store";
|
|
7
|
+
import { createHerdrClient, hasHerdrErrorCode, herdrCommandFailure, startPiAgent, } from "@henryqw/pi-herdr";
|
|
8
|
+
import { isCleanCommitted, } from "./schema.js";
|
|
9
|
+
import { buildChangesetTaskPrompt, withTransientLaunch, } from "./runner.js";
|
|
10
|
+
import { runProcess as defaultRunProcess } from "./process.js";
|
|
11
|
+
const MIN_HERDR_VERSION = [0, 9, 0];
|
|
12
|
+
const MIN_HERDR_PROTOCOL = 22;
|
|
13
|
+
const HERDR_OPERATION_CAP_MS = 30_000;
|
|
14
|
+
const LSOF_OPERATION_CAP_MS = 3_000;
|
|
15
|
+
const PROCESS_INSPECTION_CAP_MS = 3_000;
|
|
16
|
+
const GIT_INSPECTION_CAP_MS = 30_000;
|
|
17
|
+
const STALLED_PROMPT_POLL_MS = 250;
|
|
18
|
+
const HOST_LAYOUT_POLL_MS = 50;
|
|
19
|
+
const HOST_LAYOUT_MAX_POLLS = 40;
|
|
20
|
+
const DIAGNOSTIC_LIMIT = 8 * 1024;
|
|
21
|
+
const LEASE_MODE = 0o600;
|
|
22
|
+
const DIRECTORY_MODE = 0o700;
|
|
23
|
+
const PROCESS_LEASE_ENV = "PI_SUBAGENT_PROCESS_LEASE";
|
|
24
|
+
const CORRELATION_TOKEN_PATTERN = /^[A-Za-z0-9_-]{16,128}$/;
|
|
25
|
+
const HERDR_AGENT_NAME_PATTERN = /^[a-z][a-z0-9_-]{0,31}$/;
|
|
26
|
+
const SETTLED_AGENT_STATES = new Set(["idle", "done"]);
|
|
27
|
+
function record(value, label) {
|
|
28
|
+
if (!value || typeof value !== "object" || Array.isArray(value))
|
|
29
|
+
throw new Error(`${label} is malformed.`);
|
|
30
|
+
return value;
|
|
31
|
+
}
|
|
32
|
+
function resultRecord(value, label) {
|
|
33
|
+
return record(record(value, label).result, `${label} result`);
|
|
34
|
+
}
|
|
35
|
+
function parseJsonObject(value, label) {
|
|
36
|
+
try {
|
|
37
|
+
return record(JSON.parse(value), label);
|
|
38
|
+
}
|
|
39
|
+
catch (error) {
|
|
40
|
+
throw new Error(`${label} returned malformed JSON.`, { cause: error });
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
function exactString(value, label) {
|
|
44
|
+
if (typeof value !== "string" || !value.trim() || value.trim() !== value || value.includes("\0")) {
|
|
45
|
+
throw new Error(`${label} must be a non-empty exact string.`);
|
|
46
|
+
}
|
|
47
|
+
return value;
|
|
48
|
+
}
|
|
49
|
+
function exactAbsolutePath(value, label) {
|
|
50
|
+
const path = exactString(value, label);
|
|
51
|
+
if (!isAbsolute(path))
|
|
52
|
+
throw new Error(`${label} must be an absolute path.`);
|
|
53
|
+
return path;
|
|
54
|
+
}
|
|
55
|
+
function safeText(error) {
|
|
56
|
+
return (error instanceof Error ? error.message : String(error)).slice(0, 1_000);
|
|
57
|
+
}
|
|
58
|
+
function compareVersion(actual, minimum) {
|
|
59
|
+
const match = /^(\d+)\.(\d+)\.(\d+)(?:(-[0-9A-Za-z.-]+)|\+[0-9A-Za-z.-]+)?$/.exec(actual);
|
|
60
|
+
if (!match)
|
|
61
|
+
return false;
|
|
62
|
+
const values = match.slice(1, 4).map(Number);
|
|
63
|
+
for (let index = 0; index < minimum.length; index += 1) {
|
|
64
|
+
if (values[index] !== minimum[index])
|
|
65
|
+
return values[index] > minimum[index];
|
|
66
|
+
}
|
|
67
|
+
return match[4] === undefined;
|
|
68
|
+
}
|
|
69
|
+
function statusFields(stdout) {
|
|
70
|
+
const result = new Map();
|
|
71
|
+
for (const line of stdout.trim().split(/\r?\n/)) {
|
|
72
|
+
const separator = line.indexOf(":");
|
|
73
|
+
if (separator < 1)
|
|
74
|
+
throw new Error("herdr status server returned malformed output.");
|
|
75
|
+
const key = line.slice(0, separator).trim();
|
|
76
|
+
const value = line.slice(separator + 1).trim();
|
|
77
|
+
if (!key || !value || result.has(key))
|
|
78
|
+
throw new Error("herdr status server returned ambiguous output.");
|
|
79
|
+
result.set(key, value);
|
|
80
|
+
}
|
|
81
|
+
return result;
|
|
82
|
+
}
|
|
83
|
+
function localSchemaReference(schema, reference) {
|
|
84
|
+
if (typeof reference !== "string" || !reference.startsWith("#/schemas/request/$defs/")) {
|
|
85
|
+
throw new Error("Herdr API schema contains an unsupported request reference.");
|
|
86
|
+
}
|
|
87
|
+
const name = reference.slice("#/schemas/request/$defs/".length);
|
|
88
|
+
const schemas = record(schema.schemas, "Herdr API schemas");
|
|
89
|
+
const request = record(schemas.request, "Herdr request schema");
|
|
90
|
+
const definitions = record(request.$defs, "Herdr request definitions");
|
|
91
|
+
return record(definitions[name], `Herdr request definition ${name}`);
|
|
92
|
+
}
|
|
93
|
+
function supportsString(value) {
|
|
94
|
+
if (value === "string")
|
|
95
|
+
return true;
|
|
96
|
+
return Array.isArray(value) && value.includes("string");
|
|
97
|
+
}
|
|
98
|
+
function requireSchemaCapabilities(schema) {
|
|
99
|
+
if (typeof schema.protocol !== "number" || !Number.isInteger(schema.protocol) || schema.protocol < MIN_HERDR_PROTOCOL) {
|
|
100
|
+
throw new Error(`Herdr API schema protocol must be at least ${MIN_HERDR_PROTOCOL}.`);
|
|
101
|
+
}
|
|
102
|
+
const schemas = record(schema.schemas, "Herdr API schemas");
|
|
103
|
+
const request = record(schemas.request, "Herdr request schema");
|
|
104
|
+
const definitions = request.oneOf;
|
|
105
|
+
if (!Array.isArray(definitions))
|
|
106
|
+
throw new Error("Herdr API request schema has no command definitions.");
|
|
107
|
+
const method = (name) => {
|
|
108
|
+
const matches = definitions.filter((entry) => {
|
|
109
|
+
try {
|
|
110
|
+
const properties = record(record(entry, "Herdr method").properties, "Herdr method properties");
|
|
111
|
+
return record(properties.method, "Herdr method name").const === name;
|
|
112
|
+
}
|
|
113
|
+
catch {
|
|
114
|
+
return false;
|
|
115
|
+
}
|
|
116
|
+
});
|
|
117
|
+
if (matches.length !== 1)
|
|
118
|
+
throw new Error(`Herdr API schema must advertise exactly one ${name} request.`);
|
|
119
|
+
return record(matches[0], `Herdr ${name} request`);
|
|
120
|
+
};
|
|
121
|
+
const params = (name) => {
|
|
122
|
+
const properties = record(method(name).properties, `Herdr ${name} properties`);
|
|
123
|
+
return localSchemaReference(schema, record(properties.params, `Herdr ${name} params`).$ref);
|
|
124
|
+
};
|
|
125
|
+
for (const name of ["pane.close", "pane.process_info"]) {
|
|
126
|
+
const properties = record(params(name).properties, `Herdr ${name} parameter properties`);
|
|
127
|
+
if (!supportsString(record(properties.pane_id, `Herdr ${name} pane_id`).type)) {
|
|
128
|
+
throw new Error(`Herdr API schema does not support ID-addressable ${name}.`);
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
const tabProperties = record(params("tab.create").properties, "Herdr tab.create parameter properties");
|
|
132
|
+
const env = record(tabProperties.env, "Herdr tab.create env");
|
|
133
|
+
const additional = record(env.additionalProperties, "Herdr tab.create env values");
|
|
134
|
+
if (env.type !== "object" || !supportsString(additional.type)) {
|
|
135
|
+
throw new Error("Herdr API schema does not advertise tab.create env support.");
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
function ownedIntent(attempt, kind) {
|
|
139
|
+
const matches = attempt.allocations.filter((intent) => intent.kind === kind && intent.status === "owned");
|
|
140
|
+
if (matches.length !== 1)
|
|
141
|
+
throw new Error(`Task lacks one exact owned ${kind} allocation.`);
|
|
142
|
+
return matches[0];
|
|
143
|
+
}
|
|
144
|
+
function worktreeIntent(attempt) {
|
|
145
|
+
const intent = ownedIntent(attempt, "worktree");
|
|
146
|
+
if (!intent.worktree || intent.worktree.cwd !== intent.worktree.path) {
|
|
147
|
+
throw new Error("Task lacks exact owned worktree metadata.");
|
|
148
|
+
}
|
|
149
|
+
return intent.worktree;
|
|
150
|
+
}
|
|
151
|
+
export function workspaceLabel(attempt) {
|
|
152
|
+
return attempt.correlationToken.slice(-6);
|
|
153
|
+
}
|
|
154
|
+
function legacyWorkspaceLabel(requestId, task, attempt) {
|
|
155
|
+
return `${requestId}/${task.id}#${attempt.number}`;
|
|
156
|
+
}
|
|
157
|
+
function expectedWorkerLabel(task) {
|
|
158
|
+
return `${task.role}/${task.modelClass}`;
|
|
159
|
+
}
|
|
160
|
+
function expectedAgentName(token) {
|
|
161
|
+
if (!CORRELATION_TOKEN_PATTERN.test(token))
|
|
162
|
+
throw new Error("Agent correlation token is invalid.");
|
|
163
|
+
const segment = token.length <= 24 && !/[^0-9a-f]/.test(token)
|
|
164
|
+
? token
|
|
165
|
+
: createHash("sha256").update(token).digest("hex").slice(0, 24);
|
|
166
|
+
const name = `o-${segment}-agent`;
|
|
167
|
+
if (!HERDR_AGENT_NAME_PATTERN.test(name))
|
|
168
|
+
throw new Error("Agent correlation token cannot produce a valid Herdr name.");
|
|
169
|
+
return name;
|
|
170
|
+
}
|
|
171
|
+
function requireIntentIdentity(intent, attempt) {
|
|
172
|
+
if (intent.token !== attempt.correlationToken || intent.generation !== attempt.allocationGeneration) {
|
|
173
|
+
throw new Error(`${intent.kind} allocation intent does not match the task attempt.`);
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
function assertWorkspaceIntent(intent, requestId, task, attempt) {
|
|
177
|
+
const worktree = worktreeIntent(attempt);
|
|
178
|
+
const label = exactString(intent.label, "workspace label");
|
|
179
|
+
if ((label !== workspaceLabel(attempt) && label !== legacyWorkspaceLabel(requestId, task, attempt))
|
|
180
|
+
|| exactAbsolutePath(intent.worktreeCwd, "workspace worktree cwd") !== worktree.cwd
|
|
181
|
+
|| exactAbsolutePath(intent.mainRoot, "workspace Main root") !== worktree.repoRoot) {
|
|
182
|
+
throw new Error("Workspace allocation plan drifted from the exact owned worktree.");
|
|
183
|
+
}
|
|
184
|
+
exactAbsolutePath(intent.repoKey, "workspace repository key");
|
|
185
|
+
exactAbsolutePath(intent.herdrRepoRoot, "workspace Herdr repository root");
|
|
186
|
+
}
|
|
187
|
+
function assertWorkerTabIntent(intent, task, attempt) {
|
|
188
|
+
const worktree = worktreeIntent(attempt);
|
|
189
|
+
const workspace = ownedIntent(attempt, "workspace");
|
|
190
|
+
if (exactString(intent.label, "worker-tab label") !== expectedWorkerLabel(task)
|
|
191
|
+
|| exactString(intent.workspaceId, "worker-tab workspace ID") !== workspace.workspaceId
|
|
192
|
+
|| exactString(intent.workspaceRootTabId, "workspace root tab ID") !== workspace.rootTabId
|
|
193
|
+
|| exactString(intent.workspaceRootPaneId, "workspace root pane ID") !== workspace.rootPaneId
|
|
194
|
+
|| exactAbsolutePath(intent.worktreeCwd, "worker-tab cwd") !== worktree.cwd) {
|
|
195
|
+
throw new Error("Worker-tab allocation plan drifted from its exact parent resources.");
|
|
196
|
+
}
|
|
197
|
+
exactAbsolutePath(intent.leasePath, "worker-tab lease path");
|
|
198
|
+
}
|
|
199
|
+
function assertAgentIntent(intent, attempt) {
|
|
200
|
+
const worktree = worktreeIntent(attempt);
|
|
201
|
+
const workspace = ownedIntent(attempt, "workspace");
|
|
202
|
+
const tab = ownedIntent(attempt, "worker_tab");
|
|
203
|
+
if (exactString(intent.agentName, "agent name") !== expectedAgentName(intent.token)
|
|
204
|
+
|| exactString(intent.workspaceId, "agent workspace ID") !== workspace.workspaceId
|
|
205
|
+
|| exactString(intent.tabId, "agent tab ID") !== tab.tabId
|
|
206
|
+
|| exactString(intent.paneId, "agent pane ID") !== tab.paneId
|
|
207
|
+
|| exactAbsolutePath(intent.worktreeCwd, "agent worktree cwd") !== worktree.cwd
|
|
208
|
+
|| exactAbsolutePath(intent.leasePath, "agent lease path") !== tab.leasePath) {
|
|
209
|
+
throw new Error("Agent allocation plan drifted from its exact parent resources.");
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
function parseWorkspaceInfo(response, expectedId) {
|
|
213
|
+
const result = resultRecord(response, "Herdr workspace response");
|
|
214
|
+
if (result.type !== "workspace_info")
|
|
215
|
+
throw new Error("Herdr workspace response has the wrong type.");
|
|
216
|
+
const workspace = record(result.workspace, "Herdr workspace");
|
|
217
|
+
const workspaceId = exactString(workspace.workspace_id, "Herdr workspace_id");
|
|
218
|
+
if (expectedId && workspaceId !== expectedId)
|
|
219
|
+
throw new Error("Herdr workspace response returned a different workspace ID.");
|
|
220
|
+
return workspace;
|
|
221
|
+
}
|
|
222
|
+
function parseAgent(response, expected, acceptedTypes) {
|
|
223
|
+
const result = resultRecord(response, "Herdr agent response");
|
|
224
|
+
if (!acceptedTypes.includes(String(result.type)))
|
|
225
|
+
throw new Error("Herdr agent response has the wrong type.");
|
|
226
|
+
const agent = record(result.agent, "Herdr agent");
|
|
227
|
+
if (exactString(agent.name, "Herdr agent name") !== expected.agentName
|
|
228
|
+
|| exactString(agent.pane_id, "Herdr agent pane_id") !== expected.paneId
|
|
229
|
+
|| exactString(agent.tab_id, "Herdr agent tab_id") !== expected.tabId
|
|
230
|
+
|| exactString(agent.workspace_id, "Herdr agent workspace_id") !== expected.workspaceId
|
|
231
|
+
|| exactString(agent.cwd, "Herdr agent cwd") !== expected.worktreeCwd) {
|
|
232
|
+
throw new Error("Herdr agent response does not match the exact saved worker identity.");
|
|
233
|
+
}
|
|
234
|
+
const status = exactString(agent.agent_status, "Herdr agent status");
|
|
235
|
+
if (!["idle", "working", "blocked", "done", "unknown"].includes(status))
|
|
236
|
+
throw new Error("Herdr agent status is unsupported.");
|
|
237
|
+
return { status, interactiveReady: agent.interactive_ready === true };
|
|
238
|
+
}
|
|
239
|
+
function requireOkResponse(stdout, label) {
|
|
240
|
+
if (resultRecord(parseJsonObject(stdout, label), label).type !== "ok") {
|
|
241
|
+
throw new Error(`${label} has the wrong type.`);
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
export class HerdrHostRuntime {
|
|
245
|
+
inspectInFlightCandidate;
|
|
246
|
+
execute;
|
|
247
|
+
herdr;
|
|
248
|
+
kill;
|
|
249
|
+
delay;
|
|
250
|
+
now;
|
|
251
|
+
randomId;
|
|
252
|
+
env;
|
|
253
|
+
leaseDirectory;
|
|
254
|
+
lsofCommand;
|
|
255
|
+
constructor(options) {
|
|
256
|
+
this.inspectInFlightCandidate = options.inspectInFlightTaskCandidate;
|
|
257
|
+
this.execute = options.runProcess ?? defaultRunProcess;
|
|
258
|
+
this.herdr = createHerdrClient(this.execute);
|
|
259
|
+
this.kill = options.killProcess ?? ((pid, signal) => process.kill(pid, signal));
|
|
260
|
+
this.delay = options.delay ?? (async (milliseconds, signal) => await sleep(milliseconds, undefined, { signal, ref: false }));
|
|
261
|
+
this.now = options.now ?? Date.now;
|
|
262
|
+
this.randomId = options.randomId ?? (() => randomBytes(16).toString("hex"));
|
|
263
|
+
this.env = options.env ?? process.env;
|
|
264
|
+
this.leaseDirectory = resolve(options.leaseDirectory ?? join(extensionConfigDir("pi-subagent"), "leases"));
|
|
265
|
+
this.lsofCommand = options.lsofCommand ?? "lsof";
|
|
266
|
+
}
|
|
267
|
+
/** Fail-closed Herdr caller/capability gate. This method creates no state or host resource. */
|
|
268
|
+
async preflightHost(input, context) {
|
|
269
|
+
context.signal.throwIfAborted();
|
|
270
|
+
if (this.env.HERDR_ENV !== "1")
|
|
271
|
+
throw new Error("Pi Subagent requires HERDR_ENV=1.");
|
|
272
|
+
const paneId = exactString(this.env.HERDR_PANE_ID, "HERDR_PANE_ID");
|
|
273
|
+
const root = await realpath(input.root);
|
|
274
|
+
const versionOutput = await this.herdr.run(["--version"], this.processOptions(root, context, HERDR_OPERATION_CAP_MS));
|
|
275
|
+
const versionMatch = /^herdr (\S+)\s*$/.exec(versionOutput);
|
|
276
|
+
if (!versionMatch || !compareVersion(versionMatch[1], MIN_HERDR_VERSION)) {
|
|
277
|
+
throw new Error("Herdr client version must be at least 0.9.0.");
|
|
278
|
+
}
|
|
279
|
+
const status = statusFields(await this.herdr.run(["status", "server"], this.processOptions(root, context, HERDR_OPERATION_CAP_MS)));
|
|
280
|
+
if (status.get("status") !== "running"
|
|
281
|
+
|| !compareVersion(status.get("version") ?? "", MIN_HERDR_VERSION)
|
|
282
|
+
|| status.get("endpoint_compatible") !== "yes"
|
|
283
|
+
|| status.get("private_protocol_compatible") !== "yes"
|
|
284
|
+
|| !/^\d+$/.test(status.get("private_protocol") ?? "")
|
|
285
|
+
|| Number(status.get("private_protocol")) < MIN_HERDR_PROTOCOL) {
|
|
286
|
+
throw new Error("Herdr server must be compatible version >=0.9.0 with private protocol >=22.");
|
|
287
|
+
}
|
|
288
|
+
const schema = await this.herdr.json(["api", "schema", "--json"], this.processOptions(root, context, HERDR_OPERATION_CAP_MS));
|
|
289
|
+
requireSchemaCapabilities(schema);
|
|
290
|
+
const lsof = await this.execute(this.lsofCommand, ["-v"], this.processOptions(root, context, LSOF_OPERATION_CAP_MS));
|
|
291
|
+
if (lsof.code !== 0 || lsof.killed)
|
|
292
|
+
throw new Error("Native lsof is required for exact worker process leases.");
|
|
293
|
+
const paneResponse = await this.herdr.json(["pane", "get", paneId], this.processOptions(root, context, HERDR_OPERATION_CAP_MS));
|
|
294
|
+
const paneResult = resultRecord(paneResponse, "Herdr current pane response");
|
|
295
|
+
if (paneResult.type !== "pane_info")
|
|
296
|
+
throw new Error("Herdr current pane response has the wrong type.");
|
|
297
|
+
const pane = record(paneResult.pane, "Herdr current pane");
|
|
298
|
+
if (exactString(pane.pane_id, "Herdr current pane_id") !== paneId)
|
|
299
|
+
throw new Error("Herdr current pane ID does not match HERDR_PANE_ID.");
|
|
300
|
+
const workspaceId = exactString(pane.workspace_id, "Herdr current workspace_id");
|
|
301
|
+
const workspaceResponse = await this.herdr.json(["workspace", "get", workspaceId], this.processOptions(root, context, HERDR_OPERATION_CAP_MS));
|
|
302
|
+
const workspace = parseWorkspaceInfo(workspaceResponse, workspaceId);
|
|
303
|
+
const worktree = record(workspace.worktree, "Herdr current workspace worktree");
|
|
304
|
+
const checkout = await realpath(exactString(worktree.checkout_path, "Herdr current checkout_path"));
|
|
305
|
+
if (checkout !== root)
|
|
306
|
+
throw new Error("The current Herdr workspace checkout does not match canonical Main.");
|
|
307
|
+
const identity = await this.repositoryIdentity(root, context);
|
|
308
|
+
const repoKey = await realpath(exactString(worktree.repo_key, "Herdr current repo_key"));
|
|
309
|
+
if (repoKey !== identity.repoKey)
|
|
310
|
+
throw new Error("The current Herdr workspace repo_key does not match Git's common directory.");
|
|
311
|
+
const repoRoot = await realpath(exactString(worktree.repo_root, "Herdr current repo_root"));
|
|
312
|
+
if (repoRoot !== identity.herdrRepoRoot) {
|
|
313
|
+
throw new Error("The current Herdr workspace repo_root does not match Git's primary checkout.");
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
async planHostAllocation(input, context) {
|
|
317
|
+
context.signal.throwIfAborted();
|
|
318
|
+
const worktree = worktreeIntent(input.attempt);
|
|
319
|
+
const worktreeCwd = exactAbsolutePath(worktree.cwd, "owned worktree cwd");
|
|
320
|
+
if (input.kind === "workspace") {
|
|
321
|
+
if (input.task.kind !== "changeset")
|
|
322
|
+
throw new Error("Herdr assignment requires a changeset task.");
|
|
323
|
+
buildChangesetTaskPrompt({ goal: input.goal, contexts: [], task: input.task, kind: "initial", worktreeCwd });
|
|
324
|
+
const mainRoot = exactAbsolutePath(worktree.repoRoot, "owned worktree Main root");
|
|
325
|
+
const identity = await this.repositoryIdentity(mainRoot, context);
|
|
326
|
+
return {
|
|
327
|
+
kind: "workspace",
|
|
328
|
+
label: workspaceLabel(input.attempt),
|
|
329
|
+
worktreeCwd,
|
|
330
|
+
mainRoot,
|
|
331
|
+
...identity,
|
|
332
|
+
};
|
|
333
|
+
}
|
|
334
|
+
const workspace = ownedIntent(input.attempt, "workspace");
|
|
335
|
+
const workspaceId = exactString(workspace.workspaceId, "owned workspace ID");
|
|
336
|
+
if (input.kind === "worker_tab") {
|
|
337
|
+
const leasePath = join(this.leaseDirectory, input.attempt.correlationToken, `${this.randomId()}.lease`);
|
|
338
|
+
this.assertLeasePath(leasePath, input.attempt.correlationToken);
|
|
339
|
+
return {
|
|
340
|
+
kind: "worker_tab",
|
|
341
|
+
label: expectedWorkerLabel(input.task),
|
|
342
|
+
workspaceId,
|
|
343
|
+
workspaceRootTabId: exactString(workspace.rootTabId, "owned workspace root tab ID"),
|
|
344
|
+
workspaceRootPaneId: exactString(workspace.rootPaneId, "owned workspace root pane ID"),
|
|
345
|
+
worktreeCwd,
|
|
346
|
+
leasePath,
|
|
347
|
+
};
|
|
348
|
+
}
|
|
349
|
+
const tab = ownedIntent(input.attempt, "worker_tab");
|
|
350
|
+
const tabId = exactString(tab.tabId, "owned worker tab ID");
|
|
351
|
+
const leasePath = exactString(tab.leasePath, "owned worker lease path");
|
|
352
|
+
this.assertLeasePath(leasePath, input.attempt.correlationToken);
|
|
353
|
+
return {
|
|
354
|
+
kind: "agent",
|
|
355
|
+
agentName: expectedAgentName(input.attempt.correlationToken),
|
|
356
|
+
workspaceId,
|
|
357
|
+
tabId,
|
|
358
|
+
paneId: exactString(tab.paneId, "owned worker pane ID"),
|
|
359
|
+
worktreeCwd,
|
|
360
|
+
leasePath,
|
|
361
|
+
};
|
|
362
|
+
}
|
|
363
|
+
async allocateHost(input, context) {
|
|
364
|
+
requireIntentIdentity(input.intent, input.attempt);
|
|
365
|
+
if (input.intent.kind === "workspace") {
|
|
366
|
+
if (input.acquireLaunch)
|
|
367
|
+
throw new Error("Role launch acquisition is valid only at the exact agent allocation boundary.");
|
|
368
|
+
return await this.allocateWorkspace(input.intent, input.requestId, input.task, input.attempt, context);
|
|
369
|
+
}
|
|
370
|
+
if (input.intent.kind === "worker_tab") {
|
|
371
|
+
if (input.acquireLaunch)
|
|
372
|
+
throw new Error("Role launch acquisition is valid only at the exact agent allocation boundary.");
|
|
373
|
+
return await this.allocateWorkerTab(input.intent, input.task, input.attempt, context);
|
|
374
|
+
}
|
|
375
|
+
return await this.allocateAgent(input.intent, input.task.role, input.attempt, input.acquireLaunch, context);
|
|
376
|
+
}
|
|
377
|
+
async reconcileHostAllocation(input, context) {
|
|
378
|
+
requireIntentIdentity(input.intent, input.attempt);
|
|
379
|
+
const allocation = input.intent;
|
|
380
|
+
if (allocation.kind === "workspace") {
|
|
381
|
+
assertWorkspaceIntent(allocation, input.requestId, input.task, input.attempt);
|
|
382
|
+
await this.assertRepositoryIdentity(allocation, context);
|
|
383
|
+
const response = await this.herdr.json(["worktree", "list", "--cwd", allocation.worktreeCwd], this.processOptions(allocation.herdrRepoRoot, context, HERDR_OPERATION_CAP_MS));
|
|
384
|
+
const result = resultRecord(response, "Herdr worktree list response");
|
|
385
|
+
if (result.type !== "worktree_list" || !Array.isArray(result.worktrees))
|
|
386
|
+
throw new Error("Herdr worktree list response is malformed.");
|
|
387
|
+
const source = record(result.source, "Herdr worktree list source");
|
|
388
|
+
if (exactString(source.source_checkout_path, "Herdr worktree list source checkout_path") !== allocation.worktreeCwd
|
|
389
|
+
|| exactString(source.repo_key, "Herdr worktree list source repo_key") !== allocation.repoKey
|
|
390
|
+
|| exactString(source.repo_root, "Herdr worktree list source repo_root") !== allocation.herdrRepoRoot) {
|
|
391
|
+
throw new Error("Herdr worktree list source does not match the exact saved repository identity.");
|
|
392
|
+
}
|
|
393
|
+
const worktrees = result.worktrees.map((entry) => {
|
|
394
|
+
const item = record(entry, "Herdr listed worktree");
|
|
395
|
+
const path = exactString(item.path, "Herdr listed worktree path");
|
|
396
|
+
const label = exactString(item.label, "Herdr listed worktree label");
|
|
397
|
+
const openWorkspaceId = item.open_workspace_id;
|
|
398
|
+
if (openWorkspaceId !== undefined && openWorkspaceId !== null)
|
|
399
|
+
exactString(openWorkspaceId, "Herdr listed open workspace ID");
|
|
400
|
+
return { path, label, openWorkspaceId: typeof openWorkspaceId === "string" ? openWorkspaceId : undefined };
|
|
401
|
+
});
|
|
402
|
+
const target = worktrees.filter((item) => item.path === allocation.worktreeCwd);
|
|
403
|
+
const tokenMatches = worktrees.filter((item) => item.label === allocation.label);
|
|
404
|
+
const possible = [
|
|
405
|
+
...(target.length === 1 ? [] : [`expected worktree match count ${target.length}`]),
|
|
406
|
+
...target.filter((item) => item.openWorkspaceId).map((item) => `possible workspace ${item.openWorkspaceId}`),
|
|
407
|
+
...tokenMatches.map((item) => `token-labelled worktree ${item.path}${item.openWorkspaceId ? ` in workspace ${item.openWorkspaceId}` : ""}`),
|
|
408
|
+
];
|
|
409
|
+
return possible.length
|
|
410
|
+
? { kind: "workspace", outcome: "possible", failure: "A possible prior Herdr workspace allocation or parent mismatch remains; it was not adopted or closed.", possibleResources: possible }
|
|
411
|
+
: { kind: "workspace", outcome: "absent" };
|
|
412
|
+
}
|
|
413
|
+
if (allocation.kind === "worker_tab") {
|
|
414
|
+
assertWorkerTabIntent(allocation, input.task, input.attempt);
|
|
415
|
+
this.assertLeasePath(allocation.leasePath, input.intent.token);
|
|
416
|
+
const tabs = (await this.listTabs(allocation.workspaceId, allocation.worktreeCwd, context)).map((tab) => ({
|
|
417
|
+
id: exactString(tab.tab_id, "Herdr listed tab ID"),
|
|
418
|
+
workspaceId: exactString(tab.workspace_id, "Herdr listed tab workspace ID"),
|
|
419
|
+
label: exactString(tab.label, "Herdr listed tab label"),
|
|
420
|
+
}));
|
|
421
|
+
if (tabs.some((tab) => tab.workspaceId !== allocation.workspaceId)) {
|
|
422
|
+
throw new Error("Herdr tab list escaped the exact saved workspace scope.");
|
|
423
|
+
}
|
|
424
|
+
const rootTabs = tabs.filter((tab) => tab.id === allocation.workspaceRootTabId);
|
|
425
|
+
const unexpected = tabs.filter((tab) => tab.label === allocation.label);
|
|
426
|
+
const holders = await this.scanLease(allocation.leasePath, allocation.worktreeCwd, context, undefined, true);
|
|
427
|
+
const possible = [
|
|
428
|
+
...(rootTabs.length === 1 ? [] : [`workspace root tab match count ${rootTabs.length}`]),
|
|
429
|
+
...unexpected.map((tab) => `possible tab ${tab.id}`),
|
|
430
|
+
...holders.map((pid) => `lease holder pid ${pid}`),
|
|
431
|
+
];
|
|
432
|
+
return possible.length
|
|
433
|
+
? { kind: "worker_tab", outcome: "possible", failure: "A possible prior worker-tab allocation, parent mismatch, or lease holder remains; it was not adopted or touched.", possibleResources: possible }
|
|
434
|
+
: { kind: "worker_tab", outcome: "absent" };
|
|
435
|
+
}
|
|
436
|
+
assertAgentIntent(allocation, input.attempt);
|
|
437
|
+
this.assertLeasePath(allocation.leasePath, input.intent.token);
|
|
438
|
+
try {
|
|
439
|
+
await this.assertPrivateLease(allocation.leasePath, false);
|
|
440
|
+
const agents = (await this.listAgents(allocation.worktreeCwd, context)).map((agent) => {
|
|
441
|
+
const name = agent.name;
|
|
442
|
+
if (name !== undefined && name !== null && typeof name !== "string")
|
|
443
|
+
throw new Error("Herdr listed agent name is malformed.");
|
|
444
|
+
return {
|
|
445
|
+
name: typeof name === "string" ? name : undefined,
|
|
446
|
+
paneId: exactString(agent.pane_id, "Herdr listed agent pane ID"),
|
|
447
|
+
tabId: exactString(agent.tab_id, "Herdr listed agent tab ID"),
|
|
448
|
+
};
|
|
449
|
+
});
|
|
450
|
+
const matches = agents.filter((agent) => agent.name === allocation.agentName || agent.paneId === allocation.paneId || agent.tabId === allocation.tabId);
|
|
451
|
+
const holders = await this.scanLease(allocation.leasePath, allocation.worktreeCwd, context);
|
|
452
|
+
const possible = [
|
|
453
|
+
...matches.map((agent) => `possible agent ${agent.name ?? "without expected name"} in pane ${agent.paneId}`),
|
|
454
|
+
...holders.map((pid) => `lease holder pid ${pid}`),
|
|
455
|
+
];
|
|
456
|
+
if (possible.length) {
|
|
457
|
+
return { kind: "agent", outcome: "possible", failure: "A possible prior agent allocation or lease holder remains; it was not adopted or touched.", possibleResources: possible };
|
|
458
|
+
}
|
|
459
|
+
await this.assertStartableAgentPane(allocation, context, { requireExclusiveTty: true });
|
|
460
|
+
return { kind: "agent", outcome: "absent" };
|
|
461
|
+
}
|
|
462
|
+
catch (error) {
|
|
463
|
+
return {
|
|
464
|
+
kind: "agent",
|
|
465
|
+
outcome: "possible",
|
|
466
|
+
failure: `A prior agent allocation cannot be proved absent: ${safeText(error)}`,
|
|
467
|
+
possibleResources: [allocation.agentName, allocation.paneId, allocation.leasePath],
|
|
468
|
+
};
|
|
469
|
+
}
|
|
470
|
+
}
|
|
471
|
+
async runWorker(input, context) {
|
|
472
|
+
const allocation = ownedIntent(input.attempt, "agent");
|
|
473
|
+
requireIntentIdentity(allocation, input.attempt);
|
|
474
|
+
assertAgentIntent(allocation, input.attempt);
|
|
475
|
+
if (input.workerId !== allocation.agentName) {
|
|
476
|
+
throw new Error("Worker prompt does not target the exact saved agent.");
|
|
477
|
+
}
|
|
478
|
+
let text;
|
|
479
|
+
try {
|
|
480
|
+
if (input.task.kind !== "changeset")
|
|
481
|
+
throw new Error("Herdr assignment requires a changeset task.");
|
|
482
|
+
text = buildChangesetTaskPrompt({
|
|
483
|
+
goal: input.goal,
|
|
484
|
+
contexts: input.contexts,
|
|
485
|
+
task: input.task,
|
|
486
|
+
kind: input.kind,
|
|
487
|
+
worktreeCwd: allocation.worktreeCwd,
|
|
488
|
+
...(input.failure ? { failure: input.failure } : {}),
|
|
489
|
+
...(input.instruction ? { instruction: input.instruction } : {}),
|
|
490
|
+
});
|
|
491
|
+
}
|
|
492
|
+
catch (error) {
|
|
493
|
+
return { outcome: "not_prompted", diagnostic: `Worker assignment was not submitted: ${safeText(error)}` };
|
|
494
|
+
}
|
|
495
|
+
let ready;
|
|
496
|
+
try {
|
|
497
|
+
ready = await this.waitForSettledAgent(allocation, context);
|
|
498
|
+
}
|
|
499
|
+
catch (error) {
|
|
500
|
+
return { outcome: context.signal.aborted ? "interrupted" : "unknown", diagnostic: `Exact worker readiness is unknown: ${safeText(error)}` };
|
|
501
|
+
}
|
|
502
|
+
if (ready.status === "blocked")
|
|
503
|
+
return { outcome: "not_prompted", diagnostic: await this.diagnostic(allocation, context, "Worker was blocked before prompt submission.") };
|
|
504
|
+
if (!SETTLED_AGENT_STATES.has(ready.status) || !ready.interactiveReady) {
|
|
505
|
+
return { outcome: "unknown", diagnostic: `Exact worker was not interactively ready: ${ready.status}.` };
|
|
506
|
+
}
|
|
507
|
+
const promptOptions = this.processOptions(allocation.worktreeCwd, context, HERDR_OPERATION_CAP_MS);
|
|
508
|
+
const promptArgs = [
|
|
509
|
+
"agent", "prompt", allocation.agentName, text, "--wait",
|
|
510
|
+
"--until", "working", "--until", "done", "--until", "blocked",
|
|
511
|
+
"--timeout", String(promptOptions.timeoutMs),
|
|
512
|
+
];
|
|
513
|
+
const prompted = await this.herdr.exec(promptArgs, promptOptions);
|
|
514
|
+
if (prompted.code !== 0 || prompted.killed) {
|
|
515
|
+
if (!prompted.killed && !context.signal.aborted && hasHerdrErrorCode(prompted, "agent_prompt_stalled")) {
|
|
516
|
+
return await this.reconcileDeliveredPrompt(input, allocation, context);
|
|
517
|
+
}
|
|
518
|
+
return {
|
|
519
|
+
outcome: prompted.killed || context.signal.aborted ? "interrupted" : "unknown",
|
|
520
|
+
diagnostic: `Worker prompt result is unknown and will not be replayed: ${safeText(herdrCommandFailure(["agent", "prompt"], prompted))}`,
|
|
521
|
+
};
|
|
522
|
+
}
|
|
523
|
+
let settled;
|
|
524
|
+
try {
|
|
525
|
+
settled = parseAgent(parseJsonObject(prompted.stdout, "Herdr agent prompt"), allocation, ["agent_prompted"]);
|
|
526
|
+
}
|
|
527
|
+
catch (error) {
|
|
528
|
+
return { outcome: "unknown", diagnostic: `Worker prompt response is malformed: ${safeText(error)}` };
|
|
529
|
+
}
|
|
530
|
+
if (settled.status === "working")
|
|
531
|
+
return await this.reconcileDeliveredPrompt(input, allocation, context);
|
|
532
|
+
if (settled.status === "blocked")
|
|
533
|
+
return { outcome: "blocked", diagnostic: await this.diagnostic(allocation, context, "Worker settled as blocked.") };
|
|
534
|
+
if (!SETTLED_AGENT_STATES.has(settled.status))
|
|
535
|
+
return { outcome: "unknown", diagnostic: `Worker prompt did not return a settled state: ${settled.status}.` };
|
|
536
|
+
return await this.candidateResult(input, allocation, context);
|
|
537
|
+
}
|
|
538
|
+
async terminateWorker(input, context) {
|
|
539
|
+
try {
|
|
540
|
+
const allocation = ownedIntent(input.attempt, "agent");
|
|
541
|
+
requireIntentIdentity(allocation, input.attempt);
|
|
542
|
+
assertAgentIntent(allocation, input.attempt);
|
|
543
|
+
if (input.workerId !== allocation.agentName)
|
|
544
|
+
throw new Error("Worker termination identity does not match the exact saved agent.");
|
|
545
|
+
this.assertLeasePath(allocation.leasePath, allocation.token);
|
|
546
|
+
await this.assertPrivateLease(allocation.leasePath, false);
|
|
547
|
+
const agents = (await this.listAgents(allocation.worktreeCwd, context)).map((agent) => ({
|
|
548
|
+
name: typeof agent.name === "string" ? agent.name : undefined,
|
|
549
|
+
paneId: exactString(agent.pane_id, "Herdr listed agent pane ID"),
|
|
550
|
+
tabId: exactString(agent.tab_id, "Herdr listed agent tab ID"),
|
|
551
|
+
}));
|
|
552
|
+
const related = agents.filter((agent) => agent.name === allocation.agentName
|
|
553
|
+
|| agent.paneId === allocation.paneId || agent.tabId === allocation.tabId);
|
|
554
|
+
if (related.length !== 1 || related[0].name !== allocation.agentName
|
|
555
|
+
|| related[0].paneId !== allocation.paneId || related[0].tabId !== allocation.tabId) {
|
|
556
|
+
throw new Error("Worker termination requires one exact current agent, tab, and pane match.");
|
|
557
|
+
}
|
|
558
|
+
const processInfo = await this.herdr.json(["pane", "process-info", "--pane", allocation.paneId], this.processOptions(allocation.worktreeCwd, context, HERDR_OPERATION_CAP_MS));
|
|
559
|
+
const processResult = resultRecord(processInfo, "Herdr pane process-info response");
|
|
560
|
+
if (processResult.type !== "pane_process_info")
|
|
561
|
+
throw new Error("Herdr pane process-info response has the wrong type.");
|
|
562
|
+
const captured = record(processResult.process_info, "Herdr pane process-info");
|
|
563
|
+
if (captured.pane_id !== allocation.paneId) {
|
|
564
|
+
throw new Error("Herdr pane process-info did not match the exact saved pane.");
|
|
565
|
+
}
|
|
566
|
+
const closed = await this.herdr.exec(["pane", "close", allocation.paneId], this.processOptions(allocation.worktreeCwd, context, HERDR_OPERATION_CAP_MS));
|
|
567
|
+
if (closed.code !== 0 || closed.killed)
|
|
568
|
+
throw new Error(herdrCommandFailure(["pane", "close"], closed));
|
|
569
|
+
requireOkResponse(closed.stdout, "Herdr pane close response");
|
|
570
|
+
if (!await this.paneAbsent(allocation.paneId, allocation.worktreeCwd, context))
|
|
571
|
+
throw new Error("The exact saved pane still exists after pane close.");
|
|
572
|
+
let holders = await this.scanLease(allocation.leasePath, allocation.worktreeCwd, context);
|
|
573
|
+
if (holders.length) {
|
|
574
|
+
await this.signalExactHolders(holders, allocation, "SIGTERM", context);
|
|
575
|
+
await this.delay(250, context.signal);
|
|
576
|
+
holders = await this.scanLease(allocation.leasePath, allocation.worktreeCwd, context);
|
|
577
|
+
}
|
|
578
|
+
if (holders.length) {
|
|
579
|
+
await this.signalExactHolders(holders, allocation, "SIGKILL", context);
|
|
580
|
+
await this.delay(100, context.signal);
|
|
581
|
+
holders = await this.scanLease(allocation.leasePath, allocation.worktreeCwd, context);
|
|
582
|
+
}
|
|
583
|
+
if (holders.length)
|
|
584
|
+
throw new Error(`Exact process lease still has ${holders.length} surviving holder(s).`);
|
|
585
|
+
await this.delay(50, context.signal);
|
|
586
|
+
if ((await this.scanLease(allocation.leasePath, allocation.worktreeCwd, context)).length) {
|
|
587
|
+
throw new Error("Exact process lease did not remain empty for two consecutive scans.");
|
|
588
|
+
}
|
|
589
|
+
return { outcome: "terminated" };
|
|
590
|
+
}
|
|
591
|
+
catch (error) {
|
|
592
|
+
return { outcome: "unknown", failure: safeText(error) };
|
|
593
|
+
}
|
|
594
|
+
}
|
|
595
|
+
async reconcileWorkerTermination(input, context) {
|
|
596
|
+
try {
|
|
597
|
+
const allocation = ownedIntent(input.attempt, "agent");
|
|
598
|
+
requireIntentIdentity(allocation, input.attempt);
|
|
599
|
+
assertAgentIntent(allocation, input.attempt);
|
|
600
|
+
if (input.workerId !== allocation.agentName)
|
|
601
|
+
throw new Error("Saved termination worker does not match the exact owned agent.");
|
|
602
|
+
this.assertLeasePath(allocation.leasePath, allocation.token);
|
|
603
|
+
await this.assertPrivateLease(allocation.leasePath, false);
|
|
604
|
+
const agents = (await this.listAgents(allocation.worktreeCwd, context)).map((agent) => ({
|
|
605
|
+
name: typeof agent.name === "string" ? agent.name : undefined,
|
|
606
|
+
paneId: exactString(agent.pane_id, "Herdr listed agent pane ID"),
|
|
607
|
+
tabId: exactString(agent.tab_id, "Herdr listed agent tab ID"),
|
|
608
|
+
}));
|
|
609
|
+
const related = agents.filter((agent) => agent.name === allocation.agentName
|
|
610
|
+
|| agent.paneId === allocation.paneId || agent.tabId === allocation.tabId);
|
|
611
|
+
if (related.length) {
|
|
612
|
+
if (related.length !== 1 || related[0].name !== allocation.agentName
|
|
613
|
+
|| related[0].paneId !== allocation.paneId || related[0].tabId !== allocation.tabId) {
|
|
614
|
+
throw new Error("Observed worker identity does not exactly match the saved owned agent and pane.");
|
|
615
|
+
}
|
|
616
|
+
if (await this.paneAbsent(allocation.paneId, allocation.worktreeCwd, context)) {
|
|
617
|
+
throw new Error("Exact saved agent exists but its saved pane is absent.");
|
|
618
|
+
}
|
|
619
|
+
const processInfo = await this.herdr.json(["pane", "process-info", "--pane", allocation.paneId], this.processOptions(allocation.worktreeCwd, context, HERDR_OPERATION_CAP_MS));
|
|
620
|
+
const result = resultRecord(processInfo, "Herdr pane process-info response");
|
|
621
|
+
if (result.type !== "pane_process_info"
|
|
622
|
+
|| record(result.process_info, "Herdr pane process-info").pane_id !== allocation.paneId) {
|
|
623
|
+
throw new Error("Exact saved pane process ownership could not be proved.");
|
|
624
|
+
}
|
|
625
|
+
return { outcome: "active" };
|
|
626
|
+
}
|
|
627
|
+
if (!await this.paneAbsent(allocation.paneId, allocation.worktreeCwd, context)) {
|
|
628
|
+
throw new Error("Saved agent is absent but the exact saved pane still exists.");
|
|
629
|
+
}
|
|
630
|
+
if ((await this.scanLease(allocation.leasePath, allocation.worktreeCwd, context)).length) {
|
|
631
|
+
throw new Error("Saved worker is absent but its private process lease still has holders.");
|
|
632
|
+
}
|
|
633
|
+
await this.delay(50, context.signal);
|
|
634
|
+
if ((await this.scanLease(allocation.leasePath, allocation.worktreeCwd, context)).length) {
|
|
635
|
+
throw new Error("Private process lease did not remain empty for two consecutive scans.");
|
|
636
|
+
}
|
|
637
|
+
return { outcome: "terminated" };
|
|
638
|
+
}
|
|
639
|
+
catch (error) {
|
|
640
|
+
return { outcome: "unknown", failure: safeText(error) };
|
|
641
|
+
}
|
|
642
|
+
}
|
|
643
|
+
async cleanupHost(input, context) {
|
|
644
|
+
if (input.attempt.termination?.status !== "terminated") {
|
|
645
|
+
return { outcome: "blocked", failure: "Host cleanup requires exact recorded worker termination." };
|
|
646
|
+
}
|
|
647
|
+
try {
|
|
648
|
+
if (input.kind === "worker_tab") {
|
|
649
|
+
const allocation = ownedIntent(input.attempt, "worker_tab");
|
|
650
|
+
const workspace = ownedIntent(input.attempt, "workspace");
|
|
651
|
+
requireIntentIdentity(allocation, input.attempt);
|
|
652
|
+
requireIntentIdentity(workspace, input.attempt);
|
|
653
|
+
assertWorkerTabIntent(allocation, input.task, input.attempt);
|
|
654
|
+
const leasePath = exactString(allocation.leasePath, "saved worker lease path");
|
|
655
|
+
this.assertLeasePath(leasePath, input.attempt.correlationToken);
|
|
656
|
+
assertWorkspaceIntent(workspace, input.requestId, input.task, input.attempt);
|
|
657
|
+
await this.assertRepositoryIdentity(workspace, context);
|
|
658
|
+
const tabId = exactString(allocation.tabId, "saved worker tab ID");
|
|
659
|
+
const workspaceId = exactString(workspace.workspaceId, "saved workspace ID");
|
|
660
|
+
if (tabId === allocation.workspaceRootTabId)
|
|
661
|
+
throw new Error("Saved worker tab aliases the workspace root tab.");
|
|
662
|
+
const paneId = exactString(allocation.paneId, "saved worker pane ID");
|
|
663
|
+
if (!await this.paneAbsent(paneId, allocation.worktreeCwd, context)) {
|
|
664
|
+
return { outcome: "blocked", failure: "The exact saved worker pane still exists after termination." };
|
|
665
|
+
}
|
|
666
|
+
const workspaceIsAbsent = await this.workspaceAbsent(workspaceId, workspace, context);
|
|
667
|
+
const tab = await this.getTab(tabId, allocation.worktreeCwd, context);
|
|
668
|
+
if (tab) {
|
|
669
|
+
if (workspaceIsAbsent || tab.workspace_id !== workspaceId || tab.label !== allocation.label || tab.pane_count !== 0) {
|
|
670
|
+
return { outcome: "blocked", failure: "The exact saved worker tab no longer matches its owned empty tab identity." };
|
|
671
|
+
}
|
|
672
|
+
const closed = await this.herdr.exec(["tab", "close", tabId], this.processOptions(allocation.worktreeCwd, context, HERDR_OPERATION_CAP_MS));
|
|
673
|
+
if (closed.code !== 0 || closed.killed)
|
|
674
|
+
return { outcome: "blocked", failure: safeText(herdrCommandFailure(["tab", "close"], closed)) };
|
|
675
|
+
requireOkResponse(closed.stdout, "Herdr tab close response");
|
|
676
|
+
if (await this.getTab(tabId, allocation.worktreeCwd, context)) {
|
|
677
|
+
return { outcome: "blocked", failure: "The exact saved worker tab still exists after close." };
|
|
678
|
+
}
|
|
679
|
+
}
|
|
680
|
+
await this.removeLeaseArtifacts(allocation, input.attempt.correlationToken, paneId, tabId, context);
|
|
681
|
+
return tab ? { outcome: "completed" } : { outcome: "absent" };
|
|
682
|
+
}
|
|
683
|
+
const workerCleanup = input.attempt.cleanup.find((step) => step.kind === "worker_tab");
|
|
684
|
+
if (workerCleanup?.status !== "completed")
|
|
685
|
+
return { outcome: "blocked", failure: "Workspace cleanup must follow worker-tab reconciliation." };
|
|
686
|
+
const allocation = ownedIntent(input.attempt, "workspace");
|
|
687
|
+
requireIntentIdentity(allocation, input.attempt);
|
|
688
|
+
assertWorkspaceIntent(allocation, input.requestId, input.task, input.attempt);
|
|
689
|
+
await this.assertRepositoryIdentity(allocation, context);
|
|
690
|
+
const workspaceId = exactString(allocation.workspaceId, "saved workspace ID");
|
|
691
|
+
if (await this.workspaceAbsent(workspaceId, allocation, context))
|
|
692
|
+
return { outcome: "absent" };
|
|
693
|
+
const response = await this.herdr.json(["workspace", "get", workspaceId], this.processOptions(allocation.herdrRepoRoot, context, HERDR_OPERATION_CAP_MS));
|
|
694
|
+
const workspace = parseWorkspaceInfo(response, workspaceId);
|
|
695
|
+
this.assertWorkspaceEvidence(workspace, allocation);
|
|
696
|
+
const closed = await this.herdr.exec(["workspace", "close", workspaceId], this.processOptions(allocation.herdrRepoRoot, context, HERDR_OPERATION_CAP_MS));
|
|
697
|
+
if (closed.code !== 0 || closed.killed)
|
|
698
|
+
return { outcome: "blocked", failure: safeText(herdrCommandFailure(["workspace", "close"], closed)) };
|
|
699
|
+
requireOkResponse(closed.stdout, "Herdr workspace close response");
|
|
700
|
+
return await this.workspaceAbsent(workspaceId, allocation, context)
|
|
701
|
+
? { outcome: "completed" }
|
|
702
|
+
: { outcome: "blocked", failure: "The exact saved workspace still exists after close." };
|
|
703
|
+
}
|
|
704
|
+
catch (error) {
|
|
705
|
+
return { outcome: "blocked", failure: safeText(error) };
|
|
706
|
+
}
|
|
707
|
+
}
|
|
708
|
+
async allocateWorkspace(allocation, requestId, task, attempt, context) {
|
|
709
|
+
assertWorkspaceIntent(allocation, requestId, task, attempt);
|
|
710
|
+
await this.assertRepositoryIdentity(allocation, context);
|
|
711
|
+
const args = ["worktree", "open", "--cwd", allocation.herdrRepoRoot, "--path", allocation.worktreeCwd, "--label", allocation.label, "--no-focus"];
|
|
712
|
+
const response = await this.herdr.exec(args, this.processOptions(allocation.herdrRepoRoot, context, HERDR_OPERATION_CAP_MS));
|
|
713
|
+
if (response.code !== 0 || response.killed) {
|
|
714
|
+
return { kind: "workspace", outcome: "unknown", failure: safeText(herdrCommandFailure(args, response)), possibleResources: [allocation.label] };
|
|
715
|
+
}
|
|
716
|
+
try {
|
|
717
|
+
const result = resultRecord(parseJsonObject(response.stdout, "Herdr worktree open response"), "Herdr worktree open response");
|
|
718
|
+
if (result.type !== "worktree_opened" || result.already_open !== false)
|
|
719
|
+
throw new Error("Herdr did not create one fresh worktree workspace.");
|
|
720
|
+
const workspace = record(result.workspace, "Herdr opened workspace");
|
|
721
|
+
const tab = record(result.tab, "Herdr opened root tab");
|
|
722
|
+
const pane = record(result.root_pane, "Herdr opened root pane");
|
|
723
|
+
const worktree = record(result.worktree, "Herdr opened worktree");
|
|
724
|
+
const workspaceId = exactString(workspace.workspace_id, "opened workspace_id");
|
|
725
|
+
const tabId = exactString(tab.tab_id, "opened tab_id");
|
|
726
|
+
const rootPaneId = exactString(pane.pane_id, "opened root pane_id");
|
|
727
|
+
const workspaceWorktree = record(workspace.worktree, "opened workspace worktree");
|
|
728
|
+
if (workspace.label !== allocation.label || workspace.focused !== false || tab.workspace_id !== workspaceId
|
|
729
|
+
|| tab.focused !== false || pane.workspace_id !== workspaceId || pane.tab_id !== tabId || pane.focused !== false
|
|
730
|
+
|| worktree.path !== allocation.worktreeCwd || workspaceWorktree.checkout_path !== allocation.worktreeCwd
|
|
731
|
+
|| workspaceWorktree.repo_key !== allocation.repoKey || workspaceWorktree.repo_root !== allocation.herdrRepoRoot) {
|
|
732
|
+
throw new Error("Herdr worktree open response does not prove the exact non-focused checkout and repository.");
|
|
733
|
+
}
|
|
734
|
+
return { kind: "workspace", outcome: "owned", workspaceId, rootTabId: tabId, rootPaneId };
|
|
735
|
+
}
|
|
736
|
+
catch (error) {
|
|
737
|
+
return { kind: "workspace", outcome: "unknown", failure: safeText(error), possibleResources: [allocation.label] };
|
|
738
|
+
}
|
|
739
|
+
}
|
|
740
|
+
async allocateWorkerTab(allocation, task, attempt, context) {
|
|
741
|
+
assertWorkerTabIntent(allocation, task, attempt);
|
|
742
|
+
await this.createPrivateLease(allocation.leasePath, allocation.token);
|
|
743
|
+
await this.waitForStableWorkspacePanes(allocation.workspaceId, allocation.worktreeCwd, context);
|
|
744
|
+
const args = [
|
|
745
|
+
"tab", "create", "--workspace", allocation.workspaceId, "--cwd", allocation.worktreeCwd,
|
|
746
|
+
"--label", allocation.label, "--env", `${PROCESS_LEASE_ENV}=${allocation.leasePath}`, "--no-focus",
|
|
747
|
+
];
|
|
748
|
+
const response = await this.herdr.exec(args, this.processOptions(allocation.worktreeCwd, context, HERDR_OPERATION_CAP_MS));
|
|
749
|
+
if (response.code !== 0 || response.killed) {
|
|
750
|
+
return { kind: "worker_tab", outcome: "unknown", failure: safeText(herdrCommandFailure(args, response)), possibleResources: [allocation.label, allocation.leasePath] };
|
|
751
|
+
}
|
|
752
|
+
try {
|
|
753
|
+
const result = resultRecord(parseJsonObject(response.stdout, "Herdr tab create response"), "Herdr tab create response");
|
|
754
|
+
if (result.type !== "tab_created")
|
|
755
|
+
throw new Error("Herdr tab create response has the wrong type.");
|
|
756
|
+
const tab = record(result.tab, "Herdr created tab");
|
|
757
|
+
const pane = record(result.root_pane, "Herdr created tab root pane");
|
|
758
|
+
const tabId = exactString(tab.tab_id, "created tab_id");
|
|
759
|
+
const paneId = exactString(pane.pane_id, "created root pane_id");
|
|
760
|
+
if (tabId === allocation.workspaceRootTabId || paneId === allocation.workspaceRootPaneId
|
|
761
|
+
|| tab.workspace_id !== allocation.workspaceId || tab.label !== allocation.label || tab.focused !== false || tab.pane_count !== 1
|
|
762
|
+
|| pane.workspace_id !== allocation.workspaceId || pane.tab_id !== tabId || pane.cwd !== allocation.worktreeCwd || pane.focused !== false) {
|
|
763
|
+
throw new Error("Herdr tab create response does not prove one exact non-focused worker pane distinct from the workspace root.");
|
|
764
|
+
}
|
|
765
|
+
return { kind: "worker_tab", outcome: "owned", tabId, paneId };
|
|
766
|
+
}
|
|
767
|
+
catch (error) {
|
|
768
|
+
return { kind: "worker_tab", outcome: "unknown", failure: safeText(error), possibleResources: [allocation.label, allocation.leasePath] };
|
|
769
|
+
}
|
|
770
|
+
}
|
|
771
|
+
async allocateAgent(allocation, expectedRole, attempt, acquireLaunch, context) {
|
|
772
|
+
assertAgentIntent(allocation, attempt);
|
|
773
|
+
if (!acquireLaunch)
|
|
774
|
+
throw new Error("Agent start requires immediate Role launch acquisition.");
|
|
775
|
+
this.assertLeasePath(allocation.leasePath, allocation.token);
|
|
776
|
+
await this.assertPrivateLease(allocation.leasePath, false);
|
|
777
|
+
if ((await this.scanLease(allocation.leasePath, allocation.worktreeCwd, context)).length) {
|
|
778
|
+
throw new Error("Agent start requires an empty exact process lease.");
|
|
779
|
+
}
|
|
780
|
+
await this.assertStartableAgentPane(allocation, context, { requireExclusiveTty: false });
|
|
781
|
+
const options = this.processOptions(allocation.worktreeCwd, context, HERDR_OPERATION_CAP_MS);
|
|
782
|
+
const handle = await acquireLaunch();
|
|
783
|
+
return await withTransientLaunch(handle, async (launch) => {
|
|
784
|
+
if (launch.role !== expectedRole)
|
|
785
|
+
throw new Error("Agent start acquisition returned the wrong Role.");
|
|
786
|
+
if (Object.keys(launch.env).length)
|
|
787
|
+
throw new Error("Herdr agent launch must not receive caller Role environment variables.");
|
|
788
|
+
const response = await startPiAgent(this.herdr, {
|
|
789
|
+
name: allocation.agentName,
|
|
790
|
+
pane: allocation.paneId,
|
|
791
|
+
args: launch.args,
|
|
792
|
+
options,
|
|
793
|
+
shouldRetry: () => false,
|
|
794
|
+
});
|
|
795
|
+
if (response.code !== 0 || response.killed) {
|
|
796
|
+
const failure = safeText(herdrCommandFailure(["agent", "start"], response));
|
|
797
|
+
return hasHerdrErrorCode(response, "agent_pane_busy") && !response.killed
|
|
798
|
+
? { kind: "agent", outcome: "absent", failure }
|
|
799
|
+
: { kind: "agent", outcome: "unknown", failure, possibleResources: [allocation.agentName, allocation.paneId] };
|
|
800
|
+
}
|
|
801
|
+
try {
|
|
802
|
+
const agent = parseAgent(parseJsonObject(response.stdout, "Herdr agent start response"), allocation, ["agent_started"]);
|
|
803
|
+
if (!agent.interactiveReady || agent.status !== "idle")
|
|
804
|
+
throw new Error("Started Herdr agent is not exactly ready and idle.");
|
|
805
|
+
return { kind: "agent", outcome: "owned" };
|
|
806
|
+
}
|
|
807
|
+
catch (error) {
|
|
808
|
+
return { kind: "agent", outcome: "unknown", failure: safeText(error), possibleResources: [allocation.agentName, allocation.paneId] };
|
|
809
|
+
}
|
|
810
|
+
});
|
|
811
|
+
}
|
|
812
|
+
async reconcileDeliveredPrompt(input, allocation, context, promptMayBeInFlight = true) {
|
|
813
|
+
for (;;) {
|
|
814
|
+
let lifecycle;
|
|
815
|
+
try {
|
|
816
|
+
lifecycle = await this.waitForAgentLifecycle(allocation, context, true);
|
|
817
|
+
}
|
|
818
|
+
catch (error) {
|
|
819
|
+
return {
|
|
820
|
+
outcome: this.contextInterrupted(context) ? "interrupted" : "unknown",
|
|
821
|
+
diagnostic: `Delivered prompt lifecycle is unknown: ${safeText(error)}`,
|
|
822
|
+
};
|
|
823
|
+
}
|
|
824
|
+
if (lifecycle.status === "blocked") {
|
|
825
|
+
return { outcome: "blocked", diagnostic: await this.diagnostic(allocation, context, "Delivered prompt settled as blocked.") };
|
|
826
|
+
}
|
|
827
|
+
if (lifecycle.status === "unknown") {
|
|
828
|
+
return { outcome: "unknown", diagnostic: "Delivered prompt lifecycle is unknown." };
|
|
829
|
+
}
|
|
830
|
+
let inspection;
|
|
831
|
+
try {
|
|
832
|
+
inspection = await this.inspectInFlightCandidate({ root: allocation.worktreeCwd, task: input.task, attempt: input.attempt }, this.childContext(context, GIT_INSPECTION_CAP_MS));
|
|
833
|
+
}
|
|
834
|
+
catch (error) {
|
|
835
|
+
return {
|
|
836
|
+
outcome: this.contextInterrupted(context) ? "interrupted" : "unknown",
|
|
837
|
+
diagnostic: `Delivered prompt candidate inspection failed: ${safeText(error)}`,
|
|
838
|
+
};
|
|
839
|
+
}
|
|
840
|
+
if (this.contextInterrupted(context)) {
|
|
841
|
+
return { outcome: "interrupted", diagnostic: "Delivered prompt reconciliation was interrupted." };
|
|
842
|
+
}
|
|
843
|
+
if (SETTLED_AGENT_STATES.has(lifecycle.status)
|
|
844
|
+
&& inspection.valid
|
|
845
|
+
&& inspection.clean
|
|
846
|
+
&& this.isExpectedCandidate(input, inspection.candidate)) {
|
|
847
|
+
return {
|
|
848
|
+
outcome: "candidate",
|
|
849
|
+
candidate: inspection.candidate,
|
|
850
|
+
diagnostic: await this.diagnostic(allocation, context, "Delivered prompt settled with a candidate."),
|
|
851
|
+
};
|
|
852
|
+
}
|
|
853
|
+
if (!promptMayBeInFlight && SETTLED_AGENT_STATES.has(lifecycle.status)) {
|
|
854
|
+
return {
|
|
855
|
+
outcome: "blocked",
|
|
856
|
+
diagnostic: await this.diagnostic(allocation, context, "Settled worker did not produce an exact changed clean committed candidate."),
|
|
857
|
+
};
|
|
858
|
+
}
|
|
859
|
+
try {
|
|
860
|
+
await this.delay(STALLED_PROMPT_POLL_MS, context.signal);
|
|
861
|
+
}
|
|
862
|
+
catch (error) {
|
|
863
|
+
return {
|
|
864
|
+
outcome: this.contextInterrupted(context) ? "interrupted" : "unknown",
|
|
865
|
+
diagnostic: `Delivered prompt polling failed: ${safeText(error)}`,
|
|
866
|
+
};
|
|
867
|
+
}
|
|
868
|
+
}
|
|
869
|
+
}
|
|
870
|
+
async candidateResult(input, allocation, context) {
|
|
871
|
+
let inspection;
|
|
872
|
+
try {
|
|
873
|
+
inspection = await this.inspectInFlightCandidate({ root: allocation.worktreeCwd, task: input.task, attempt: input.attempt }, this.childContext(context, GIT_INSPECTION_CAP_MS));
|
|
874
|
+
}
|
|
875
|
+
catch (error) {
|
|
876
|
+
return { outcome: "unknown", diagnostic: `Settled worker candidate inspection failed: ${safeText(error)}` };
|
|
877
|
+
}
|
|
878
|
+
if (!inspection.valid || !inspection.clean || !this.isExpectedCandidate(input, inspection.candidate)) {
|
|
879
|
+
return await this.reconcileDeliveredPrompt(input, allocation, context, false);
|
|
880
|
+
}
|
|
881
|
+
return {
|
|
882
|
+
outcome: "candidate",
|
|
883
|
+
candidate: inspection.candidate,
|
|
884
|
+
diagnostic: await this.diagnostic(allocation, context, "Worker settled with a candidate."),
|
|
885
|
+
};
|
|
886
|
+
}
|
|
887
|
+
isExpectedCandidate(input, candidate) {
|
|
888
|
+
const expectedBranch = `refs/heads/${worktreeIntent(input.attempt).branch}`;
|
|
889
|
+
return isCleanCommitted(candidate) && candidate.branch === expectedBranch && candidate.head !== input.preCandidate.head;
|
|
890
|
+
}
|
|
891
|
+
async waitForSettledAgent(allocation, context) {
|
|
892
|
+
return await this.waitForAgentLifecycle(allocation, context, false);
|
|
893
|
+
}
|
|
894
|
+
async waitForAgentLifecycle(allocation, context, includeWorking) {
|
|
895
|
+
const options = this.processOptions(allocation.worktreeCwd, context, HERDR_OPERATION_CAP_MS);
|
|
896
|
+
const response = await this.herdr.json([
|
|
897
|
+
"agent", "wait", allocation.agentName,
|
|
898
|
+
"--until", "idle", "--until", "done", "--until", "blocked",
|
|
899
|
+
...(includeWorking ? ["--until", "working"] : []),
|
|
900
|
+
"--until", "unknown", "--timeout", String(options.timeoutMs),
|
|
901
|
+
], options);
|
|
902
|
+
return parseAgent(response, allocation, ["agent_info"]);
|
|
903
|
+
}
|
|
904
|
+
async diagnostic(allocation, context, prefix) {
|
|
905
|
+
try {
|
|
906
|
+
const response = await this.herdr.exec(["agent", "read", allocation.agentName, "--source", "recent", "--lines", "80", "--format", "text"], this.processOptions(allocation.worktreeCwd, context, HERDR_OPERATION_CAP_MS));
|
|
907
|
+
if (response.code === 0 && !response.killed && response.stdout.trim())
|
|
908
|
+
return `${prefix}\n${response.stdout.slice(0, DIAGNOSTIC_LIMIT)}`;
|
|
909
|
+
}
|
|
910
|
+
catch {
|
|
911
|
+
// Terminal text is diagnostic only; lifecycle evidence remains authoritative.
|
|
912
|
+
}
|
|
913
|
+
return prefix;
|
|
914
|
+
}
|
|
915
|
+
async assertStartableAgentPane(allocation, context, options) {
|
|
916
|
+
const paneResponse = await this.herdr.json(["pane", "get", allocation.paneId], this.processOptions(allocation.worktreeCwd, context, HERDR_OPERATION_CAP_MS));
|
|
917
|
+
const paneResult = resultRecord(paneResponse, "Herdr agent pane response");
|
|
918
|
+
if (paneResult.type !== "pane_info")
|
|
919
|
+
throw new Error("Herdr agent pane response has the wrong type.");
|
|
920
|
+
const pane = record(paneResult.pane, "Herdr agent pane");
|
|
921
|
+
if (exactString(pane.pane_id, "Herdr agent pane ID") !== allocation.paneId
|
|
922
|
+
|| exactString(pane.tab_id, "Herdr agent pane tab ID") !== allocation.tabId
|
|
923
|
+
|| exactString(pane.workspace_id, "Herdr agent pane workspace ID") !== allocation.workspaceId
|
|
924
|
+
|| pane.cwd !== allocation.worktreeCwd || pane.foreground_cwd !== allocation.worktreeCwd
|
|
925
|
+
|| (pane.agent !== undefined && pane.agent !== null) || pane.agent_status !== "unknown") {
|
|
926
|
+
throw new Error("The exact saved agent pane is not empty and startable in its owned worktree.");
|
|
927
|
+
}
|
|
928
|
+
const processResponse = await this.herdr.json(["pane", "process-info", "--pane", allocation.paneId], this.processOptions(allocation.worktreeCwd, context, HERDR_OPERATION_CAP_MS));
|
|
929
|
+
const processResult = resultRecord(processResponse, "Herdr agent pane process-info response");
|
|
930
|
+
if (processResult.type !== "pane_process_info")
|
|
931
|
+
throw new Error("Herdr agent pane process-info response has the wrong type.");
|
|
932
|
+
const processInfo = record(processResult.process_info, "Herdr agent pane process-info");
|
|
933
|
+
const shellPid = processInfo.shell_pid;
|
|
934
|
+
const foreground = processInfo.foreground_processes;
|
|
935
|
+
if (exactString(processInfo.pane_id, "Herdr agent process pane ID") !== allocation.paneId
|
|
936
|
+
|| !Number.isSafeInteger(shellPid) || Number(shellPid) <= 0
|
|
937
|
+
|| processInfo.foreground_process_group_id !== shellPid
|
|
938
|
+
|| !Array.isArray(foreground) || foreground.length !== 1) {
|
|
939
|
+
throw new Error("The exact saved agent pane process state is not an idle foreground shell.");
|
|
940
|
+
}
|
|
941
|
+
const shell = record(foreground[0], "Herdr agent pane foreground process");
|
|
942
|
+
if (shell.pid !== shellPid || shell.cwd !== allocation.worktreeCwd) {
|
|
943
|
+
throw new Error("The exact saved agent pane foreground process is not its owned idle shell.");
|
|
944
|
+
}
|
|
945
|
+
exactString(shell.name, "Herdr agent pane shell name");
|
|
946
|
+
if (!options.requireExclusiveTty)
|
|
947
|
+
return;
|
|
948
|
+
// Herdr's foreground list excludes background jobs, so recovery inventories every process on the shell's controlling TTY.
|
|
949
|
+
const inventory = await this.execute("ps", ["-axo", "pid=,tty="], this.processOptions(allocation.worktreeCwd, context, PROCESS_INSPECTION_CAP_MS));
|
|
950
|
+
if (inventory.code !== 0 || inventory.killed || inventory.stderr.trim() || !inventory.stdout.trim()) {
|
|
951
|
+
throw new Error("The exact saved agent pane process inventory failed or was ambiguous.");
|
|
952
|
+
}
|
|
953
|
+
const processes = inventory.stdout.trim().split(/\r?\n/).map((line) => {
|
|
954
|
+
const match = /^\s*([1-9]\d*)\s+(\S+)\s*$/.exec(line);
|
|
955
|
+
if (!match)
|
|
956
|
+
throw new Error("The exact saved agent pane process inventory is malformed.");
|
|
957
|
+
return { pid: Number(match[1]), tty: match[2] };
|
|
958
|
+
});
|
|
959
|
+
if (processes.some(({ pid }) => !Number.isSafeInteger(pid)) || new Set(processes.map(({ pid }) => pid)).size !== processes.length) {
|
|
960
|
+
throw new Error("The exact saved agent pane process inventory is malformed.");
|
|
961
|
+
}
|
|
962
|
+
const savedShell = processes.filter(({ pid }) => pid === shellPid);
|
|
963
|
+
if (savedShell.length !== 1 || ["?", "??", "-"].includes(savedShell[0].tty)
|
|
964
|
+
|| processes.filter(({ tty }) => tty === savedShell[0].tty).length !== 1) {
|
|
965
|
+
throw new Error("The exact saved agent pane contains a process other than its owned idle shell.");
|
|
966
|
+
}
|
|
967
|
+
}
|
|
968
|
+
async createPrivateLease(path, token) {
|
|
969
|
+
this.assertLeasePath(path, token);
|
|
970
|
+
const directory = resolve(this.leaseDirectory, token);
|
|
971
|
+
await mkdir(directory, { recursive: true, mode: DIRECTORY_MODE });
|
|
972
|
+
await this.assertPrivateLeaseDirectories(path, false);
|
|
973
|
+
let file;
|
|
974
|
+
try {
|
|
975
|
+
file = await open(path, constants.O_CREAT | constants.O_EXCL | constants.O_WRONLY, LEASE_MODE);
|
|
976
|
+
await file.chmod(LEASE_MODE);
|
|
977
|
+
}
|
|
978
|
+
finally {
|
|
979
|
+
await file?.close();
|
|
980
|
+
}
|
|
981
|
+
await this.assertPrivateLease(path, false);
|
|
982
|
+
}
|
|
983
|
+
async removeLeaseArtifacts(allocation, token, paneId, tabId, context) {
|
|
984
|
+
this.assertLeasePath(allocation.leasePath, token);
|
|
985
|
+
await this.assertPrivateLeaseDirectories(allocation.leasePath, true);
|
|
986
|
+
const leaseWasPresent = await this.assertPrivateLease(allocation.leasePath, true);
|
|
987
|
+
if ((await this.scanLease(allocation.leasePath, allocation.worktreeCwd, context, undefined, true)).length) {
|
|
988
|
+
throw new Error("Exact process lease still has a holder during cleanup.");
|
|
989
|
+
}
|
|
990
|
+
await this.delay(50, context.signal);
|
|
991
|
+
if ((await this.scanLease(allocation.leasePath, allocation.worktreeCwd, context, undefined, true)).length) {
|
|
992
|
+
throw new Error("Exact process lease did not remain empty for two consecutive cleanup scans.");
|
|
993
|
+
}
|
|
994
|
+
if (!await this.paneAbsent(paneId, allocation.worktreeCwd, context)) {
|
|
995
|
+
throw new Error("The exact saved worker pane reappeared before lease cleanup.");
|
|
996
|
+
}
|
|
997
|
+
if (await this.getTab(tabId, allocation.worktreeCwd, context)) {
|
|
998
|
+
throw new Error("The exact saved worker tab reappeared before lease cleanup.");
|
|
999
|
+
}
|
|
1000
|
+
const directoryPresent = await this.assertPrivateLeaseDirectories(allocation.leasePath, true);
|
|
1001
|
+
if (leaseWasPresent) {
|
|
1002
|
+
if (!directoryPresent)
|
|
1003
|
+
throw new Error("Exact process lease directory disappeared during cleanup.");
|
|
1004
|
+
await this.assertPrivateLease(allocation.leasePath, false);
|
|
1005
|
+
await unlink(allocation.leasePath);
|
|
1006
|
+
}
|
|
1007
|
+
else if (await this.assertPrivateLease(allocation.leasePath, true)) {
|
|
1008
|
+
throw new Error("Exact process lease appeared during cleanup.");
|
|
1009
|
+
}
|
|
1010
|
+
if (!directoryPresent)
|
|
1011
|
+
return;
|
|
1012
|
+
try {
|
|
1013
|
+
await rmdir(dirname(allocation.leasePath));
|
|
1014
|
+
}
|
|
1015
|
+
catch (error) {
|
|
1016
|
+
if (!["ENOENT", "ENOTEMPTY", "EEXIST"].includes(error.code ?? ""))
|
|
1017
|
+
throw error;
|
|
1018
|
+
}
|
|
1019
|
+
}
|
|
1020
|
+
async assertPrivateLeaseDirectories(path, allowMissing) {
|
|
1021
|
+
for (const checked of [this.leaseDirectory, dirname(path)]) {
|
|
1022
|
+
let info;
|
|
1023
|
+
try {
|
|
1024
|
+
info = await lstat(checked);
|
|
1025
|
+
}
|
|
1026
|
+
catch (error) {
|
|
1027
|
+
if (allowMissing && error.code === "ENOENT")
|
|
1028
|
+
return false;
|
|
1029
|
+
throw new Error(`Process lease directory cannot be inspected: ${checked}`, { cause: error });
|
|
1030
|
+
}
|
|
1031
|
+
const uid = process.getuid?.();
|
|
1032
|
+
if (info.isSymbolicLink() || !info.isDirectory() || (info.mode & 0o077) !== 0 || (uid !== undefined && info.uid !== uid)) {
|
|
1033
|
+
throw new Error(`Process lease directory must be a private current-user non-symlink directory: ${checked}`);
|
|
1034
|
+
}
|
|
1035
|
+
}
|
|
1036
|
+
return true;
|
|
1037
|
+
}
|
|
1038
|
+
assertLeasePath(path, token) {
|
|
1039
|
+
const parent = resolve(this.leaseDirectory, token);
|
|
1040
|
+
const candidate = resolve(path);
|
|
1041
|
+
if (candidate !== path || !candidate.startsWith(`${parent}${sep}`) || !/^[0-9a-f]{32}\.lease$/.test(candidate.slice(parent.length + 1))) {
|
|
1042
|
+
throw new Error("Process lease path is not the exact random path owned by this task token.");
|
|
1043
|
+
}
|
|
1044
|
+
}
|
|
1045
|
+
async assertPrivateLease(path, allowMissing) {
|
|
1046
|
+
let info;
|
|
1047
|
+
try {
|
|
1048
|
+
info = await lstat(path);
|
|
1049
|
+
}
|
|
1050
|
+
catch (error) {
|
|
1051
|
+
if (allowMissing && error.code === "ENOENT")
|
|
1052
|
+
return false;
|
|
1053
|
+
throw new Error(`Exact process lease cannot be inspected: ${path}`, { cause: error });
|
|
1054
|
+
}
|
|
1055
|
+
const uid = process.getuid?.();
|
|
1056
|
+
if (info.isSymbolicLink() || !info.isFile() || (info.mode & 0o777) !== LEASE_MODE || (uid !== undefined && info.uid !== uid)) {
|
|
1057
|
+
throw new Error("Exact process lease must be a current-user regular non-symlink mode-0600 file.");
|
|
1058
|
+
}
|
|
1059
|
+
return true;
|
|
1060
|
+
}
|
|
1061
|
+
async scanLease(path, cwd, context, pid, allowMissing = false) {
|
|
1062
|
+
if (!await this.assertPrivateLease(path, allowMissing))
|
|
1063
|
+
return [];
|
|
1064
|
+
const args = ["-nP", "-a", ...(pid === undefined ? [] : ["-p", String(pid)]), "-F", "p", "--", path];
|
|
1065
|
+
const result = await this.execute(this.lsofCommand, args, this.processOptions(cwd, context, LSOF_OPERATION_CAP_MS));
|
|
1066
|
+
if (result.killed || ![0, 1].includes(result.code) || (result.code === 1 && (result.stdout.trim() || result.stderr.trim()))) {
|
|
1067
|
+
throw new Error("Exact process lease lsof scan failed or was ambiguous.");
|
|
1068
|
+
}
|
|
1069
|
+
if (result.code === 1)
|
|
1070
|
+
return [];
|
|
1071
|
+
if (result.stderr.trim())
|
|
1072
|
+
throw new Error("Exact process lease lsof scan returned unexpected diagnostics.");
|
|
1073
|
+
const lines = result.stdout.trim().split(/\r?\n/).filter(Boolean);
|
|
1074
|
+
if (!lines.length || lines.some((line) => !/^p[1-9]\d*$/.test(line))) {
|
|
1075
|
+
throw new Error("Exact process lease lsof scan returned malformed PID fields.");
|
|
1076
|
+
}
|
|
1077
|
+
const holders = [...new Set(lines.map((line) => Number(line.slice(1))))];
|
|
1078
|
+
if (holders.some((holder) => !Number.isSafeInteger(holder) || holder <= 0) || (pid !== undefined && holders.some((holder) => holder !== pid))) {
|
|
1079
|
+
throw new Error("Exact process lease lsof scan returned an unexpected PID.");
|
|
1080
|
+
}
|
|
1081
|
+
return holders;
|
|
1082
|
+
}
|
|
1083
|
+
async signalExactHolders(holders, allocation, signal, context) {
|
|
1084
|
+
for (const pid of holders) {
|
|
1085
|
+
context.signal.throwIfAborted();
|
|
1086
|
+
if (!(await this.scanLease(allocation.leasePath, allocation.worktreeCwd, context, pid)).includes(pid))
|
|
1087
|
+
continue;
|
|
1088
|
+
try {
|
|
1089
|
+
this.kill(pid, signal);
|
|
1090
|
+
}
|
|
1091
|
+
catch (error) {
|
|
1092
|
+
if (error.code !== "ESRCH")
|
|
1093
|
+
throw error;
|
|
1094
|
+
}
|
|
1095
|
+
}
|
|
1096
|
+
}
|
|
1097
|
+
async listTabs(workspaceId, cwd, context) {
|
|
1098
|
+
const response = await this.herdr.json(["tab", "list", "--workspace", workspaceId], this.processOptions(cwd, context, HERDR_OPERATION_CAP_MS));
|
|
1099
|
+
const result = resultRecord(response, "Herdr tab list response");
|
|
1100
|
+
if (result.type !== "tab_list" || !Array.isArray(result.tabs))
|
|
1101
|
+
throw new Error("Herdr tab list response is malformed.");
|
|
1102
|
+
return result.tabs.map((item) => record(item, "Herdr listed tab"));
|
|
1103
|
+
}
|
|
1104
|
+
async listAgents(cwd, context) {
|
|
1105
|
+
const response = await this.herdr.json(["agent", "list"], this.processOptions(cwd, context, HERDR_OPERATION_CAP_MS));
|
|
1106
|
+
const result = resultRecord(response, "Herdr agent list response");
|
|
1107
|
+
if (result.type !== "agent_list" || !Array.isArray(result.agents))
|
|
1108
|
+
throw new Error("Herdr agent list response is malformed.");
|
|
1109
|
+
return result.agents.map((item) => record(item, "Herdr listed agent"));
|
|
1110
|
+
}
|
|
1111
|
+
async listPanes(workspaceId, cwd, context) {
|
|
1112
|
+
const response = await this.herdr.json(["pane", "list", "--workspace", workspaceId], this.processOptions(cwd, context, HERDR_OPERATION_CAP_MS));
|
|
1113
|
+
const result = resultRecord(response, "Herdr pane list response");
|
|
1114
|
+
if (result.type !== "pane_list" || !Array.isArray(result.panes))
|
|
1115
|
+
throw new Error("Herdr pane list response is malformed.");
|
|
1116
|
+
return result.panes.map((item) => record(item, "Herdr listed pane"));
|
|
1117
|
+
}
|
|
1118
|
+
async waitForStableWorkspacePanes(workspaceId, cwd, context) {
|
|
1119
|
+
let previous;
|
|
1120
|
+
for (let poll = 0; poll < HOST_LAYOUT_MAX_POLLS; poll += 1) {
|
|
1121
|
+
const snapshot = (await this.listPanes(workspaceId, cwd, context)).map((pane) => {
|
|
1122
|
+
const paneId = exactString(pane.pane_id, "Herdr listed pane ID");
|
|
1123
|
+
const tabId = exactString(pane.tab_id, "Herdr listed pane tab ID");
|
|
1124
|
+
if (exactString(pane.workspace_id, "Herdr listed pane workspace ID") !== workspaceId) {
|
|
1125
|
+
throw new Error("Herdr pane list escaped the exact saved workspace scope.");
|
|
1126
|
+
}
|
|
1127
|
+
return `${tabId}\0${paneId}`;
|
|
1128
|
+
}).sort().join("\n");
|
|
1129
|
+
if (previous === snapshot)
|
|
1130
|
+
return;
|
|
1131
|
+
previous = snapshot;
|
|
1132
|
+
if (poll === HOST_LAYOUT_MAX_POLLS - 1)
|
|
1133
|
+
return;
|
|
1134
|
+
await this.delay(HOST_LAYOUT_POLL_MS, context.signal);
|
|
1135
|
+
}
|
|
1136
|
+
}
|
|
1137
|
+
async getTab(tabId, cwd, context) {
|
|
1138
|
+
const response = await this.herdr.exec(["tab", "get", tabId], this.processOptions(cwd, context, HERDR_OPERATION_CAP_MS));
|
|
1139
|
+
if (response.code === 0 && !response.killed) {
|
|
1140
|
+
const result = resultRecord(parseJsonObject(response.stdout, "Herdr tab get response"), "Herdr tab get response");
|
|
1141
|
+
if (result.type !== "tab_info")
|
|
1142
|
+
throw new Error("Herdr tab get response has the wrong type.");
|
|
1143
|
+
const tab = record(result.tab, "Herdr tab");
|
|
1144
|
+
if (exactString(tab.tab_id, "Herdr tab ID") !== tabId)
|
|
1145
|
+
throw new Error("Herdr tab get returned mismatched data.");
|
|
1146
|
+
return tab;
|
|
1147
|
+
}
|
|
1148
|
+
if (!response.killed && hasHerdrErrorCode(response, "tab_not_found"))
|
|
1149
|
+
return undefined;
|
|
1150
|
+
throw new Error("Exact saved tab presence is ambiguous.");
|
|
1151
|
+
}
|
|
1152
|
+
async paneAbsent(paneId, cwd, context) {
|
|
1153
|
+
const response = await this.herdr.exec(["pane", "get", paneId], this.processOptions(cwd, context, HERDR_OPERATION_CAP_MS));
|
|
1154
|
+
if (response.code === 0 && !response.killed) {
|
|
1155
|
+
const result = resultRecord(parseJsonObject(response.stdout, "Herdr pane get response"), "Herdr pane get response");
|
|
1156
|
+
if (result.type !== "pane_info" || record(result.pane, "Herdr pane").pane_id !== paneId)
|
|
1157
|
+
throw new Error("Herdr pane get returned mismatched data.");
|
|
1158
|
+
return false;
|
|
1159
|
+
}
|
|
1160
|
+
if (!response.killed && hasHerdrErrorCode(response, "pane_not_found"))
|
|
1161
|
+
return true;
|
|
1162
|
+
throw new Error("Exact saved pane presence is ambiguous.");
|
|
1163
|
+
}
|
|
1164
|
+
async workspaceAbsent(workspaceId, allocation, context) {
|
|
1165
|
+
const response = await this.herdr.exec(["workspace", "get", workspaceId], this.processOptions(allocation.herdrRepoRoot, context, HERDR_OPERATION_CAP_MS));
|
|
1166
|
+
if (response.code === 0 && !response.killed) {
|
|
1167
|
+
const workspace = parseWorkspaceInfo(parseJsonObject(response.stdout, "Herdr workspace get response"), workspaceId);
|
|
1168
|
+
this.assertWorkspaceEvidence(workspace, allocation);
|
|
1169
|
+
return false;
|
|
1170
|
+
}
|
|
1171
|
+
if (!response.killed && hasHerdrErrorCode(response, "workspace_not_found"))
|
|
1172
|
+
return true;
|
|
1173
|
+
throw new Error("Exact saved workspace presence is ambiguous.");
|
|
1174
|
+
}
|
|
1175
|
+
assertWorkspaceEvidence(workspace, allocation) {
|
|
1176
|
+
const worktree = record(workspace.worktree, "Owned Herdr workspace worktree");
|
|
1177
|
+
if (exactString(workspace.label, "Owned Herdr workspace label") !== allocation.label
|
|
1178
|
+
|| exactString(worktree.checkout_path, "Owned Herdr workspace checkout_path") !== allocation.worktreeCwd
|
|
1179
|
+
|| exactString(worktree.repo_key, "Owned Herdr workspace repo_key") !== allocation.repoKey
|
|
1180
|
+
|| exactString(worktree.repo_root, "Owned Herdr workspace repo_root") !== allocation.herdrRepoRoot) {
|
|
1181
|
+
throw new Error("The exact saved workspace no longer matches its owned label, checkout, and repository.");
|
|
1182
|
+
}
|
|
1183
|
+
}
|
|
1184
|
+
async repositoryIdentity(mainRoot, context) {
|
|
1185
|
+
const root = exactAbsolutePath(mainRoot, "Git common-directory probe root");
|
|
1186
|
+
const result = await this.execute("git", ["rev-parse", "--path-format=absolute", "--git-common-dir"], this.processOptions(root, context, GIT_INSPECTION_CAP_MS));
|
|
1187
|
+
if (result.code !== 0 || result.killed)
|
|
1188
|
+
throw new Error("Git common-directory identity probe failed.");
|
|
1189
|
+
const output = result.stdout.replace(/\r?\n$/, "");
|
|
1190
|
+
if (!output || /[\r\n\0]/.test(output) || !isAbsolute(output)) {
|
|
1191
|
+
throw new Error("Git common-directory identity probe returned malformed output.");
|
|
1192
|
+
}
|
|
1193
|
+
const repoKey = await realpath(output);
|
|
1194
|
+
const repoKeyInfo = await lstat(repoKey);
|
|
1195
|
+
if (!repoKeyInfo.isDirectory() || basename(repoKey) !== ".git") {
|
|
1196
|
+
throw new Error("Git common-directory identity does not name a real .git directory.");
|
|
1197
|
+
}
|
|
1198
|
+
const herdrRepoRoot = dirname(repoKey);
|
|
1199
|
+
const resolvedRepoRoot = await realpath(herdrRepoRoot);
|
|
1200
|
+
const repoRootInfo = await lstat(resolvedRepoRoot);
|
|
1201
|
+
if (!isAbsolute(herdrRepoRoot) || resolvedRepoRoot !== herdrRepoRoot || !repoRootInfo.isDirectory()) {
|
|
1202
|
+
throw new Error("Git common-directory identity does not derive an absolute existing real primary repository root.");
|
|
1203
|
+
}
|
|
1204
|
+
return { repoKey, herdrRepoRoot };
|
|
1205
|
+
}
|
|
1206
|
+
async assertRepositoryIdentity(allocation, context) {
|
|
1207
|
+
const identity = await this.repositoryIdentity(allocation.mainRoot, context);
|
|
1208
|
+
if (identity.repoKey !== allocation.repoKey || identity.herdrRepoRoot !== allocation.herdrRepoRoot) {
|
|
1209
|
+
throw new Error("Persisted workspace repository identity no longer matches Git.");
|
|
1210
|
+
}
|
|
1211
|
+
}
|
|
1212
|
+
processOptions(cwd, context, cap = HERDR_OPERATION_CAP_MS) {
|
|
1213
|
+
context.signal.throwIfAborted();
|
|
1214
|
+
return { cwd, signal: context.signal, timeoutMs: this.callTimeout(context, cap) };
|
|
1215
|
+
}
|
|
1216
|
+
childContext(context, cap) {
|
|
1217
|
+
return { signal: context.signal, deadline: context.deadline, timeoutMs: this.callTimeout(context, cap) };
|
|
1218
|
+
}
|
|
1219
|
+
callTimeout(context, cap) {
|
|
1220
|
+
const remaining = Math.min(context.timeoutMs ?? cap, context.deadline === undefined ? cap : context.deadline - this.now(), cap);
|
|
1221
|
+
if (!Number.isFinite(remaining) || remaining <= 0)
|
|
1222
|
+
throw new Error("Operation deadline is exhausted.");
|
|
1223
|
+
return Math.max(1, Math.floor(remaining));
|
|
1224
|
+
}
|
|
1225
|
+
contextInterrupted(context) {
|
|
1226
|
+
return context.signal.aborted || (context.deadline !== undefined && context.deadline <= this.now());
|
|
1227
|
+
}
|
|
1228
|
+
}
|