@codetreker/daynight 0.21.9 → 0.21.11
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/README.md +204 -27
- package/dist/api/client.d.ts +3 -1
- package/dist/api/client.js +12 -1
- package/dist/api/client.js.map +1 -1
- package/dist/api/parse.d.ts +2 -1
- package/dist/api/parse.js +8 -0
- package/dist/api/parse.js.map +1 -1
- package/dist/api/types.d.ts +7 -0
- package/dist/cli/commands.js +2 -0
- package/dist/cli/commands.js.map +1 -1
- package/dist/commands/worker/cleanup.d.ts +6 -0
- package/dist/commands/worker/cleanup.js +173 -0
- package/dist/commands/worker/cleanup.js.map +1 -0
- package/dist/commands/worker/config.d.ts +21 -0
- package/dist/commands/worker/config.js +144 -0
- package/dist/commands/worker/config.js.map +1 -0
- package/dist/commands/worker/coordinator.d.ts +34 -0
- package/dist/commands/worker/coordinator.js +508 -0
- package/dist/commands/worker/coordinator.js.map +1 -0
- package/dist/commands/worker/definition.d.ts +2 -0
- package/dist/commands/worker/definition.js +14 -0
- package/dist/commands/worker/definition.js.map +1 -0
- package/dist/commands/worker/diagnostics.d.ts +20 -0
- package/dist/commands/worker/diagnostics.js +56 -0
- package/dist/commands/worker/diagnostics.js.map +1 -0
- package/dist/commands/worker/execution.d.ts +41 -0
- package/dist/commands/worker/execution.js +255 -0
- package/dist/commands/worker/execution.js.map +1 -0
- package/dist/commands/worker/herdr/agents.d.ts +185 -0
- package/dist/commands/worker/herdr/agents.js +654 -0
- package/dist/commands/worker/herdr/agents.js.map +1 -0
- package/dist/commands/worker/herdr/arguments.d.ts +8 -0
- package/dist/commands/worker/herdr/arguments.js +58 -0
- package/dist/commands/worker/herdr/arguments.js.map +1 -0
- package/dist/commands/worker/herdr/command.d.ts +22 -0
- package/dist/commands/worker/herdr/command.js +115 -0
- package/dist/commands/worker/herdr/command.js.map +1 -0
- package/dist/commands/worker/herdr/instance.d.ts +56 -0
- package/dist/commands/worker/herdr/instance.js +322 -0
- package/dist/commands/worker/herdr/instance.js.map +1 -0
- package/dist/commands/worker/herdr/preflight.d.ts +40 -0
- package/dist/commands/worker/herdr/preflight.js +251 -0
- package/dist/commands/worker/herdr/preflight.js.map +1 -0
- package/dist/commands/worker/herdr/rpc.d.ts +29 -0
- package/dist/commands/worker/herdr/rpc.js +186 -0
- package/dist/commands/worker/herdr/rpc.js.map +1 -0
- package/dist/commands/worker/herdr.d.ts +95 -0
- package/dist/commands/worker/herdr.js +35 -0
- package/dist/commands/worker/herdr.js.map +1 -0
- package/dist/commands/worker/index.d.ts +10 -0
- package/dist/commands/worker/index.js +14 -0
- package/dist/commands/worker/index.js.map +1 -0
- package/dist/commands/worker/prompts.d.ts +4 -0
- package/dist/commands/worker/prompts.js +64 -0
- package/dist/commands/worker/prompts.js.map +1 -0
- package/dist/commands/worker/remote.d.ts +18 -0
- package/dist/commands/worker/remote.js +134 -0
- package/dist/commands/worker/remote.js.map +1 -0
- package/dist/commands/worker/runtime.d.ts +8 -0
- package/dist/commands/worker/runtime.js +246 -0
- package/dist/commands/worker/runtime.js.map +1 -0
- package/dist/commands/worker/store.d.ts +55 -5
- package/dist/commands/worker/store.js +381 -26
- package/dist/commands/worker/store.js.map +1 -1
- package/dist/commands/worker/types.d.ts +37 -1
- package/dist/commands/worker/worktrees.d.ts +1 -0
- package/dist/commands/worker/worktrees.js +9 -0
- package/dist/commands/worker/worktrees.js.map +1 -1
- package/dist/config/document.d.ts +1 -1
- package/dist/config/document.js +1 -0
- package/dist/config/document.js.map +1 -1
- package/package.json +2 -2
|
@@ -0,0 +1,654 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { realpath } from "node:fs/promises";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { canonicalId } from "../../../api/parse.js";
|
|
5
|
+
import { normalizeServerUrl } from "../../../config/server.js";
|
|
6
|
+
import { buildHerdrAgentArguments } from "./arguments.js";
|
|
7
|
+
import { HerdrRpcError } from "./rpc.js";
|
|
8
|
+
export { buildHerdrAgentArguments, validateHerdrAgentArguments } from "./arguments.js";
|
|
9
|
+
const object = (value, name) => {
|
|
10
|
+
if (typeof value !== "object" || value === null || Array.isArray(value))
|
|
11
|
+
throw new TypeError(`${name} must be an object.`);
|
|
12
|
+
return value;
|
|
13
|
+
};
|
|
14
|
+
const text = (value, name) => {
|
|
15
|
+
if (typeof value !== "string" || value === "" || /[\r\n\0]/.test(value))
|
|
16
|
+
throw new TypeError(`${name} must be nonempty text without line breaks or NUL.`);
|
|
17
|
+
return value;
|
|
18
|
+
};
|
|
19
|
+
const array = (value, name) => { if (!Array.isArray(value))
|
|
20
|
+
throw new TypeError(`${name} must be an array.`); return value; };
|
|
21
|
+
const optionalText = (value, name) => value === undefined ? null : text(value, name);
|
|
22
|
+
const optionalBoolean = (value, name) => { if (value === undefined)
|
|
23
|
+
return false; if (typeof value !== "boolean")
|
|
24
|
+
throw new TypeError(`${name} must be boolean.`); return value; };
|
|
25
|
+
const pid = (value) => { if (typeof value !== "number" || !Number.isSafeInteger(value) || value < 1 || value > 2_147_483_647)
|
|
26
|
+
throw new TypeError("Herdr process ID must be a positive process identifier."); return value; };
|
|
27
|
+
const counter = (value, name) => { if (typeof value !== "number" || !Number.isSafeInteger(value) || value < 0)
|
|
28
|
+
throw new TypeError(`${name} must be a nonnegative safe integer.`); return value; };
|
|
29
|
+
const nativeProcessEvidence = (value, agent) => {
|
|
30
|
+
const executable = (candidate) => {
|
|
31
|
+
if (typeof candidate !== "string")
|
|
32
|
+
return false;
|
|
33
|
+
const leaf = candidate.split(/[\\/]/).at(-1)?.toLowerCase();
|
|
34
|
+
return leaf === agent || leaf === `${agent}.exe` || leaf === `${agent}.js`;
|
|
35
|
+
};
|
|
36
|
+
if (value.argv !== undefined) {
|
|
37
|
+
const argv = array(value.argv, "Native process arguments");
|
|
38
|
+
if (argv.some((argument) => typeof argument !== "string"))
|
|
39
|
+
throw new TypeError("Native process arguments must be strings.");
|
|
40
|
+
if (executable(argv[0]))
|
|
41
|
+
return true;
|
|
42
|
+
const interpreter = typeof argv[0] === "string" ? argv[0].split(/[\\/]/).at(-1)?.toLowerCase() : undefined;
|
|
43
|
+
if ((interpreter === "node" || interpreter === "node.exe") && executable(argv[1]))
|
|
44
|
+
return true;
|
|
45
|
+
}
|
|
46
|
+
return executable(value.argv0) || executable(value.name);
|
|
47
|
+
};
|
|
48
|
+
const expectedResult = (value, type) => { if (value.type !== type)
|
|
49
|
+
throw new TypeError(`Expected Herdr ${type} result; received ${value.type}.`); return value; };
|
|
50
|
+
const sameBinding = (left, right) => left.session === right.session && left.workspaceId === right.workspaceId && left.paneId === right.paneId && left.terminalId === right.terminalId;
|
|
51
|
+
const contextIdentity = (context) => {
|
|
52
|
+
if (context.agent !== "codex" && context.agent !== "copilot")
|
|
53
|
+
throw new TypeError("Execution agent must be codex or copilot.");
|
|
54
|
+
if (!path.isAbsolute(context.cwd) || /[\r\n\0]/.test(context.cwd))
|
|
55
|
+
throw new TypeError("The task cwd must be an absolute path without line breaks or NUL.");
|
|
56
|
+
const identifier = (value, label) => { const result = canonicalId(value); if (result === null)
|
|
57
|
+
throw new TypeError(`${label} must be a UUID.`); return result; };
|
|
58
|
+
return { serverOrigin: normalizeServerUrl(context.serverOrigin), projectId: identifier(context.projectId, "Project ID"), taskId: identifier(context.taskId, "Task ID"), claimId: identifier(context.claimId, "Claim ID"), worktreeId: identifier(context.worktreeId, "Worktree ID") };
|
|
59
|
+
};
|
|
60
|
+
export const herdrExecutionLabel = (context) => `daynight:${JSON.stringify(contextIdentity(context))}`;
|
|
61
|
+
export const herdrExecutionAgentName = (context) => `dn_${createHash("sha256").update(herdrExecutionLabel(context)).digest("hex").slice(0, 24)}`;
|
|
62
|
+
export const parseHerdrExecutionLabel = (label) => {
|
|
63
|
+
if (!label.startsWith("daynight:"))
|
|
64
|
+
return null;
|
|
65
|
+
const data = object(JSON.parse(label.slice("daynight:".length)), "Herdr execution label");
|
|
66
|
+
const fields = ["serverOrigin", "projectId", "taskId", "claimId", "worktreeId"];
|
|
67
|
+
if (Object.keys(data).length !== fields.length || fields.some((field) => !Object.hasOwn(data, field)))
|
|
68
|
+
throw new TypeError("The reserved Herdr execution label has invalid fields.");
|
|
69
|
+
const owner = { serverOrigin: normalizeServerUrl(text(data.serverOrigin, "Label server origin")), projectId: text(data.projectId, "Label project ID"), taskId: text(data.taskId, "Label task ID"), claimId: text(data.claimId, "Label claim ID"), worktreeId: text(data.worktreeId, "Label worktree ID") };
|
|
70
|
+
for (const value of [owner.projectId, owner.taskId, owner.claimId, owner.worktreeId])
|
|
71
|
+
if (canonicalId(value) !== value)
|
|
72
|
+
throw new TypeError("Reserved Herdr label identifiers must be canonical UUIDs.");
|
|
73
|
+
if (`daynight:${JSON.stringify(owner)}` !== label)
|
|
74
|
+
throw new TypeError("The reserved Herdr execution label must use its canonical identity encoding.");
|
|
75
|
+
return owner;
|
|
76
|
+
};
|
|
77
|
+
export class HerdrAgentActionError extends Error {
|
|
78
|
+
action;
|
|
79
|
+
submission;
|
|
80
|
+
promptSubmission;
|
|
81
|
+
launchEvidence;
|
|
82
|
+
constructor(message, action, submission, options) {
|
|
83
|
+
super(message, options);
|
|
84
|
+
this.action = action;
|
|
85
|
+
this.submission = submission;
|
|
86
|
+
this.name = "HerdrAgentActionError";
|
|
87
|
+
this.promptSubmission = options?.promptSubmission === undefined ? null : options.promptSubmission;
|
|
88
|
+
this.launchEvidence = options?.launchEvidence === undefined ? null : options.launchEvidence;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
const rejectionCodes = {
|
|
92
|
+
"agent.start": new Set(["invalid_agent_name", "unsupported_agent_kind", "invalid_agent_argument", "invalid_agent_timeout", "agent_pane_not_found", "agent_pane_busy", "agent_pane_unavailable", "agent_name_taken"]),
|
|
93
|
+
"agent.prompt": new Set(["empty_agent_prompt", "agent_blocked", "agent_not_ready", "agent_not_found", "ambiguous_agent_target"]),
|
|
94
|
+
};
|
|
95
|
+
const classifyMutationError = (action, cause) => {
|
|
96
|
+
if (cause instanceof HerdrRpcError) {
|
|
97
|
+
const submission = cause.code !== null && rejectionCodes[action]?.has(cause.code) ? "rejected" : cause.submission;
|
|
98
|
+
return new HerdrAgentActionError(`Herdr ${action} did not establish a successful result.`, action, submission, { cause, ...(action === "agent.prompt" ? { promptSubmission: submission === "unknown" ? "unknown" : "not-submitted" } : {}) });
|
|
99
|
+
}
|
|
100
|
+
return new HerdrAgentActionError(`Herdr ${action} did not establish a successful result.`, action, "unknown", { cause, ...(action === "agent.prompt" ? { promptSubmission: "unknown" } : {}) });
|
|
101
|
+
};
|
|
102
|
+
const processExecuting = async (processId) => {
|
|
103
|
+
try {
|
|
104
|
+
process.kill(processId, 0);
|
|
105
|
+
return true;
|
|
106
|
+
}
|
|
107
|
+
catch (cause) {
|
|
108
|
+
if (cause instanceof Error && "code" in cause && cause.code === "ESRCH")
|
|
109
|
+
return false;
|
|
110
|
+
throw cause;
|
|
111
|
+
}
|
|
112
|
+
};
|
|
113
|
+
export const createHerdrAgentManager = (options) => {
|
|
114
|
+
const session = text(options.session, "Herdr session");
|
|
115
|
+
const suppliedEnvironment = object(options.workspaceEnvironment, "Task workspace environment");
|
|
116
|
+
const xdgFields = ["XDG_CONFIG_HOME", "XDG_STATE_HOME"];
|
|
117
|
+
if (Object.keys(suppliedEnvironment).length !== xdgFields.length || xdgFields.some((field) => !Object.hasOwn(suppliedEnvironment, field) || typeof suppliedEnvironment[field] !== "string" || suppliedEnvironment[field].includes("\0")))
|
|
118
|
+
throw new TypeError("Task workspace environment must contain only XDG_CONFIG_HOME and XDG_STATE_HOME string values without NUL.");
|
|
119
|
+
const workspaceEnvironment = { XDG_CONFIG_HOME: suppliedEnvironment.XDG_CONFIG_HOME, XDG_STATE_HOME: suppliedEnvironment.XDG_STATE_HOME };
|
|
120
|
+
const validWorkspaceEvidence = (context, binding, evidence) => {
|
|
121
|
+
if (evidence === undefined || evidence === null || typeof evidence.binding !== "object" || evidence.binding === null || !sameBinding(evidence.binding, binding) || evidence.ownerLabel !== herdrExecutionLabel(context))
|
|
122
|
+
return false;
|
|
123
|
+
const environment = evidence.environment;
|
|
124
|
+
if (typeof environment !== "object" || environment === null || Array.isArray(environment))
|
|
125
|
+
return false;
|
|
126
|
+
const values = environment;
|
|
127
|
+
return Object.keys(values).length === xdgFields.length && xdgFields.every((field) => Object.hasOwn(values, field) && values[field] === workspaceEnvironment[field]);
|
|
128
|
+
};
|
|
129
|
+
const isExecuting = options.processExecuting === undefined ? processExecuting : options.processExecuting;
|
|
130
|
+
const pollInterval = options.pollIntervalMs === undefined ? 100 : options.pollIntervalMs;
|
|
131
|
+
if (!Number.isSafeInteger(pollInterval) || pollInterval <= 0 || pollInterval > 1000)
|
|
132
|
+
throw new TypeError("The Herdr observation interval must be between 1 and 1000 milliseconds.");
|
|
133
|
+
const inputTails = new Map();
|
|
134
|
+
const serialized = async (key, operation, action) => {
|
|
135
|
+
operation.check();
|
|
136
|
+
const previous = inputTails.get(key);
|
|
137
|
+
let release;
|
|
138
|
+
const gate = new Promise((resolve) => { release = resolve; });
|
|
139
|
+
const tail = previous === undefined ? gate : previous.then(() => gate);
|
|
140
|
+
inputTails.set(key, tail);
|
|
141
|
+
void tail.then(() => { if (inputTails.get(key) === tail)
|
|
142
|
+
inputTails.delete(key); });
|
|
143
|
+
try {
|
|
144
|
+
if (previous !== undefined)
|
|
145
|
+
await new Promise((resolve, reject) => {
|
|
146
|
+
let completed = false;
|
|
147
|
+
const abort = () => { if (completed)
|
|
148
|
+
return; completed = true; operation.signal.removeEventListener("abort", abort); reject(operation.signal.reason); };
|
|
149
|
+
operation.signal.addEventListener("abort", abort, { once: true });
|
|
150
|
+
void previous.then(() => { if (completed)
|
|
151
|
+
return; completed = true; operation.signal.removeEventListener("abort", abort); resolve(); });
|
|
152
|
+
if (operation.signal.aborted)
|
|
153
|
+
abort();
|
|
154
|
+
});
|
|
155
|
+
operation.check();
|
|
156
|
+
return await action();
|
|
157
|
+
}
|
|
158
|
+
finally {
|
|
159
|
+
release();
|
|
160
|
+
}
|
|
161
|
+
};
|
|
162
|
+
const wait = (operation) => new Promise((resolve, reject) => {
|
|
163
|
+
operation.check();
|
|
164
|
+
const abort = () => { clearTimeout(timer); operation.signal.removeEventListener("abort", abort); reject(operation.signal.reason); };
|
|
165
|
+
const timer = setTimeout(() => { operation.signal.removeEventListener("abort", abort); resolve(); }, Math.min(pollInterval, Math.max(1, operation.remainingMs())));
|
|
166
|
+
operation.signal.addEventListener("abort", abort, { once: true });
|
|
167
|
+
if (operation.signal.aborted)
|
|
168
|
+
abort();
|
|
169
|
+
});
|
|
170
|
+
const snapshot = async (operation) => {
|
|
171
|
+
const result = expectedResult(await options.rpc.request("session.snapshot", {}, operation), "session_snapshot");
|
|
172
|
+
const data = object(result.snapshot, "Herdr snapshot");
|
|
173
|
+
return { workspaces: array(data.workspaces, "Herdr workspaces").map((value) => object(value, "Herdr workspace")), panes: array(data.panes, "Herdr panes").map((value) => object(value, "Herdr pane")), agents: array(data.agents, "Herdr agents").map((value) => object(value, "Herdr agent")), tabs: array(data.tabs, "Herdr tabs").map((value) => object(value, "Herdr tab")) };
|
|
174
|
+
};
|
|
175
|
+
const verifyCwd = async (actual, context) => {
|
|
176
|
+
const actualPath = text(actual, "Herdr pane cwd");
|
|
177
|
+
if (!path.isAbsolute(actualPath) || path.normalize(await realpath(actualPath)) !== path.normalize(await realpath(context.cwd)))
|
|
178
|
+
throw new Error("The Herdr pane cwd differs from the original task worktree.");
|
|
179
|
+
};
|
|
180
|
+
const readOwned = async (context, binding, operation, allowMissingPane = false) => {
|
|
181
|
+
if (binding.session !== session)
|
|
182
|
+
throw new Error("The task binding belongs to another Herdr session.");
|
|
183
|
+
const data = await snapshot(operation);
|
|
184
|
+
const label = herdrExecutionLabel(context);
|
|
185
|
+
const matching = data.workspaces.filter((workspace) => workspace.label === label);
|
|
186
|
+
if (matching.length !== 1 || matching[0].workspace_id !== binding.workspaceId)
|
|
187
|
+
throw new Error("The original task workspace is missing, duplicated or has different ownership.");
|
|
188
|
+
const panes = data.panes.filter((pane) => pane.pane_id === binding.paneId);
|
|
189
|
+
if (panes.length === 0 && allowMissingPane)
|
|
190
|
+
return { data, pane: null };
|
|
191
|
+
if (panes.length !== 1)
|
|
192
|
+
throw new Error("The original task pane is missing or duplicated.");
|
|
193
|
+
const pane = panes[0];
|
|
194
|
+
if (pane.workspace_id !== binding.workspaceId || pane.terminal_id !== binding.terminalId)
|
|
195
|
+
throw new Error("The task pane or terminal identity has changed.");
|
|
196
|
+
await verifyCwd(pane.cwd, context);
|
|
197
|
+
return { data, pane };
|
|
198
|
+
};
|
|
199
|
+
const nativeReference = (value, expectedAgent) => {
|
|
200
|
+
if (value === undefined)
|
|
201
|
+
return null;
|
|
202
|
+
const reference = object(value, "Native session reference");
|
|
203
|
+
if (reference.source !== `herdr:${expectedAgent}` || reference.agent !== expectedAgent || reference.kind !== "id")
|
|
204
|
+
throw new Error("The native session reference was not reported by the expected official Herdr integration.");
|
|
205
|
+
const sessionId = canonicalId(text(reference.value, "Native session ID"));
|
|
206
|
+
if (sessionId === null)
|
|
207
|
+
throw new Error("The native session reference must contain an exact session UUID.");
|
|
208
|
+
return { source: reference.source, agent: expectedAgent, sessionId };
|
|
209
|
+
};
|
|
210
|
+
const observation = async (context, binding, operation) => {
|
|
211
|
+
const { data, pane } = await readOwned(context, binding, operation);
|
|
212
|
+
const candidates = data.agents.filter((agent) => agent.pane_id === binding.paneId);
|
|
213
|
+
if (candidates.length > 1)
|
|
214
|
+
throw new Error("The original task pane has multiple agent observations.");
|
|
215
|
+
const agent = candidates[0];
|
|
216
|
+
const info = agent === undefined ? pane : agent;
|
|
217
|
+
if (info.workspace_id !== binding.workspaceId || info.terminal_id !== binding.terminalId)
|
|
218
|
+
throw new Error("The task agent observation belongs to another workspace or terminal.");
|
|
219
|
+
await verifyCwd(info.cwd, context);
|
|
220
|
+
const kind = optionalText(info.agent, "Native agent kind");
|
|
221
|
+
if (kind !== null && kind !== context.agent)
|
|
222
|
+
throw new Error("The task pane hosts a different native agent.");
|
|
223
|
+
const name = optionalText(info.name, "Managed agent name");
|
|
224
|
+
if (name !== null && name !== herdrExecutionAgentName(context))
|
|
225
|
+
throw new Error("The task pane has a different managed agent name.");
|
|
226
|
+
const status = text(info.agent_status, "Native agent status");
|
|
227
|
+
if (!["idle", "working", "blocked", "done", "unknown"].includes(status))
|
|
228
|
+
throw new TypeError("The native agent status is invalid.");
|
|
229
|
+
return { binding, kind, name, status, launchPending: optionalBoolean(info.launch_pending, "Native launch state"), interactiveReady: optionalBoolean(info.interactive_ready, "Native readiness"), nativeSession: nativeReference(info.agent_session, context.agent), stateChangeSeq: agent === undefined ? null : counter(info.state_change_seq, "Native state-change sequence"), completionSeq: agent === undefined || info.completion_seq === undefined ? null : counter(info.completion_seq, "Native completion sequence") };
|
|
230
|
+
};
|
|
231
|
+
const mutation = async (method, params, type, operation) => {
|
|
232
|
+
try {
|
|
233
|
+
return expectedResult(await options.rpc.request(method, params, operation), type);
|
|
234
|
+
}
|
|
235
|
+
catch (cause) {
|
|
236
|
+
throw classifyMutationError(method, cause);
|
|
237
|
+
}
|
|
238
|
+
};
|
|
239
|
+
const submit = async (context, binding, prompt, operation, initialPrompt = false) => {
|
|
240
|
+
if (typeof prompt !== "string" || prompt.length === 0)
|
|
241
|
+
throw new HerdrAgentActionError("The caller prompt must be nonempty text.", "agent.prompt", "not-submitted", { promptSubmission: "not-submitted" });
|
|
242
|
+
const before = await observation(context, binding, operation);
|
|
243
|
+
const knownAgent = before.kind === context.agent || before.nativeSession?.agent === context.agent || (initialPrompt && before.name === herdrExecutionAgentName(context));
|
|
244
|
+
if (!knownAgent || before.launchPending || (!initialPrompt && before.nativeSession === null) || (initialPrompt && !before.interactiveReady) || before.status === "blocked")
|
|
245
|
+
throw new HerdrAgentActionError("The original native agent is not ready for input.", "agent.prompt", "not-submitted", { promptSubmission: "not-submitted" });
|
|
246
|
+
const result = await mutation("agent.prompt", { target: binding.paneId, text: prompt }, "agent_prompted", operation);
|
|
247
|
+
try {
|
|
248
|
+
const returned = object(result.agent, "Prompt agent receipt");
|
|
249
|
+
if (returned.pane_id !== binding.paneId || returned.terminal_id !== binding.terminalId || returned.workspace_id !== binding.workspaceId)
|
|
250
|
+
throw new Error("The prompt receipt does not identify the original task agent.");
|
|
251
|
+
}
|
|
252
|
+
catch (cause) {
|
|
253
|
+
throw new HerdrAgentActionError("The prompt receipt could not identify the original task agent.", "agent.prompt", "unknown", { cause, promptSubmission: "unknown" });
|
|
254
|
+
}
|
|
255
|
+
return { submission: "submitted", observation: before };
|
|
256
|
+
};
|
|
257
|
+
const verifyStopObservation = (context, binding, evidence) => {
|
|
258
|
+
if (!sameBinding(binding, evidence.binding) || evidence.ownerLabel !== herdrExecutionLabel(context) || evidence.agent !== context.agent)
|
|
259
|
+
throw new Error("The saved process observation belongs to another execution.");
|
|
260
|
+
pid(evidence.shellPid);
|
|
261
|
+
if (evidence.nativeSessionId !== null && canonicalId(evidence.nativeSessionId) === null)
|
|
262
|
+
throw new Error("The saved native session identity is invalid.");
|
|
263
|
+
if (!Array.isArray(evidence.processIds) || evidence.processIds.length === 0 || new Set(evidence.processIds).size !== evidence.processIds.length || evidence.processIds.some((value) => pid(value) === evidence.shellPid))
|
|
264
|
+
throw new Error("The original native process observation is invalid.");
|
|
265
|
+
if (!Number.isSafeInteger(evidence.observedAt) || evidence.observedAt < 0)
|
|
266
|
+
throw new Error("The original process observation time is invalid.");
|
|
267
|
+
};
|
|
268
|
+
const absentProcesses = async (evidence, operation) => {
|
|
269
|
+
for (const processId of evidence.processIds) {
|
|
270
|
+
operation.check();
|
|
271
|
+
if (await isExecuting(processId))
|
|
272
|
+
throw new Error("The original native process is still executing or its absence is unconfirmed.");
|
|
273
|
+
}
|
|
274
|
+
};
|
|
275
|
+
const verifyOriginalStopScope = (context, binding, evidence) => {
|
|
276
|
+
verifyStopObservation(context, binding, evidence);
|
|
277
|
+
if (binding.session !== session)
|
|
278
|
+
throw new Error("The original stop observation belongs to another Herdr session.");
|
|
279
|
+
for (const field of ["session", "workspaceId", "paneId", "terminalId"])
|
|
280
|
+
text(binding[field], `Original Herdr ${field}`);
|
|
281
|
+
};
|
|
282
|
+
const originalProcessesAbsent = async (evidence, operation) => {
|
|
283
|
+
await absentProcesses(evidence, operation);
|
|
284
|
+
operation.check();
|
|
285
|
+
if (await isExecuting(evidence.shellPid))
|
|
286
|
+
throw new Error("The original task shell is still executing or its absence is unconfirmed.");
|
|
287
|
+
};
|
|
288
|
+
const requireOriginalResourceAbsent = (data, binding) => {
|
|
289
|
+
const workspaceIds = new Set();
|
|
290
|
+
for (const workspace of data.workspaces) {
|
|
291
|
+
const id = text(workspace.workspace_id, "Observed workspace ID");
|
|
292
|
+
if (typeof workspace.label !== "string" || workspaceIds.has(id))
|
|
293
|
+
throw new Error("The Herdr workspace snapshot is malformed or ambiguous.");
|
|
294
|
+
workspaceIds.add(id);
|
|
295
|
+
if (id === binding.workspaceId)
|
|
296
|
+
throw new Error("The original task workspace is still present.");
|
|
297
|
+
}
|
|
298
|
+
if ([...data.panes, ...data.agents].some((resource) => resource.workspace_id === binding.workspaceId || resource.pane_id === binding.paneId || resource.terminal_id === binding.terminalId))
|
|
299
|
+
throw new Error("The original task pane, terminal or agent is still present in the Herdr snapshot.");
|
|
300
|
+
};
|
|
301
|
+
const emptyOwnedPane = async (context, binding, operation, mode) => {
|
|
302
|
+
const current = await observation(context, binding, operation);
|
|
303
|
+
if (current.kind !== null || current.launchPending || current.interactiveReady || (mode === "unlaunched" && (current.name !== null || current.nativeSession !== null)))
|
|
304
|
+
throw new Error(mode === "unlaunched" ? "The task pane is not an unlaunched plain shell." : "The restored task pane is not an inactive shell.");
|
|
305
|
+
const owned = await readOwned(context, binding, operation);
|
|
306
|
+
if (mode === "unlaunched" && owned.data.panes.filter((pane) => pane.workspace_id === binding.workspaceId).length !== 1)
|
|
307
|
+
throw new Error("The task workspace does not contain a single unlaunched shell.");
|
|
308
|
+
const result = expectedResult(await options.rpc.request("pane.process_info", { pane_id: binding.paneId }, operation), "pane_process_info");
|
|
309
|
+
const info = object(result.process_info, "Empty task pane process observation");
|
|
310
|
+
if (info.pane_id !== binding.paneId)
|
|
311
|
+
throw new Error("The empty-shell process observation belongs to another pane.");
|
|
312
|
+
const shellPid = pid(info.shell_pid);
|
|
313
|
+
if (info.foreground_process_group_id !== undefined && pid(info.foreground_process_group_id) !== shellPid)
|
|
314
|
+
throw new Error("The task shell has a different foreground process group.");
|
|
315
|
+
const processes = info.foreground_processes === undefined ? [] : array(info.foreground_processes, "Empty task foreground processes");
|
|
316
|
+
if (processes.some((value) => pid(object(value, "Empty task foreground process").pid) !== shellPid))
|
|
317
|
+
throw new Error("The task shell still hosts foreground work.");
|
|
318
|
+
return { current, data: owned.data };
|
|
319
|
+
};
|
|
320
|
+
return {
|
|
321
|
+
listResources: async (operation) => {
|
|
322
|
+
const data = await snapshot(operation);
|
|
323
|
+
const result = [];
|
|
324
|
+
const labels = new Set();
|
|
325
|
+
const workspaceIds = new Set();
|
|
326
|
+
for (const workspace of data.workspaces) {
|
|
327
|
+
if (typeof workspace.label !== "string")
|
|
328
|
+
throw new TypeError("The Herdr workspace label must be a string.");
|
|
329
|
+
const label = workspace.label;
|
|
330
|
+
const owner = parseHerdrExecutionLabel(label);
|
|
331
|
+
if (owner === null)
|
|
332
|
+
continue;
|
|
333
|
+
const workspaceId = text(workspace.workspace_id, "Owned workspace ID");
|
|
334
|
+
if (labels.has(label) || workspaceIds.has(workspaceId) || data.workspaces.filter((value) => value.workspace_id === workspaceId).length !== 1)
|
|
335
|
+
throw new Error("Owned Herdr workspaces have duplicate labels or IDs.");
|
|
336
|
+
labels.add(label);
|
|
337
|
+
workspaceIds.add(workspaceId);
|
|
338
|
+
const resource = { owner, workspaceId, panes: [] };
|
|
339
|
+
for (const pane of data.panes.filter((value) => value.workspace_id === workspaceId)) {
|
|
340
|
+
const paneId = text(pane.pane_id, "Owned pane ID");
|
|
341
|
+
const terminalId = text(pane.terminal_id, "Owned terminal ID");
|
|
342
|
+
if (data.panes.filter((value) => value.pane_id === paneId || value.terminal_id === terminalId).length !== 1)
|
|
343
|
+
throw new Error("Owned Herdr panes have ambiguous pane or terminal IDs.");
|
|
344
|
+
const agents = data.agents.filter((value) => value.pane_id === paneId);
|
|
345
|
+
if (agents.length > 1)
|
|
346
|
+
throw new Error("An owned Herdr pane has multiple agent observations.");
|
|
347
|
+
const info = agents[0] === undefined ? pane : agents[0];
|
|
348
|
+
if (info.workspace_id !== workspaceId || info.terminal_id !== terminalId)
|
|
349
|
+
throw new Error("The owned Herdr agent association is inconsistent.");
|
|
350
|
+
const cwd = optionalText(info.cwd, "Owned pane cwd");
|
|
351
|
+
if (cwd !== null && !path.isAbsolute(cwd))
|
|
352
|
+
throw new TypeError("The owned Herdr pane cwd must be absolute.");
|
|
353
|
+
if (pane.cwd !== undefined && cwd !== null && path.normalize(text(pane.cwd, "Owned terminal cwd")) !== path.normalize(cwd))
|
|
354
|
+
throw new Error("The owned Herdr pane and agent cwd differ.");
|
|
355
|
+
const kind = optionalText(info.agent, "Owned agent kind");
|
|
356
|
+
if (kind !== null && kind !== "codex" && kind !== "copilot")
|
|
357
|
+
throw new Error("The owned task workspace hosts an unsupported native agent.");
|
|
358
|
+
const native = info.agent_session === undefined ? null : object(info.agent_session, "Owned native session");
|
|
359
|
+
const nativeAgent = native === null ? null : text(native.agent, "Owned native provider");
|
|
360
|
+
if (nativeAgent !== null && nativeAgent !== "codex" && nativeAgent !== "copilot")
|
|
361
|
+
throw new Error("The owned native reference has an unsupported provider.");
|
|
362
|
+
if (kind !== null && nativeAgent !== null && kind !== nativeAgent)
|
|
363
|
+
throw new Error("The owned agent and native history provider differ.");
|
|
364
|
+
const status = text(info.agent_status, "Owned agent status");
|
|
365
|
+
if (!["idle", "working", "blocked", "done", "unknown"].includes(status))
|
|
366
|
+
throw new TypeError("The owned native status is invalid.");
|
|
367
|
+
const name = optionalText(info.name, "Owned agent name");
|
|
368
|
+
if (name !== null && name !== `dn_${createHash("sha256").update(label).digest("hex").slice(0, 24)}`)
|
|
369
|
+
throw new Error("The owned Herdr pane has a foreign managed agent name.");
|
|
370
|
+
resource.panes.push({ binding: { session, workspaceId, paneId, terminalId }, cwd, kind, status, launchPending: optionalBoolean(info.launch_pending, "Owned launch state"), interactiveReady: optionalBoolean(info.interactive_ready, "Owned readiness"), nativeSession: nativeAgent === null ? null : nativeReference(info.agent_session, nativeAgent), stateChangeSeq: agents[0] === undefined ? null : counter(info.state_change_seq, "Owned state-change sequence"), completionSeq: agents[0] === undefined || info.completion_seq === undefined ? null : counter(info.completion_seq, "Owned completion sequence") });
|
|
371
|
+
}
|
|
372
|
+
if (resource.panes.length === 0)
|
|
373
|
+
throw new Error("The owned Herdr workspace has no associated panes.");
|
|
374
|
+
for (const agent of data.agents.filter((value) => value.workspace_id === workspaceId)) {
|
|
375
|
+
if (!resource.panes.some((value) => value.binding.paneId === agent.pane_id && value.binding.terminalId === agent.terminal_id))
|
|
376
|
+
throw new Error("The owned Herdr workspace has an unassociated native agent.");
|
|
377
|
+
}
|
|
378
|
+
result.push(resource);
|
|
379
|
+
}
|
|
380
|
+
return result;
|
|
381
|
+
},
|
|
382
|
+
prepareWorkspace: async (context, operation) => {
|
|
383
|
+
let createPossible = false;
|
|
384
|
+
try {
|
|
385
|
+
const label = herdrExecutionLabel(context);
|
|
386
|
+
await realpath(context.cwd);
|
|
387
|
+
const data = await snapshot(operation);
|
|
388
|
+
const matching = data.workspaces.filter((workspace) => workspace.label === label);
|
|
389
|
+
if (matching.length > 1)
|
|
390
|
+
throw new Error("The task workspace label is duplicated.");
|
|
391
|
+
if (matching.length === 1) {
|
|
392
|
+
const workspaceId = text(matching[0].workspace_id, "Task workspace ID");
|
|
393
|
+
const panes = data.panes.filter((pane) => pane.workspace_id === workspaceId);
|
|
394
|
+
if (panes.length !== 1)
|
|
395
|
+
throw new Error("The existing task workspace has ambiguous panes; it was retained.");
|
|
396
|
+
const pane = panes[0];
|
|
397
|
+
const binding = { session, workspaceId, paneId: text(pane.pane_id, "Task pane ID"), terminalId: text(pane.terminal_id, "Task terminal ID") };
|
|
398
|
+
await readOwned(context, binding, operation);
|
|
399
|
+
return { binding, workspaceEvidence: null };
|
|
400
|
+
}
|
|
401
|
+
const expectedCwd = path.normalize(await realpath(context.cwd));
|
|
402
|
+
for (const pane of data.panes) {
|
|
403
|
+
if (typeof pane.cwd !== "string")
|
|
404
|
+
continue;
|
|
405
|
+
const reportedCwd = path.normalize(path.resolve(pane.cwd));
|
|
406
|
+
if (reportedCwd === expectedCwd || reportedCwd === path.normalize(path.resolve(context.cwd)))
|
|
407
|
+
throw new Error("The requested task cwd already belongs to a different Herdr workspace.");
|
|
408
|
+
}
|
|
409
|
+
createPossible = true;
|
|
410
|
+
const result = await mutation("workspace.create", { cwd: context.cwd, focus: false, label, env: { ...workspaceEnvironment } }, "workspace_created", operation);
|
|
411
|
+
try {
|
|
412
|
+
const workspace = object(result.workspace, "Created workspace");
|
|
413
|
+
const pane = object(result.root_pane, "Created task pane");
|
|
414
|
+
const binding = { session, workspaceId: text(workspace.workspace_id, "Created workspace ID"), paneId: text(pane.pane_id, "Created pane ID"), terminalId: text(pane.terminal_id, "Created terminal ID") };
|
|
415
|
+
await readOwned(context, binding, operation);
|
|
416
|
+
return { binding, workspaceEvidence: { binding: { ...binding }, ownerLabel: label, environment: { ...workspaceEnvironment } } };
|
|
417
|
+
}
|
|
418
|
+
catch (cause) {
|
|
419
|
+
throw new HerdrAgentActionError("The created task workspace could not be verified.", "workspace.create", "unknown", { cause });
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
catch (cause) {
|
|
423
|
+
if (createPossible && cause instanceof HerdrAgentActionError)
|
|
424
|
+
throw cause;
|
|
425
|
+
throw new HerdrAgentActionError(cause instanceof Error ? cause.message : "The task workspace could not be prepared.", "workspace.create", createPossible ? "unknown" : "not-submitted", { cause });
|
|
426
|
+
}
|
|
427
|
+
},
|
|
428
|
+
observe: observation,
|
|
429
|
+
start: async (context, binding, input, operation) => {
|
|
430
|
+
let launchEvidence = null;
|
|
431
|
+
let promptSubmission = "not-submitted";
|
|
432
|
+
let nativeStartPossible = false;
|
|
433
|
+
try {
|
|
434
|
+
const prior = input.launchEvidence;
|
|
435
|
+
if (prior !== undefined && prior.referenceBaseline === "empty" && prior.agent === context.agent && prior.ownerLabel === herdrExecutionLabel(context) && sameBinding(prior.binding, binding) && Array.isArray(prior.argv) && prior.argv[0] === context.agent && prior.argv.every((value) => typeof value === "string"))
|
|
436
|
+
launchEvidence = prior;
|
|
437
|
+
return await serialized(binding.paneId, operation, async () => {
|
|
438
|
+
if (input.configuration.agent !== context.agent)
|
|
439
|
+
throw new TypeError("The frozen agent configuration differs from the execution identity.");
|
|
440
|
+
const args = buildHerdrAgentArguments(input.configuration, input.resumeSessionId);
|
|
441
|
+
if (typeof input.prompt !== "string" || input.prompt.length === 0)
|
|
442
|
+
throw new TypeError("The caller startup prompt must be nonempty text.");
|
|
443
|
+
const before = await observation(context, binding, operation);
|
|
444
|
+
const existing = before.kind !== null || before.name !== null || before.launchPending || before.interactiveReady;
|
|
445
|
+
if (input.resumeSessionId !== undefined && before.nativeSession !== null && before.nativeSession.sessionId !== canonicalId(input.resumeSessionId))
|
|
446
|
+
throw new Error("The retained native session differs from the original recovery session.");
|
|
447
|
+
const rebind = (reason) => ({ kind: "rebind-required", reason, observation: before, promptSubmission: "not-submitted", launchEvidence: null });
|
|
448
|
+
if (!before.launchPending && !before.interactiveReady && before.kind === null && before.nativeSession !== null)
|
|
449
|
+
return rebind("retained-native-reference");
|
|
450
|
+
let workspaceEvidence;
|
|
451
|
+
if (existing) {
|
|
452
|
+
// A reconnect observes the existing input history. Only the caller's persisted
|
|
453
|
+
// never-submitted fact and verified empty-reference launch can authorize deferred first input.
|
|
454
|
+
if (input.initialPromptNotSubmitted !== true)
|
|
455
|
+
return { kind: "reconnected", observation: before, promptSubmission: "not-submitted", launchEvidence: null };
|
|
456
|
+
const evidence = input.launchEvidence;
|
|
457
|
+
const expectedArgv = [context.agent, ...args];
|
|
458
|
+
if (evidence === undefined || evidence.referenceBaseline !== "empty" || evidence.ownerLabel !== herdrExecutionLabel(context) || evidence.agent !== context.agent || !sameBinding(evidence.binding, binding) || !Array.isArray(evidence.argv) || evidence.argv.length !== expectedArgv.length || evidence.argv.some((value, index) => value !== expectedArgv[index]))
|
|
459
|
+
return rebind("unverified-launch");
|
|
460
|
+
if (!validWorkspaceEvidence(context, binding, evidence.workspaceEvidence) || (input.workspaceEvidence !== undefined && input.workspaceEvidence !== null && !validWorkspaceEvidence(context, binding, input.workspaceEvidence)))
|
|
461
|
+
return rebind("unverified-workspace-environment");
|
|
462
|
+
workspaceEvidence = evidence.workspaceEvidence;
|
|
463
|
+
launchEvidence = evidence;
|
|
464
|
+
}
|
|
465
|
+
else if (input.initialPromptNotSubmitted === true) {
|
|
466
|
+
return rebind("unverified-launch");
|
|
467
|
+
}
|
|
468
|
+
else if (before.nativeSession !== null) {
|
|
469
|
+
return rebind("retained-native-reference");
|
|
470
|
+
}
|
|
471
|
+
else {
|
|
472
|
+
if (!validWorkspaceEvidence(context, binding, input.workspaceEvidence))
|
|
473
|
+
return rebind("unverified-workspace-environment");
|
|
474
|
+
if (input.launchEvidence !== undefined && !validWorkspaceEvidence(context, binding, input.launchEvidence.workspaceEvidence))
|
|
475
|
+
return rebind("unverified-workspace-environment");
|
|
476
|
+
workspaceEvidence = input.workspaceEvidence;
|
|
477
|
+
}
|
|
478
|
+
await options.preflight.verifyStart({ context, configuration: input.configuration, ...(input.resumeSessionId === undefined ? {} : { resumeSessionId: input.resumeSessionId }) }, operation);
|
|
479
|
+
if (!existing) {
|
|
480
|
+
const timeout = Math.floor(Math.min(30_000, operation.remainingMs()));
|
|
481
|
+
if (timeout <= 3000)
|
|
482
|
+
throw new HerdrAgentActionError("The remaining operation budget cannot start a native agent.", "agent.start", "not-submitted", { promptSubmission: "not-submitted" });
|
|
483
|
+
nativeStartPossible = true;
|
|
484
|
+
const started = await mutation("agent.start", { name: herdrExecutionAgentName(context), kind: context.agent, pane_id: binding.paneId, args, timeout_ms: timeout }, "agent_started", operation);
|
|
485
|
+
const acknowledged = object(started.agent, "Native launch receipt");
|
|
486
|
+
const argv = array(started.argv, "Native launch arguments");
|
|
487
|
+
const expectedArgv = [context.agent, ...args];
|
|
488
|
+
if (acknowledged.pane_id !== binding.paneId || acknowledged.terminal_id !== binding.terminalId || acknowledged.workspace_id !== binding.workspaceId || argv.length !== expectedArgv.length || argv.some((value, index) => value !== expectedArgv[index]))
|
|
489
|
+
throw new Error("The native launch receipt does not preserve its original target and frozen arguments.");
|
|
490
|
+
launchEvidence = { binding: { ...binding }, ownerLabel: herdrExecutionLabel(context), agent: context.agent, argv: [...expectedArgv], referenceBaseline: "empty", workspaceEvidence };
|
|
491
|
+
}
|
|
492
|
+
while (true) {
|
|
493
|
+
const current = await observation(context, binding, operation);
|
|
494
|
+
if (current.status === "blocked")
|
|
495
|
+
return { kind: "waiting", observation: current, promptSubmission: "not-submitted", launchEvidence };
|
|
496
|
+
const ready = !current.launchPending && current.interactiveReady && (current.kind === context.agent || current.nativeSession?.agent === context.agent || current.name === herdrExecutionAgentName(context));
|
|
497
|
+
const originalSessionReady = input.resumeSessionId === undefined || current.nativeSession === null || current.nativeSession.sessionId === canonicalId(input.resumeSessionId);
|
|
498
|
+
if (input.resumeSessionId !== undefined && current.nativeSession !== null && !originalSessionReady)
|
|
499
|
+
throw new Error("The resumed agent reported a different native session.");
|
|
500
|
+
if (ready && originalSessionReady) {
|
|
501
|
+
// Native SessionStart can run on the first user turn, including resume.
|
|
502
|
+
// The verified explicit launch authorizes one input; its reported identity is checked afterward.
|
|
503
|
+
// https://github.com/openai/codex/blob/6aaac694803004a63c6f393d153c96d22521a6b9/codex-rs/core/src/session/turn.rs#L340-L342
|
|
504
|
+
await submit(context, binding, input.prompt, operation, true);
|
|
505
|
+
promptSubmission = "submitted";
|
|
506
|
+
while (true) {
|
|
507
|
+
const reported = await observation(context, binding, operation);
|
|
508
|
+
if (reported.nativeSession !== null) {
|
|
509
|
+
if (input.resumeSessionId !== undefined && reported.nativeSession.sessionId !== canonicalId(input.resumeSessionId))
|
|
510
|
+
throw new Error("The resumed native identity changed after prompt submission.");
|
|
511
|
+
return { kind: "started", observation: reported, promptSubmission: "submitted", launchEvidence };
|
|
512
|
+
}
|
|
513
|
+
if (reported.status === "blocked")
|
|
514
|
+
return { kind: "waiting", observation: reported, promptSubmission: "submitted", launchEvidence };
|
|
515
|
+
await wait(operation);
|
|
516
|
+
}
|
|
517
|
+
}
|
|
518
|
+
await wait(operation);
|
|
519
|
+
}
|
|
520
|
+
});
|
|
521
|
+
}
|
|
522
|
+
catch (cause) {
|
|
523
|
+
const evidence = launchEvidence === null ? {} : { launchEvidence };
|
|
524
|
+
if (cause instanceof HerdrAgentActionError)
|
|
525
|
+
throw new HerdrAgentActionError(cause.message, cause.action, cause.submission, { cause, promptSubmission: cause.promptSubmission === null ? promptSubmission : cause.promptSubmission, ...evidence });
|
|
526
|
+
throw new HerdrAgentActionError(cause instanceof Error ? cause.message : "The native launch or initial prompt could not be confirmed; observe the original pane before any further action.", "agent.start", nativeStartPossible ? "unknown" : "not-submitted", { cause, promptSubmission, ...evidence });
|
|
527
|
+
}
|
|
528
|
+
},
|
|
529
|
+
submitPrompt: (context, binding, prompt, operation) => serialized(binding.paneId, operation, () => submit(context, binding, prompt, operation)),
|
|
530
|
+
captureStop: async (context, binding, operation) => {
|
|
531
|
+
const current = await observation(context, binding, operation);
|
|
532
|
+
const result = expectedResult(await options.rpc.request("pane.process_info", { pane_id: binding.paneId }, operation), "pane_process_info");
|
|
533
|
+
const info = object(result.process_info, "Pane process observation");
|
|
534
|
+
if (info.pane_id !== binding.paneId)
|
|
535
|
+
throw new Error("The process observation belongs to another pane.");
|
|
536
|
+
const shellPid = pid(info.shell_pid);
|
|
537
|
+
const processes = info.foreground_processes === undefined ? [] : array(info.foreground_processes, "Foreground processes");
|
|
538
|
+
const records = processes.map((value) => object(value, "Foreground process"));
|
|
539
|
+
const processIds = [...new Set(records.map((value) => pid(value.pid)).filter((value) => value !== shellPid))];
|
|
540
|
+
if (processIds.length === 0)
|
|
541
|
+
throw new Error("The original native process identities are unavailable; pane absence or an empty process list cannot establish stopped confirmation.");
|
|
542
|
+
if (current.launchPending && (current.name !== herdrExecutionAgentName(context) || !records.some((value) => pid(value.pid) !== shellPid && nativeProcessEvidence(value, context.agent))))
|
|
543
|
+
throw new Error("The pending task launch has no concrete original native process evidence.");
|
|
544
|
+
return { binding, ownerLabel: herdrExecutionLabel(context), agent: context.agent, nativeSessionId: current.nativeSession?.sessionId ?? null, shellPid, processIds, observedAt: Date.now() };
|
|
545
|
+
},
|
|
546
|
+
requestGracefulStop: (context, binding, operation) => serialized(binding.paneId, operation, () => submit(context, binding, "/exit", operation)),
|
|
547
|
+
// This observes only the current shell. Historical never-submitted launch state
|
|
548
|
+
// remains a separate durable caller precondition for deriving agentStopped.
|
|
549
|
+
confirmEmptyPane: async (context, binding, operation) => {
|
|
550
|
+
await emptyOwnedPane(context, binding, operation, "unlaunched");
|
|
551
|
+
return { noNativeProcess: true };
|
|
552
|
+
},
|
|
553
|
+
confirmStopped: async (context, binding, evidence, operation) => {
|
|
554
|
+
verifyStopObservation(context, binding, evidence);
|
|
555
|
+
const current = await readOwned(context, binding, operation, true);
|
|
556
|
+
await absentProcesses(evidence, operation);
|
|
557
|
+
if (current.pane !== null) {
|
|
558
|
+
const result = expectedResult(await options.rpc.request("pane.process_info", { pane_id: binding.paneId }, operation), "pane_process_info");
|
|
559
|
+
const info = object(result.process_info, "Current pane process observation");
|
|
560
|
+
if (info.pane_id !== binding.paneId)
|
|
561
|
+
throw new Error("The current process observation belongs to another pane.");
|
|
562
|
+
const currentShellPid = pid(info.shell_pid);
|
|
563
|
+
if (currentShellPid !== evidence.shellPid && await isExecuting(evidence.shellPid))
|
|
564
|
+
throw new Error("The original task shell is still executing or its absence is unconfirmed.");
|
|
565
|
+
const currentProcesses = info.foreground_processes === undefined ? [] : array(info.foreground_processes, "Current foreground processes");
|
|
566
|
+
if (currentProcesses.some((value) => pid(object(value, "Current foreground process").pid) !== currentShellPid))
|
|
567
|
+
throw new Error("The original pane still hosts foreground work.");
|
|
568
|
+
}
|
|
569
|
+
return { agentStopped: true };
|
|
570
|
+
},
|
|
571
|
+
confirmMissingWorkspaceStopped: async (context, originalBinding, evidence, operation) => {
|
|
572
|
+
verifyOriginalStopScope(context, originalBinding, evidence);
|
|
573
|
+
const data = await snapshot(operation);
|
|
574
|
+
requireOriginalResourceAbsent(data, originalBinding);
|
|
575
|
+
if (data.workspaces.some((workspace) => workspace.label === herdrExecutionLabel(context)))
|
|
576
|
+
throw new Error("The original task label identifies another resource.");
|
|
577
|
+
await originalProcessesAbsent(evidence, operation);
|
|
578
|
+
return { agentStopped: true };
|
|
579
|
+
},
|
|
580
|
+
confirmRestoredStop: async (context, originalBinding, restoredBinding, evidence, operation) => {
|
|
581
|
+
verifyOriginalStopScope(context, originalBinding, evidence);
|
|
582
|
+
if (originalBinding.session !== restoredBinding.session || originalBinding.workspaceId !== restoredBinding.workspaceId || originalBinding.paneId !== restoredBinding.paneId)
|
|
583
|
+
throw new Error("The restored shell does not have the original task workspace and pane association.");
|
|
584
|
+
const { current } = await emptyOwnedPane(context, restoredBinding, operation, "restored");
|
|
585
|
+
if (evidence.nativeSessionId !== null && current.nativeSession !== null && evidence.nativeSessionId !== current.nativeSession.sessionId)
|
|
586
|
+
throw new Error("The restored shell retains a different original native session.");
|
|
587
|
+
await originalProcessesAbsent(evidence, operation);
|
|
588
|
+
return { agentStopped: true, binding: { ...restoredBinding } };
|
|
589
|
+
},
|
|
590
|
+
confirmReplacedStop: async (context, originalBinding, replacementBinding, evidence, operation) => {
|
|
591
|
+
verifyOriginalStopScope(context, originalBinding, evidence);
|
|
592
|
+
if (replacementBinding.session !== originalBinding.session)
|
|
593
|
+
throw new Error("The replacement belongs to another Herdr session.");
|
|
594
|
+
const { data } = await emptyOwnedPane(context, replacementBinding, operation, "unlaunched");
|
|
595
|
+
requireOriginalResourceAbsent(data, originalBinding);
|
|
596
|
+
await originalProcessesAbsent(evidence, operation);
|
|
597
|
+
return { agentStopped: true, binding: { ...replacementBinding } };
|
|
598
|
+
},
|
|
599
|
+
forceStop: (context, binding, evidence, operation) => serialized(binding.paneId, operation, async () => {
|
|
600
|
+
verifyStopObservation(context, binding, evidence);
|
|
601
|
+
const { data, pane } = await readOwned(context, binding, operation, true);
|
|
602
|
+
if (pane !== null) {
|
|
603
|
+
const current = await observation(context, binding, operation);
|
|
604
|
+
if (evidence.nativeSessionId !== null && current.nativeSession !== null && current.nativeSession.sessionId !== evidence.nativeSessionId)
|
|
605
|
+
throw new Error("The task pane now identifies another native session; the original stop observation was retained.");
|
|
606
|
+
const processResult = expectedResult(await options.rpc.request("pane.process_info", { pane_id: binding.paneId }, operation), "pane_process_info");
|
|
607
|
+
const processInfo = object(processResult.process_info, "Current stop process observation");
|
|
608
|
+
if (processInfo.pane_id !== binding.paneId || pid(processInfo.shell_pid) !== evidence.shellPid)
|
|
609
|
+
throw new Error("The original task shell identity has changed.");
|
|
610
|
+
const foreground = processInfo.foreground_processes === undefined ? [] : array(processInfo.foreground_processes, "Current stop processes");
|
|
611
|
+
const currentPids = foreground.map((value) => pid(object(value, "Current stop process").pid)).filter((value) => value !== evidence.shellPid);
|
|
612
|
+
if (currentPids.length !== 0 && !currentPids.some((value) => evidence.processIds.includes(value)))
|
|
613
|
+
throw new Error("The task pane now hosts processes outside the original stop observation.");
|
|
614
|
+
const guardLabel = `daynight-stop:${binding.terminalId}`;
|
|
615
|
+
const guards = data.tabs.filter((tab) => tab.workspace_id === binding.workspaceId && tab.label === guardLabel);
|
|
616
|
+
if (guards.length > 1)
|
|
617
|
+
throw new Error("The task stop anchor is duplicated.");
|
|
618
|
+
if (guards.length === 0) {
|
|
619
|
+
const result = await mutation("tab.create", { workspace_id: binding.workspaceId, cwd: context.cwd, focus: false, label: guardLabel, env: { ...workspaceEnvironment } }, "tab_created", operation);
|
|
620
|
+
const tab = object(result.tab, "Created stop anchor tab");
|
|
621
|
+
const root = object(result.root_pane, "Created stop anchor pane");
|
|
622
|
+
if (tab.workspace_id !== binding.workspaceId || tab.label !== guardLabel || root.workspace_id !== binding.workspaceId || root.pane_id === binding.paneId || root.agent !== undefined)
|
|
623
|
+
throw new HerdrAgentActionError("The stop anchor does not identify an empty task-owned pane.", "tab.create", "unknown");
|
|
624
|
+
}
|
|
625
|
+
const anchored = await readOwned(context, binding, operation);
|
|
626
|
+
const ownedPanes = anchored.data.panes.filter((candidate) => candidate.workspace_id === binding.workspaceId);
|
|
627
|
+
if (ownedPanes.length < 2 || !anchored.data.tabs.some((tab) => tab.workspace_id === binding.workspaceId && tab.label === guardLabel))
|
|
628
|
+
throw new Error("The task workspace has no verified stop anchor.");
|
|
629
|
+
await mutation("pane.close", { pane_id: binding.paneId }, "ok", operation);
|
|
630
|
+
}
|
|
631
|
+
await readOwned(context, binding, operation, true);
|
|
632
|
+
await absentProcesses(evidence, operation);
|
|
633
|
+
return { agentStopped: true, originalPaneClosed: true };
|
|
634
|
+
}),
|
|
635
|
+
closeWorkspace: async (context, binding, proof, operation) => {
|
|
636
|
+
if (proof.agentStopped !== true)
|
|
637
|
+
throw new Error("Task workspace closure requires confirmed original agent stop.");
|
|
638
|
+
const before = await snapshot(operation);
|
|
639
|
+
const original = before.workspaces.find((workspace) => workspace.workspace_id === binding.workspaceId);
|
|
640
|
+
if (original === undefined) {
|
|
641
|
+
if (before.workspaces.some((workspace) => workspace.label === herdrExecutionLabel(context)))
|
|
642
|
+
throw new Error("The task label now identifies another workspace; the original binding was retained.");
|
|
643
|
+
return { workspaceClosed: true };
|
|
644
|
+
}
|
|
645
|
+
await readOwned(context, binding, operation, true);
|
|
646
|
+
await mutation("workspace.close", { workspace_id: binding.workspaceId, close_group: false }, "ok", operation);
|
|
647
|
+
const after = await snapshot(operation);
|
|
648
|
+
if (after.workspaces.some((workspace) => workspace.workspace_id === binding.workspaceId || workspace.label === herdrExecutionLabel(context)))
|
|
649
|
+
throw new HerdrAgentActionError("The original workspace still exists after closure.", "workspace.close", "unknown");
|
|
650
|
+
return { workspaceClosed: true };
|
|
651
|
+
},
|
|
652
|
+
};
|
|
653
|
+
};
|
|
654
|
+
//# sourceMappingURL=agents.js.map
|