@abitat_reece/host-daemon 0.1.10 → 0.1.11
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/local-control/codex-bridge.d.ts.map +1 -1
- package/dist/local-control/codex-bridge.js +319 -38
- package/dist/local-control/codex-bridge.js.map +1 -1
- package/dist/local-control/diagnostics-log.d.ts +2 -0
- package/dist/local-control/diagnostics-log.d.ts.map +1 -1
- package/dist/local-control/diagnostics-log.js +174 -9
- 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/server.d.ts +29 -0
- package/dist/local-control/server.d.ts.map +1 -1
- package/dist/local-control/server.js +52 -0
- package/dist/local-control/server.js.map +1 -1
- package/package.json +1 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"codex-bridge.d.ts","sourceRoot":"","sources":["../../src/local-control/codex-bridge.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"codex-bridge.d.ts","sourceRoot":"","sources":["../../src/local-control/codex-bridge.ts"],"names":[],"mappings":"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"}
|
|
@@ -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,51 @@ 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) ?? [];
|
|
119
122
|
const next = options.front ? [input, ...existing] : [...existing, input];
|
|
120
123
|
queuedTurnsByThread.set(threadId, next);
|
|
121
124
|
scheduleQueueDrain(threadId);
|
|
122
125
|
}
|
|
126
|
+
function deleteQueuedTurn(threadId, clientMessageId) {
|
|
127
|
+
const existing = queuedTurnsByThread.get(threadId);
|
|
128
|
+
if (!existing?.length) {
|
|
129
|
+
return false;
|
|
130
|
+
}
|
|
131
|
+
const next = existing.filter((turn) => turn.clientMessageId !== clientMessageId);
|
|
132
|
+
if (next.length === existing.length) {
|
|
133
|
+
return false;
|
|
134
|
+
}
|
|
135
|
+
invalidateMessageHistoryCache(threadId);
|
|
136
|
+
if (next.length > 0) {
|
|
137
|
+
queuedTurnsByThread.set(threadId, next);
|
|
138
|
+
}
|
|
139
|
+
else {
|
|
140
|
+
queuedTurnsByThread.delete(threadId);
|
|
141
|
+
const timer = queueDrainTimers.get(threadId);
|
|
142
|
+
if (timer) {
|
|
143
|
+
clearTimeout(timer);
|
|
144
|
+
queueDrainTimers.delete(threadId);
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
return true;
|
|
148
|
+
}
|
|
149
|
+
function updateQueuedTurn(threadId, clientMessageId, prompt) {
|
|
150
|
+
const existing = queuedTurnsByThread.get(threadId);
|
|
151
|
+
if (!existing?.length) {
|
|
152
|
+
return false;
|
|
153
|
+
}
|
|
154
|
+
const index = existing.findIndex((turn) => turn.clientMessageId === clientMessageId);
|
|
155
|
+
if (index < 0) {
|
|
156
|
+
return false;
|
|
157
|
+
}
|
|
158
|
+
invalidateMessageHistoryCache(threadId);
|
|
159
|
+
queuedTurnsByThread.set(threadId, existing.map((turn, turnIndex) => (turnIndex === index ? { ...turn, prompt } : turn)));
|
|
160
|
+
return true;
|
|
161
|
+
}
|
|
162
|
+
function invalidateMessageHistoryCache(threadId) {
|
|
163
|
+
messageHistoryCache.delete(threadId);
|
|
164
|
+
}
|
|
123
165
|
function scheduleQueueDrain(threadId, delayMs = QUEUED_TURN_POLL_INTERVAL_MS) {
|
|
124
166
|
if (queueDrainTimers.has(threadId)) {
|
|
125
167
|
return;
|
|
@@ -163,6 +205,7 @@ export function createLocalCodexBridge(options = {}) {
|
|
|
163
205
|
}
|
|
164
206
|
}
|
|
165
207
|
async function startConversationTurn(threadId, thread, input) {
|
|
208
|
+
invalidateMessageHistoryCache(threadId);
|
|
166
209
|
let activeThread = thread;
|
|
167
210
|
if (threadStatusType(activeThread.status) !== "active") {
|
|
168
211
|
activeThread = (await client.resumeThread({ excludeTurns: false, threadId })).thread;
|
|
@@ -196,7 +239,17 @@ export function createLocalCodexBridge(options = {}) {
|
|
|
196
239
|
const delivery = input.delivery ?? "queue";
|
|
197
240
|
if (isCodexThreadBusy(thread)) {
|
|
198
241
|
if (delivery === "steer") {
|
|
199
|
-
|
|
242
|
+
const activeTurn = latestActiveTurn(thread);
|
|
243
|
+
if (!activeTurn) {
|
|
244
|
+
throw Object.assign(new Error("No active Codex turn is available to steer"), {
|
|
245
|
+
statusCode: 409
|
|
246
|
+
});
|
|
247
|
+
}
|
|
248
|
+
invalidateMessageHistoryCache(threadId);
|
|
249
|
+
await client.steerTurn(threadId, userInput(input.prompt, input.attachments), activeTurn.id);
|
|
250
|
+
if (input.clientMessageId) {
|
|
251
|
+
deleteQueuedTurn(threadId, input.clientMessageId);
|
|
252
|
+
}
|
|
200
253
|
return {
|
|
201
254
|
conversationId: externalCodexConversationId(threadId),
|
|
202
255
|
status: "running"
|
|
@@ -221,40 +274,67 @@ export function createLocalCodexBridge(options = {}) {
|
|
|
221
274
|
status: "running"
|
|
222
275
|
};
|
|
223
276
|
},
|
|
277
|
+
async deleteQueuedTurn(conversationId, input) {
|
|
278
|
+
const threadId = toCodexThreadId(conversationId);
|
|
279
|
+
const removed = deleteQueuedTurn(threadId, input.clientMessageId);
|
|
280
|
+
return {
|
|
281
|
+
conversationId: externalCodexConversationId(threadId),
|
|
282
|
+
removed,
|
|
283
|
+
status: (queuedTurnsByThread.get(threadId)?.length ?? 0) > 0 ? "queued" : "running"
|
|
284
|
+
};
|
|
285
|
+
},
|
|
286
|
+
async updateQueuedTurn(conversationId, input) {
|
|
287
|
+
const threadId = toCodexThreadId(conversationId);
|
|
288
|
+
const updated = updateQueuedTurn(threadId, input.clientMessageId, input.prompt);
|
|
289
|
+
return {
|
|
290
|
+
conversationId: externalCodexConversationId(threadId),
|
|
291
|
+
status: (queuedTurnsByThread.get(threadId)?.length ?? 0) > 0 ? "queued" : "running",
|
|
292
|
+
updated
|
|
293
|
+
};
|
|
294
|
+
},
|
|
224
295
|
async listCompletionStates() {
|
|
225
296
|
const threads = await readThreadsWithTurns(await loadAllThreads());
|
|
297
|
+
const states = threads.map((thread) => codexThreadToCompletionState(thread, workspaceId));
|
|
226
298
|
logDiagnostics(diagnostics, "info", "completion.states.result", {
|
|
227
|
-
|
|
299
|
+
activeCount: states.filter((state) => isActiveMobileConversationStatus(state.status))
|
|
300
|
+
.length,
|
|
301
|
+
stateCount: states.length
|
|
228
302
|
});
|
|
229
|
-
return
|
|
230
|
-
.sort((left, right) => right.updatedAt - left.updatedAt)
|
|
231
|
-
.map((thread) => codexThreadToCompletionState(thread, workspaceId));
|
|
303
|
+
return states.sort((left, right) => Date.parse(right.updatedAt) - Date.parse(left.updatedAt));
|
|
232
304
|
},
|
|
233
305
|
async listMessages(conversationId, messageOptions = {}) {
|
|
234
306
|
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;
|
|
307
|
+
const messages = await readCachedThreadMessages(threadId, {
|
|
308
|
+
afterSequence: messageOptions.afterSequence,
|
|
309
|
+
forceRefresh: messageOptions.forceRefresh === true
|
|
310
|
+
});
|
|
311
|
+
const returned = filterThreadMessages(messages, messageOptions);
|
|
245
312
|
logDiagnostics(diagnostics, "info", "messages.list.bridge_result", {
|
|
246
313
|
...messageCounts(messages),
|
|
247
314
|
afterSequence: messageOptions.afterSequence,
|
|
248
315
|
conversationId: externalCodexConversationId(threadId),
|
|
316
|
+
forceRefresh: messageOptions.forceRefresh === true,
|
|
249
317
|
includeRuntime: messageOptions.includeRuntime === true,
|
|
250
318
|
returned: returned.length,
|
|
251
319
|
total: messages.length
|
|
252
320
|
});
|
|
253
|
-
if (typeof messageOptions.afterSequence !== "number") {
|
|
254
|
-
return returned;
|
|
255
|
-
}
|
|
256
321
|
return returned;
|
|
257
322
|
},
|
|
323
|
+
async listGeneratedFiles(conversationId) {
|
|
324
|
+
return generatedFilesForThread(await client.readThread(toCodexThreadId(conversationId), true));
|
|
325
|
+
},
|
|
326
|
+
async downloadGeneratedFile(conversationId, fileId) {
|
|
327
|
+
const files = await generatedFilesForThread(await client.readThread(toCodexThreadId(conversationId), true));
|
|
328
|
+
const file = files.find((candidate) => candidate.id === fileId);
|
|
329
|
+
if (!file) {
|
|
330
|
+
throw Object.assign(new Error("Generated file not found"), { statusCode: 404 });
|
|
331
|
+
}
|
|
332
|
+
const data = await readFile(file.path);
|
|
333
|
+
return {
|
|
334
|
+
...file,
|
|
335
|
+
dataBase64: data.toString("base64")
|
|
336
|
+
};
|
|
337
|
+
},
|
|
258
338
|
listModelOptions() {
|
|
259
339
|
return client.listModels();
|
|
260
340
|
},
|
|
@@ -274,6 +354,7 @@ export function createLocalCodexBridge(options = {}) {
|
|
|
274
354
|
experimentalRawEvents: false,
|
|
275
355
|
persistExtendedHistory: true
|
|
276
356
|
});
|
|
357
|
+
invalidateMessageHistoryCache(thread.id);
|
|
277
358
|
await client.startTurn(thread.id, userInput(input.prompt, input.attachments), {
|
|
278
359
|
...PHONE_FULL_ACCESS_TURN_OPTIONS,
|
|
279
360
|
cwd,
|
|
@@ -300,6 +381,33 @@ export function createLocalCodexBridge(options = {}) {
|
|
|
300
381
|
return mergeCodexThreadSnapshots(readThread, thread);
|
|
301
382
|
}));
|
|
302
383
|
}
|
|
384
|
+
async function readCachedThreadMessages(threadId, options) {
|
|
385
|
+
const cached = messageHistoryCache.get(threadId);
|
|
386
|
+
if (!options.forceRefresh && typeof options.afterSequence === "number" && cached) {
|
|
387
|
+
const summary = await client.readThread(threadId, false).catch(() => null);
|
|
388
|
+
if (summary && canUseCachedMessageHistory(cached, summary)) {
|
|
389
|
+
return cached.messages;
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
const thread = await client.readThread(threadId, true);
|
|
393
|
+
const messages = flattenThreadMessages(thread, externalCodexConversationId(threadId), diagnostics);
|
|
394
|
+
if (isCodexThreadMessageHistoryStable(thread)) {
|
|
395
|
+
messageHistoryCache.set(threadId, {
|
|
396
|
+
messages,
|
|
397
|
+
statusType: threadStatusType(thread.status),
|
|
398
|
+
updatedAt: safeSeconds(thread.updatedAt, thread.createdAt)
|
|
399
|
+
});
|
|
400
|
+
}
|
|
401
|
+
else {
|
|
402
|
+
invalidateMessageHistoryCache(threadId);
|
|
403
|
+
}
|
|
404
|
+
return messages;
|
|
405
|
+
}
|
|
406
|
+
function canUseCachedMessageHistory(cached, summary) {
|
|
407
|
+
return (!isCodexThreadBusy(summary) &&
|
|
408
|
+
cached.statusType === threadStatusType(summary.status) &&
|
|
409
|
+
cached.updatedAt === safeSeconds(summary.updatedAt, summary.createdAt));
|
|
410
|
+
}
|
|
303
411
|
}
|
|
304
412
|
function createCodexAppClient(options) {
|
|
305
413
|
const serverUrl = options.serverUrl ?? process.env.CODEX_APP_SERVER_URL ?? DEFAULT_SERVER_URL;
|
|
@@ -320,10 +428,6 @@ function createCodexAppClient(options) {
|
|
|
320
428
|
} while (cursor);
|
|
321
429
|
return models;
|
|
322
430
|
},
|
|
323
|
-
injectItems: (threadId, items) => callCodexApp(serverUrl, codexBinaryPath, "thread/inject_items", {
|
|
324
|
-
items,
|
|
325
|
-
threadId
|
|
326
|
-
}),
|
|
327
431
|
async listLoadedThreads() {
|
|
328
432
|
const threadIds = [];
|
|
329
433
|
let cursor = null;
|
|
@@ -373,7 +477,8 @@ function createCodexAppClient(options) {
|
|
|
373
477
|
persistExtendedHistory: true,
|
|
374
478
|
...params
|
|
375
479
|
}, diagnostics),
|
|
376
|
-
startTurn: (threadId, input, turnOptions) => startTurnWithKeepAlive(serverUrl, codexBinaryPath, threadId, input, turnOptions, diagnostics)
|
|
480
|
+
startTurn: (threadId, input, turnOptions) => startTurnWithKeepAlive(serverUrl, codexBinaryPath, threadId, input, turnOptions, diagnostics),
|
|
481
|
+
steerTurn: (threadId, input, expectedTurnId) => steerTurnWithKeepAlive(serverUrl, codexBinaryPath, threadId, input, expectedTurnId, diagnostics)
|
|
377
482
|
};
|
|
378
483
|
}
|
|
379
484
|
async function callCodexApp(serverUrl, codexBinaryPath, method, params, diagnostics) {
|
|
@@ -478,6 +583,75 @@ async function startTurnWithKeepAliveOnce(serverUrl, threadId, input, options) {
|
|
|
478
583
|
}
|
|
479
584
|
}
|
|
480
585
|
}
|
|
586
|
+
async function steerTurnWithKeepAlive(serverUrl, codexBinaryPath, threadId, input, expectedTurnId, diagnostics) {
|
|
587
|
+
logDiagnostics(diagnostics, "info", "codex.turn_steer.call", {
|
|
588
|
+
...turnInputDiagnostics(input),
|
|
589
|
+
expectedTurnId,
|
|
590
|
+
threadId
|
|
591
|
+
});
|
|
592
|
+
try {
|
|
593
|
+
const response = await steerTurnWithKeepAliveOnce(serverUrl, threadId, input, expectedTurnId);
|
|
594
|
+
logDiagnostics(diagnostics, "info", "codex.turn_steer.result", {
|
|
595
|
+
expectedTurnId,
|
|
596
|
+
threadId,
|
|
597
|
+
turnId: response.turnId
|
|
598
|
+
});
|
|
599
|
+
return response;
|
|
600
|
+
}
|
|
601
|
+
catch (error) {
|
|
602
|
+
if (!isConnectionFailure(error) || !canStartLocalServer(serverUrl)) {
|
|
603
|
+
logDiagnostics(diagnostics, "error", "codex.turn_steer.failure", {
|
|
604
|
+
error: errorDiagnostics(error),
|
|
605
|
+
expectedTurnId,
|
|
606
|
+
threadId
|
|
607
|
+
});
|
|
608
|
+
throw error;
|
|
609
|
+
}
|
|
610
|
+
logDiagnostics(diagnostics, "warn", "codex.app_server.connect_failure", {
|
|
611
|
+
error: errorDiagnostics(error),
|
|
612
|
+
method: "turn/steer",
|
|
613
|
+
serverUrl
|
|
614
|
+
});
|
|
615
|
+
await ensureLocalAppServer(serverUrl, codexBinaryPath, diagnostics);
|
|
616
|
+
try {
|
|
617
|
+
const response = await steerTurnWithKeepAliveOnce(serverUrl, threadId, input, expectedTurnId);
|
|
618
|
+
logDiagnostics(diagnostics, "info", "codex.turn_steer.result", {
|
|
619
|
+
expectedTurnId,
|
|
620
|
+
threadId,
|
|
621
|
+
turnId: response.turnId
|
|
622
|
+
});
|
|
623
|
+
return response;
|
|
624
|
+
}
|
|
625
|
+
catch (retryError) {
|
|
626
|
+
logDiagnostics(diagnostics, "error", "codex.turn_steer.failure", {
|
|
627
|
+
error: errorDiagnostics(retryError),
|
|
628
|
+
expectedTurnId,
|
|
629
|
+
threadId
|
|
630
|
+
});
|
|
631
|
+
throw retryError;
|
|
632
|
+
}
|
|
633
|
+
}
|
|
634
|
+
}
|
|
635
|
+
async function steerTurnWithKeepAliveOnce(serverUrl, threadId, input, expectedTurnId) {
|
|
636
|
+
const connection = await JsonRpcConnection.connect(serverUrl);
|
|
637
|
+
let keepAliveStarted = false;
|
|
638
|
+
try {
|
|
639
|
+
await initializeConnection(connection);
|
|
640
|
+
const response = await connection.call("turn/steer", {
|
|
641
|
+
expectedTurnId,
|
|
642
|
+
input,
|
|
643
|
+
threadId
|
|
644
|
+
});
|
|
645
|
+
keepAliveStarted = true;
|
|
646
|
+
keepTurnConnectionAlive(connection, threadId, response.turnId);
|
|
647
|
+
return response;
|
|
648
|
+
}
|
|
649
|
+
finally {
|
|
650
|
+
if (!keepAliveStarted) {
|
|
651
|
+
connection.close();
|
|
652
|
+
}
|
|
653
|
+
}
|
|
654
|
+
}
|
|
481
655
|
async function initializeConnection(connection) {
|
|
482
656
|
await connection.call("initialize", {
|
|
483
657
|
capabilities: {
|
|
@@ -733,7 +907,7 @@ function mergeCodexThreadSnapshots(baseThread, nextThread) {
|
|
|
733
907
|
threadListActivityAt: abitatThreadListActivityAt
|
|
734
908
|
})
|
|
735
909
|
? { activeFlags: [], type: "active" }
|
|
736
|
-
:
|
|
910
|
+
: preferredMergedThreadStatus(baseThread, nextThread),
|
|
737
911
|
turns: nextThread.turns.length > 0 ? nextThread.turns : baseThread.turns,
|
|
738
912
|
updatedAt: Math.max(safeSeconds(baseThread.updatedAt, baseThread.createdAt), safeSeconds(nextThread.updatedAt, nextThread.createdAt))
|
|
739
913
|
};
|
|
@@ -744,6 +918,14 @@ function preferredThreadStatus(baseStatus, nextStatus) {
|
|
|
744
918
|
}
|
|
745
919
|
return threadStatusRank(nextStatus) > threadStatusRank(baseStatus) ? nextStatus : baseStatus;
|
|
746
920
|
}
|
|
921
|
+
function preferredMergedThreadStatus(baseThread, nextThread) {
|
|
922
|
+
const baseLatestTurn = baseThread.turns.at(-1) ?? null;
|
|
923
|
+
const nextIsActiveSummary = threadStatusType(nextThread.status) === "active" && nextThread.turns.length === 0;
|
|
924
|
+
if (nextIsActiveSummary && baseLatestTurn && isTurnTerminal(baseLatestTurn)) {
|
|
925
|
+
return baseThread.status;
|
|
926
|
+
}
|
|
927
|
+
return preferredThreadStatus(baseThread.status, nextThread.status);
|
|
928
|
+
}
|
|
747
929
|
function threadStatusRank(status) {
|
|
748
930
|
switch (threadStatusType(status)) {
|
|
749
931
|
case "active":
|
|
@@ -790,10 +972,14 @@ function isThreadListActivitySummaryStatus(status) {
|
|
|
790
972
|
return statusType === "idle" || statusType === "notLoaded";
|
|
791
973
|
}
|
|
792
974
|
function needsConversationStatusHydration(thread) {
|
|
793
|
-
return thread.turns.length === 0 &&
|
|
975
|
+
return (thread.turns.length === 0 &&
|
|
976
|
+
(isThreadListActivitySummaryStatus(thread.status) ||
|
|
977
|
+
threadStatusType(thread.status) === "active"));
|
|
794
978
|
}
|
|
795
979
|
function threadListActivitySeconds(thread) {
|
|
796
|
-
const currentActivity = thread.turns.length === 0 &&
|
|
980
|
+
const currentActivity = thread.turns.length === 0 &&
|
|
981
|
+
(isThreadListActivitySummaryStatus(thread.status) ||
|
|
982
|
+
threadStatusType(thread.status) === "active")
|
|
797
983
|
? safeSeconds(thread.updatedAt, thread.createdAt)
|
|
798
984
|
: null;
|
|
799
985
|
return maxOptionalSeconds(thread.abitatThreadListActivityAt ?? null, currentActivity);
|
|
@@ -916,6 +1102,91 @@ export function flattenThreadMessages(thread, conversationId = externalCodexConv
|
|
|
916
1102
|
});
|
|
917
1103
|
return messages;
|
|
918
1104
|
}
|
|
1105
|
+
async function generatedFilesForThread(thread) {
|
|
1106
|
+
const paths = new Set();
|
|
1107
|
+
for (const turn of thread.turns) {
|
|
1108
|
+
for (const item of turn.items) {
|
|
1109
|
+
if (item.type !== "fileChange") {
|
|
1110
|
+
continue;
|
|
1111
|
+
}
|
|
1112
|
+
const fileChange = item;
|
|
1113
|
+
for (const change of fileChange.changes ?? []) {
|
|
1114
|
+
const path = generatedFilePath(thread.cwd, change);
|
|
1115
|
+
if (path) {
|
|
1116
|
+
paths.add(path);
|
|
1117
|
+
}
|
|
1118
|
+
}
|
|
1119
|
+
}
|
|
1120
|
+
}
|
|
1121
|
+
const files = await Promise.all(Array.from(paths)
|
|
1122
|
+
.sort()
|
|
1123
|
+
.map(async (path) => {
|
|
1124
|
+
const stats = await stat(path).catch(() => null);
|
|
1125
|
+
if (!stats?.isFile()) {
|
|
1126
|
+
return null;
|
|
1127
|
+
}
|
|
1128
|
+
return {
|
|
1129
|
+
id: generatedFileId(thread.id, path),
|
|
1130
|
+
mimeType: mimeTypeForPath(path),
|
|
1131
|
+
name: basename(path),
|
|
1132
|
+
path,
|
|
1133
|
+
size: stats.size
|
|
1134
|
+
};
|
|
1135
|
+
}));
|
|
1136
|
+
return files.filter((file) => Boolean(file));
|
|
1137
|
+
}
|
|
1138
|
+
function generatedFilePath(cwd, change) {
|
|
1139
|
+
if (!change || typeof change !== "object") {
|
|
1140
|
+
return null;
|
|
1141
|
+
}
|
|
1142
|
+
const candidate = change;
|
|
1143
|
+
const value = stringField(candidate, "path") ??
|
|
1144
|
+
stringField(candidate, "filePath") ??
|
|
1145
|
+
stringField(candidate, "absolutePath") ??
|
|
1146
|
+
stringField(candidate, "relativePath");
|
|
1147
|
+
if (!value) {
|
|
1148
|
+
return null;
|
|
1149
|
+
}
|
|
1150
|
+
return isAbsolute(value) ? normalize(value) : normalize(join(cwd, value));
|
|
1151
|
+
}
|
|
1152
|
+
function generatedFileId(threadId, path) {
|
|
1153
|
+
return `file_${createHash("sha256")
|
|
1154
|
+
.update(`${threadId}\0${normalize(path)}`)
|
|
1155
|
+
.digest("hex")
|
|
1156
|
+
.slice(0, 24)}`;
|
|
1157
|
+
}
|
|
1158
|
+
function stringField(value, key) {
|
|
1159
|
+
const field = value[key];
|
|
1160
|
+
return typeof field === "string" && field.trim() ? field.trim() : null;
|
|
1161
|
+
}
|
|
1162
|
+
function mimeTypeForPath(path) {
|
|
1163
|
+
switch (extname(path).toLowerCase()) {
|
|
1164
|
+
case ".html":
|
|
1165
|
+
return "text/html";
|
|
1166
|
+
case ".md":
|
|
1167
|
+
case ".markdown":
|
|
1168
|
+
return "text/markdown";
|
|
1169
|
+
case ".mp4":
|
|
1170
|
+
return "video/mp4";
|
|
1171
|
+
case ".mov":
|
|
1172
|
+
return "video/quicktime";
|
|
1173
|
+
case ".png":
|
|
1174
|
+
return "image/png";
|
|
1175
|
+
case ".jpg":
|
|
1176
|
+
case ".jpeg":
|
|
1177
|
+
return "image/jpeg";
|
|
1178
|
+
case ".gif":
|
|
1179
|
+
return "image/gif";
|
|
1180
|
+
case ".pdf":
|
|
1181
|
+
return "application/pdf";
|
|
1182
|
+
case ".json":
|
|
1183
|
+
return "application/json";
|
|
1184
|
+
case ".txt":
|
|
1185
|
+
return "text/plain";
|
|
1186
|
+
default:
|
|
1187
|
+
return "application/octet-stream";
|
|
1188
|
+
}
|
|
1189
|
+
}
|
|
919
1190
|
function codexThreadToConversationStatus(thread) {
|
|
920
1191
|
const latestTurn = thread.turns.at(-1) ?? null;
|
|
921
1192
|
if (threadStatusType(thread.status) === "active") {
|
|
@@ -936,7 +1207,19 @@ function codexThreadToConversationStatus(thread) {
|
|
|
936
1207
|
return "approved";
|
|
937
1208
|
}
|
|
938
1209
|
function isCodexThreadBusy(thread) {
|
|
939
|
-
return threadStatusType(thread.status) === "active";
|
|
1210
|
+
return threadStatusType(thread.status) === "active" || thread.turns.some(isTurnInProgress);
|
|
1211
|
+
}
|
|
1212
|
+
function isCodexThreadMessageHistoryStable(thread) {
|
|
1213
|
+
return !isCodexThreadBusy(thread) && !thread.turns.some(isTurnInProgress);
|
|
1214
|
+
}
|
|
1215
|
+
function latestActiveTurn(thread) {
|
|
1216
|
+
for (let index = thread.turns.length - 1; index >= 0; index -= 1) {
|
|
1217
|
+
const turn = thread.turns[index];
|
|
1218
|
+
if (turn && isTurnInProgress(turn)) {
|
|
1219
|
+
return turn;
|
|
1220
|
+
}
|
|
1221
|
+
}
|
|
1222
|
+
return null;
|
|
940
1223
|
}
|
|
941
1224
|
function isActiveMobileConversationStatus(status) {
|
|
942
1225
|
return status === "running" || status === "awaiting_approval";
|
|
@@ -991,6 +1274,14 @@ function isKnownCodexItemType(type) {
|
|
|
991
1274
|
type === "fileChange" ||
|
|
992
1275
|
HIDDEN_CODEX_ITEM_TYPES.has(type));
|
|
993
1276
|
}
|
|
1277
|
+
function filterThreadMessages(messages, options) {
|
|
1278
|
+
const filtered = options.includeRuntime
|
|
1279
|
+
? messages
|
|
1280
|
+
: messages.filter((message) => message.role !== "runtime");
|
|
1281
|
+
return typeof options.afterSequence === "number"
|
|
1282
|
+
? filtered.filter((message) => message.sequence > options.afterSequence)
|
|
1283
|
+
: filtered;
|
|
1284
|
+
}
|
|
994
1285
|
function messageCounts(messages) {
|
|
995
1286
|
const counts = {
|
|
996
1287
|
assistant: 0,
|
|
@@ -1049,16 +1340,6 @@ function userInputToText(input) {
|
|
|
1049
1340
|
return `[Mention: ${input.name}]`;
|
|
1050
1341
|
}
|
|
1051
1342
|
}
|
|
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
1343
|
function turnModelSettings(modelSettings) {
|
|
1063
1344
|
return modelSettings ? { effort: modelSettings.effort, model: modelSettings.model } : {};
|
|
1064
1345
|
}
|