letagents 0.12.11 → 0.12.13
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/dist/mcp/git-remote.js +7 -7
- package/dist/mcp/local-state/agent-sessions.js +78 -5
- package/dist/mcp/local-state/local-chat.js +189 -48
- package/dist/mcp/local-state/storage.js +16 -9
- package/dist/mcp/server/daemon-tool-executor.js +92 -0
- package/dist/mcp/server/register-tools.js +25 -12
- package/dist/mcp/server/runtime/agent-sessions.js +58 -9
- package/dist/mcp/server/runtime/api.js +40 -4
- package/dist/mcp/server/runtime/daemon-tool-context.js +11 -0
- package/dist/mcp/server/runtime/execution-profile.js +19 -0
- package/dist/mcp/server/runtime/identity/directory.js +2 -2
- package/dist/mcp/server/runtime/messages.js +55 -0
- package/dist/mcp/server/runtime/presence.js +4 -2
- package/dist/mcp/server/runtime/room-api.js +24 -7
- package/dist/mcp/server/runtime/room-state.js +59 -3
- package/dist/mcp/server/runtime/rooms.js +67 -32
- package/dist/mcp/server/runtime/supervised-room-authority.js +8 -0
- package/dist/mcp/server/runtime/supervisor-bridge.js +702 -24
- package/dist/mcp/server/runtime/tool-surface-policy.js +26 -0
- package/dist/mcp/server/runtime/worker-bearer.js +44 -6
- package/dist/mcp/server/runtime-contract.js +27 -0
- package/dist/mcp/server/runtime.js +15 -4
- package/dist/mcp/server/supervised-tool-facade.js +134 -0
- package/dist/mcp/server/tools/agent-sessions.js +74 -7
- package/dist/mcp/server/tools/messages/index.js +3 -2
- package/dist/mcp/server/tools/messages/read-tool.js +54 -97
- package/dist/mcp/server/tools/messages/reasoning-tool.js +2 -0
- package/dist/mcp/server/tools/messages/send-tool.js +5 -0
- package/dist/mcp/server/tools/messages/status-tool.js +2 -0
- package/dist/mcp/server/tools/messages/wait-tool.js +344 -71
- package/dist/mcp/server/tools/onboarding/status-tool.js +9 -8
- package/dist/mcp/server/tools/rooms/inspection-tools.js +40 -22
- package/dist/mcp/server/tools/rooms/repo-initialization-tool.js +2 -1
- package/dist/mcp/server/tools/supervised-room-turn.js +42 -0
- package/dist/mcp/server/tools/tasks/board-tools.js +34 -2
- package/dist/mcp/server.js +14 -7
- package/dist/mcp/sse-client.js +163 -20
- package/dist/shared/activation-routing.js +187 -20
- package/dist/shared/agent-presence.js +6 -0
- package/dist/shared/desktop-release-manifest.js +63 -0
- package/dist/shared/desktop-release.js +60 -0
- package/dist/shared/scoped-ids.js +6 -0
- package/package.json +11 -3
- package/shared/message-contracts.d.mts +32 -0
- package/shared/message-contracts.mjs +109 -0
- package/shared/routing-aliases.d.mts +18 -0
- package/shared/routing-aliases.mjs +66 -0
- package/shared/sqlite-thread-routing.d.mts +72 -0
- package/shared/sqlite-thread-routing.mjs +1038 -0
|
@@ -1,33 +1,340 @@
|
|
|
1
|
-
import { randomUUID } from "node:crypto";
|
|
1
|
+
import { createHash, randomUUID } from "node:crypto";
|
|
2
|
+
import { lstat, readFile, realpath } from "node:fs/promises";
|
|
2
3
|
import { createConnection } from "node:net";
|
|
4
|
+
import { homedir } from "node:os";
|
|
5
|
+
import { join } from "node:path";
|
|
6
|
+
import { parsePositivePgIntegerScopedId } from "../../../../shared/message-contracts.mjs";
|
|
7
|
+
import { getCurrentSupervisedRoomAuthority } from "./supervised-room-authority.js";
|
|
3
8
|
const NEGOTIATION_PROTOCOL_VERSION = 1;
|
|
4
9
|
const SUPPORTED_SUPERVISOR_PROTOCOL_VERSIONS = new Set([1, 2]);
|
|
5
10
|
const DEFAULT_REQUEST_TIMEOUT_MS = 5_000;
|
|
11
|
+
const CONFIRMED_BINDING_VERIFY_TIMEOUT_MS = 250;
|
|
12
|
+
const SUPERVISOR_CONTEXT_FILE = ".letagents-supervisor-context.json";
|
|
13
|
+
const WORK_ATTEMPT_MARKER_FILE = ".letagents-work-attempt.json";
|
|
14
|
+
const MAX_SUPERVISOR_CONTEXT_BYTES = 4 * 1024;
|
|
15
|
+
const confirmedBindingsBySession = new Map();
|
|
16
|
+
const confirmedRequestsBySession = new Map();
|
|
17
|
+
const confirmedProtocolsBySession = new Map();
|
|
18
|
+
const pendingCursorCheckpoints = new Map();
|
|
19
|
+
const activeCursorCheckpointDrains = new Set();
|
|
20
|
+
const cursorCheckpointRetryTimers = new Map();
|
|
21
|
+
const CURSOR_CHECKPOINT_RETRY_DELAYS_MS = [250, 1_000, 3_000];
|
|
22
|
+
export async function executeCurrentSupervisedTool(input, env = process.env, options = {}) {
|
|
23
|
+
const coordinates = await requireCurrentSupervisedCoordinates(env, options);
|
|
24
|
+
const timeoutMs = options.requestTimeoutMs ?? DEFAULT_REQUEST_TIMEOUT_MS;
|
|
25
|
+
const negotiated = await negotiateSupervisor(coordinates.socketPath, timeoutMs);
|
|
26
|
+
if (negotiated.generation === null)
|
|
27
|
+
throw new Error("The supervised daemon generation is unavailable.");
|
|
28
|
+
const response = await supervisorRequest(coordinates.socketPath, {
|
|
29
|
+
version: negotiated.protocolVersion,
|
|
30
|
+
id: randomUUID(),
|
|
31
|
+
method: "supervisor.execute_bounded_tool",
|
|
32
|
+
params: {
|
|
33
|
+
entry_id: coordinates.entryId,
|
|
34
|
+
work_attempt_id: coordinates.workAttemptId,
|
|
35
|
+
execution_generation_id: coordinates.executionGenerationId,
|
|
36
|
+
...(coordinates.providerTurnId ? { provider_turn_id: coordinates.providerTurnId } : {}),
|
|
37
|
+
daemon_generation: negotiated.generation,
|
|
38
|
+
mcp_request_id: input.mcpRequestId,
|
|
39
|
+
tool_name: input.toolName,
|
|
40
|
+
input: input.input,
|
|
41
|
+
},
|
|
42
|
+
}, null);
|
|
43
|
+
if (!response.ok) {
|
|
44
|
+
if (/Unsupported daemon method:\s*supervisor\.execute_bounded_tool/i.test(response.error ?? "")) {
|
|
45
|
+
return { state: "unsupported" };
|
|
46
|
+
}
|
|
47
|
+
throw new Error(response.error || "The daemon-owned supervised tool was rejected.");
|
|
48
|
+
}
|
|
49
|
+
const result = response.result && typeof response.result === "object"
|
|
50
|
+
? response.result
|
|
51
|
+
: {};
|
|
52
|
+
const roomId = typeof result.room_id === "string" ? result.room_id.trim() : "";
|
|
53
|
+
if (!roomId || roomId.length > 1_024 || /[\u0000-\u001f\u007f]/.test(roomId)) {
|
|
54
|
+
throw new Error("The supervised daemon did not return valid exact room authority.");
|
|
55
|
+
}
|
|
56
|
+
return { state: "completed", roomId, result: result.result };
|
|
57
|
+
}
|
|
58
|
+
export async function prepareCurrentSupervisedEffect(input, env = process.env, options = {}) {
|
|
59
|
+
const coordinates = await requireCurrentSupervisedCoordinates(env, options);
|
|
60
|
+
const timeoutMs = options.requestTimeoutMs ?? DEFAULT_REQUEST_TIMEOUT_MS;
|
|
61
|
+
const negotiated = await negotiateSupervisor(coordinates.socketPath, timeoutMs);
|
|
62
|
+
if (negotiated.generation === null)
|
|
63
|
+
throw new Error("The supervised daemon generation is unavailable.");
|
|
64
|
+
const response = await supervisorRequest(coordinates.socketPath, {
|
|
65
|
+
version: negotiated.protocolVersion,
|
|
66
|
+
id: randomUUID(),
|
|
67
|
+
method: "supervisor.prepare_bounded_effect",
|
|
68
|
+
params: {
|
|
69
|
+
entry_id: coordinates.entryId,
|
|
70
|
+
work_attempt_id: coordinates.workAttemptId,
|
|
71
|
+
execution_generation_id: coordinates.executionGenerationId,
|
|
72
|
+
...(coordinates.providerTurnId ? { provider_turn_id: coordinates.providerTurnId } : {}),
|
|
73
|
+
daemon_generation: negotiated.generation,
|
|
74
|
+
mcp_request_id: input.mcpRequestId,
|
|
75
|
+
tool_name: input.toolName,
|
|
76
|
+
input: input.input,
|
|
77
|
+
mutation: input.mutation,
|
|
78
|
+
},
|
|
79
|
+
}, timeoutMs);
|
|
80
|
+
if (!response.ok)
|
|
81
|
+
throw new Error(response.error || "The supervised effect was rejected.");
|
|
82
|
+
const result = response.result && typeof response.result === "object" ? response.result : {};
|
|
83
|
+
const roomId = typeof result.room_id === "string" ? result.room_id.trim() : "";
|
|
84
|
+
if (!roomId || roomId.length > 1_024 || /[\u0000-\u001f\u007f]/.test(roomId)) {
|
|
85
|
+
throw new Error("The supervised daemon did not return valid exact room authority.");
|
|
86
|
+
}
|
|
87
|
+
if (result.state === "completed")
|
|
88
|
+
return { state: "completed", roomId, result: result.result };
|
|
89
|
+
const effectId = typeof result.effect_id === "string" ? result.effect_id : "";
|
|
90
|
+
if (!effectId)
|
|
91
|
+
throw new Error("The supervised effect journal did not return an effect id.");
|
|
92
|
+
if (result.state === "uncertain") {
|
|
93
|
+
const error = typeof result.error === "string" && result.error.trim()
|
|
94
|
+
? result.error
|
|
95
|
+
: "The mutating tool outcome is uncertain.";
|
|
96
|
+
return { state: "uncertain", roomId, effectId, error };
|
|
97
|
+
}
|
|
98
|
+
if (result.state !== "prepared") {
|
|
99
|
+
throw new Error("The supervised effect journal returned an unsupported state.");
|
|
100
|
+
}
|
|
101
|
+
if (result.action === "execute") {
|
|
102
|
+
return { state: "prepared", roomId, effectId, action: "execute" };
|
|
103
|
+
}
|
|
104
|
+
if (result.action === "use_final_answer" && typeof result.source_message_id === "string" && result.source_message_id.trim()) {
|
|
105
|
+
return { state: "prepared", roomId, effectId, action: "use_final_answer", sourceMessageId: result.source_message_id };
|
|
106
|
+
}
|
|
107
|
+
if (result.action === "room_move_prepared" && typeof result.destination_room === "string" && result.destination_room.trim()) {
|
|
108
|
+
return { state: "prepared", roomId, effectId, action: "room_move_prepared", destinationRoom: result.destination_room };
|
|
109
|
+
}
|
|
110
|
+
throw new Error("The supervised effect journal returned an unsupported action.");
|
|
111
|
+
}
|
|
112
|
+
export async function completeCurrentSupervisedEffect(input, env = process.env, options = {}) {
|
|
113
|
+
const coordinates = await requireCurrentSupervisedCoordinates(env, options);
|
|
114
|
+
const timeoutMs = options.requestTimeoutMs ?? DEFAULT_REQUEST_TIMEOUT_MS;
|
|
115
|
+
const deadline = Date.now() + timeoutMs;
|
|
116
|
+
let retryAttempt = 0;
|
|
117
|
+
while (true) {
|
|
118
|
+
let response;
|
|
119
|
+
try {
|
|
120
|
+
// Completion is idempotent by effect id. A daemon handoff can race the
|
|
121
|
+
// first socket request after the external callback already succeeded,
|
|
122
|
+
// so keep renegotiating within one bounded request budget instead of
|
|
123
|
+
// stranding an executing journal or tempting the caller to repeat the
|
|
124
|
+
// side effect. The successor's unlink-to-listen gap can span multiple
|
|
125
|
+
// failed socket attempts during process startup.
|
|
126
|
+
const negotiated = await negotiateSupervisor(coordinates.socketPath, remainingRequestTimeout(deadline));
|
|
127
|
+
if (negotiated.generation === null)
|
|
128
|
+
throw new Error("The supervised daemon generation is unavailable.");
|
|
129
|
+
response = await supervisorRequest(coordinates.socketPath, {
|
|
130
|
+
version: negotiated.protocolVersion,
|
|
131
|
+
id: randomUUID(),
|
|
132
|
+
method: "supervisor.complete_bounded_effect",
|
|
133
|
+
params: {
|
|
134
|
+
entry_id: coordinates.entryId,
|
|
135
|
+
work_attempt_id: coordinates.workAttemptId,
|
|
136
|
+
execution_generation_id: coordinates.executionGenerationId,
|
|
137
|
+
...(coordinates.providerTurnId ? { provider_turn_id: coordinates.providerTurnId } : {}),
|
|
138
|
+
daemon_generation: negotiated.generation,
|
|
139
|
+
effect_id: input.effectId,
|
|
140
|
+
result: input.result,
|
|
141
|
+
error: input.error,
|
|
142
|
+
},
|
|
143
|
+
}, remainingRequestTimeout(deadline));
|
|
144
|
+
}
|
|
145
|
+
catch (error) {
|
|
146
|
+
const failure = error instanceof Error ? error : new Error(String(error));
|
|
147
|
+
if (!completionMayHaveRacedHandoff(failure)
|
|
148
|
+
|| !await waitForCompletionHandoffRetry(deadline, retryAttempt++))
|
|
149
|
+
throw error;
|
|
150
|
+
continue;
|
|
151
|
+
}
|
|
152
|
+
if (response.ok)
|
|
153
|
+
return;
|
|
154
|
+
const rejection = new Error(response.error || "The supervised effect completion was rejected.");
|
|
155
|
+
if (!completionMayHaveRacedHandoff(rejection)
|
|
156
|
+
|| !await waitForCompletionHandoffRetry(deadline, retryAttempt++))
|
|
157
|
+
throw rejection;
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
function completionMayHaveRacedHandoff(error) {
|
|
161
|
+
return isRetryableSupervisorBridgeError(error)
|
|
162
|
+
|| /stale.*(?:daemon|supervisor).*generation|handoff/i.test(error.message);
|
|
163
|
+
}
|
|
164
|
+
async function waitForCompletionHandoffRetry(deadline, attempt) {
|
|
165
|
+
const remaining = deadline - Date.now();
|
|
166
|
+
if (remaining <= 0)
|
|
167
|
+
return false;
|
|
168
|
+
const delay = Math.min(25 * (2 ** Math.min(attempt, 4)), 250, remaining);
|
|
169
|
+
await new Promise((resolve) => setTimeout(resolve, delay));
|
|
170
|
+
return Date.now() < deadline;
|
|
171
|
+
}
|
|
172
|
+
async function requireCurrentSupervisedCoordinates(env, options) {
|
|
173
|
+
if (env.LETAGENTS_EXECUTION_PROFILE?.trim() !== "supervised_room_turn") {
|
|
174
|
+
throw new Error("Supervised effects require the supervised_room_turn execution profile.");
|
|
175
|
+
}
|
|
176
|
+
const coordinates = await resolveSupervisorCoordinates(supervisedContextSession(env), env, options);
|
|
177
|
+
if (!coordinates)
|
|
178
|
+
throw new Error("The exact supervised daemon coordinates are unavailable.");
|
|
179
|
+
return coordinates;
|
|
180
|
+
}
|
|
181
|
+
/**
|
|
182
|
+
* Resolve and borrow for the MCP process itself. Unlike registration, this
|
|
183
|
+
* never reads local agent state: the daemon-owned launch context is the sole
|
|
184
|
+
* authority for the exact worker session identity.
|
|
185
|
+
*/
|
|
186
|
+
export async function borrowCurrentSupervisedWorkerCredential(env = process.env, options = {}) {
|
|
187
|
+
if (env.LETAGENTS_SUPERVISED_BOUNDED_TURNS?.trim() !== "1") {
|
|
188
|
+
return { state: "not_supervised" };
|
|
189
|
+
}
|
|
190
|
+
const seed = supervisedContextSession(env);
|
|
191
|
+
const coordinates = await resolveSupervisorCoordinates(seed, env, options);
|
|
192
|
+
const roomId = getCurrentSupervisedRoomAuthority();
|
|
193
|
+
if (!coordinates?.agentSessionId || !roomId) {
|
|
194
|
+
return { state: "stale", code: "SUPERVISED_CREDENTIAL_STALE" };
|
|
195
|
+
}
|
|
196
|
+
return borrowSupervisedWorkerCredential({
|
|
197
|
+
...seed,
|
|
198
|
+
session_id: coordinates.agentSessionId,
|
|
199
|
+
room_id: roomId,
|
|
200
|
+
}, env, { ...options, resolvedCoordinates: coordinates });
|
|
201
|
+
}
|
|
202
|
+
/** A public, non-secret session-shaped marker for supervised MCP tools. */
|
|
203
|
+
export async function resolveCurrentSupervisedWorkerSession(roomId, env = process.env, options = {}) {
|
|
204
|
+
const seed = supervisedContextSession(env);
|
|
205
|
+
const coordinates = await resolveSupervisorCoordinates(seed, env, options);
|
|
206
|
+
const boundRoomId = getCurrentSupervisedRoomAuthority();
|
|
207
|
+
if (!coordinates?.agentSessionId || !boundRoomId) {
|
|
208
|
+
throw new Error("Daemon-supervised bounded turn is missing its exact worker session context.");
|
|
209
|
+
}
|
|
210
|
+
if (roomId && roomId !== boundRoomId) {
|
|
211
|
+
throw new Error(`Daemon-supervised worker session is registered for ${boundRoomId}, not ${roomId}.`);
|
|
212
|
+
}
|
|
213
|
+
const displayName = coordinates.agentDisplayName || "Daemon-supervised worker";
|
|
214
|
+
return {
|
|
215
|
+
...seed,
|
|
216
|
+
session_id: coordinates.agentSessionId,
|
|
217
|
+
room_id: boundRoomId,
|
|
218
|
+
agent_key: coordinates.agentSessionId,
|
|
219
|
+
actor_label: displayName,
|
|
220
|
+
display_name: displayName,
|
|
221
|
+
};
|
|
222
|
+
}
|
|
223
|
+
function supervisedContextSession(env = process.env) {
|
|
224
|
+
const now = new Date(0).toISOString();
|
|
225
|
+
// Codex is the only provider that can recover supervisor coordinates from
|
|
226
|
+
// the worktree context file. Other supervised providers pass their exact
|
|
227
|
+
// coordinates and provider identity through the daemon-created MCP
|
|
228
|
+
// environment, so the fallback must remain Codex for existing context-file
|
|
229
|
+
// sessions rather than inventing a provider that cannot own the file.
|
|
230
|
+
const provider = env.LETAGENTS_SUPERVISOR_PROVIDER?.trim() || "codex";
|
|
231
|
+
const label = provider === "open-model"
|
|
232
|
+
? "Open Model"
|
|
233
|
+
: provider === "claude-code"
|
|
234
|
+
? "Claude Code"
|
|
235
|
+
: provider === "codex"
|
|
236
|
+
? "Codex"
|
|
237
|
+
: "Supervised agent";
|
|
238
|
+
return {
|
|
239
|
+
session_id: "", session_token: "", room_id: "", session_kind: "worker", runtime: provider,
|
|
240
|
+
actor_label: "Daemon-supervised worker", agent_key: "daemon-supervised-worker",
|
|
241
|
+
display_name: "Daemon-supervised worker", owner_label: "", ide_label: label,
|
|
242
|
+
created_at: now, updated_at: now, last_seen_at: now,
|
|
243
|
+
};
|
|
244
|
+
}
|
|
245
|
+
/**
|
|
246
|
+
* Borrow an in-memory, exact-generation worker bearer. This intentionally has
|
|
247
|
+
* no fallback to the owner/session token: a daemon restarted without Electron
|
|
248
|
+
* handoff must retain its durable inbox and wait.
|
|
249
|
+
*/
|
|
250
|
+
export async function borrowSupervisedWorkerCredential(session, env = process.env, options = {}) {
|
|
251
|
+
const coordinates = options.resolvedCoordinates ?? await resolveSupervisorCoordinates(session, env, options);
|
|
252
|
+
if (!coordinates)
|
|
253
|
+
return { state: "not_supervised" };
|
|
254
|
+
if (session.session_kind !== "worker")
|
|
255
|
+
return { state: "stale", code: "SUPERVISED_CREDENTIAL_STALE" };
|
|
256
|
+
const timeoutMs = options.requestTimeoutMs ?? DEFAULT_REQUEST_TIMEOUT_MS;
|
|
257
|
+
const apiUrl = normalizedWorkerApiOrigin(env);
|
|
258
|
+
const negotiated = await negotiateSupervisor(coordinates.socketPath, timeoutMs);
|
|
259
|
+
if (negotiated.generation === null)
|
|
260
|
+
return { state: "stale", code: "SUPERVISED_CREDENTIAL_STALE" };
|
|
261
|
+
const response = await supervisorRequest(coordinates.socketPath, {
|
|
262
|
+
version: negotiated.protocolVersion,
|
|
263
|
+
id: randomUUID(),
|
|
264
|
+
method: "supervisor.borrow_worker_credential",
|
|
265
|
+
params: {
|
|
266
|
+
entry_id: coordinates.entryId, room_id: session.room_id, work_attempt_id: coordinates.workAttemptId,
|
|
267
|
+
execution_generation_id: coordinates.executionGenerationId, agent_session_id: session.session_id,
|
|
268
|
+
...(coordinates.providerTurnId ? { provider_turn_id: coordinates.providerTurnId } : {}),
|
|
269
|
+
daemon_generation: negotiated.generation, api_url: apiUrl,
|
|
270
|
+
},
|
|
271
|
+
}, timeoutMs);
|
|
272
|
+
if (!response.ok || response.version !== negotiated.protocolVersion)
|
|
273
|
+
return { state: "stale", code: "SUPERVISED_CREDENTIAL_STALE" };
|
|
274
|
+
const result = response.result && typeof response.result === "object" ? response.result : {};
|
|
275
|
+
if (result.status === "deferred")
|
|
276
|
+
return { state: "deferred", code: "SUPERVISED_CREDENTIAL_UNAVAILABLE" };
|
|
277
|
+
if (result.status !== "available" || typeof result.credential !== "string" || !result.credential.trim()) {
|
|
278
|
+
return { state: "stale", code: "SUPERVISED_CREDENTIAL_STALE" };
|
|
279
|
+
}
|
|
280
|
+
return { state: "available", credential: result.credential };
|
|
281
|
+
}
|
|
282
|
+
function normalizedWorkerApiOrigin(env) {
|
|
283
|
+
const apiUrl = env.LETAGENTS_API_URL?.trim() || "https://letagents.chat";
|
|
284
|
+
let parsed;
|
|
285
|
+
try {
|
|
286
|
+
parsed = new URL(apiUrl);
|
|
287
|
+
}
|
|
288
|
+
catch {
|
|
289
|
+
throw new Error("Daemon-supervised worker requires a valid LETAGENTS_API_URL.");
|
|
290
|
+
}
|
|
291
|
+
const loopbackHosts = new Set(["localhost", "127.0.0.1", "::1", "[::1]"]);
|
|
292
|
+
if (parsed.protocol !== "https:" && !(parsed.protocol === "http:" && loopbackHosts.has(parsed.hostname.toLowerCase()))) {
|
|
293
|
+
throw new Error("Daemon-supervised worker requires HTTPS unless LETAGENTS_API_URL uses an exact loopback host.");
|
|
294
|
+
}
|
|
295
|
+
return parsed.origin;
|
|
296
|
+
}
|
|
6
297
|
/** Bind the exact worker credential minted by registration to its daemon lane. */
|
|
7
298
|
export async function bindSupervisedWorkerSession(session, env = process.env, options = {}) {
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
299
|
+
return (await bindSupervisedWorkerSessionWithContext(session, env, options)).bound;
|
|
300
|
+
}
|
|
301
|
+
/**
|
|
302
|
+
* Bind and return the validated file-backed route, when one was used.
|
|
303
|
+
* Registration persists this route so later wait/checkpoint calls do not fall
|
|
304
|
+
* back to an unrelated long-lived MCP process cwd.
|
|
305
|
+
*/
|
|
306
|
+
export async function bindSupervisedWorkerSessionWithContext(session, env = process.env, options = {}) {
|
|
307
|
+
const coordinates = options.resolvedCoordinates ?? await resolveSupervisorCoordinates(session, env, options);
|
|
308
|
+
if (!coordinates)
|
|
309
|
+
return { bound: false, supervisorContextCwd: null };
|
|
310
|
+
const { entryId, socketPath, workAttemptId, executionGenerationId } = coordinates;
|
|
16
311
|
if (session.session_kind !== "worker")
|
|
17
312
|
throw new Error("A supervised provider must register a worker session.");
|
|
18
313
|
const timeoutMs = options.requestTimeoutMs ?? DEFAULT_REQUEST_TIMEOUT_MS;
|
|
19
314
|
if (!Number.isSafeInteger(timeoutMs) || timeoutMs < 1)
|
|
20
315
|
throw new Error("Supervisor bridge timeout must be a positive integer.");
|
|
21
|
-
const
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
316
|
+
const requestKey = bindingRequestKey(session, coordinates, env);
|
|
317
|
+
if (options.allowConfirmedFastPath && confirmedRequestsBySession.get(session.session_id) === requestKey) {
|
|
318
|
+
const confirmedProtocol = confirmedProtocolsBySession.get(session.session_id);
|
|
319
|
+
if (!confirmedProtocol)
|
|
320
|
+
throw new Error("The supervised worker binding protocol is not confirmed.");
|
|
321
|
+
try {
|
|
322
|
+
await verifyConfirmedBinding(session, coordinates, env, confirmedProtocol, Math.min(timeoutMs, CONFIRMED_BINDING_VERIFY_TIMEOUT_MS));
|
|
323
|
+
}
|
|
324
|
+
catch (error) {
|
|
325
|
+
clearBindingConfirmationIfCurrent(session.session_id, requestKey);
|
|
326
|
+
throw error;
|
|
327
|
+
}
|
|
328
|
+
return { bound: true, supervisorContextCwd: coordinates.supervisorContextCwd };
|
|
329
|
+
}
|
|
330
|
+
const deadline = Date.now() + timeoutMs;
|
|
331
|
+
const { protocolVersion, daemonIdentity } = await negotiateSupervisor(socketPath, timeoutMs);
|
|
332
|
+
const bindingKey = daemonIdentity ? `${requestKey}\u0000${daemonIdentity}` : null;
|
|
333
|
+
if (bindingKey && confirmedBindingsBySession.get(session.session_id) === bindingKey) {
|
|
334
|
+
confirmedRequestsBySession.set(session.session_id, requestKey);
|
|
335
|
+
confirmedProtocolsBySession.set(session.session_id, protocolVersion);
|
|
336
|
+
return { bound: true, supervisorContextCwd: coordinates.supervisorContextCwd };
|
|
337
|
+
}
|
|
31
338
|
const response = await supervisorRequest(socketPath, {
|
|
32
339
|
version: protocolVersion,
|
|
33
340
|
id: randomUUID(),
|
|
@@ -41,13 +348,355 @@ export async function bindSupervisedWorkerSession(session, env = process.env, op
|
|
|
41
348
|
agent_session_token: session.session_token,
|
|
42
349
|
api_url: env.LETAGENTS_API_URL?.trim() || "https://letagents.chat",
|
|
43
350
|
},
|
|
44
|
-
},
|
|
351
|
+
}, remainingRequestTimeout(deadline));
|
|
45
352
|
if (!response.ok)
|
|
46
353
|
throw new Error(response.error || "Supervisor rejected the worker session binding.");
|
|
47
354
|
if (response.version !== protocolVersion)
|
|
48
355
|
throw new Error("Supervisor binding response used an unexpected protocol version.");
|
|
356
|
+
if (bindingKey) {
|
|
357
|
+
confirmedRequestsBySession.set(session.session_id, requestKey);
|
|
358
|
+
confirmedBindingsBySession.set(session.session_id, bindingKey);
|
|
359
|
+
confirmedProtocolsBySession.set(session.session_id, protocolVersion);
|
|
360
|
+
}
|
|
361
|
+
else {
|
|
362
|
+
// Without a stable daemon identity we cannot prove that a later process at
|
|
363
|
+
// the same socket owns this confirmation, so every wait binds strictly.
|
|
364
|
+
confirmedRequestsBySession.delete(session.session_id);
|
|
365
|
+
confirmedBindingsBySession.delete(session.session_id);
|
|
366
|
+
confirmedProtocolsBySession.delete(session.session_id);
|
|
367
|
+
}
|
|
368
|
+
return { bound: true, supervisorContextCwd: coordinates.supervisorContextCwd };
|
|
369
|
+
}
|
|
370
|
+
function bindingRequestKey(session, coordinates, env) {
|
|
371
|
+
const tokenDigest = createHash("sha256").update(session.session_token).digest("hex");
|
|
372
|
+
return [
|
|
373
|
+
coordinates.socketPath,
|
|
374
|
+
coordinates.entryId,
|
|
375
|
+
coordinates.workAttemptId,
|
|
376
|
+
coordinates.executionGenerationId,
|
|
377
|
+
session.session_id,
|
|
378
|
+
session.room_id,
|
|
379
|
+
tokenDigest,
|
|
380
|
+
new URL(env.LETAGENTS_API_URL?.trim() || "https://letagents.chat").origin,
|
|
381
|
+
].join("\u0000");
|
|
382
|
+
}
|
|
383
|
+
async function verifyConfirmedBinding(session, coordinates, env, protocolVersion, timeoutMs) {
|
|
384
|
+
const response = await supervisorRequest(coordinates.socketPath, {
|
|
385
|
+
version: protocolVersion,
|
|
386
|
+
id: randomUUID(),
|
|
387
|
+
method: "supervisor.verify_worker_session",
|
|
388
|
+
params: {
|
|
389
|
+
entry_id: coordinates.entryId,
|
|
390
|
+
room_id: session.room_id,
|
|
391
|
+
work_attempt_id: coordinates.workAttemptId,
|
|
392
|
+
execution_generation_id: coordinates.executionGenerationId,
|
|
393
|
+
agent_session_id: session.session_id,
|
|
394
|
+
agent_session_token: session.session_token,
|
|
395
|
+
api_url: env.LETAGENTS_API_URL?.trim() || "https://letagents.chat",
|
|
396
|
+
},
|
|
397
|
+
}, timeoutMs);
|
|
398
|
+
if (!response.ok)
|
|
399
|
+
throw new Error(response.error || "Supervisor rejected the worker session verification.");
|
|
400
|
+
if (response.version !== protocolVersion)
|
|
401
|
+
throw new Error("Supervisor verification response used an unexpected protocol version.");
|
|
402
|
+
}
|
|
403
|
+
function clearBindingConfirmationIfCurrent(sessionId, requestKey) {
|
|
404
|
+
if (confirmedRequestsBySession.get(sessionId) !== requestKey)
|
|
405
|
+
return;
|
|
406
|
+
confirmedRequestsBySession.delete(sessionId);
|
|
407
|
+
confirmedBindingsBySession.delete(sessionId);
|
|
408
|
+
confirmedProtocolsBySession.delete(sessionId);
|
|
409
|
+
}
|
|
410
|
+
/** Persist the room-delivery cursor beside the daemon-private exact worker credential. */
|
|
411
|
+
export async function checkpointSupervisedWorkerCursor(session, roomCursor, env = process.env, options = {}) {
|
|
412
|
+
const coordinates = options.resolvedCoordinates ?? await resolveSupervisorCoordinates(session, env, options);
|
|
413
|
+
if (!coordinates)
|
|
414
|
+
return false;
|
|
415
|
+
const { entryId, socketPath, workAttemptId, executionGenerationId } = coordinates;
|
|
416
|
+
if (session.session_kind !== "worker")
|
|
417
|
+
throw new Error("A supervised provider must use a worker session.");
|
|
418
|
+
if (!roomCursor.trim())
|
|
419
|
+
throw new Error("Supervised worker cursor is required.");
|
|
420
|
+
const timeoutMs = options.requestTimeoutMs ?? DEFAULT_REQUEST_TIMEOUT_MS;
|
|
421
|
+
const deadline = Date.now() + timeoutMs;
|
|
422
|
+
const { protocolVersion } = await negotiateSupervisor(socketPath, timeoutMs);
|
|
423
|
+
const response = await supervisorRequest(socketPath, {
|
|
424
|
+
version: protocolVersion,
|
|
425
|
+
id: randomUUID(),
|
|
426
|
+
method: "supervisor.checkpoint_worker_cursor",
|
|
427
|
+
params: {
|
|
428
|
+
entry_id: entryId,
|
|
429
|
+
work_attempt_id: workAttemptId,
|
|
430
|
+
execution_generation_id: executionGenerationId,
|
|
431
|
+
agent_session_id: session.session_id,
|
|
432
|
+
room_cursor: roomCursor,
|
|
433
|
+
},
|
|
434
|
+
}, remainingRequestTimeout(deadline));
|
|
435
|
+
if (!response.ok)
|
|
436
|
+
throw new Error(response.error || "Supervisor rejected the worker cursor checkpoint.");
|
|
437
|
+
if (response.version !== protocolVersion)
|
|
438
|
+
throw new Error("Supervisor cursor checkpoint response used an unexpected protocol version.");
|
|
49
439
|
return true;
|
|
50
440
|
}
|
|
441
|
+
/**
|
|
442
|
+
* Coalesce an already-acknowledged cursor outside the MCP response path. The
|
|
443
|
+
* caller acknowledges a prior delivery by passing it as `after_message_id` on
|
|
444
|
+
* the next wait; newly observed output must never be checkpointed here.
|
|
445
|
+
*/
|
|
446
|
+
export function scheduleSupervisedWorkerCursorCheckpoint(session, roomCursor, env = process.env, options = {}) {
|
|
447
|
+
void enqueueSupervisedWorkerCursorCheckpoint(session, roomCursor, env, options).catch((error) => {
|
|
448
|
+
console.error("[letagents] Supervised cursor checkpoint could not be scheduled:", error instanceof Error ? error.message : "unknown supervisor error");
|
|
449
|
+
});
|
|
450
|
+
}
|
|
451
|
+
async function enqueueSupervisedWorkerCursorCheckpoint(session, roomCursor, env, options) {
|
|
452
|
+
if (parseRoomMessageNumber(roomCursor) === null)
|
|
453
|
+
return;
|
|
454
|
+
const coordinates = await resolveSupervisorCoordinates(session, env, options);
|
|
455
|
+
if (!coordinates)
|
|
456
|
+
return;
|
|
457
|
+
const key = [
|
|
458
|
+
session.session_id,
|
|
459
|
+
session.room_id,
|
|
460
|
+
coordinates.socketPath,
|
|
461
|
+
coordinates.entryId,
|
|
462
|
+
coordinates.workAttemptId,
|
|
463
|
+
coordinates.executionGenerationId,
|
|
464
|
+
].join("\u0000");
|
|
465
|
+
const existing = pendingCursorCheckpoints.get(key);
|
|
466
|
+
if (existing && !isNewerRoomCursor(roomCursor, existing.roomCursor))
|
|
467
|
+
return;
|
|
468
|
+
pendingCursorCheckpoints.set(key, {
|
|
469
|
+
session,
|
|
470
|
+
roomCursor,
|
|
471
|
+
env,
|
|
472
|
+
options: { ...options, resolvedCoordinates: coordinates },
|
|
473
|
+
retryAttempt: 0,
|
|
474
|
+
});
|
|
475
|
+
const retryTimer = cursorCheckpointRetryTimers.get(key);
|
|
476
|
+
if (retryTimer) {
|
|
477
|
+
clearTimeout(retryTimer);
|
|
478
|
+
cursorCheckpointRetryTimers.delete(key);
|
|
479
|
+
}
|
|
480
|
+
startCursorCheckpointDrain(key);
|
|
481
|
+
}
|
|
482
|
+
async function drainCursorCheckpoints(key) {
|
|
483
|
+
try {
|
|
484
|
+
while (true) {
|
|
485
|
+
const pending = pendingCursorCheckpoints.get(key);
|
|
486
|
+
if (!pending)
|
|
487
|
+
return;
|
|
488
|
+
pendingCursorCheckpoints.delete(key);
|
|
489
|
+
try {
|
|
490
|
+
await checkpointSupervisedWorkerCursor(pending.session, pending.roomCursor, pending.env, pending.options);
|
|
491
|
+
}
|
|
492
|
+
catch (error) {
|
|
493
|
+
const queued = pendingCursorCheckpoints.get(key);
|
|
494
|
+
if (queued && isNewerRoomCursor(queued.roomCursor, pending.roomCursor))
|
|
495
|
+
continue;
|
|
496
|
+
if (isRetryableSupervisorBridgeError(error)
|
|
497
|
+
&& pending.retryAttempt < CURSOR_CHECKPOINT_RETRY_DELAYS_MS.length) {
|
|
498
|
+
// A failed newer acknowledgement must not be replaced by an older
|
|
499
|
+
// concurrent wait that happened to enqueue while I/O was in flight.
|
|
500
|
+
if (queued)
|
|
501
|
+
pendingCursorCheckpoints.delete(key);
|
|
502
|
+
const delayMs = CURSOR_CHECKPOINT_RETRY_DELAYS_MS[pending.retryAttempt];
|
|
503
|
+
pendingCursorCheckpoints.set(key, { ...pending, retryAttempt: pending.retryAttempt + 1 });
|
|
504
|
+
const timer = setTimeout(() => {
|
|
505
|
+
cursorCheckpointRetryTimers.delete(key);
|
|
506
|
+
startCursorCheckpointDrain(key);
|
|
507
|
+
}, delayMs);
|
|
508
|
+
timer.unref?.();
|
|
509
|
+
cursorCheckpointRetryTimers.set(key, timer);
|
|
510
|
+
console.warn("[letagents] Supervised cursor checkpoint is pending:", error instanceof Error ? error.message : "unknown supervisor error");
|
|
511
|
+
return;
|
|
512
|
+
}
|
|
513
|
+
// An authority/generation rejection is not a harmless transport blip.
|
|
514
|
+
// Make the next wait prove the exact binding again before it can read.
|
|
515
|
+
await clearCheckpointBindingConfirmationIfCurrent(pending);
|
|
516
|
+
pendingCursorCheckpoints.delete(key);
|
|
517
|
+
console.error("[letagents] Supervised cursor checkpoint was rejected:", error instanceof Error ? error.message : "unknown supervisor error");
|
|
518
|
+
}
|
|
519
|
+
}
|
|
520
|
+
}
|
|
521
|
+
finally {
|
|
522
|
+
activeCursorCheckpointDrains.delete(key);
|
|
523
|
+
if (pendingCursorCheckpoints.has(key) && !cursorCheckpointRetryTimers.has(key)) {
|
|
524
|
+
startCursorCheckpointDrain(key);
|
|
525
|
+
}
|
|
526
|
+
}
|
|
527
|
+
}
|
|
528
|
+
async function clearCheckpointBindingConfirmationIfCurrent(pending) {
|
|
529
|
+
try {
|
|
530
|
+
const coordinates = pending.options.resolvedCoordinates
|
|
531
|
+
?? await resolveSupervisorCoordinates(pending.session, pending.env, pending.options);
|
|
532
|
+
if (!coordinates)
|
|
533
|
+
return;
|
|
534
|
+
clearBindingConfirmationIfCurrent(pending.session.session_id, bindingRequestKey(pending.session, coordinates, pending.env));
|
|
535
|
+
}
|
|
536
|
+
catch {
|
|
537
|
+
// Missing or invalid context already fails the next wait closed.
|
|
538
|
+
}
|
|
539
|
+
}
|
|
540
|
+
function startCursorCheckpointDrain(key) {
|
|
541
|
+
if (activeCursorCheckpointDrains.has(key) || cursorCheckpointRetryTimers.has(key))
|
|
542
|
+
return;
|
|
543
|
+
activeCursorCheckpointDrains.add(key);
|
|
544
|
+
setImmediate(() => { void drainCursorCheckpoints(key); });
|
|
545
|
+
}
|
|
546
|
+
function isNewerRoomCursor(candidate, current) {
|
|
547
|
+
if (candidate === current)
|
|
548
|
+
return false;
|
|
549
|
+
const candidateNumber = parseRoomMessageNumber(candidate);
|
|
550
|
+
const currentNumber = parseRoomMessageNumber(current);
|
|
551
|
+
if (candidateNumber === null)
|
|
552
|
+
return false;
|
|
553
|
+
if (currentNumber === null)
|
|
554
|
+
return true;
|
|
555
|
+
return candidateNumber > currentNumber;
|
|
556
|
+
}
|
|
557
|
+
function parseRoomMessageNumber(cursor) {
|
|
558
|
+
return parsePositivePgIntegerScopedId(cursor, "msg");
|
|
559
|
+
}
|
|
560
|
+
/** Transport failures are retryable bookkeeping failures, not worker failures. */
|
|
561
|
+
export function isRetryableSupervisorBridgeError(error) {
|
|
562
|
+
const code = error?.code;
|
|
563
|
+
if (code && ["ECONNREFUSED", "ECONNRESET", "EPIPE", "ENOENT", "ETIMEDOUT"].includes(code))
|
|
564
|
+
return true;
|
|
565
|
+
const message = error instanceof Error ? error.message : String(error ?? "");
|
|
566
|
+
return /timed out communicating|socket hang up|connection (?:closed|refused|reset)|broken pipe/i.test(message);
|
|
567
|
+
}
|
|
568
|
+
function remainingRequestTimeout(deadline) {
|
|
569
|
+
const remaining = deadline - Date.now();
|
|
570
|
+
if (remaining < 1)
|
|
571
|
+
throw new Error("Timed out communicating with the supervisor daemon.");
|
|
572
|
+
return remaining;
|
|
573
|
+
}
|
|
574
|
+
async function resolveSupervisorCoordinates(session, env, options) {
|
|
575
|
+
const environmentCoordinates = {
|
|
576
|
+
entryId: env.LETAGENTS_SUPERVISOR_ENTRY_ID?.trim() ?? "",
|
|
577
|
+
socketPath: env.LETAGENTS_SUPERVISOR_DAEMON_SOCKET?.trim() ?? "",
|
|
578
|
+
workAttemptId: env.LETAGENTS_SUPERVISOR_WORK_ATTEMPT_ID?.trim() ?? "",
|
|
579
|
+
executionGenerationId: env.LETAGENTS_SUPERVISOR_EXECUTION_GENERATION_ID?.trim() ?? "",
|
|
580
|
+
providerTurnId: env.LETAGENTS_SUPERVISOR_PROVIDER_TURN_ID?.trim() || undefined,
|
|
581
|
+
agentSessionId: env.LETAGENTS_SUPERVISOR_AGENT_SESSION_ID?.trim() || undefined,
|
|
582
|
+
roomId: env.LETAGENTS_SUPERVISOR_ROOM_ID?.trim() || undefined,
|
|
583
|
+
agentDisplayName: env.LETAGENTS_SUPERVISOR_AGENT_DISPLAY_NAME?.trim() || undefined,
|
|
584
|
+
};
|
|
585
|
+
const values = [
|
|
586
|
+
environmentCoordinates.entryId,
|
|
587
|
+
environmentCoordinates.socketPath,
|
|
588
|
+
environmentCoordinates.workAttemptId,
|
|
589
|
+
environmentCoordinates.executionGenerationId,
|
|
590
|
+
];
|
|
591
|
+
const hasEnvironmentCoordinates = values.some((value) => Boolean(value));
|
|
592
|
+
if (hasEnvironmentCoordinates && values.some((value) => !value)) {
|
|
593
|
+
throw new Error("Supervised worker bridge environment is incomplete.");
|
|
594
|
+
}
|
|
595
|
+
if (hasEnvironmentCoordinates) {
|
|
596
|
+
return { ...environmentCoordinates, supervisorContextCwd: null };
|
|
597
|
+
}
|
|
598
|
+
const context = await readCodexSupervisorContext(options.cwd ?? session.supervisor_context_cwd ?? process.cwd());
|
|
599
|
+
if (!context) {
|
|
600
|
+
if (session.supervisor_context_cwd?.trim()) {
|
|
601
|
+
throw new Error("The persisted supervised worker context is missing.");
|
|
602
|
+
}
|
|
603
|
+
return null;
|
|
604
|
+
}
|
|
605
|
+
if (!/^codex(?::|$)/i.test(session.runtime.trim())) {
|
|
606
|
+
throw new Error("Codex supervisor bridge context cannot bind a non-Codex worker session.");
|
|
607
|
+
}
|
|
608
|
+
if (session.room_id && context.roomId !== session.room_id) {
|
|
609
|
+
throw new Error("Codex supervisor bridge context does not match the worker room.");
|
|
610
|
+
}
|
|
611
|
+
return {
|
|
612
|
+
...context,
|
|
613
|
+
socketPath: options.trustedDaemonSocketPath ?? join(homedir(), ".letagents", "daemon.sock"),
|
|
614
|
+
};
|
|
615
|
+
}
|
|
616
|
+
async function readCodexSupervisorContext(cwd) {
|
|
617
|
+
const path = join(cwd, SUPERVISOR_CONTEXT_FILE);
|
|
618
|
+
let encoded;
|
|
619
|
+
try {
|
|
620
|
+
const info = await lstat(path);
|
|
621
|
+
if (!info.isFile() || info.isSymbolicLink() || info.size > MAX_SUPERVISOR_CONTEXT_BYTES) {
|
|
622
|
+
throw new Error("Codex supervisor bridge context must be a small regular file.");
|
|
623
|
+
}
|
|
624
|
+
encoded = await readFile(path, "utf8");
|
|
625
|
+
}
|
|
626
|
+
catch (error) {
|
|
627
|
+
if (error.code === "ENOENT")
|
|
628
|
+
return null;
|
|
629
|
+
throw error;
|
|
630
|
+
}
|
|
631
|
+
let value;
|
|
632
|
+
try {
|
|
633
|
+
value = JSON.parse(encoded);
|
|
634
|
+
}
|
|
635
|
+
catch {
|
|
636
|
+
throw new Error("Codex supervisor bridge context is not valid JSON.");
|
|
637
|
+
}
|
|
638
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
639
|
+
throw new Error("Codex supervisor bridge context is malformed.");
|
|
640
|
+
}
|
|
641
|
+
const record = value;
|
|
642
|
+
if (record.version !== 1 || record.provider !== "codex") {
|
|
643
|
+
throw new Error("Codex supervisor bridge context has an unsupported identity.");
|
|
644
|
+
}
|
|
645
|
+
const required = {
|
|
646
|
+
entryId: record.entry_id,
|
|
647
|
+
roomId: record.room_id,
|
|
648
|
+
workAttemptId: record.work_attempt_id,
|
|
649
|
+
executionGenerationId: record.execution_generation_id,
|
|
650
|
+
};
|
|
651
|
+
for (const [field, candidate] of Object.entries(required)) {
|
|
652
|
+
if (typeof candidate !== "string" || !candidate.trim()) {
|
|
653
|
+
throw new Error(`Codex supervisor bridge context ${field} is required.`);
|
|
654
|
+
}
|
|
655
|
+
}
|
|
656
|
+
const coordinates = Object.fromEntries(Object.entries(required).map(([field, candidate]) => [field, candidate.trim()]));
|
|
657
|
+
const agentSessionId = typeof record.agent_session_id === "string" && record.agent_session_id.trim()
|
|
658
|
+
? record.agent_session_id.trim()
|
|
659
|
+
: undefined;
|
|
660
|
+
const agentDisplayName = typeof record.agent_display_name === "string" && record.agent_display_name.trim()
|
|
661
|
+
? record.agent_display_name.trim()
|
|
662
|
+
: undefined;
|
|
663
|
+
const marker = await readWorkAttemptMarker(cwd);
|
|
664
|
+
if (marker.workAttemptId !== coordinates.workAttemptId) {
|
|
665
|
+
throw new Error("Codex supervisor bridge context does not match the daemon-owned worktree.");
|
|
666
|
+
}
|
|
667
|
+
return { ...coordinates, agentSessionId, agentDisplayName, supervisorContextCwd: await realpath(cwd) };
|
|
668
|
+
}
|
|
669
|
+
async function readWorkAttemptMarker(cwd) {
|
|
670
|
+
const path = join(cwd, WORK_ATTEMPT_MARKER_FILE);
|
|
671
|
+
let encoded;
|
|
672
|
+
try {
|
|
673
|
+
const info = await lstat(path);
|
|
674
|
+
if (!info.isFile() || info.isSymbolicLink() || info.size > MAX_SUPERVISOR_CONTEXT_BYTES) {
|
|
675
|
+
throw new Error("Daemon work-attempt marker must be a small regular file.");
|
|
676
|
+
}
|
|
677
|
+
encoded = await readFile(path, "utf8");
|
|
678
|
+
}
|
|
679
|
+
catch (error) {
|
|
680
|
+
if (error.code === "ENOENT") {
|
|
681
|
+
throw new Error("Codex supervisor bridge context is outside a daemon-owned worktree.");
|
|
682
|
+
}
|
|
683
|
+
throw error;
|
|
684
|
+
}
|
|
685
|
+
let value;
|
|
686
|
+
try {
|
|
687
|
+
value = JSON.parse(encoded);
|
|
688
|
+
}
|
|
689
|
+
catch {
|
|
690
|
+
throw new Error("Daemon work-attempt marker is not valid JSON.");
|
|
691
|
+
}
|
|
692
|
+
const workAttemptId = value && typeof value === "object" && !Array.isArray(value)
|
|
693
|
+
? value.work_attempt_id
|
|
694
|
+
: null;
|
|
695
|
+
if (value?.version !== 1 || typeof workAttemptId !== "string" || !workAttemptId.trim()) {
|
|
696
|
+
throw new Error("Daemon work-attempt marker is malformed.");
|
|
697
|
+
}
|
|
698
|
+
return { workAttemptId: workAttemptId.trim() };
|
|
699
|
+
}
|
|
51
700
|
function negotiationProtocolVersion(result) {
|
|
52
701
|
if (!result || typeof result !== "object")
|
|
53
702
|
throw new Error("Supervisor protocol negotiation returned a malformed result.");
|
|
@@ -58,21 +707,50 @@ function negotiationProtocolVersion(result) {
|
|
|
58
707
|
}
|
|
59
708
|
return protocolVersion;
|
|
60
709
|
}
|
|
710
|
+
async function negotiateSupervisor(socketPath, timeoutMs) {
|
|
711
|
+
const negotiation = await supervisorRequest(socketPath, {
|
|
712
|
+
version: NEGOTIATION_PROTOCOL_VERSION,
|
|
713
|
+
id: randomUUID(),
|
|
714
|
+
method: "daemon.negotiate",
|
|
715
|
+
}, timeoutMs);
|
|
716
|
+
if (!negotiation.ok)
|
|
717
|
+
throw new Error(negotiation.error || "Supervisor protocol negotiation failed.");
|
|
718
|
+
const protocolVersion = negotiationProtocolVersion(negotiation.result);
|
|
719
|
+
if (negotiation.version !== protocolVersion)
|
|
720
|
+
throw new Error("Supervisor negotiation response version does not match its negotiated protocol.");
|
|
721
|
+
const result = negotiation.result;
|
|
722
|
+
const hasCompleteIdentity = typeof result.generation === "number"
|
|
723
|
+
&& Number.isSafeInteger(result.generation)
|
|
724
|
+
&& typeof result.pid === "number"
|
|
725
|
+
&& Number.isSafeInteger(result.pid)
|
|
726
|
+
&& typeof result.started_at === "string"
|
|
727
|
+
&& Boolean(result.started_at.trim());
|
|
728
|
+
const daemonIdentity = hasCompleteIdentity
|
|
729
|
+
? [result.generation, result.pid, result.started_at].join(":")
|
|
730
|
+
: null;
|
|
731
|
+
return { protocolVersion, daemonIdentity, generation: hasCompleteIdentity ? Number(result.generation) : null };
|
|
732
|
+
}
|
|
61
733
|
function supervisorRequest(socketPath, request, timeoutMs) {
|
|
62
734
|
return new Promise((resolve, reject) => {
|
|
63
735
|
const socket = createConnection(socketPath);
|
|
64
736
|
let buffer = "";
|
|
65
|
-
|
|
737
|
+
let finished = false;
|
|
738
|
+
const timer = timeoutMs === null ? null : setTimeout(() => {
|
|
66
739
|
socket.destroy();
|
|
67
|
-
reject(new Error("Timed out communicating with the supervisor daemon."));
|
|
740
|
+
finish(() => reject(new Error("Timed out communicating with the supervisor daemon.")));
|
|
68
741
|
}, timeoutMs);
|
|
69
|
-
timer
|
|
742
|
+
timer?.unref();
|
|
70
743
|
const finish = (operation) => {
|
|
71
|
-
|
|
744
|
+
if (finished)
|
|
745
|
+
return;
|
|
746
|
+
finished = true;
|
|
747
|
+
if (timer)
|
|
748
|
+
clearTimeout(timer);
|
|
72
749
|
operation();
|
|
73
750
|
};
|
|
74
751
|
socket.setEncoding("utf8");
|
|
75
752
|
socket.once("error", (error) => finish(() => reject(error)));
|
|
753
|
+
socket.once("close", () => finish(() => reject(new Error("Supervisor connection closed before a response."))));
|
|
76
754
|
socket.on("data", (chunk) => {
|
|
77
755
|
buffer += chunk;
|
|
78
756
|
const newline = buffer.indexOf("\n");
|