letagents 0.12.27 → 0.12.29
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/codex-session/session-start.js +2 -1
- package/dist/mcp/server/runtime/supervisor-bridge.js +92 -50
- package/dist/mcp/server/tools/tasks/board-intent-tools.js +2 -2
- package/dist/mcp/server/tools/tasks/verdict-tools.js +19 -6
- package/package.json +1 -1
- package/shared/codex-thread-history.d.mts +1 -0
- package/shared/codex-thread-history.mjs +4 -0
|
@@ -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
|
|
99
|
-
|
|
100
|
-
|
|
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
|
|
135
|
-
|
|
136
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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",
|
|
834
|
-
socket.once("close", () =>
|
|
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", () =>
|
|
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
|
}
|
|
@@ -170,7 +170,7 @@ export function registerBoardIntentTools(server) {
|
|
|
170
170
|
return taskToolError(String(error));
|
|
171
171
|
}
|
|
172
172
|
});
|
|
173
|
-
server.tool("register_task_claim_intent", "Register intent to claim an accepted task as the current worker.
|
|
173
|
+
server.tool("register_task_claim_intent", "Register intent to claim an accepted task as the current worker. Manager approval assigns the task and work lease immediately for authenticated workers; no second claim_task call is needed. Follow requires_follow_up only for legacy approvals.", {
|
|
174
174
|
task_id: z.string().describe("Task id to claim, e.g. task_12."),
|
|
175
175
|
...workerTaskIdentitySchema,
|
|
176
176
|
}, async ({ task_id, room_id, conversation_id: _conversation_id, agent_session_id }) => {
|
|
@@ -277,7 +277,7 @@ export function registerBoardIntentTools(server) {
|
|
|
277
277
|
return taskToolError(String(error));
|
|
278
278
|
}
|
|
279
279
|
});
|
|
280
|
-
server.tool("approve_board_intent", "Approve a pending board intent. For task_create intents this creates the task immediately and returns result.kind=\"task_created\"; do not call add_task afterward.
|
|
280
|
+
server.tool("approve_board_intent", "Approve a pending board intent. For task_create intents this creates the task immediately and returns result.kind=\"task_created\"; do not call add_task afterward. Authenticated-worker task_claim intents assign the task and work lease immediately with result.kind=\"task_claimed\" and requires_follow_up=false; do not call claim_task afterward. Other intents and legacy unverified claims require the exact follow-up with board_intent_id and the proposer's own worker session. The scoped approval token remains available for legacy callers; do not post it in room messages.", {
|
|
281
281
|
intent_id: z.string().describe("Board intent id to approve."),
|
|
282
282
|
reason: z.string().optional().describe("Short approval reason."),
|
|
283
283
|
...workerTaskIdentitySchema,
|
|
@@ -5,7 +5,7 @@ import { resolveCanonicalRoomId, resolveCanonicalTaskToolIdentity, taskActorPayl
|
|
|
5
5
|
import { jsonToolResponse, taskToolError } from "./response.js";
|
|
6
6
|
import { taskReviewIdentitySchema } from "./schemas.js";
|
|
7
7
|
export function registerTaskVerdictTools(server) {
|
|
8
|
-
server.tool("submit_review_verdict", "Submit a GitHub review verdict through the durable effect journal. Requires this exact worker session to hold an active review lease. Ambiguous
|
|
8
|
+
server.tool("submit_review_verdict", "Submit a GitHub review verdict through the durable effect journal. Requires this exact worker session to hold an active review lease. Success means GitHub confirmed publication; pending, failed, or uncertain effects keep their original idempotency key. Ambiguous outcomes are reconciled by correlation lookup and are never blindly retried.", {
|
|
9
9
|
task_id: z.string().describe("Task in review, e.g. 'task_1'."),
|
|
10
10
|
verdict: z.enum(["approve", "request_changes", "comment"]).describe("GitHub review verdict."),
|
|
11
11
|
body: z.string().max(65_536).optional().describe("Review explanation. Empty or junk blocking verdicts are quarantined."),
|
|
@@ -34,13 +34,26 @@ export function registerTaskVerdictTools(server) {
|
|
|
34
34
|
}),
|
|
35
35
|
},
|
|
36
36
|
});
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
37
|
+
const published = result.effect.state === "succeeded";
|
|
38
|
+
const publicationStatus = published ? `published ${verdict} verdict for ${task_id}`
|
|
39
|
+
: result.effect.quarantined_at ? `review publication blocked for ${task_id}`
|
|
40
|
+
: result.effect.state === "failed" ? `review publication failed for ${task_id}`
|
|
41
|
+
: result.effect.state === "pending" ? `review publication pending for ${task_id}`
|
|
42
|
+
: `review publication outcome unknown for ${task_id}`;
|
|
43
|
+
// Presence is a secondary notification. Its failure cannot erase an
|
|
44
|
+
// exact journal receipt or turn a known publication into a new retry.
|
|
45
|
+
try {
|
|
46
|
+
await syncRoomPresence(targetRoomId, identity, {
|
|
47
|
+
status: "reviewing",
|
|
48
|
+
status_text: publicationStatus,
|
|
49
|
+
}, agentSession);
|
|
50
|
+
}
|
|
51
|
+
catch { /* Return the durable effect even when presence is unavailable. */ }
|
|
41
52
|
return jsonToolResponse({
|
|
42
|
-
success: true,
|
|
43
53
|
...result,
|
|
54
|
+
success: published,
|
|
55
|
+
message: published ? "The review was published on GitHub."
|
|
56
|
+
: `${publicationStatus}. The returned effect is the publication record; do not submit a new idempotency key to retry it.`,
|
|
44
57
|
agent_identity: toPublicAgentIdentity(identity),
|
|
45
58
|
}, 2);
|
|
46
59
|
}
|
package/package.json
CHANGED
|
@@ -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";
|