@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.
@@ -1 +1 @@
1
- {"version":3,"file":"codex-bridge.d.ts","sourceRoot":"","sources":["../../src/local-control/codex-bridge.ts"],"names":[],"mappings":"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"}
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 { basename, normalize } from "node:path";
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
- await client.injectItems(threadId, steerItems(input.prompt, input.attachments));
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
- stateCount: threads.length
299
+ activeCount: states.filter((state) => isActiveMobileConversationStatus(state.status))
300
+ .length,
301
+ stateCount: states.length
228
302
  });
229
- return threads
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 = flattenThreadMessages(await client.readThread(threadId, true), externalCodexConversationId(threadId), diagnostics);
236
- const filtered = messageOptions.includeRuntime
237
- ? messages
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;
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
- : preferredThreadStatus(baseThread.status, nextThread.status),
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 && isThreadListActivitySummaryStatus(thread.status);
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 && isThreadListActivitySummaryStatus(thread.status)
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
  }