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.
Files changed (49) hide show
  1. package/dist/mcp/git-remote.js +7 -7
  2. package/dist/mcp/local-state/agent-sessions.js +78 -5
  3. package/dist/mcp/local-state/local-chat.js +189 -48
  4. package/dist/mcp/local-state/storage.js +16 -9
  5. package/dist/mcp/server/daemon-tool-executor.js +92 -0
  6. package/dist/mcp/server/register-tools.js +25 -12
  7. package/dist/mcp/server/runtime/agent-sessions.js +58 -9
  8. package/dist/mcp/server/runtime/api.js +40 -4
  9. package/dist/mcp/server/runtime/daemon-tool-context.js +11 -0
  10. package/dist/mcp/server/runtime/execution-profile.js +19 -0
  11. package/dist/mcp/server/runtime/identity/directory.js +2 -2
  12. package/dist/mcp/server/runtime/messages.js +55 -0
  13. package/dist/mcp/server/runtime/presence.js +4 -2
  14. package/dist/mcp/server/runtime/room-api.js +24 -7
  15. package/dist/mcp/server/runtime/room-state.js +59 -3
  16. package/dist/mcp/server/runtime/rooms.js +67 -32
  17. package/dist/mcp/server/runtime/supervised-room-authority.js +8 -0
  18. package/dist/mcp/server/runtime/supervisor-bridge.js +702 -24
  19. package/dist/mcp/server/runtime/tool-surface-policy.js +26 -0
  20. package/dist/mcp/server/runtime/worker-bearer.js +44 -6
  21. package/dist/mcp/server/runtime-contract.js +27 -0
  22. package/dist/mcp/server/runtime.js +15 -4
  23. package/dist/mcp/server/supervised-tool-facade.js +134 -0
  24. package/dist/mcp/server/tools/agent-sessions.js +74 -7
  25. package/dist/mcp/server/tools/messages/index.js +3 -2
  26. package/dist/mcp/server/tools/messages/read-tool.js +54 -97
  27. package/dist/mcp/server/tools/messages/reasoning-tool.js +2 -0
  28. package/dist/mcp/server/tools/messages/send-tool.js +5 -0
  29. package/dist/mcp/server/tools/messages/status-tool.js +2 -0
  30. package/dist/mcp/server/tools/messages/wait-tool.js +344 -71
  31. package/dist/mcp/server/tools/onboarding/status-tool.js +9 -8
  32. package/dist/mcp/server/tools/rooms/inspection-tools.js +40 -22
  33. package/dist/mcp/server/tools/rooms/repo-initialization-tool.js +2 -1
  34. package/dist/mcp/server/tools/supervised-room-turn.js +42 -0
  35. package/dist/mcp/server/tools/tasks/board-tools.js +34 -2
  36. package/dist/mcp/server.js +14 -7
  37. package/dist/mcp/sse-client.js +163 -20
  38. package/dist/shared/activation-routing.js +187 -20
  39. package/dist/shared/agent-presence.js +6 -0
  40. package/dist/shared/desktop-release-manifest.js +63 -0
  41. package/dist/shared/desktop-release.js +60 -0
  42. package/dist/shared/scoped-ids.js +6 -0
  43. package/package.json +11 -3
  44. package/shared/message-contracts.d.mts +32 -0
  45. package/shared/message-contracts.mjs +109 -0
  46. package/shared/routing-aliases.d.mts +18 -0
  47. package/shared/routing-aliases.mjs +66 -0
  48. package/shared/sqlite-thread-routing.d.mts +72 -0
  49. 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
- const entryId = env.LETAGENTS_SUPERVISOR_ENTRY_ID?.trim();
9
- const socketPath = env.LETAGENTS_SUPERVISOR_DAEMON_SOCKET?.trim();
10
- const workAttemptId = env.LETAGENTS_SUPERVISOR_WORK_ATTEMPT_ID?.trim();
11
- const executionGenerationId = env.LETAGENTS_SUPERVISOR_EXECUTION_GENERATION_ID?.trim();
12
- if (!entryId && !socketPath && !workAttemptId && !executionGenerationId)
13
- return false;
14
- if (!entryId || !socketPath || !workAttemptId || !executionGenerationId)
15
- throw new Error("Supervised worker bridge environment is incomplete.");
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 negotiation = await supervisorRequest(socketPath, {
22
- version: NEGOTIATION_PROTOCOL_VERSION,
23
- id: randomUUID(),
24
- method: "daemon.negotiate",
25
- }, timeoutMs);
26
- if (!negotiation.ok)
27
- throw new Error(negotiation.error || "Supervisor protocol negotiation failed.");
28
- const protocolVersion = negotiationProtocolVersion(negotiation.result);
29
- if (negotiation.version !== protocolVersion)
30
- throw new Error("Supervisor negotiation response version does not match its negotiated protocol.");
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
- }, timeoutMs);
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
- const timer = setTimeout(() => {
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.unref();
742
+ timer?.unref();
70
743
  const finish = (operation) => {
71
- clearTimeout(timer);
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");