letagents 0.12.27 → 0.12.28

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.
@@ -1,5 +1,6 @@
1
1
  import { randomUUID } from "crypto";
2
2
  import { resolve } from "path";
3
+ import { CODEX_THREAD_HISTORY_MODE } from "../../../shared/codex-thread-history.mjs";
3
4
  import { getCurrentCodexLiveSession, getStoredCodexLiveSession, saveCodexLiveSession, updateCodexLiveSession, } from "../local-state.js";
4
5
  import { getGitCurrentBranch } from "../git-remote.js";
5
6
  import { isServerReady, launchAppServer, resolveCodexServerUrl, terminateSpawnedProcess, waitForServer, } from "./app-server.js";
@@ -109,7 +110,7 @@ export async function startLocalCodexSession(input) {
109
110
  });
110
111
  });
111
112
  await client.connect();
112
- const threadStart = await client.request("thread/start", {});
113
+ const threadStart = await client.request("thread/start", { historyMode: CODEX_THREAD_HISTORY_MODE });
113
114
  const threadId = threadStart.thread?.id;
114
115
  if (!threadId) {
115
116
  throw new Error("Codex app-server did not return a thread id.");
@@ -9,6 +9,9 @@ import { getCurrentSupervisedRoomAuthority } from "./supervised-room-authority.j
9
9
  const NEGOTIATION_PROTOCOL_VERSION = 1;
10
10
  const SUPPORTED_SUPERVISOR_PROTOCOL_VERSIONS = new Set([1, 2, 3]);
11
11
  const DEFAULT_REQUEST_TIMEOUT_MS = 5_000;
12
+ // A signed desktop update can leave the socket absent for roughly 20 seconds.
13
+ // Only unsent operations receive this startup grace; it never permits replay.
14
+ const SUPERVISED_STARTUP_TIMEOUT_MS = 30_000;
12
15
  const CONFIRMED_BINDING_VERIFY_TIMEOUT_MS = 250;
13
16
  const SUPERVISOR_CONTEXT_FILE = ".letagents-supervisor-context.json";
14
17
  const WORK_ATTEMPT_MARKER_FILE = ".letagents-work-attempt.json";
@@ -95,26 +98,9 @@ const activeCursorCheckpointDrains = new Set();
95
98
  const cursorCheckpointRetryTimers = new Map();
96
99
  const CURSOR_CHECKPOINT_RETRY_DELAYS_MS = [250, 1_000, 3_000];
97
100
  export async function executeCurrentSupervisedTool(input, env = process.env, options = {}) {
98
- const coordinates = await requireCurrentSupervisedCoordinates(env, options);
99
- const timeoutMs = options.requestTimeoutMs ?? DEFAULT_REQUEST_TIMEOUT_MS;
100
- const negotiated = await negotiateSupervisor(coordinates.socketPath, timeoutMs);
101
- if (negotiated.generation === null)
102
- throw new Error("The supervised daemon generation is unavailable.");
103
- const response = await supervisorRequest(coordinates.socketPath, {
104
- version: negotiated.protocolVersion,
105
- id: randomUUID(),
106
- method: "supervisor.execute_bounded_tool",
107
- params: {
108
- entry_id: coordinates.entryId,
109
- work_attempt_id: coordinates.workAttemptId,
110
- execution_generation_id: coordinates.executionGenerationId,
111
- ...(coordinates.providerTurnId ? { provider_turn_id: coordinates.providerTurnId } : {}),
112
- daemon_generation: negotiated.generation,
113
- mcp_request_id: input.mcpRequestId,
114
- tool_name: input.toolName,
115
- input: input.input,
116
- },
117
- }, null);
101
+ const response = await requestCurrentSupervisedOperation("supervisor.execute_bounded_tool", {
102
+ mcp_request_id: input.mcpRequestId, tool_name: input.toolName, input: input.input,
103
+ }, env, options);
118
104
  if (!response.ok) {
119
105
  if (/Unsupported daemon method:\s*supervisor\.execute_bounded_tool/i.test(response.error ?? "")) {
120
106
  return { state: "unsupported" };
@@ -131,27 +117,9 @@ export async function executeCurrentSupervisedTool(input, env = process.env, opt
131
117
  return { state: "completed", roomId, result: result.result };
132
118
  }
133
119
  export async function prepareCurrentSupervisedEffect(input, env = process.env, options = {}) {
134
- const coordinates = await requireCurrentSupervisedCoordinates(env, options);
135
- const timeoutMs = options.requestTimeoutMs ?? DEFAULT_REQUEST_TIMEOUT_MS;
136
- const negotiated = await negotiateSupervisor(coordinates.socketPath, timeoutMs);
137
- if (negotiated.generation === null)
138
- throw new Error("The supervised daemon generation is unavailable.");
139
- const response = await supervisorRequest(coordinates.socketPath, {
140
- version: negotiated.protocolVersion,
141
- id: randomUUID(),
142
- method: "supervisor.prepare_bounded_effect",
143
- params: {
144
- entry_id: coordinates.entryId,
145
- work_attempt_id: coordinates.workAttemptId,
146
- execution_generation_id: coordinates.executionGenerationId,
147
- ...(coordinates.providerTurnId ? { provider_turn_id: coordinates.providerTurnId } : {}),
148
- daemon_generation: negotiated.generation,
149
- mcp_request_id: input.mcpRequestId,
150
- tool_name: input.toolName,
151
- input: input.input,
152
- mutation: input.mutation,
153
- },
154
- }, timeoutMs);
120
+ const response = await requestCurrentSupervisedOperation("supervisor.prepare_bounded_effect", {
121
+ mcp_request_id: input.mcpRequestId, tool_name: input.toolName, input: input.input, mutation: input.mutation,
122
+ }, env, options);
155
123
  if (!response.ok)
156
124
  throw new Error(response.error || "The supervised effect was rejected.");
157
125
  const result = response.result && typeof response.result === "object" ? response.result : {};
@@ -184,6 +152,50 @@ export async function prepareCurrentSupervisedEffect(input, env = process.env, o
184
152
  }
185
153
  throw new Error("The supervised effect journal returned an unsupported action.");
186
154
  }
155
+ async function requestCurrentSupervisedOperation(method, params, env, options) {
156
+ // Keep the original invocation and authority across an unlink-to-listen gap.
157
+ const invocation = JSON.parse(JSON.stringify(params));
158
+ const coordinates = await requireCurrentSupervisedCoordinates(env, options);
159
+ const requestId = randomUUID();
160
+ const deadline = Date.now() + (options.requestTimeoutMs ?? SUPERVISED_STARTUP_TIMEOUT_MS);
161
+ let retryAttempt = 0;
162
+ while (true) {
163
+ let negotiated;
164
+ try {
165
+ negotiated = await negotiateSupervisor(coordinates.socketPath, remainingRequestTimeout(deadline));
166
+ }
167
+ catch (error) {
168
+ // Negotiation is read-only; no operation has been sent at this point.
169
+ if (!(error instanceof SupervisorTransportError) || !isRetryableSupervisorBridgeError(error)
170
+ || !await waitForCompletionHandoffRetry(deadline, retryAttempt++))
171
+ throw error;
172
+ continue;
173
+ }
174
+ if (negotiated.generation === null)
175
+ throw new Error("The supervised daemon generation is unavailable.");
176
+ try {
177
+ return await supervisorRequest(coordinates.socketPath, {
178
+ version: negotiated.protocolVersion, id: requestId, method,
179
+ params: {
180
+ ...invocation,
181
+ entry_id: coordinates.entryId,
182
+ work_attempt_id: coordinates.workAttemptId,
183
+ execution_generation_id: coordinates.executionGenerationId,
184
+ ...(coordinates.providerTurnId ? { provider_turn_id: coordinates.providerTurnId } : {}),
185
+ daemon_generation: negotiated.generation,
186
+ },
187
+ }, method === "supervisor.execute_bounded_tool" ? null : remainingRequestTimeout(deadline), remainingRequestTimeout(deadline));
188
+ }
189
+ catch (error) {
190
+ // Once a write begins, failure is ambiguous. Never replay an operation,
191
+ // even when its error looks like a transient daemon restart.
192
+ if (!(error instanceof SupervisorTransportError) || error.writeAttempted
193
+ || !isRetryableSupervisorBridgeError(error)
194
+ || !await waitForCompletionHandoffRetry(deadline, retryAttempt++))
195
+ throw error;
196
+ }
197
+ }
198
+ }
187
199
  export async function completeCurrentSupervisedEffect(input, env = process.env, options = {}) {
188
200
  const coordinates = await requireCurrentSupervisedCoordinates(env, options);
189
201
  const timeoutMs = options.requestTimeoutMs ?? DEFAULT_REQUEST_TIMEOUT_MS;
@@ -811,27 +823,44 @@ async function negotiateSupervisor(socketPath, timeoutMs) {
811
823
  custodialPollingV1: result.capabilities?.custodialPollingV1 === true,
812
824
  custodialPollingOffersV1: result.capabilities?.custodialPollingOffersV1 === true };
813
825
  }
814
- function supervisorRequest(socketPath, request, timeoutMs) {
826
+ class SupervisorTransportError extends Error {
827
+ writeAttempted;
828
+ code;
829
+ constructor(error, writeAttempted) {
830
+ super(error.message, { cause: error });
831
+ this.writeAttempted = writeAttempted;
832
+ this.code = error.code;
833
+ }
834
+ }
835
+ function supervisorRequest(socketPath, request, timeoutMs, connectTimeoutMs = null) {
815
836
  return new Promise((resolve, reject) => {
837
+ const encoded = `${JSON.stringify(request)}\n`;
816
838
  const socket = createConnection(socketPath);
817
839
  let buffer = "";
818
840
  let finished = false;
819
- const timer = timeoutMs === null ? null : setTimeout(() => {
820
- socket.destroy();
821
- finish(() => reject(new Error("Timed out communicating with the supervisor daemon.")));
822
- }, timeoutMs);
823
- timer?.unref();
841
+ let writeAttempted = false;
824
842
  const finish = (operation) => {
825
843
  if (finished)
826
844
  return;
827
845
  finished = true;
828
846
  if (timer)
829
847
  clearTimeout(timer);
848
+ if (connectTimer)
849
+ clearTimeout(connectTimer);
830
850
  operation();
831
851
  };
852
+ const failTransport = (error) => finish(() => reject(new SupervisorTransportError(error, writeAttempted)));
853
+ const timeout = () => {
854
+ failTransport(new Error("Timed out communicating with the supervisor daemon."));
855
+ socket.destroy();
856
+ };
857
+ const timer = timeoutMs === null ? null : setTimeout(timeout, timeoutMs);
858
+ const connectTimer = connectTimeoutMs === null ? null : setTimeout(timeout, connectTimeoutMs);
859
+ timer?.unref();
860
+ connectTimer?.unref();
832
861
  socket.setEncoding("utf8");
833
- socket.once("error", (error) => finish(() => reject(error)));
834
- socket.once("close", () => finish(() => reject(new Error("Supervisor connection closed before a response."))));
862
+ socket.once("error", failTransport);
863
+ socket.once("close", () => failTransport(new Error("Supervisor connection closed before a response.")));
835
864
  socket.on("data", (chunk) => {
836
865
  buffer += chunk;
837
866
  const newline = buffer.indexOf("\n");
@@ -848,6 +877,19 @@ function supervisorRequest(socketPath, request, timeoutMs) {
848
877
  finish(() => reject(error));
849
878
  }
850
879
  });
851
- socket.once("connect", () => socket.write(`${JSON.stringify(request)}\n`));
880
+ socket.once("connect", () => {
881
+ if (finished)
882
+ return;
883
+ if (connectTimer)
884
+ clearTimeout(connectTimer);
885
+ writeAttempted = true;
886
+ try {
887
+ socket.write(encoded);
888
+ }
889
+ catch (error) {
890
+ failTransport(error instanceof Error ? error : new Error(String(error)));
891
+ socket.destroy();
892
+ }
893
+ });
852
894
  });
853
895
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "letagents",
3
- "version": "0.12.27",
3
+ "version": "0.12.28",
4
4
  "description": "Let Agents Chat \u2014 MCP server for AI agent communication",
5
5
  "type": "module",
6
6
  "main": "dist/mcp/server.js",
@@ -0,0 +1 @@
1
+ export const CODEX_THREAD_HISTORY_MODE: "legacy";
@@ -0,0 +1,4 @@
1
+ // Attachment and recovery require readable native turn history. Codex 0.153.4
2
+ // can default to paginated threads whose history API is unavailable. Select
3
+ // the verified contract only for creation; never reinterpret an existing thread.
4
+ export const CODEX_THREAD_HISTORY_MODE = "legacy";