@abitat_reece/host-daemon 0.1.8 → 0.1.10
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/cli/index.js +28 -12
- package/dist/cli/index.js.map +1 -1
- package/dist/local-control/codex-bridge.d.ts +73 -1
- package/dist/local-control/codex-bridge.d.ts.map +1 -1
- package/dist/local-control/codex-bridge.js +476 -59
- package/dist/local-control/codex-bridge.js.map +1 -1
- package/dist/local-control/diagnostics-log.d.ts +31 -0
- package/dist/local-control/diagnostics-log.d.ts.map +1 -0
- package/dist/local-control/diagnostics-log.js +96 -0
- package/dist/local-control/diagnostics-log.js.map +1 -0
- package/dist/local-control/push-notifications.d.ts +3 -0
- package/dist/local-control/push-notifications.d.ts.map +1 -1
- package/dist/local-control/push-notifications.js +69 -7
- package/dist/local-control/push-notifications.js.map +1 -1
- package/dist/local-control/relay-client.d.ts +4 -0
- package/dist/local-control/relay-client.d.ts.map +1 -1
- package/dist/local-control/relay-client.js +32 -4
- package/dist/local-control/relay-client.js.map +1 -1
- package/dist/local-control/server.d.ts +4 -0
- package/dist/local-control/server.d.ts.map +1 -1
- package/dist/local-control/server.js +250 -49
- package/dist/local-control/server.js.map +1 -1
- package/dist/local-control/transport.d.ts +1 -0
- package/dist/local-control/transport.d.ts.map +1 -1
- package/dist/local-control/transport.js +3 -0
- package/dist/local-control/transport.js.map +1 -1
- package/package.json +1 -1
|
@@ -3,6 +3,7 @@ import { createHash } from "node:crypto";
|
|
|
3
3
|
import { basename, normalize } from "node:path";
|
|
4
4
|
import { setTimeout as delay } from "node:timers/promises";
|
|
5
5
|
import WebSocket from "ws";
|
|
6
|
+
import { attachmentDiagnostics, errorDiagnostics, logDiagnostics, promptDiagnostics } from "./diagnostics-log.js";
|
|
6
7
|
const CODEX_PROJECT_PREFIX = "codex_project_";
|
|
7
8
|
const CODEX_THREAD_PREFIX = "codex_thread_";
|
|
8
9
|
const DEFAULT_SERVER_URL = "ws://127.0.0.1:47777";
|
|
@@ -10,7 +11,10 @@ const DEFAULT_CODEX_BINARY = "/Applications/Codex.app/Contents/Resources/codex";
|
|
|
10
11
|
const REQUEST_TIMEOUT_MS = 30_000;
|
|
11
12
|
const START_TIMEOUT_MS = 15_000;
|
|
12
13
|
const TURN_KEEPALIVE_TIMEOUT_MS = 30 * 60_000;
|
|
14
|
+
const QUEUED_TURN_POLL_INTERVAL_MS = 250;
|
|
15
|
+
const THREAD_LIST_CACHE_TTL_MS = 2_500;
|
|
13
16
|
const MAX_CODEX_MESSAGE_CONTENT_LENGTH = 12_000;
|
|
17
|
+
const HIDDEN_CODEX_ITEM_TYPES = new Set(["reasoning"]);
|
|
14
18
|
const PHONE_FULL_ACCESS_TURN_OPTIONS = {
|
|
15
19
|
approvalPolicy: "never",
|
|
16
20
|
sandboxPolicy: { type: "dangerFullAccess" }
|
|
@@ -27,14 +31,37 @@ export class LocalCodexConversationBusyError extends Error {
|
|
|
27
31
|
}
|
|
28
32
|
export function createLocalCodexBridge(options = {}) {
|
|
29
33
|
const client = createCodexAppClient(options);
|
|
34
|
+
const diagnostics = options.diagnostics;
|
|
30
35
|
const workspaceId = options.workspaceId ?? "local";
|
|
31
36
|
const userId = options.userId ?? "local";
|
|
37
|
+
const queuedTurnsByThread = new Map();
|
|
38
|
+
const queueDrainTimers = new Map();
|
|
39
|
+
const threadListCache = new Map();
|
|
32
40
|
async function listAllThreads(params = {}) {
|
|
33
|
-
const
|
|
34
|
-
|
|
35
|
-
|
|
41
|
+
const cacheKey = threadListCacheKey(params);
|
|
42
|
+
const cached = threadListCache.get(cacheKey);
|
|
43
|
+
const now = Date.now();
|
|
44
|
+
if (cached && cached.expiresAt > now) {
|
|
45
|
+
return cached.promise;
|
|
46
|
+
}
|
|
47
|
+
const promise = loadAllThreads(params).catch((error) => {
|
|
48
|
+
threadListCache.delete(cacheKey);
|
|
49
|
+
throw error;
|
|
50
|
+
});
|
|
51
|
+
threadListCache.set(cacheKey, {
|
|
52
|
+
expiresAt: now + THREAD_LIST_CACHE_TTL_MS,
|
|
53
|
+
promise
|
|
54
|
+
});
|
|
55
|
+
return promise;
|
|
56
|
+
}
|
|
57
|
+
async function loadAllThreads(params = {}) {
|
|
58
|
+
if (params.useStateDbOnly !== undefined) {
|
|
59
|
+
return listAllThreadsOnce(params);
|
|
36
60
|
}
|
|
37
|
-
|
|
61
|
+
const stateThreads = await listAllThreadsOnce({ ...params, useStateDbOnly: true });
|
|
62
|
+
const liveThreads = await listAllThreadsOnce({ ...params, useStateDbOnly: false }).catch(() => []);
|
|
63
|
+
const loadedThreads = await listLoadedThreads(params).catch(() => []);
|
|
64
|
+
return mergeCodexThreadLists(stateThreads, liveThreads, loadedThreads);
|
|
38
65
|
}
|
|
39
66
|
async function listAllThreadsOnce(params) {
|
|
40
67
|
const threads = [];
|
|
@@ -46,12 +73,21 @@ export function createLocalCodexBridge(options = {}) {
|
|
|
46
73
|
} while (cursor);
|
|
47
74
|
return threads;
|
|
48
75
|
}
|
|
76
|
+
async function listLoadedThreads(params) {
|
|
77
|
+
const loadedThreadIds = await client.listLoadedThreads();
|
|
78
|
+
const threads = await Promise.all(loadedThreadIds.map(async (threadId) => {
|
|
79
|
+
const thread = await client.readThread(threadId, false).catch(() => null);
|
|
80
|
+
return thread ? { ...thread, abitatLoadedFromAppServer: true } : null;
|
|
81
|
+
}));
|
|
82
|
+
return threads.filter((thread) => Boolean(thread && !thread.ephemeral && thread.cwd && threadMatchesListParams(thread, params)));
|
|
83
|
+
}
|
|
49
84
|
async function resolveProjectCwd(projectId) {
|
|
50
|
-
const
|
|
51
|
-
|
|
52
|
-
|
|
85
|
+
for (const thread of await listAllThreads()) {
|
|
86
|
+
if (externalCodexProjectId(thread.cwd) === projectId) {
|
|
87
|
+
return thread.cwd;
|
|
88
|
+
}
|
|
53
89
|
}
|
|
54
|
-
|
|
90
|
+
throw Object.assign(new Error("Codex project not found"), { statusCode: 404 });
|
|
55
91
|
}
|
|
56
92
|
async function listProjects() {
|
|
57
93
|
const grouped = new Map();
|
|
@@ -78,61 +114,153 @@ export function createLocalCodexBridge(options = {}) {
|
|
|
78
114
|
workspaceId
|
|
79
115
|
}));
|
|
80
116
|
}
|
|
117
|
+
function enqueueTurn(threadId, input, options = { front: false }) {
|
|
118
|
+
const existing = queuedTurnsByThread.get(threadId) ?? [];
|
|
119
|
+
const next = options.front ? [input, ...existing] : [...existing, input];
|
|
120
|
+
queuedTurnsByThread.set(threadId, next);
|
|
121
|
+
scheduleQueueDrain(threadId);
|
|
122
|
+
}
|
|
123
|
+
function scheduleQueueDrain(threadId, delayMs = QUEUED_TURN_POLL_INTERVAL_MS) {
|
|
124
|
+
if (queueDrainTimers.has(threadId)) {
|
|
125
|
+
return;
|
|
126
|
+
}
|
|
127
|
+
const timer = setTimeout(() => {
|
|
128
|
+
queueDrainTimers.delete(threadId);
|
|
129
|
+
void drainQueuedTurns(threadId);
|
|
130
|
+
}, delayMs);
|
|
131
|
+
timer.unref?.();
|
|
132
|
+
queueDrainTimers.set(threadId, timer);
|
|
133
|
+
}
|
|
134
|
+
async function drainQueuedTurns(threadId) {
|
|
135
|
+
const queue = queuedTurnsByThread.get(threadId);
|
|
136
|
+
if (!queue?.length) {
|
|
137
|
+
queuedTurnsByThread.delete(threadId);
|
|
138
|
+
return;
|
|
139
|
+
}
|
|
140
|
+
let thread;
|
|
141
|
+
try {
|
|
142
|
+
thread = await client.readThread(threadId, true);
|
|
143
|
+
}
|
|
144
|
+
catch {
|
|
145
|
+
scheduleQueueDrain(threadId);
|
|
146
|
+
return;
|
|
147
|
+
}
|
|
148
|
+
if (isCodexThreadBusy(thread)) {
|
|
149
|
+
scheduleQueueDrain(threadId);
|
|
150
|
+
return;
|
|
151
|
+
}
|
|
152
|
+
const nextInput = queue.shift();
|
|
153
|
+
if (!nextInput) {
|
|
154
|
+
queuedTurnsByThread.delete(threadId);
|
|
155
|
+
return;
|
|
156
|
+
}
|
|
157
|
+
if (queue.length === 0) {
|
|
158
|
+
queuedTurnsByThread.delete(threadId);
|
|
159
|
+
}
|
|
160
|
+
await startConversationTurn(threadId, thread, nextInput);
|
|
161
|
+
if ((queuedTurnsByThread.get(threadId)?.length ?? 0) > 0) {
|
|
162
|
+
scheduleQueueDrain(threadId);
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
async function startConversationTurn(threadId, thread, input) {
|
|
166
|
+
let activeThread = thread;
|
|
167
|
+
if (threadStatusType(activeThread.status) !== "active") {
|
|
168
|
+
activeThread = (await client.resumeThread({ excludeTurns: false, threadId })).thread;
|
|
169
|
+
}
|
|
170
|
+
await client.startTurn(threadId, userInput(input.prompt, input.attachments), {
|
|
171
|
+
...PHONE_FULL_ACCESS_TURN_OPTIONS,
|
|
172
|
+
cwd: activeThread.cwd,
|
|
173
|
+
...turnModelSettings(input.modelSettings)
|
|
174
|
+
});
|
|
175
|
+
}
|
|
81
176
|
return {
|
|
82
177
|
async bootstrap() {
|
|
83
178
|
try {
|
|
84
179
|
await client.listThreads({ limit: 1, useStateDbOnly: true });
|
|
180
|
+
logDiagnostics(diagnostics, "info", "codex.app_server.bootstrap", {
|
|
181
|
+
available: true
|
|
182
|
+
});
|
|
85
183
|
return { available: true };
|
|
86
184
|
}
|
|
87
185
|
catch (error) {
|
|
186
|
+
logDiagnostics(diagnostics, "error", "codex.app_server.bootstrap_failure", {
|
|
187
|
+
available: false,
|
|
188
|
+
error: errorDiagnostics(error)
|
|
189
|
+
});
|
|
88
190
|
return { available: false, error: errorMessage(error) };
|
|
89
191
|
}
|
|
90
192
|
},
|
|
91
193
|
async continueConversation(conversationId, input) {
|
|
92
194
|
const threadId = toCodexThreadId(conversationId);
|
|
93
|
-
|
|
195
|
+
const thread = await client.readThread(threadId, true);
|
|
196
|
+
const delivery = input.delivery ?? "queue";
|
|
94
197
|
if (isCodexThreadBusy(thread)) {
|
|
95
|
-
|
|
198
|
+
if (delivery === "steer") {
|
|
199
|
+
await client.injectItems(threadId, steerItems(input.prompt, input.attachments));
|
|
200
|
+
return {
|
|
201
|
+
conversationId: externalCodexConversationId(threadId),
|
|
202
|
+
status: "running"
|
|
203
|
+
};
|
|
204
|
+
}
|
|
205
|
+
enqueueTurn(threadId, input);
|
|
206
|
+
return {
|
|
207
|
+
conversationId: externalCodexConversationId(threadId),
|
|
208
|
+
status: "queued"
|
|
209
|
+
};
|
|
96
210
|
}
|
|
97
|
-
if (
|
|
98
|
-
|
|
211
|
+
if ((queuedTurnsByThread.get(threadId)?.length ?? 0) > 0) {
|
|
212
|
+
enqueueTurn(threadId, input, { front: delivery === "steer" });
|
|
213
|
+
return {
|
|
214
|
+
conversationId: externalCodexConversationId(threadId),
|
|
215
|
+
status: "queued"
|
|
216
|
+
};
|
|
99
217
|
}
|
|
100
|
-
await
|
|
101
|
-
...PHONE_FULL_ACCESS_TURN_OPTIONS,
|
|
102
|
-
cwd: thread.cwd,
|
|
103
|
-
...turnModelSettings(input.modelSettings)
|
|
104
|
-
});
|
|
218
|
+
await startConversationTurn(threadId, thread, input);
|
|
105
219
|
return {
|
|
106
220
|
conversationId: externalCodexConversationId(threadId),
|
|
107
221
|
status: "running"
|
|
108
222
|
};
|
|
109
223
|
},
|
|
110
224
|
async listCompletionStates() {
|
|
111
|
-
const threads = await readThreadsWithTurns(await
|
|
225
|
+
const threads = await readThreadsWithTurns(await loadAllThreads());
|
|
226
|
+
logDiagnostics(diagnostics, "info", "completion.states.result", {
|
|
227
|
+
stateCount: threads.length
|
|
228
|
+
});
|
|
112
229
|
return threads
|
|
113
230
|
.sort((left, right) => right.updatedAt - left.updatedAt)
|
|
114
231
|
.map((thread) => codexThreadToCompletionState(thread, workspaceId));
|
|
115
232
|
},
|
|
116
233
|
async listMessages(conversationId, messageOptions = {}) {
|
|
117
234
|
const threadId = toCodexThreadId(conversationId);
|
|
118
|
-
const messages = flattenThreadMessages(await client.readThread(threadId, true), externalCodexConversationId(threadId));
|
|
235
|
+
const messages = flattenThreadMessages(await client.readThread(threadId, true), externalCodexConversationId(threadId), diagnostics);
|
|
119
236
|
const filtered = messageOptions.includeRuntime
|
|
120
237
|
? messages
|
|
121
238
|
: messages.filter((message) => message.role !== "runtime");
|
|
239
|
+
const returned = typeof messageOptions.afterSequence === "number"
|
|
240
|
+
? messageOptions.afterSequence >
|
|
241
|
+
filtered.reduce((max, message) => Math.max(max, message.sequence), 0)
|
|
242
|
+
? filtered
|
|
243
|
+
: filtered.filter((message) => message.sequence > messageOptions.afterSequence)
|
|
244
|
+
: filtered;
|
|
245
|
+
logDiagnostics(diagnostics, "info", "messages.list.bridge_result", {
|
|
246
|
+
...messageCounts(messages),
|
|
247
|
+
afterSequence: messageOptions.afterSequence,
|
|
248
|
+
conversationId: externalCodexConversationId(threadId),
|
|
249
|
+
includeRuntime: messageOptions.includeRuntime === true,
|
|
250
|
+
returned: returned.length,
|
|
251
|
+
total: messages.length
|
|
252
|
+
});
|
|
122
253
|
if (typeof messageOptions.afterSequence !== "number") {
|
|
123
|
-
return
|
|
254
|
+
return returned;
|
|
124
255
|
}
|
|
125
|
-
|
|
126
|
-
return messageOptions.afterSequence > maxSequence
|
|
127
|
-
? filtered
|
|
128
|
-
: filtered.filter((message) => message.sequence > messageOptions.afterSequence);
|
|
256
|
+
return returned;
|
|
129
257
|
},
|
|
130
258
|
listModelOptions() {
|
|
131
259
|
return client.listModels();
|
|
132
260
|
},
|
|
133
261
|
async listProjectConversations(projectId) {
|
|
134
262
|
const cwd = await resolveProjectCwd(projectId);
|
|
135
|
-
const threads = await
|
|
263
|
+
const threads = await hydrateConversationStatusThreads(await listAllThreads({ cwd }));
|
|
136
264
|
return threads
|
|
137
265
|
.filter((thread) => externalCodexProjectId(thread.cwd) === projectId)
|
|
138
266
|
.sort((left, right) => right.updatedAt - left.updatedAt)
|
|
@@ -158,12 +286,25 @@ export function createLocalCodexBridge(options = {}) {
|
|
|
158
286
|
}
|
|
159
287
|
};
|
|
160
288
|
async function readThreadsWithTurns(threads) {
|
|
161
|
-
return Promise.all(threads.map(
|
|
289
|
+
return Promise.all(threads.map(async (thread) => {
|
|
290
|
+
const readThread = await client.readThread(thread.id, true).catch(() => thread);
|
|
291
|
+
return mergeCodexThreadSnapshots(readThread, thread);
|
|
292
|
+
}));
|
|
293
|
+
}
|
|
294
|
+
async function hydrateConversationStatusThreads(threads) {
|
|
295
|
+
return Promise.all(threads.map(async (thread) => {
|
|
296
|
+
if (!needsConversationStatusHydration(thread)) {
|
|
297
|
+
return thread;
|
|
298
|
+
}
|
|
299
|
+
const readThread = await client.readThread(thread.id, true).catch(() => thread);
|
|
300
|
+
return mergeCodexThreadSnapshots(readThread, thread);
|
|
301
|
+
}));
|
|
162
302
|
}
|
|
163
303
|
}
|
|
164
304
|
function createCodexAppClient(options) {
|
|
165
305
|
const serverUrl = options.serverUrl ?? process.env.CODEX_APP_SERVER_URL ?? DEFAULT_SERVER_URL;
|
|
166
306
|
const codexBinaryPath = options.codexBinaryPath ?? process.env.CODEX_APP_BINARY ?? DEFAULT_CODEX_BINARY;
|
|
307
|
+
const diagnostics = options.diagnostics;
|
|
167
308
|
return {
|
|
168
309
|
listModels: async () => {
|
|
169
310
|
const models = [];
|
|
@@ -173,33 +314,69 @@ function createCodexAppClient(options) {
|
|
|
173
314
|
cursor,
|
|
174
315
|
includeHidden: false,
|
|
175
316
|
limit: 200
|
|
176
|
-
});
|
|
317
|
+
}, diagnostics);
|
|
177
318
|
models.push(...response.data.flatMap(normalizeCodexModel));
|
|
178
319
|
cursor = response.nextCursor;
|
|
179
320
|
} while (cursor);
|
|
180
321
|
return models;
|
|
181
322
|
},
|
|
323
|
+
injectItems: (threadId, items) => callCodexApp(serverUrl, codexBinaryPath, "thread/inject_items", {
|
|
324
|
+
items,
|
|
325
|
+
threadId
|
|
326
|
+
}),
|
|
327
|
+
async listLoadedThreads() {
|
|
328
|
+
const threadIds = [];
|
|
329
|
+
let cursor = null;
|
|
330
|
+
do {
|
|
331
|
+
const response = await callCodexApp(serverUrl, codexBinaryPath, "thread/loaded/list", {
|
|
332
|
+
cursor,
|
|
333
|
+
limit: 200
|
|
334
|
+
}, diagnostics);
|
|
335
|
+
threadIds.push(...response.data);
|
|
336
|
+
cursor = response.nextCursor;
|
|
337
|
+
} while (cursor);
|
|
338
|
+
return threadIds;
|
|
339
|
+
},
|
|
182
340
|
listThreads: (params = {}) => callCodexApp(serverUrl, codexBinaryPath, "thread/list", {
|
|
183
341
|
archived: false,
|
|
184
342
|
limit: 200,
|
|
185
343
|
sortDirection: "desc",
|
|
186
344
|
useStateDbOnly: true,
|
|
187
345
|
...params
|
|
188
|
-
}),
|
|
346
|
+
}, diagnostics),
|
|
189
347
|
async readThread(threadId, includeTurns = true) {
|
|
190
|
-
|
|
191
|
-
|
|
348
|
+
logDiagnostics(diagnostics, "info", "codex.thread_read.call", {
|
|
349
|
+
includeTurns,
|
|
350
|
+
threadId
|
|
351
|
+
});
|
|
352
|
+
try {
|
|
353
|
+
const response = await callCodexApp(serverUrl, codexBinaryPath, "thread/read", { includeTurns, threadId }, diagnostics);
|
|
354
|
+
logDiagnostics(diagnostics, "info", "codex.thread_read.result", {
|
|
355
|
+
includeTurns,
|
|
356
|
+
threadId,
|
|
357
|
+
turnCount: response.thread.turns.length
|
|
358
|
+
});
|
|
359
|
+
return response.thread;
|
|
360
|
+
}
|
|
361
|
+
catch (error) {
|
|
362
|
+
logDiagnostics(diagnostics, "error", "codex.thread_read.failure", {
|
|
363
|
+
error: errorDiagnostics(error),
|
|
364
|
+
includeTurns,
|
|
365
|
+
threadId
|
|
366
|
+
});
|
|
367
|
+
throw error;
|
|
368
|
+
}
|
|
192
369
|
},
|
|
193
|
-
resumeThread: (params) => callCodexApp(serverUrl, codexBinaryPath, "thread/resume", params),
|
|
370
|
+
resumeThread: (params) => callCodexApp(serverUrl, codexBinaryPath, "thread/resume", params, diagnostics),
|
|
194
371
|
startThread: (params) => callCodexApp(serverUrl, codexBinaryPath, "thread/start", {
|
|
195
372
|
experimentalRawEvents: false,
|
|
196
373
|
persistExtendedHistory: true,
|
|
197
374
|
...params
|
|
198
|
-
}),
|
|
199
|
-
startTurn: (threadId, input, turnOptions) => startTurnWithKeepAlive(serverUrl, codexBinaryPath, threadId, input, turnOptions)
|
|
375
|
+
}, diagnostics),
|
|
376
|
+
startTurn: (threadId, input, turnOptions) => startTurnWithKeepAlive(serverUrl, codexBinaryPath, threadId, input, turnOptions, diagnostics)
|
|
200
377
|
};
|
|
201
378
|
}
|
|
202
|
-
async function callCodexApp(serverUrl, codexBinaryPath, method, params) {
|
|
379
|
+
async function callCodexApp(serverUrl, codexBinaryPath, method, params, diagnostics) {
|
|
203
380
|
try {
|
|
204
381
|
return await callCodexAppOnce(serverUrl, method, params);
|
|
205
382
|
}
|
|
@@ -207,7 +384,12 @@ async function callCodexApp(serverUrl, codexBinaryPath, method, params) {
|
|
|
207
384
|
if (!isConnectionFailure(error) || !canStartLocalServer(serverUrl)) {
|
|
208
385
|
throw error;
|
|
209
386
|
}
|
|
210
|
-
|
|
387
|
+
logDiagnostics(diagnostics, "warn", "codex.app_server.connect_failure", {
|
|
388
|
+
error: errorDiagnostics(error),
|
|
389
|
+
method,
|
|
390
|
+
serverUrl
|
|
391
|
+
});
|
|
392
|
+
await ensureLocalAppServer(serverUrl, codexBinaryPath, diagnostics);
|
|
211
393
|
return callCodexAppOnce(serverUrl, method, params);
|
|
212
394
|
}
|
|
213
395
|
}
|
|
@@ -221,16 +403,53 @@ async function callCodexAppOnce(serverUrl, method, params) {
|
|
|
221
403
|
connection.close();
|
|
222
404
|
}
|
|
223
405
|
}
|
|
224
|
-
async function startTurnWithKeepAlive(serverUrl, codexBinaryPath, threadId, input, options) {
|
|
406
|
+
async function startTurnWithKeepAlive(serverUrl, codexBinaryPath, threadId, input, options, diagnostics) {
|
|
407
|
+
logDiagnostics(diagnostics, "info", "codex.turn_start.call", {
|
|
408
|
+
...turnInputDiagnostics(input),
|
|
409
|
+
cwd: options.cwd,
|
|
410
|
+
effort: options.effort,
|
|
411
|
+
model: options.model,
|
|
412
|
+
threadId
|
|
413
|
+
});
|
|
225
414
|
try {
|
|
226
|
-
|
|
415
|
+
const response = await startTurnWithKeepAliveOnce(serverUrl, threadId, input, options);
|
|
416
|
+
logDiagnostics(diagnostics, "info", "codex.turn_start.result", {
|
|
417
|
+
status: turnStatusType(response.turn),
|
|
418
|
+
threadId,
|
|
419
|
+
turnId: response.turn.id
|
|
420
|
+
});
|
|
421
|
+
return response;
|
|
227
422
|
}
|
|
228
423
|
catch (error) {
|
|
229
424
|
if (!isConnectionFailure(error) || !canStartLocalServer(serverUrl)) {
|
|
425
|
+
logDiagnostics(diagnostics, "error", "codex.turn_start.failure", {
|
|
426
|
+
error: errorDiagnostics(error),
|
|
427
|
+
threadId
|
|
428
|
+
});
|
|
230
429
|
throw error;
|
|
231
430
|
}
|
|
232
|
-
|
|
233
|
-
|
|
431
|
+
logDiagnostics(diagnostics, "warn", "codex.app_server.connect_failure", {
|
|
432
|
+
error: errorDiagnostics(error),
|
|
433
|
+
method: "turn/start",
|
|
434
|
+
serverUrl
|
|
435
|
+
});
|
|
436
|
+
await ensureLocalAppServer(serverUrl, codexBinaryPath, diagnostics);
|
|
437
|
+
try {
|
|
438
|
+
const response = await startTurnWithKeepAliveOnce(serverUrl, threadId, input, options);
|
|
439
|
+
logDiagnostics(diagnostics, "info", "codex.turn_start.result", {
|
|
440
|
+
status: turnStatusType(response.turn),
|
|
441
|
+
threadId,
|
|
442
|
+
turnId: response.turn.id
|
|
443
|
+
});
|
|
444
|
+
return response;
|
|
445
|
+
}
|
|
446
|
+
catch (retryError) {
|
|
447
|
+
logDiagnostics(diagnostics, "error", "codex.turn_start.failure", {
|
|
448
|
+
error: errorDiagnostics(retryError),
|
|
449
|
+
threadId
|
|
450
|
+
});
|
|
451
|
+
throw retryError;
|
|
452
|
+
}
|
|
234
453
|
}
|
|
235
454
|
}
|
|
236
455
|
async function startTurnWithKeepAliveOnce(serverUrl, threadId, input, options) {
|
|
@@ -410,8 +629,12 @@ function keepTurnConnectionAlive(connection, threadId, turnId) {
|
|
|
410
629
|
activeTurnKeepAlives.add(keepAlive);
|
|
411
630
|
void keepAlive;
|
|
412
631
|
}
|
|
413
|
-
async function ensureLocalAppServer(serverUrl, codexBinaryPath) {
|
|
632
|
+
async function ensureLocalAppServer(serverUrl, codexBinaryPath, diagnostics) {
|
|
414
633
|
if (!spawnedAppServer || spawnedAppServer.exitCode !== null || spawnedAppServer.killed) {
|
|
634
|
+
logDiagnostics(diagnostics, "info", "codex.app_server.bootstrap_start", {
|
|
635
|
+
codexBinaryPath,
|
|
636
|
+
serverUrl
|
|
637
|
+
});
|
|
415
638
|
spawnedAppServer = spawn(codexBinaryPath, ["app-server", "--listen", serverUrl, "--analytics-default-enabled"], {
|
|
416
639
|
detached: true,
|
|
417
640
|
stdio: "ignore"
|
|
@@ -421,10 +644,18 @@ async function ensureLocalAppServer(serverUrl, codexBinaryPath) {
|
|
|
421
644
|
const startedAt = Date.now();
|
|
422
645
|
while (Date.now() - startedAt < START_TIMEOUT_MS) {
|
|
423
646
|
if (await canOpenWebSocket(serverUrl)) {
|
|
647
|
+
logDiagnostics(diagnostics, "info", "codex.app_server.bootstrap_connected", {
|
|
648
|
+
elapsedMs: Date.now() - startedAt,
|
|
649
|
+
serverUrl
|
|
650
|
+
});
|
|
424
651
|
return;
|
|
425
652
|
}
|
|
426
653
|
await delay(250);
|
|
427
654
|
}
|
|
655
|
+
logDiagnostics(diagnostics, "error", "codex.app_server.bootstrap_failure", {
|
|
656
|
+
elapsedMs: Date.now() - startedAt,
|
|
657
|
+
serverUrl
|
|
658
|
+
});
|
|
428
659
|
throw new Error("Unable to start Codex app-server. Open Codex.app or set CODEX_APP_SERVER_URL.");
|
|
429
660
|
}
|
|
430
661
|
function canOpenWebSocket(serverUrl) {
|
|
@@ -469,6 +700,111 @@ function toCodexThreadId(conversationId) {
|
|
|
469
700
|
? decodeURIComponent(conversationId.slice(CODEX_THREAD_PREFIX.length))
|
|
470
701
|
: conversationId;
|
|
471
702
|
}
|
|
703
|
+
function threadListCacheKey(params) {
|
|
704
|
+
return JSON.stringify({
|
|
705
|
+
archived: params.archived ?? null,
|
|
706
|
+
cursor: params.cursor ?? null,
|
|
707
|
+
cwd: params.cwd ?? null,
|
|
708
|
+
limit: params.limit ?? null,
|
|
709
|
+
sortDirection: params.sortDirection ?? null,
|
|
710
|
+
useStateDbOnly: params.useStateDbOnly ?? null
|
|
711
|
+
});
|
|
712
|
+
}
|
|
713
|
+
function mergeCodexThreadLists(...threadLists) {
|
|
714
|
+
const byId = new Map();
|
|
715
|
+
for (const thread of threadLists.flat()) {
|
|
716
|
+
byId.set(thread.id, mergeCodexThreadSnapshots(byId.get(thread.id), thread));
|
|
717
|
+
}
|
|
718
|
+
return Array.from(byId.values()).sort((left, right) => safeSeconds(right.updatedAt, right.createdAt) - safeSeconds(left.updatedAt, left.createdAt));
|
|
719
|
+
}
|
|
720
|
+
function mergeCodexThreadSnapshots(baseThread, nextThread) {
|
|
721
|
+
if (!baseThread) {
|
|
722
|
+
return nextThread;
|
|
723
|
+
}
|
|
724
|
+
const abitatLoadedFromAppServer = baseThread.abitatLoadedFromAppServer || nextThread.abitatLoadedFromAppServer || undefined;
|
|
725
|
+
const abitatThreadListActivityAt = maxOptionalSeconds(threadListActivitySeconds(baseThread), threadListActivitySeconds(nextThread));
|
|
726
|
+
return {
|
|
727
|
+
...baseThread,
|
|
728
|
+
...nextThread,
|
|
729
|
+
abitatLoadedFromAppServer,
|
|
730
|
+
abitatThreadListActivityAt: abitatThreadListActivityAt ?? undefined,
|
|
731
|
+
createdAt: safeSeconds(baseThread.createdAt, nextThread.createdAt),
|
|
732
|
+
status: hasUnpersistedThreadListActivity(baseThread, nextThread, {
|
|
733
|
+
threadListActivityAt: abitatThreadListActivityAt
|
|
734
|
+
})
|
|
735
|
+
? { activeFlags: [], type: "active" }
|
|
736
|
+
: preferredThreadStatus(baseThread.status, nextThread.status),
|
|
737
|
+
turns: nextThread.turns.length > 0 ? nextThread.turns : baseThread.turns,
|
|
738
|
+
updatedAt: Math.max(safeSeconds(baseThread.updatedAt, baseThread.createdAt), safeSeconds(nextThread.updatedAt, nextThread.createdAt))
|
|
739
|
+
};
|
|
740
|
+
}
|
|
741
|
+
function preferredThreadStatus(baseStatus, nextStatus) {
|
|
742
|
+
if (threadStatusType(baseStatus) === "active" && threadStatusType(nextStatus) === "active") {
|
|
743
|
+
return { activeFlags: uniqueActiveFlags(baseStatus, nextStatus), type: "active" };
|
|
744
|
+
}
|
|
745
|
+
return threadStatusRank(nextStatus) > threadStatusRank(baseStatus) ? nextStatus : baseStatus;
|
|
746
|
+
}
|
|
747
|
+
function threadStatusRank(status) {
|
|
748
|
+
switch (threadStatusType(status)) {
|
|
749
|
+
case "active":
|
|
750
|
+
return 4;
|
|
751
|
+
case "systemError":
|
|
752
|
+
return 3;
|
|
753
|
+
case "idle":
|
|
754
|
+
return 2;
|
|
755
|
+
case "notLoaded":
|
|
756
|
+
return 0;
|
|
757
|
+
default:
|
|
758
|
+
return 1;
|
|
759
|
+
}
|
|
760
|
+
}
|
|
761
|
+
function threadMatchesListParams(thread, params) {
|
|
762
|
+
if (!params.cwd) {
|
|
763
|
+
return true;
|
|
764
|
+
}
|
|
765
|
+
const cwds = Array.isArray(params.cwd) ? params.cwd : [params.cwd];
|
|
766
|
+
return cwds.includes(thread.cwd);
|
|
767
|
+
}
|
|
768
|
+
function uniqueActiveFlags(...statuses) {
|
|
769
|
+
return [...new Set(statuses.flatMap((status) => activeFlags(status)))];
|
|
770
|
+
}
|
|
771
|
+
function activeFlags(status) {
|
|
772
|
+
return status && typeof status === "object" && Array.isArray(status.activeFlags)
|
|
773
|
+
? status.activeFlags.filter((flag) => typeof flag === "string")
|
|
774
|
+
: [];
|
|
775
|
+
}
|
|
776
|
+
function hasUnpersistedThreadListActivity(persistedThread, listedThread, input) {
|
|
777
|
+
if (input.threadListActivityAt === null) {
|
|
778
|
+
return false;
|
|
779
|
+
}
|
|
780
|
+
const latestTurn = threadWithTurns(persistedThread, listedThread)?.turns.at(-1) ?? null;
|
|
781
|
+
if (!latestTurn) {
|
|
782
|
+
return false;
|
|
783
|
+
}
|
|
784
|
+
return (isTurnInterrupted(latestTurn) &&
|
|
785
|
+
latestTurn.completedAt === null &&
|
|
786
|
+
input.threadListActivityAt >= safeSeconds(latestTurn.startedAt));
|
|
787
|
+
}
|
|
788
|
+
function isThreadListActivitySummaryStatus(status) {
|
|
789
|
+
const statusType = threadStatusType(status);
|
|
790
|
+
return statusType === "idle" || statusType === "notLoaded";
|
|
791
|
+
}
|
|
792
|
+
function needsConversationStatusHydration(thread) {
|
|
793
|
+
return thread.turns.length === 0 && isThreadListActivitySummaryStatus(thread.status);
|
|
794
|
+
}
|
|
795
|
+
function threadListActivitySeconds(thread) {
|
|
796
|
+
const currentActivity = thread.turns.length === 0 && isThreadListActivitySummaryStatus(thread.status)
|
|
797
|
+
? safeSeconds(thread.updatedAt, thread.createdAt)
|
|
798
|
+
: null;
|
|
799
|
+
return maxOptionalSeconds(thread.abitatThreadListActivityAt ?? null, currentActivity);
|
|
800
|
+
}
|
|
801
|
+
function threadWithTurns(...threads) {
|
|
802
|
+
return threads.find((thread) => thread.turns.length > 0) ?? null;
|
|
803
|
+
}
|
|
804
|
+
function maxOptionalSeconds(...values) {
|
|
805
|
+
const seconds = values.filter((value) => typeof value === "number" && Number.isFinite(value));
|
|
806
|
+
return seconds.length > 0 ? Math.max(...seconds) : null;
|
|
807
|
+
}
|
|
472
808
|
function codexThreadToConversation(thread, workspaceId, userId) {
|
|
473
809
|
return {
|
|
474
810
|
agentId: "codex_app",
|
|
@@ -491,15 +827,15 @@ function codexThreadToConversation(thread, workspaceId, userId) {
|
|
|
491
827
|
function codexThreadToCompletionState(thread, workspaceId) {
|
|
492
828
|
const latestTurn = thread.turns.at(-1) ?? null;
|
|
493
829
|
const status = codexThreadToConversationStatus(thread);
|
|
494
|
-
const
|
|
830
|
+
const activelyWorking = isActiveMobileConversationStatus(status);
|
|
831
|
+
const failed = !activelyWorking &&
|
|
832
|
+
(Boolean(latestTurn?.error) || Boolean(latestTurn && isTurnInterrupted(latestTurn)));
|
|
495
833
|
const latestTurnCompletedAt = latestTurnCompletedAtIso(thread, latestTurn);
|
|
496
834
|
const latestTurnFinished = Boolean(latestTurn && isTurnTerminal(latestTurn));
|
|
497
835
|
return {
|
|
498
836
|
conversationId: externalCodexConversationId(thread.id),
|
|
499
837
|
failed,
|
|
500
|
-
isComplete: Boolean(latestTurn?.id &&
|
|
501
|
-
(latestTurnCompletedAt || latestTurnFinished || failed) &&
|
|
502
|
-
status !== "running"),
|
|
838
|
+
isComplete: Boolean(latestTurn?.id && (latestTurnCompletedAt || latestTurnFinished || failed) && !activelyWorking),
|
|
503
839
|
latestTurnCompletedAt,
|
|
504
840
|
latestTurnId: latestTurn?.id ?? null,
|
|
505
841
|
projectId: externalCodexProjectId(thread.cwd),
|
|
@@ -511,14 +847,28 @@ function codexThreadToCompletionState(thread, workspaceId) {
|
|
|
511
847
|
workspaceId
|
|
512
848
|
};
|
|
513
849
|
}
|
|
514
|
-
function flattenThreadMessages(thread, conversationId = externalCodexConversationId(thread.id)) {
|
|
850
|
+
export function flattenThreadMessages(thread, conversationId = externalCodexConversationId(thread.id), diagnostics) {
|
|
515
851
|
const messages = [];
|
|
516
852
|
const itemOccurrences = new Map();
|
|
853
|
+
const itemTypeCounts = new Map();
|
|
854
|
+
const unknownItemTypes = new Set();
|
|
855
|
+
const assistantMessagesByTurn = new Map();
|
|
517
856
|
let sequence = 1;
|
|
518
857
|
for (const turn of thread.turns) {
|
|
519
858
|
for (const [itemIndex, item] of turn.items.entries()) {
|
|
859
|
+
itemTypeCounts.set(item.type, (itemTypeCounts.get(item.type) ?? 0) + 1);
|
|
520
860
|
const flattened = threadItemToMessageContent(item);
|
|
521
861
|
if (!flattened) {
|
|
862
|
+
if (!HIDDEN_CODEX_ITEM_TYPES.has(item.type) && !isKnownCodexItemType(item.type)) {
|
|
863
|
+
unknownItemTypes.add(item.type);
|
|
864
|
+
logDiagnostics(diagnostics, "warn", "codex.thread_item.unknown", {
|
|
865
|
+
codexItemId: item.id,
|
|
866
|
+
codexItemType: item.type,
|
|
867
|
+
conversationId,
|
|
868
|
+
threadId: thread.id,
|
|
869
|
+
turnId: turn.id
|
|
870
|
+
});
|
|
871
|
+
}
|
|
522
872
|
continue;
|
|
523
873
|
}
|
|
524
874
|
const codexItemId = item.id ?? `item_${itemIndex}`;
|
|
@@ -541,36 +891,55 @@ function flattenThreadMessages(thread, conversationId = externalCodexConversatio
|
|
|
541
891
|
sequence,
|
|
542
892
|
sourceDeviceId: null
|
|
543
893
|
});
|
|
894
|
+
if (flattened.role === "assistant") {
|
|
895
|
+
assistantMessagesByTurn.set(turn.id, (assistantMessagesByTurn.get(turn.id) ?? 0) + 1);
|
|
896
|
+
}
|
|
544
897
|
sequence += 1;
|
|
545
898
|
}
|
|
899
|
+
if (isTurnCompleted(turn) && (assistantMessagesByTurn.get(turn.id) ?? 0) === 0) {
|
|
900
|
+
logDiagnostics(diagnostics, "warn", "codex.turn.completed_without_visible_assistant", {
|
|
901
|
+
conversationId,
|
|
902
|
+
itemTypeCounts: Object.fromEntries(itemTypeCounts.entries()),
|
|
903
|
+
threadId: thread.id,
|
|
904
|
+
turnId: turn.id,
|
|
905
|
+
unknownItemTypes: [...unknownItemTypes]
|
|
906
|
+
});
|
|
907
|
+
}
|
|
546
908
|
}
|
|
909
|
+
logDiagnostics(diagnostics, "info", "codex.thread.messages_flattened", {
|
|
910
|
+
...messageCounts(messages),
|
|
911
|
+
conversationId,
|
|
912
|
+
itemTypeCounts: Object.fromEntries(itemTypeCounts.entries()),
|
|
913
|
+
threadId: thread.id,
|
|
914
|
+
total: messages.length,
|
|
915
|
+
unknownItemTypes: [...unknownItemTypes]
|
|
916
|
+
});
|
|
547
917
|
return messages;
|
|
548
918
|
}
|
|
549
919
|
function codexThreadToConversationStatus(thread) {
|
|
550
920
|
const latestTurn = thread.turns.at(-1) ?? null;
|
|
551
|
-
if (latestTurn?.error) {
|
|
552
|
-
return "failed";
|
|
553
|
-
}
|
|
554
|
-
if (latestTurn && isTurnInterrupted(latestTurn)) {
|
|
555
|
-
return "cancelled";
|
|
556
|
-
}
|
|
557
921
|
if (threadStatusType(thread.status) === "active") {
|
|
558
|
-
if (latestTurn && isTurnTerminal(latestTurn)) {
|
|
559
|
-
return "approved";
|
|
560
|
-
}
|
|
561
922
|
if (hasActiveFlag(thread.status, "waitingOnApproval")) {
|
|
562
923
|
return "awaiting_approval";
|
|
563
924
|
}
|
|
564
925
|
return "running";
|
|
565
926
|
}
|
|
927
|
+
if (latestTurn && isTurnInProgress(latestTurn)) {
|
|
928
|
+
return "running";
|
|
929
|
+
}
|
|
930
|
+
if (latestTurn?.error) {
|
|
931
|
+
return "failed";
|
|
932
|
+
}
|
|
933
|
+
if (latestTurn && isTurnInterrupted(latestTurn)) {
|
|
934
|
+
return "cancelled";
|
|
935
|
+
}
|
|
566
936
|
return "approved";
|
|
567
937
|
}
|
|
568
938
|
function isCodexThreadBusy(thread) {
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
return !(latestTurn && isTurnTerminal(latestTurn));
|
|
939
|
+
return threadStatusType(thread.status) === "active";
|
|
940
|
+
}
|
|
941
|
+
function isActiveMobileConversationStatus(status) {
|
|
942
|
+
return status === "running" || status === "awaiting_approval";
|
|
574
943
|
}
|
|
575
944
|
function latestTurnCompletedAtIso(thread, turn) {
|
|
576
945
|
if (!turn) {
|
|
@@ -614,6 +983,44 @@ function threadItemToMessageContent(item) {
|
|
|
614
983
|
}
|
|
615
984
|
return null;
|
|
616
985
|
}
|
|
986
|
+
function isKnownCodexItemType(type) {
|
|
987
|
+
return (type === "userMessage" ||
|
|
988
|
+
type === "agentMessage" ||
|
|
989
|
+
type === "plan" ||
|
|
990
|
+
type === "commandExecution" ||
|
|
991
|
+
type === "fileChange" ||
|
|
992
|
+
HIDDEN_CODEX_ITEM_TYPES.has(type));
|
|
993
|
+
}
|
|
994
|
+
function messageCounts(messages) {
|
|
995
|
+
const counts = {
|
|
996
|
+
assistant: 0,
|
|
997
|
+
runtime: 0,
|
|
998
|
+
user: 0,
|
|
999
|
+
visible: 0
|
|
1000
|
+
};
|
|
1001
|
+
for (const message of messages) {
|
|
1002
|
+
if (message.role === "assistant") {
|
|
1003
|
+
counts.assistant += 1;
|
|
1004
|
+
counts.visible += 1;
|
|
1005
|
+
}
|
|
1006
|
+
else if (message.role === "user") {
|
|
1007
|
+
counts.user += 1;
|
|
1008
|
+
counts.visible += 1;
|
|
1009
|
+
}
|
|
1010
|
+
else if (message.role === "runtime") {
|
|
1011
|
+
counts.runtime += 1;
|
|
1012
|
+
}
|
|
1013
|
+
}
|
|
1014
|
+
return counts;
|
|
1015
|
+
}
|
|
1016
|
+
function turnInputDiagnostics(input) {
|
|
1017
|
+
const text = input.flatMap((item) => (item.type === "text" ? [item.text] : [])).join("\n");
|
|
1018
|
+
const attachments = input.filter((item) => item.type !== "text");
|
|
1019
|
+
return {
|
|
1020
|
+
...promptDiagnostics(text),
|
|
1021
|
+
...attachmentDiagnostics(attachments)
|
|
1022
|
+
};
|
|
1023
|
+
}
|
|
617
1024
|
function userInput(prompt, attachments = []) {
|
|
618
1025
|
return [
|
|
619
1026
|
{ text: normalizedPrompt(prompt), text_elements: [], type: "text" },
|
|
@@ -642,6 +1049,16 @@ function userInputToText(input) {
|
|
|
642
1049
|
return `[Mention: ${input.name}]`;
|
|
643
1050
|
}
|
|
644
1051
|
}
|
|
1052
|
+
function steerItems(prompt, attachments = []) {
|
|
1053
|
+
const text = userInput(prompt, attachments).map(userInputToText).filter(Boolean).join("\n");
|
|
1054
|
+
return [
|
|
1055
|
+
{
|
|
1056
|
+
content: [{ text, type: "input_text" }],
|
|
1057
|
+
role: "user",
|
|
1058
|
+
type: "message"
|
|
1059
|
+
}
|
|
1060
|
+
];
|
|
1061
|
+
}
|
|
645
1062
|
function turnModelSettings(modelSettings) {
|
|
646
1063
|
return modelSettings ? { effort: modelSettings.effort, model: modelSettings.model } : {};
|
|
647
1064
|
}
|