@abitat_reece/host-daemon 0.1.10 → 0.1.13
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli/index.js +2 -2
- package/dist/cli/index.js.map +1 -1
- package/dist/local-control/codex-bridge.d.ts.map +1 -1
- package/dist/local-control/codex-bridge.js +419 -44
- package/dist/local-control/codex-bridge.js.map +1 -1
- package/dist/local-control/diagnostics-log.d.ts +12 -0
- package/dist/local-control/diagnostics-log.d.ts.map +1 -1
- package/dist/local-control/diagnostics-log.js +308 -10
- package/dist/local-control/diagnostics-log.js.map +1 -1
- package/dist/local-control/push-notifications.d.ts.map +1 -1
- package/dist/local-control/push-notifications.js +4 -0
- package/dist/local-control/push-notifications.js.map +1 -1
- package/dist/local-control/relay-client.d.ts +1 -0
- package/dist/local-control/relay-client.d.ts.map +1 -1
- package/dist/local-control/relay-client.js +77 -19
- package/dist/local-control/relay-client.js.map +1 -1
- package/dist/local-control/server.d.ts +29 -0
- package/dist/local-control/server.d.ts.map +1 -1
- package/dist/local-control/server.js +106 -2
- package/dist/local-control/server.js.map +1 -1
- package/package.json +1 -1
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { spawn } from "node:child_process";
|
|
2
2
|
import { createHash } from "node:crypto";
|
|
3
|
-
import {
|
|
3
|
+
import { readFile, stat } from "node:fs/promises";
|
|
4
|
+
import { basename, extname, isAbsolute, join, normalize } from "node:path";
|
|
4
5
|
import { setTimeout as delay } from "node:timers/promises";
|
|
5
6
|
import WebSocket from "ws";
|
|
6
7
|
import { attachmentDiagnostics, errorDiagnostics, logDiagnostics, promptDiagnostics } from "./diagnostics-log.js";
|
|
@@ -37,6 +38,7 @@ export function createLocalCodexBridge(options = {}) {
|
|
|
37
38
|
const queuedTurnsByThread = new Map();
|
|
38
39
|
const queueDrainTimers = new Map();
|
|
39
40
|
const threadListCache = new Map();
|
|
41
|
+
const messageHistoryCache = new Map();
|
|
40
42
|
async function listAllThreads(params = {}) {
|
|
41
43
|
const cacheKey = threadListCacheKey(params);
|
|
42
44
|
const cached = threadListCache.get(cacheKey);
|
|
@@ -115,11 +117,56 @@ export function createLocalCodexBridge(options = {}) {
|
|
|
115
117
|
}));
|
|
116
118
|
}
|
|
117
119
|
function enqueueTurn(threadId, input, options = { front: false }) {
|
|
120
|
+
invalidateMessageHistoryCache(threadId);
|
|
118
121
|
const existing = queuedTurnsByThread.get(threadId) ?? [];
|
|
122
|
+
if (input.clientMessageId &&
|
|
123
|
+
existing.some((turn) => turn.clientMessageId === input.clientMessageId)) {
|
|
124
|
+
scheduleQueueDrain(threadId);
|
|
125
|
+
return;
|
|
126
|
+
}
|
|
119
127
|
const next = options.front ? [input, ...existing] : [...existing, input];
|
|
120
128
|
queuedTurnsByThread.set(threadId, next);
|
|
121
129
|
scheduleQueueDrain(threadId);
|
|
122
130
|
}
|
|
131
|
+
function deleteQueuedTurn(threadId, clientMessageId) {
|
|
132
|
+
const existing = queuedTurnsByThread.get(threadId);
|
|
133
|
+
if (!existing?.length) {
|
|
134
|
+
return false;
|
|
135
|
+
}
|
|
136
|
+
const next = existing.filter((turn) => turn.clientMessageId !== clientMessageId);
|
|
137
|
+
if (next.length === existing.length) {
|
|
138
|
+
return false;
|
|
139
|
+
}
|
|
140
|
+
invalidateMessageHistoryCache(threadId);
|
|
141
|
+
if (next.length > 0) {
|
|
142
|
+
queuedTurnsByThread.set(threadId, next);
|
|
143
|
+
}
|
|
144
|
+
else {
|
|
145
|
+
queuedTurnsByThread.delete(threadId);
|
|
146
|
+
const timer = queueDrainTimers.get(threadId);
|
|
147
|
+
if (timer) {
|
|
148
|
+
clearTimeout(timer);
|
|
149
|
+
queueDrainTimers.delete(threadId);
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
return true;
|
|
153
|
+
}
|
|
154
|
+
function updateQueuedTurn(threadId, clientMessageId, prompt) {
|
|
155
|
+
const existing = queuedTurnsByThread.get(threadId);
|
|
156
|
+
if (!existing?.length) {
|
|
157
|
+
return false;
|
|
158
|
+
}
|
|
159
|
+
const index = existing.findIndex((turn) => turn.clientMessageId === clientMessageId);
|
|
160
|
+
if (index < 0) {
|
|
161
|
+
return false;
|
|
162
|
+
}
|
|
163
|
+
invalidateMessageHistoryCache(threadId);
|
|
164
|
+
queuedTurnsByThread.set(threadId, existing.map((turn, turnIndex) => (turnIndex === index ? { ...turn, prompt } : turn)));
|
|
165
|
+
return true;
|
|
166
|
+
}
|
|
167
|
+
function invalidateMessageHistoryCache(threadId) {
|
|
168
|
+
messageHistoryCache.delete(threadId);
|
|
169
|
+
}
|
|
123
170
|
function scheduleQueueDrain(threadId, delayMs = QUEUED_TURN_POLL_INTERVAL_MS) {
|
|
124
171
|
if (queueDrainTimers.has(threadId)) {
|
|
125
172
|
return;
|
|
@@ -157,21 +204,31 @@ export function createLocalCodexBridge(options = {}) {
|
|
|
157
204
|
if (queue.length === 0) {
|
|
158
205
|
queuedTurnsByThread.delete(threadId);
|
|
159
206
|
}
|
|
160
|
-
|
|
207
|
+
try {
|
|
208
|
+
await startConversationTurn(threadId, thread, nextInput);
|
|
209
|
+
}
|
|
210
|
+
catch (error) {
|
|
211
|
+
logDiagnostics(diagnostics, "error", "conversation.queue.start_failure", {
|
|
212
|
+
error: errorDiagnostics(error),
|
|
213
|
+
threadId
|
|
214
|
+
});
|
|
215
|
+
}
|
|
161
216
|
if ((queuedTurnsByThread.get(threadId)?.length ?? 0) > 0) {
|
|
162
217
|
scheduleQueueDrain(threadId);
|
|
163
218
|
}
|
|
164
219
|
}
|
|
165
220
|
async function startConversationTurn(threadId, thread, input) {
|
|
221
|
+
invalidateMessageHistoryCache(threadId);
|
|
166
222
|
let activeThread = thread;
|
|
167
223
|
if (threadStatusType(activeThread.status) !== "active") {
|
|
168
224
|
activeThread = (await client.resumeThread({ excludeTurns: false, threadId })).thread;
|
|
169
225
|
}
|
|
170
|
-
await client.startTurn(threadId, userInput(input.prompt, input.attachments), {
|
|
226
|
+
const response = await client.startTurn(threadId, userInput(input.prompt, input.attachments), {
|
|
171
227
|
...PHONE_FULL_ACCESS_TURN_OPTIONS,
|
|
172
228
|
cwd: activeThread.cwd,
|
|
173
229
|
...turnModelSettings(input.modelSettings)
|
|
174
230
|
});
|
|
231
|
+
return startedTurnStatusOrThrow(threadId, response.turn, diagnostics);
|
|
175
232
|
}
|
|
176
233
|
return {
|
|
177
234
|
async bootstrap() {
|
|
@@ -196,7 +253,17 @@ export function createLocalCodexBridge(options = {}) {
|
|
|
196
253
|
const delivery = input.delivery ?? "queue";
|
|
197
254
|
if (isCodexThreadBusy(thread)) {
|
|
198
255
|
if (delivery === "steer") {
|
|
199
|
-
|
|
256
|
+
const activeTurn = latestActiveTurn(thread);
|
|
257
|
+
if (!activeTurn) {
|
|
258
|
+
throw Object.assign(new Error("No active Codex turn is available to steer"), {
|
|
259
|
+
statusCode: 409
|
|
260
|
+
});
|
|
261
|
+
}
|
|
262
|
+
invalidateMessageHistoryCache(threadId);
|
|
263
|
+
await client.steerTurn(threadId, userInput(input.prompt, input.attachments), activeTurn.id);
|
|
264
|
+
if (input.clientMessageId) {
|
|
265
|
+
deleteQueuedTurn(threadId, input.clientMessageId);
|
|
266
|
+
}
|
|
200
267
|
return {
|
|
201
268
|
conversationId: externalCodexConversationId(threadId),
|
|
202
269
|
status: "running"
|
|
@@ -215,46 +282,73 @@ export function createLocalCodexBridge(options = {}) {
|
|
|
215
282
|
status: "queued"
|
|
216
283
|
};
|
|
217
284
|
}
|
|
218
|
-
await startConversationTurn(threadId, thread, input);
|
|
285
|
+
const startedStatus = await startConversationTurn(threadId, thread, input);
|
|
286
|
+
return {
|
|
287
|
+
conversationId: externalCodexConversationId(threadId),
|
|
288
|
+
status: startedStatus
|
|
289
|
+
};
|
|
290
|
+
},
|
|
291
|
+
async deleteQueuedTurn(conversationId, input) {
|
|
292
|
+
const threadId = toCodexThreadId(conversationId);
|
|
293
|
+
const removed = deleteQueuedTurn(threadId, input.clientMessageId);
|
|
219
294
|
return {
|
|
220
295
|
conversationId: externalCodexConversationId(threadId),
|
|
221
|
-
|
|
296
|
+
removed,
|
|
297
|
+
status: (queuedTurnsByThread.get(threadId)?.length ?? 0) > 0 ? "queued" : "running"
|
|
298
|
+
};
|
|
299
|
+
},
|
|
300
|
+
async updateQueuedTurn(conversationId, input) {
|
|
301
|
+
const threadId = toCodexThreadId(conversationId);
|
|
302
|
+
const updated = updateQueuedTurn(threadId, input.clientMessageId, input.prompt);
|
|
303
|
+
return {
|
|
304
|
+
conversationId: externalCodexConversationId(threadId),
|
|
305
|
+
status: (queuedTurnsByThread.get(threadId)?.length ?? 0) > 0 ? "queued" : "running",
|
|
306
|
+
updated
|
|
222
307
|
};
|
|
223
308
|
},
|
|
224
309
|
async listCompletionStates() {
|
|
225
310
|
const threads = await readThreadsWithTurns(await loadAllThreads());
|
|
311
|
+
const states = threads.map((thread) => codexThreadToCompletionState(thread, workspaceId));
|
|
226
312
|
logDiagnostics(diagnostics, "info", "completion.states.result", {
|
|
227
|
-
|
|
313
|
+
activeCount: states.filter((state) => isActiveMobileConversationStatus(state.status))
|
|
314
|
+
.length,
|
|
315
|
+
stateCount: states.length
|
|
228
316
|
});
|
|
229
|
-
return
|
|
230
|
-
.sort((left, right) => right.updatedAt - left.updatedAt)
|
|
231
|
-
.map((thread) => codexThreadToCompletionState(thread, workspaceId));
|
|
317
|
+
return states.sort((left, right) => Date.parse(right.updatedAt) - Date.parse(left.updatedAt));
|
|
232
318
|
},
|
|
233
319
|
async listMessages(conversationId, messageOptions = {}) {
|
|
234
320
|
const threadId = toCodexThreadId(conversationId);
|
|
235
|
-
const messages =
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
const returned =
|
|
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;
|
|
321
|
+
const messages = await readCachedThreadMessages(threadId, {
|
|
322
|
+
afterSequence: messageOptions.afterSequence,
|
|
323
|
+
forceRefresh: messageOptions.forceRefresh === true
|
|
324
|
+
});
|
|
325
|
+
const returned = filterThreadMessages(messages, messageOptions);
|
|
245
326
|
logDiagnostics(diagnostics, "info", "messages.list.bridge_result", {
|
|
246
327
|
...messageCounts(messages),
|
|
247
328
|
afterSequence: messageOptions.afterSequence,
|
|
248
329
|
conversationId: externalCodexConversationId(threadId),
|
|
330
|
+
forceRefresh: messageOptions.forceRefresh === true,
|
|
249
331
|
includeRuntime: messageOptions.includeRuntime === true,
|
|
250
332
|
returned: returned.length,
|
|
251
333
|
total: messages.length
|
|
252
334
|
});
|
|
253
|
-
if (typeof messageOptions.afterSequence !== "number") {
|
|
254
|
-
return returned;
|
|
255
|
-
}
|
|
256
335
|
return returned;
|
|
257
336
|
},
|
|
337
|
+
async listGeneratedFiles(conversationId) {
|
|
338
|
+
return generatedFilesForThread(await client.readThread(toCodexThreadId(conversationId), true));
|
|
339
|
+
},
|
|
340
|
+
async downloadGeneratedFile(conversationId, fileId) {
|
|
341
|
+
const files = await generatedFilesForThread(await client.readThread(toCodexThreadId(conversationId), true));
|
|
342
|
+
const file = files.find((candidate) => candidate.id === fileId);
|
|
343
|
+
if (!file) {
|
|
344
|
+
throw Object.assign(new Error("Generated file not found"), { statusCode: 404 });
|
|
345
|
+
}
|
|
346
|
+
const data = await readFile(file.path);
|
|
347
|
+
return {
|
|
348
|
+
...file,
|
|
349
|
+
dataBase64: data.toString("base64")
|
|
350
|
+
};
|
|
351
|
+
},
|
|
258
352
|
listModelOptions() {
|
|
259
353
|
return client.listModels();
|
|
260
354
|
},
|
|
@@ -274,14 +368,16 @@ export function createLocalCodexBridge(options = {}) {
|
|
|
274
368
|
experimentalRawEvents: false,
|
|
275
369
|
persistExtendedHistory: true
|
|
276
370
|
});
|
|
277
|
-
|
|
371
|
+
invalidateMessageHistoryCache(thread.id);
|
|
372
|
+
const response = await client.startTurn(thread.id, userInput(input.prompt, input.attachments), {
|
|
278
373
|
...PHONE_FULL_ACCESS_TURN_OPTIONS,
|
|
279
374
|
cwd,
|
|
280
375
|
...turnModelSettings(input.modelSettings)
|
|
281
376
|
});
|
|
377
|
+
const startedStatus = startedTurnStatusOrThrow(thread.id, response.turn, diagnostics);
|
|
282
378
|
return {
|
|
283
379
|
conversationId: externalCodexConversationId(thread.id),
|
|
284
|
-
status:
|
|
380
|
+
status: startedStatus
|
|
285
381
|
};
|
|
286
382
|
}
|
|
287
383
|
};
|
|
@@ -300,6 +396,33 @@ export function createLocalCodexBridge(options = {}) {
|
|
|
300
396
|
return mergeCodexThreadSnapshots(readThread, thread);
|
|
301
397
|
}));
|
|
302
398
|
}
|
|
399
|
+
async function readCachedThreadMessages(threadId, options) {
|
|
400
|
+
const cached = messageHistoryCache.get(threadId);
|
|
401
|
+
if (!options.forceRefresh && typeof options.afterSequence === "number" && cached) {
|
|
402
|
+
const summary = await client.readThread(threadId, false).catch(() => null);
|
|
403
|
+
if (summary && canUseCachedMessageHistory(cached, summary)) {
|
|
404
|
+
return cached.messages;
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
const thread = await client.readThread(threadId, true);
|
|
408
|
+
const messages = flattenThreadMessages(thread, externalCodexConversationId(threadId), diagnostics);
|
|
409
|
+
if (isCodexThreadMessageHistoryStable(thread)) {
|
|
410
|
+
messageHistoryCache.set(threadId, {
|
|
411
|
+
messages,
|
|
412
|
+
statusType: threadStatusType(thread.status),
|
|
413
|
+
updatedAt: safeSeconds(thread.updatedAt, thread.createdAt)
|
|
414
|
+
});
|
|
415
|
+
}
|
|
416
|
+
else {
|
|
417
|
+
invalidateMessageHistoryCache(threadId);
|
|
418
|
+
}
|
|
419
|
+
return messages;
|
|
420
|
+
}
|
|
421
|
+
function canUseCachedMessageHistory(cached, summary) {
|
|
422
|
+
return (!isCodexThreadBusy(summary) &&
|
|
423
|
+
cached.statusType === threadStatusType(summary.status) &&
|
|
424
|
+
cached.updatedAt === safeSeconds(summary.updatedAt, summary.createdAt));
|
|
425
|
+
}
|
|
303
426
|
}
|
|
304
427
|
function createCodexAppClient(options) {
|
|
305
428
|
const serverUrl = options.serverUrl ?? process.env.CODEX_APP_SERVER_URL ?? DEFAULT_SERVER_URL;
|
|
@@ -320,10 +443,6 @@ function createCodexAppClient(options) {
|
|
|
320
443
|
} while (cursor);
|
|
321
444
|
return models;
|
|
322
445
|
},
|
|
323
|
-
injectItems: (threadId, items) => callCodexApp(serverUrl, codexBinaryPath, "thread/inject_items", {
|
|
324
|
-
items,
|
|
325
|
-
threadId
|
|
326
|
-
}),
|
|
327
446
|
async listLoadedThreads() {
|
|
328
447
|
const threadIds = [];
|
|
329
448
|
let cursor = null;
|
|
@@ -373,7 +492,8 @@ function createCodexAppClient(options) {
|
|
|
373
492
|
persistExtendedHistory: true,
|
|
374
493
|
...params
|
|
375
494
|
}, diagnostics),
|
|
376
|
-
startTurn: (threadId, input, turnOptions) => startTurnWithKeepAlive(serverUrl, codexBinaryPath, threadId, input, turnOptions, diagnostics)
|
|
495
|
+
startTurn: (threadId, input, turnOptions) => startTurnWithKeepAlive(serverUrl, codexBinaryPath, threadId, input, turnOptions, diagnostics),
|
|
496
|
+
steerTurn: (threadId, input, expectedTurnId) => steerTurnWithKeepAlive(serverUrl, codexBinaryPath, threadId, input, expectedTurnId, diagnostics)
|
|
377
497
|
};
|
|
378
498
|
}
|
|
379
499
|
async function callCodexApp(serverUrl, codexBinaryPath, method, params, diagnostics) {
|
|
@@ -478,6 +598,75 @@ async function startTurnWithKeepAliveOnce(serverUrl, threadId, input, options) {
|
|
|
478
598
|
}
|
|
479
599
|
}
|
|
480
600
|
}
|
|
601
|
+
async function steerTurnWithKeepAlive(serverUrl, codexBinaryPath, threadId, input, expectedTurnId, diagnostics) {
|
|
602
|
+
logDiagnostics(diagnostics, "info", "codex.turn_steer.call", {
|
|
603
|
+
...turnInputDiagnostics(input),
|
|
604
|
+
expectedTurnId,
|
|
605
|
+
threadId
|
|
606
|
+
});
|
|
607
|
+
try {
|
|
608
|
+
const response = await steerTurnWithKeepAliveOnce(serverUrl, threadId, input, expectedTurnId);
|
|
609
|
+
logDiagnostics(diagnostics, "info", "codex.turn_steer.result", {
|
|
610
|
+
expectedTurnId,
|
|
611
|
+
threadId,
|
|
612
|
+
turnId: response.turnId
|
|
613
|
+
});
|
|
614
|
+
return response;
|
|
615
|
+
}
|
|
616
|
+
catch (error) {
|
|
617
|
+
if (!isConnectionFailure(error) || !canStartLocalServer(serverUrl)) {
|
|
618
|
+
logDiagnostics(diagnostics, "error", "codex.turn_steer.failure", {
|
|
619
|
+
error: errorDiagnostics(error),
|
|
620
|
+
expectedTurnId,
|
|
621
|
+
threadId
|
|
622
|
+
});
|
|
623
|
+
throw error;
|
|
624
|
+
}
|
|
625
|
+
logDiagnostics(diagnostics, "warn", "codex.app_server.connect_failure", {
|
|
626
|
+
error: errorDiagnostics(error),
|
|
627
|
+
method: "turn/steer",
|
|
628
|
+
serverUrl
|
|
629
|
+
});
|
|
630
|
+
await ensureLocalAppServer(serverUrl, codexBinaryPath, diagnostics);
|
|
631
|
+
try {
|
|
632
|
+
const response = await steerTurnWithKeepAliveOnce(serverUrl, threadId, input, expectedTurnId);
|
|
633
|
+
logDiagnostics(diagnostics, "info", "codex.turn_steer.result", {
|
|
634
|
+
expectedTurnId,
|
|
635
|
+
threadId,
|
|
636
|
+
turnId: response.turnId
|
|
637
|
+
});
|
|
638
|
+
return response;
|
|
639
|
+
}
|
|
640
|
+
catch (retryError) {
|
|
641
|
+
logDiagnostics(diagnostics, "error", "codex.turn_steer.failure", {
|
|
642
|
+
error: errorDiagnostics(retryError),
|
|
643
|
+
expectedTurnId,
|
|
644
|
+
threadId
|
|
645
|
+
});
|
|
646
|
+
throw retryError;
|
|
647
|
+
}
|
|
648
|
+
}
|
|
649
|
+
}
|
|
650
|
+
async function steerTurnWithKeepAliveOnce(serverUrl, threadId, input, expectedTurnId) {
|
|
651
|
+
const connection = await JsonRpcConnection.connect(serverUrl);
|
|
652
|
+
let keepAliveStarted = false;
|
|
653
|
+
try {
|
|
654
|
+
await initializeConnection(connection);
|
|
655
|
+
const response = await connection.call("turn/steer", {
|
|
656
|
+
expectedTurnId,
|
|
657
|
+
input,
|
|
658
|
+
threadId
|
|
659
|
+
});
|
|
660
|
+
keepAliveStarted = true;
|
|
661
|
+
keepTurnConnectionAlive(connection, threadId, response.turnId);
|
|
662
|
+
return response;
|
|
663
|
+
}
|
|
664
|
+
finally {
|
|
665
|
+
if (!keepAliveStarted) {
|
|
666
|
+
connection.close();
|
|
667
|
+
}
|
|
668
|
+
}
|
|
669
|
+
}
|
|
481
670
|
async function initializeConnection(connection) {
|
|
482
671
|
await connection.call("initialize", {
|
|
483
672
|
capabilities: {
|
|
@@ -733,7 +922,7 @@ function mergeCodexThreadSnapshots(baseThread, nextThread) {
|
|
|
733
922
|
threadListActivityAt: abitatThreadListActivityAt
|
|
734
923
|
})
|
|
735
924
|
? { activeFlags: [], type: "active" }
|
|
736
|
-
:
|
|
925
|
+
: preferredMergedThreadStatus(baseThread, nextThread),
|
|
737
926
|
turns: nextThread.turns.length > 0 ? nextThread.turns : baseThread.turns,
|
|
738
927
|
updatedAt: Math.max(safeSeconds(baseThread.updatedAt, baseThread.createdAt), safeSeconds(nextThread.updatedAt, nextThread.createdAt))
|
|
739
928
|
};
|
|
@@ -744,6 +933,14 @@ function preferredThreadStatus(baseStatus, nextStatus) {
|
|
|
744
933
|
}
|
|
745
934
|
return threadStatusRank(nextStatus) > threadStatusRank(baseStatus) ? nextStatus : baseStatus;
|
|
746
935
|
}
|
|
936
|
+
function preferredMergedThreadStatus(baseThread, nextThread) {
|
|
937
|
+
const baseLatestTurn = baseThread.turns.at(-1) ?? null;
|
|
938
|
+
const nextIsActiveSummary = threadStatusType(nextThread.status) === "active" && nextThread.turns.length === 0;
|
|
939
|
+
if (nextIsActiveSummary && baseLatestTurn && isTurnTerminal(baseLatestTurn)) {
|
|
940
|
+
return baseThread.status;
|
|
941
|
+
}
|
|
942
|
+
return preferredThreadStatus(baseThread.status, nextThread.status);
|
|
943
|
+
}
|
|
747
944
|
function threadStatusRank(status) {
|
|
748
945
|
switch (threadStatusType(status)) {
|
|
749
946
|
case "active":
|
|
@@ -790,10 +987,14 @@ function isThreadListActivitySummaryStatus(status) {
|
|
|
790
987
|
return statusType === "idle" || statusType === "notLoaded";
|
|
791
988
|
}
|
|
792
989
|
function needsConversationStatusHydration(thread) {
|
|
793
|
-
return thread.turns.length === 0 &&
|
|
990
|
+
return (thread.turns.length === 0 &&
|
|
991
|
+
(isThreadListActivitySummaryStatus(thread.status) ||
|
|
992
|
+
threadStatusType(thread.status) === "active"));
|
|
794
993
|
}
|
|
795
994
|
function threadListActivitySeconds(thread) {
|
|
796
|
-
const currentActivity = thread.turns.length === 0 &&
|
|
995
|
+
const currentActivity = thread.turns.length === 0 &&
|
|
996
|
+
(isThreadListActivitySummaryStatus(thread.status) ||
|
|
997
|
+
threadStatusType(thread.status) === "active")
|
|
797
998
|
? safeSeconds(thread.updatedAt, thread.createdAt)
|
|
798
999
|
: null;
|
|
799
1000
|
return maxOptionalSeconds(thread.abitatThreadListActivityAt ?? null, currentActivity);
|
|
@@ -847,6 +1048,32 @@ function codexThreadToCompletionState(thread, workspaceId) {
|
|
|
847
1048
|
workspaceId
|
|
848
1049
|
};
|
|
849
1050
|
}
|
|
1051
|
+
function startedTurnStatusOrThrow(threadId, turn, diagnostics) {
|
|
1052
|
+
if (isTurnInProgress(turn)) {
|
|
1053
|
+
return "running";
|
|
1054
|
+
}
|
|
1055
|
+
if (turnHasVisibleAssistantMessage(turn)) {
|
|
1056
|
+
return "approved";
|
|
1057
|
+
}
|
|
1058
|
+
const message = codexTurnDidNotStartMessage(turn);
|
|
1059
|
+
logDiagnostics(diagnostics, "error", "codex.turn_start.unstarted", {
|
|
1060
|
+
error: message,
|
|
1061
|
+
status: turnStatusType(turn),
|
|
1062
|
+
threadId,
|
|
1063
|
+
turnId: turn.id
|
|
1064
|
+
});
|
|
1065
|
+
throw Object.assign(new Error(message), { statusCode: 502 });
|
|
1066
|
+
}
|
|
1067
|
+
function codexTurnDidNotStartMessage(turn) {
|
|
1068
|
+
const turnError = codexTurnErrorMessage(turn.error);
|
|
1069
|
+
if (turnError) {
|
|
1070
|
+
return `Codex turn did not start: ${turnError}`;
|
|
1071
|
+
}
|
|
1072
|
+
const status = turnStatusType(turn);
|
|
1073
|
+
return status
|
|
1074
|
+
? `Codex turn did not start; Codex returned status ${status} without a visible reply.`
|
|
1075
|
+
: "Codex turn did not start; Codex returned without a visible reply.";
|
|
1076
|
+
}
|
|
850
1077
|
export function flattenThreadMessages(thread, conversationId = externalCodexConversationId(thread.id), diagnostics) {
|
|
851
1078
|
const messages = [];
|
|
852
1079
|
const itemOccurrences = new Map();
|
|
@@ -896,6 +1123,28 @@ export function flattenThreadMessages(thread, conversationId = externalCodexConv
|
|
|
896
1123
|
}
|
|
897
1124
|
sequence += 1;
|
|
898
1125
|
}
|
|
1126
|
+
if (turn.error && (assistantMessagesByTurn.get(turn.id) ?? 0) === 0) {
|
|
1127
|
+
const errorContent = codexTurnVisibleErrorContent(turn.error);
|
|
1128
|
+
if (errorContent) {
|
|
1129
|
+
messages.push({
|
|
1130
|
+
content: errorContent,
|
|
1131
|
+
conversationId,
|
|
1132
|
+
createdAt: itemCreatedAt(thread, turn, sequence),
|
|
1133
|
+
id: `${conversationId}_${encodeURIComponent(turn.id)}_turn_error`,
|
|
1134
|
+
metadata: {
|
|
1135
|
+
codexItemId: "turn_error",
|
|
1136
|
+
codexItemType: "turnError",
|
|
1137
|
+
codexThreadId: thread.id,
|
|
1138
|
+
codexTurnId: turn.id
|
|
1139
|
+
},
|
|
1140
|
+
role: "assistant",
|
|
1141
|
+
sequence,
|
|
1142
|
+
sourceDeviceId: null
|
|
1143
|
+
});
|
|
1144
|
+
assistantMessagesByTurn.set(turn.id, 1);
|
|
1145
|
+
sequence += 1;
|
|
1146
|
+
}
|
|
1147
|
+
}
|
|
899
1148
|
if (isTurnCompleted(turn) && (assistantMessagesByTurn.get(turn.id) ?? 0) === 0) {
|
|
900
1149
|
logDiagnostics(diagnostics, "warn", "codex.turn.completed_without_visible_assistant", {
|
|
901
1150
|
conversationId,
|
|
@@ -916,6 +1165,91 @@ export function flattenThreadMessages(thread, conversationId = externalCodexConv
|
|
|
916
1165
|
});
|
|
917
1166
|
return messages;
|
|
918
1167
|
}
|
|
1168
|
+
async function generatedFilesForThread(thread) {
|
|
1169
|
+
const paths = new Set();
|
|
1170
|
+
for (const turn of thread.turns) {
|
|
1171
|
+
for (const item of turn.items) {
|
|
1172
|
+
if (item.type !== "fileChange") {
|
|
1173
|
+
continue;
|
|
1174
|
+
}
|
|
1175
|
+
const fileChange = item;
|
|
1176
|
+
for (const change of fileChange.changes ?? []) {
|
|
1177
|
+
const path = generatedFilePath(thread.cwd, change);
|
|
1178
|
+
if (path) {
|
|
1179
|
+
paths.add(path);
|
|
1180
|
+
}
|
|
1181
|
+
}
|
|
1182
|
+
}
|
|
1183
|
+
}
|
|
1184
|
+
const files = await Promise.all(Array.from(paths)
|
|
1185
|
+
.sort()
|
|
1186
|
+
.map(async (path) => {
|
|
1187
|
+
const stats = await stat(path).catch(() => null);
|
|
1188
|
+
if (!stats?.isFile()) {
|
|
1189
|
+
return null;
|
|
1190
|
+
}
|
|
1191
|
+
return {
|
|
1192
|
+
id: generatedFileId(thread.id, path),
|
|
1193
|
+
mimeType: mimeTypeForPath(path),
|
|
1194
|
+
name: basename(path),
|
|
1195
|
+
path,
|
|
1196
|
+
size: stats.size
|
|
1197
|
+
};
|
|
1198
|
+
}));
|
|
1199
|
+
return files.filter((file) => Boolean(file));
|
|
1200
|
+
}
|
|
1201
|
+
function generatedFilePath(cwd, change) {
|
|
1202
|
+
if (!change || typeof change !== "object") {
|
|
1203
|
+
return null;
|
|
1204
|
+
}
|
|
1205
|
+
const candidate = change;
|
|
1206
|
+
const value = stringField(candidate, "path") ??
|
|
1207
|
+
stringField(candidate, "filePath") ??
|
|
1208
|
+
stringField(candidate, "absolutePath") ??
|
|
1209
|
+
stringField(candidate, "relativePath");
|
|
1210
|
+
if (!value) {
|
|
1211
|
+
return null;
|
|
1212
|
+
}
|
|
1213
|
+
return isAbsolute(value) ? normalize(value) : normalize(join(cwd, value));
|
|
1214
|
+
}
|
|
1215
|
+
function generatedFileId(threadId, path) {
|
|
1216
|
+
return `file_${createHash("sha256")
|
|
1217
|
+
.update(`${threadId}\0${normalize(path)}`)
|
|
1218
|
+
.digest("hex")
|
|
1219
|
+
.slice(0, 24)}`;
|
|
1220
|
+
}
|
|
1221
|
+
function stringField(value, key) {
|
|
1222
|
+
const field = value[key];
|
|
1223
|
+
return typeof field === "string" && field.trim() ? field.trim() : null;
|
|
1224
|
+
}
|
|
1225
|
+
function mimeTypeForPath(path) {
|
|
1226
|
+
switch (extname(path).toLowerCase()) {
|
|
1227
|
+
case ".html":
|
|
1228
|
+
return "text/html";
|
|
1229
|
+
case ".md":
|
|
1230
|
+
case ".markdown":
|
|
1231
|
+
return "text/markdown";
|
|
1232
|
+
case ".mp4":
|
|
1233
|
+
return "video/mp4";
|
|
1234
|
+
case ".mov":
|
|
1235
|
+
return "video/quicktime";
|
|
1236
|
+
case ".png":
|
|
1237
|
+
return "image/png";
|
|
1238
|
+
case ".jpg":
|
|
1239
|
+
case ".jpeg":
|
|
1240
|
+
return "image/jpeg";
|
|
1241
|
+
case ".gif":
|
|
1242
|
+
return "image/gif";
|
|
1243
|
+
case ".pdf":
|
|
1244
|
+
return "application/pdf";
|
|
1245
|
+
case ".json":
|
|
1246
|
+
return "application/json";
|
|
1247
|
+
case ".txt":
|
|
1248
|
+
return "text/plain";
|
|
1249
|
+
default:
|
|
1250
|
+
return "application/octet-stream";
|
|
1251
|
+
}
|
|
1252
|
+
}
|
|
919
1253
|
function codexThreadToConversationStatus(thread) {
|
|
920
1254
|
const latestTurn = thread.turns.at(-1) ?? null;
|
|
921
1255
|
if (threadStatusType(thread.status) === "active") {
|
|
@@ -936,7 +1270,19 @@ function codexThreadToConversationStatus(thread) {
|
|
|
936
1270
|
return "approved";
|
|
937
1271
|
}
|
|
938
1272
|
function isCodexThreadBusy(thread) {
|
|
939
|
-
return threadStatusType(thread.status) === "active";
|
|
1273
|
+
return threadStatusType(thread.status) === "active" || thread.turns.some(isTurnInProgress);
|
|
1274
|
+
}
|
|
1275
|
+
function isCodexThreadMessageHistoryStable(thread) {
|
|
1276
|
+
return !isCodexThreadBusy(thread) && !thread.turns.some(isTurnInProgress);
|
|
1277
|
+
}
|
|
1278
|
+
function latestActiveTurn(thread) {
|
|
1279
|
+
for (let index = thread.turns.length - 1; index >= 0; index -= 1) {
|
|
1280
|
+
const turn = thread.turns[index];
|
|
1281
|
+
if (turn && isTurnInProgress(turn)) {
|
|
1282
|
+
return turn;
|
|
1283
|
+
}
|
|
1284
|
+
}
|
|
1285
|
+
return null;
|
|
940
1286
|
}
|
|
941
1287
|
function isActiveMobileConversationStatus(status) {
|
|
942
1288
|
return status === "running" || status === "awaiting_approval";
|
|
@@ -991,6 +1337,14 @@ function isKnownCodexItemType(type) {
|
|
|
991
1337
|
type === "fileChange" ||
|
|
992
1338
|
HIDDEN_CODEX_ITEM_TYPES.has(type));
|
|
993
1339
|
}
|
|
1340
|
+
function filterThreadMessages(messages, options) {
|
|
1341
|
+
const filtered = options.includeRuntime
|
|
1342
|
+
? messages
|
|
1343
|
+
: messages.filter((message) => message.role !== "runtime");
|
|
1344
|
+
return typeof options.afterSequence === "number"
|
|
1345
|
+
? filtered.filter((message) => message.sequence > options.afterSequence)
|
|
1346
|
+
: filtered;
|
|
1347
|
+
}
|
|
994
1348
|
function messageCounts(messages) {
|
|
995
1349
|
const counts = {
|
|
996
1350
|
assistant: 0,
|
|
@@ -1021,6 +1375,37 @@ function turnInputDiagnostics(input) {
|
|
|
1021
1375
|
...attachmentDiagnostics(attachments)
|
|
1022
1376
|
};
|
|
1023
1377
|
}
|
|
1378
|
+
function turnHasVisibleAssistantMessage(turn) {
|
|
1379
|
+
return turn.items.some((item) => threadItemToMessageContent(item)?.role === "assistant");
|
|
1380
|
+
}
|
|
1381
|
+
function codexTurnVisibleErrorContent(error) {
|
|
1382
|
+
const message = codexTurnErrorMessage(error);
|
|
1383
|
+
return message ? `Codex could not complete this turn: ${message}` : null;
|
|
1384
|
+
}
|
|
1385
|
+
function codexTurnErrorMessage(error) {
|
|
1386
|
+
if (!error) {
|
|
1387
|
+
return null;
|
|
1388
|
+
}
|
|
1389
|
+
if (error instanceof Error) {
|
|
1390
|
+
return error.message;
|
|
1391
|
+
}
|
|
1392
|
+
if (typeof error === "string") {
|
|
1393
|
+
return error.trim() || null;
|
|
1394
|
+
}
|
|
1395
|
+
if (typeof error === "object") {
|
|
1396
|
+
const message = error.message;
|
|
1397
|
+
if (typeof message === "string" && message.trim()) {
|
|
1398
|
+
return message.trim();
|
|
1399
|
+
}
|
|
1400
|
+
try {
|
|
1401
|
+
return JSON.stringify(error);
|
|
1402
|
+
}
|
|
1403
|
+
catch {
|
|
1404
|
+
return String(error);
|
|
1405
|
+
}
|
|
1406
|
+
}
|
|
1407
|
+
return String(error);
|
|
1408
|
+
}
|
|
1024
1409
|
function userInput(prompt, attachments = []) {
|
|
1025
1410
|
return [
|
|
1026
1411
|
{ text: normalizedPrompt(prompt), text_elements: [], type: "text" },
|
|
@@ -1049,16 +1434,6 @@ function userInputToText(input) {
|
|
|
1049
1434
|
return `[Mention: ${input.name}]`;
|
|
1050
1435
|
}
|
|
1051
1436
|
}
|
|
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
|
-
}
|
|
1062
1437
|
function turnModelSettings(modelSettings) {
|
|
1063
1438
|
return modelSettings ? { effort: modelSettings.effort, model: modelSettings.model } : {};
|
|
1064
1439
|
}
|