@ccpocket/bridge 1.63.0 → 1.63.2

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/websocket.js CHANGED
@@ -50,6 +50,11 @@ const FALLBACK_CODEX_MODELS = [
50
50
  "gpt-5.3-codex-spark",
51
51
  ];
52
52
  const FALLBACK_CODEX_REASONING_EFFORTS = ["low", "medium", "high", "xhigh"];
53
+ const CODEX_RECENT_THREAD_SOURCE_KINDS = [
54
+ "cli",
55
+ "vscode",
56
+ "appServer",
57
+ ];
53
58
  const CODEX_USER_TURN_UUID_RE = /^codex:user-turn:(\d+)$/;
54
59
  const OPT_IN_SERVER_MESSAGES = new Set([
55
60
  "conversation_queue",
@@ -849,37 +854,8 @@ export class BridgeWebSocketServer {
849
854
  pastMessages.push(raw);
850
855
  continue;
851
856
  }
852
- const paths = new Set();
853
- if (Array.isArray(msg.imagePaths)) {
854
- for (const path of msg.imagePaths) {
855
- if (typeof path === "string" && path.length > 0)
856
- paths.add(path);
857
- }
858
- }
859
857
  const content = typeof msg.content === "string" ? msg.content : "";
860
- if (this.imageStore && content) {
861
- for (const path of this.imageStore.extractImagePaths(content)) {
862
- paths.add(path);
863
- }
864
- }
865
- const images = this.imageStore && paths.size > 0
866
- ? await this.imageStore.registerImages([...paths], session.projectPath)
867
- : [];
868
- if (this.imageStore && Array.isArray(msg.imageBase64)) {
869
- for (const image of msg.imageBase64) {
870
- const rawImage = image;
871
- if (typeof rawImage.data !== "string"
872
- || typeof rawImage.mimeType !== "string") {
873
- continue;
874
- }
875
- const ref = this.imageStore.registerFromBase64(rawImage.data, rawImage.mimeType);
876
- if (ref)
877
- images.push(ref);
878
- }
879
- }
880
- const existingImages = Array.isArray(msg.images)
881
- ? msg.images
882
- : [];
858
+ const images = await this.registerPastToolResultImages(session, msg);
883
859
  pastMessages.push({
884
860
  role: "tool_result",
885
861
  toolUseId: typeof msg.toolUseId === "string"
@@ -887,13 +863,308 @@ export class BridgeWebSocketServer {
887
863
  : `past-tool-result-${pastMessages.length}`,
888
864
  content,
889
865
  ...(typeof msg.toolName === "string" ? { toolName: msg.toolName } : {}),
890
- ...(existingImages.length > 0 || images.length > 0
891
- ? { images: [...existingImages, ...images] }
892
- : {}),
866
+ ...(images.length > 0 ? { images } : {}),
893
867
  });
894
868
  }
895
869
  return { pastMessages, historyMessages };
896
870
  }
871
+ codexThreadIdForSession(session) {
872
+ if (session.provider !== "codex")
873
+ return undefined;
874
+ if (session.claudeSessionId)
875
+ return session.claudeSessionId;
876
+ if (session.process instanceof CodexProcess) {
877
+ return session.process.sessionId ?? undefined;
878
+ }
879
+ return undefined;
880
+ }
881
+ async codexCanonicalHistoryEntries(session) {
882
+ const threadId = this.codexThreadIdForSession(session);
883
+ if (!threadId)
884
+ return null;
885
+ const history = await this.getCodexThreadHistory(threadId, session.projectPath);
886
+ session.claudeSessionId = threadId;
887
+ const messages = await this.codexHistoryToServerMessages(session, history);
888
+ const entries = messages.map((message, index) => ({
889
+ seq: index + 1,
890
+ message,
891
+ }));
892
+ this.applyCodexCanonicalHistoryBaseline(session, history, entries);
893
+ return [...entries, ...session.historyEntries];
894
+ }
895
+ applyCodexCanonicalHistoryBaseline(session, history, canonicalEntries) {
896
+ const liveEntries = session.historyEntries.map((entry) => ({
897
+ seq: entry.seq,
898
+ message: entry.message,
899
+ }));
900
+ const canonicalKeys = new Set();
901
+ const canonicalUserUuids = new Set();
902
+ for (const entry of canonicalEntries) {
903
+ for (const key of this.codexHistoryMessageIdentityKeys(entry.message)) {
904
+ canonicalKeys.add(key);
905
+ }
906
+ const message = entry.message;
907
+ if (message.type === "user_input" && message.userMessageUuid) {
908
+ canonicalUserUuids.add(message.userMessageUuid);
909
+ }
910
+ }
911
+ this.seedCodexCanonicalUserTurnUuidMap(session, history);
912
+ let nextSeq = canonicalEntries.at(-1)?.seq ?? 0;
913
+ const retainedMessages = [];
914
+ const retainedEntries = [];
915
+ for (const entry of liveEntries) {
916
+ if (!this.shouldRetainCodexLiveHistoryMessage(entry.message))
917
+ continue;
918
+ const keys = this.codexHistoryMessageIdentityKeys(entry.message);
919
+ if (keys.some((key) => canonicalKeys.has(key)))
920
+ continue;
921
+ const seq = ++nextSeq;
922
+ entry.message.historySeq = seq;
923
+ retainedMessages.push(entry.message);
924
+ retainedEntries.push({ seq, message: entry.message });
925
+ }
926
+ session.pastMessages = history;
927
+ session.history = retainedMessages;
928
+ session.historyEntries = retainedEntries;
929
+ session.historyRevision = nextSeq;
930
+ session.codexCanonicalHistoryRevision = canonicalEntries.at(-1)?.seq ?? 0;
931
+ session.historyLowWatermark =
932
+ retainedEntries[0]?.seq ?? session.historyRevision + 1;
933
+ if (session.pendingCodexUserEchoUuids) {
934
+ for (const uuid of canonicalUserUuids) {
935
+ session.pendingCodexUserEchoUuids.delete(uuid);
936
+ }
937
+ for (const uuid of [...session.pendingCodexUserEchoUuids]) {
938
+ const stillLive = retainedMessages.some((message) => message.type === "user_input" && message.userMessageUuid === uuid);
939
+ if (!stillLive)
940
+ session.pendingCodexUserEchoUuids.delete(uuid);
941
+ }
942
+ }
943
+ }
944
+ seedCodexCanonicalUserTurnUuidMap(session, history) {
945
+ if (session.provider !== "codex")
946
+ return;
947
+ for (const message of history) {
948
+ if (message.role !== "user" ||
949
+ !message.rawItemId ||
950
+ !message.uuid) {
951
+ continue;
952
+ }
953
+ session.codexUserTurnUuidByRawId ??= new Map();
954
+ session.codexUserTurnUuidByRawId.set(message.rawItemId, message.uuid);
955
+ }
956
+ }
957
+ shouldRetainCodexLiveHistoryMessage(message) {
958
+ return !(message.type === "system" &&
959
+ message.subtype === "tip");
960
+ }
961
+ codexHistoryMessageIdentityKeys(message) {
962
+ if (message.type === "user_input") {
963
+ return message.userMessageUuid ? [`user:${message.userMessageUuid}`] : [];
964
+ }
965
+ if (message.type === "assistant") {
966
+ const assistantId = message.messageUuid ?? message.message.id;
967
+ if (assistantId)
968
+ return [`assistant:${assistantId}`];
969
+ return [
970
+ `assistant-content:${this.historyValueKey(message.message.content)}`,
971
+ ];
972
+ }
973
+ if (message.type === "tool_result") {
974
+ if (message.toolUseId) {
975
+ return [`tool-result:${message.toolUseId}:${message.toolName ?? ""}`];
976
+ }
977
+ return [
978
+ `tool-result-content:${message.toolName ?? ""}:${message.content}`,
979
+ ];
980
+ }
981
+ return [];
982
+ }
983
+ historyValueKey(value) {
984
+ try {
985
+ return JSON.stringify(value);
986
+ }
987
+ catch {
988
+ return String(value);
989
+ }
990
+ }
991
+ async sendCodexCanonicalHistorySnapshot(ws, sessionId, session, options = {}) {
992
+ try {
993
+ const entries = await this.codexCanonicalHistoryEntries(session);
994
+ if (!entries)
995
+ return false;
996
+ this.send(ws, {
997
+ type: "history_snapshot",
998
+ sessionId,
999
+ fromSeq: entries[0]?.seq ?? 1,
1000
+ toSeq: entries.at(-1)?.seq ?? 0,
1001
+ messages: entries,
1002
+ status: session.status,
1003
+ reason: "reset",
1004
+ });
1005
+ this.sendCodexQueueState(ws, sessionId, session);
1006
+ if (options.includeCachedCommands) {
1007
+ this.sendCachedCommands(ws, sessionId, session);
1008
+ }
1009
+ return true;
1010
+ }
1011
+ catch (err) {
1012
+ this.send(ws, {
1013
+ type: "error",
1014
+ message: `Failed to read Codex thread history: ${err instanceof Error ? err.message : String(err)}`,
1015
+ });
1016
+ return true;
1017
+ }
1018
+ }
1019
+ async sendCodexCanonicalLegacyHistory(ws, sessionId, session) {
1020
+ try {
1021
+ const entries = await this.codexCanonicalHistoryEntries(session);
1022
+ if (!entries)
1023
+ return false;
1024
+ this.send(ws, {
1025
+ type: "history",
1026
+ messages: entries.map((entry) => entry.message),
1027
+ sessionId,
1028
+ });
1029
+ this.send(ws, {
1030
+ type: "status",
1031
+ status: session.status,
1032
+ sessionId,
1033
+ });
1034
+ this.sendCodexQueueState(ws, sessionId, session);
1035
+ this.sendCachedCommands(ws, sessionId, session);
1036
+ return true;
1037
+ }
1038
+ catch (err) {
1039
+ this.send(ws, {
1040
+ type: "error",
1041
+ message: `Failed to read Codex thread history: ${err instanceof Error ? err.message : String(err)}`,
1042
+ });
1043
+ return true;
1044
+ }
1045
+ }
1046
+ shouldResetCodexHistoryDelta(session, sinceSeq, resultKind) {
1047
+ if (!this.codexThreadIdForSession(session))
1048
+ return false;
1049
+ if (typeof session.codexCanonicalHistoryRevision !== "number")
1050
+ return true;
1051
+ if (sinceSeq < session.codexCanonicalHistoryRevision)
1052
+ return true;
1053
+ return resultKind === "snapshot";
1054
+ }
1055
+ async codexHistoryToServerMessages(session, history) {
1056
+ const messages = [];
1057
+ for (const item of history) {
1058
+ const converted = await this.codexHistoryMessageToServerMessage(session, item);
1059
+ if (converted)
1060
+ messages.push(converted);
1061
+ }
1062
+ return messages;
1063
+ }
1064
+ async codexHistoryMessageToServerMessage(session, item) {
1065
+ if (item.role === "user") {
1066
+ const images = await this.registerPastUserMessageImages(session, item);
1067
+ const text = this.sessionHistoryText(item.content);
1068
+ if (!text && item.imageCount == null && images.length === 0)
1069
+ return null;
1070
+ return {
1071
+ type: "user_input",
1072
+ text,
1073
+ ...(item.uuid ? { userMessageUuid: item.uuid } : {}),
1074
+ ...(item.isMeta ? { isMeta: true } : {}),
1075
+ ...(item.imageCount != null || images.length > 0
1076
+ ? { imageCount: Math.max(item.imageCount ?? 0, images.length) }
1077
+ : {}),
1078
+ ...(item.timestamp ? { timestamp: item.timestamp } : {}),
1079
+ ...(images.length > 0 ? { images } : {}),
1080
+ };
1081
+ }
1082
+ if (item.role === "assistant") {
1083
+ const content = this.sessionHistoryAssistantContent(item.content);
1084
+ if (content.length === 0)
1085
+ return null;
1086
+ const messageId = item.uuid ??
1087
+ this.sessionHistorySingleToolUseId(item.content) ??
1088
+ randomUUID();
1089
+ return {
1090
+ type: "assistant",
1091
+ message: {
1092
+ id: messageId,
1093
+ role: "assistant",
1094
+ content,
1095
+ model: session.codexSettings?.model ?? "",
1096
+ },
1097
+ ...(item.uuid ? { messageUuid: item.uuid } : {}),
1098
+ };
1099
+ }
1100
+ const images = await this.registerPastToolResultImages(session, item);
1101
+ const content = this.sessionHistoryText(item.content);
1102
+ if (!content && images.length === 0)
1103
+ return null;
1104
+ return {
1105
+ type: "tool_result",
1106
+ toolUseId: item.toolUseId ?? item.uuid ?? `codex-history-tool-${randomUUID()}`,
1107
+ content,
1108
+ ...(item.toolName ? { toolName: item.toolName } : {}),
1109
+ ...(images.length > 0 ? { images } : {}),
1110
+ };
1111
+ }
1112
+ sessionHistoryText(content) {
1113
+ if (typeof content === "string")
1114
+ return content;
1115
+ if (!Array.isArray(content))
1116
+ return "";
1117
+ return content
1118
+ .map((item) => {
1119
+ if (item.type === "text" && typeof item.text === "string") {
1120
+ return item.text;
1121
+ }
1122
+ if (item.type === "thinking" && typeof item.thinking === "string") {
1123
+ return item.thinking;
1124
+ }
1125
+ return "";
1126
+ })
1127
+ .filter((text) => text.length > 0)
1128
+ .join("\n");
1129
+ }
1130
+ sessionHistoryAssistantContent(content) {
1131
+ if (typeof content === "string") {
1132
+ return content.trim().length > 0
1133
+ ? [{ type: "text", text: content }]
1134
+ : [];
1135
+ }
1136
+ if (!Array.isArray(content))
1137
+ return [];
1138
+ const items = [];
1139
+ for (const item of content) {
1140
+ if (item.type === "text" && typeof item.text === "string") {
1141
+ items.push({ type: "text", text: item.text });
1142
+ }
1143
+ else if (item.type === "thinking" &&
1144
+ typeof item.thinking === "string") {
1145
+ items.push({ type: "thinking", thinking: item.thinking });
1146
+ }
1147
+ else if (item.type === "tool_use" &&
1148
+ typeof item.id === "string" &&
1149
+ typeof item.name === "string") {
1150
+ items.push({
1151
+ type: "tool_use",
1152
+ id: item.id,
1153
+ name: item.name,
1154
+ input: item.input ?? {},
1155
+ });
1156
+ }
1157
+ }
1158
+ return items;
1159
+ }
1160
+ sessionHistorySingleToolUseId(content) {
1161
+ if (!Array.isArray(content) || content.length !== 1)
1162
+ return undefined;
1163
+ const item = content[0];
1164
+ return item.type === "tool_use" && typeof item.id === "string"
1165
+ ? item.id
1166
+ : undefined;
1167
+ }
897
1168
  async registerPastUserMessageImages(session, msg) {
898
1169
  if (!this.imageStore)
899
1170
  return [];
@@ -901,6 +1172,12 @@ export class BridgeWebSocketServer {
901
1172
  ? msg.images
902
1173
  : [];
903
1174
  const refs = [...existingImages];
1175
+ if (Array.isArray(msg.imagePaths)) {
1176
+ const paths = msg.imagePaths.filter((path) => typeof path === "string" && path.length > 0);
1177
+ if (paths.length > 0) {
1178
+ refs.push(...(await this.imageStore.registerImages(paths, session.projectPath)));
1179
+ }
1180
+ }
904
1181
  if (Array.isArray(msg.imageBase64)) {
905
1182
  for (const image of msg.imageBase64) {
906
1183
  const rawImage = image;
@@ -932,6 +1209,43 @@ export class BridgeWebSocketServer {
932
1209
  }
933
1210
  return refs;
934
1211
  }
1212
+ async registerPastToolResultImages(session, msg) {
1213
+ const existingImages = Array.isArray(msg.images)
1214
+ ? msg.images
1215
+ : [];
1216
+ if (!this.imageStore)
1217
+ return [...existingImages];
1218
+ const paths = new Set();
1219
+ if (Array.isArray(msg.imagePaths)) {
1220
+ for (const path of msg.imagePaths) {
1221
+ if (typeof path === "string" && path.length > 0)
1222
+ paths.add(path);
1223
+ }
1224
+ }
1225
+ const content = typeof msg.content === "string" ? msg.content : "";
1226
+ if (content) {
1227
+ for (const path of this.imageStore.extractImagePaths(content)) {
1228
+ paths.add(path);
1229
+ }
1230
+ }
1231
+ const refs = [...existingImages];
1232
+ if (paths.size > 0) {
1233
+ refs.push(...(await this.imageStore.registerImages([...paths], session.projectPath)));
1234
+ }
1235
+ if (Array.isArray(msg.imageBase64)) {
1236
+ for (const image of msg.imageBase64) {
1237
+ const rawImage = image;
1238
+ if (typeof rawImage.data !== "string" ||
1239
+ typeof rawImage.mimeType !== "string") {
1240
+ continue;
1241
+ }
1242
+ const ref = this.imageStore.registerFromBase64(rawImage.data, rawImage.mimeType);
1243
+ if (ref)
1244
+ refs.push(ref);
1245
+ }
1246
+ }
1247
+ return refs;
1248
+ }
935
1249
  async getCodexThreadHistoryFromRpc(threadId, projectPath) {
936
1250
  const activeProcess = this.getActiveCodexProcess();
937
1251
  const process = activeProcess ?? (await this.createStandaloneCodexProcess(projectPath));
@@ -940,23 +1254,27 @@ export class BridgeWebSocketServer {
940
1254
  const thread = await process.readThread(threadId, true);
941
1255
  return codexThreadToSessionHistory(thread);
942
1256
  }
1257
+ catch (err) {
1258
+ if (this.isCodexThreadNotMaterializedError(err))
1259
+ return [];
1260
+ throw err;
1261
+ }
943
1262
  finally {
944
1263
  if (isStandalone) {
945
1264
  process.stop();
946
1265
  }
947
1266
  }
948
1267
  }
1268
+ isCodexThreadNotMaterializedError(err) {
1269
+ const message = err instanceof Error ? err.message : String(err);
1270
+ return (message.includes("is not materialized yet") &&
1271
+ message.includes("includeTurns is unavailable before first user message"));
1272
+ }
949
1273
  async getCodexThreadHistory(threadId, projectPath) {
950
1274
  if (!this.getActiveCodexProcess() && process.env.NODE_ENV === "test") {
951
1275
  return getCodexSessionHistory(threadId);
952
1276
  }
953
- try {
954
- return await this.getCodexThreadHistoryFromRpc(threadId, projectPath);
955
- }
956
- catch (err) {
957
- console.warn(`[ws] thread/read failed for ${threadId}; falling back to JSONL: ${err instanceof Error ? err.message : String(err)}`);
958
- return getCodexSessionHistory(threadId);
959
- }
1277
+ return this.getCodexThreadHistoryFromRpc(threadId, projectPath);
960
1278
  }
961
1279
  codexHistoryFromThreadOrFallback(params) {
962
1280
  const messages = codexThreadToSessionHistory(params.thread);
@@ -1319,7 +1637,7 @@ export class BridgeWebSocketServer {
1319
1637
  }
1320
1638
  if (clientMessageId &&
1321
1639
  baseSeq !== undefined &&
1322
- this.hasInputConflictSince(session.id, baseSeq)) {
1640
+ this.hasInputConflictSince(session, baseSeq)) {
1323
1641
  this.send(ws, {
1324
1642
  type: "input_rejected",
1325
1643
  sessionId: session.id,
@@ -2319,6 +2637,12 @@ export class BridgeWebSocketServer {
2319
2637
  case "get_history": {
2320
2638
  const session = this.sessionManager.get(msg.sessionId);
2321
2639
  if (session) {
2640
+ if (session.provider === "codex") {
2641
+ const handled = await this.sendCodexCanonicalLegacyHistory(ws, msg.sessionId, session);
2642
+ if (handled) {
2643
+ break;
2644
+ }
2645
+ }
2322
2646
  const splitPastHistory = session.pastMessages && session.pastMessages.length > 0
2323
2647
  ? await this.splitPastHistoryMessages(session)
2324
2648
  : { pastMessages: [], historyMessages: [] };
@@ -2342,56 +2666,9 @@ export class BridgeWebSocketServer {
2342
2666
  sessionId: msg.sessionId,
2343
2667
  });
2344
2668
  if (session.provider === "codex") {
2345
- const item = session.codexQueuedInput;
2346
- this.sendConversationQueue(ws, {
2347
- type: "conversation_queue",
2348
- sessionId: msg.sessionId,
2349
- limit: 1,
2350
- items: item
2351
- ? [
2352
- {
2353
- itemId: item.itemId,
2354
- text: item.text,
2355
- createdAt: item.createdAt,
2356
- ...(item.updatedAt ? { updatedAt: item.updatedAt } : {}),
2357
- ...(item.imageCount
2358
- ? { imageCount: item.imageCount }
2359
- : {}),
2360
- ...(item.skills?.length ? { skills: item.skills } : {}),
2361
- ...(item.mentions?.length
2362
- ? { mentions: item.mentions }
2363
- : {}),
2364
- },
2365
- ]
2366
- : [],
2367
- });
2368
- }
2369
- // Send cached slash commands so the client can restore them even when
2370
- // the original init/supported_commands message was evicted from the
2371
- // in-memory history (MAX_HISTORY_PER_SESSION overflow).
2372
- const cached = this.sessionManager.getCachedCommands(session.projectPath);
2373
- if (cached &&
2374
- (cached.slashCommands.length > 0 ||
2375
- cached.skills.length > 0 ||
2376
- cached.apps.length > 0 ||
2377
- cached.plugins.length > 0)) {
2378
- this.send(ws, {
2379
- type: "system",
2380
- subtype: "supported_commands",
2381
- sessionId: msg.sessionId,
2382
- slashCommands: cached.slashCommands,
2383
- skills: cached.skills,
2384
- ...(cached.skillMetadata
2385
- ? { skillMetadata: cached.skillMetadata }
2386
- : {}),
2387
- apps: cached.apps,
2388
- ...(cached.appMetadata ? { appMetadata: cached.appMetadata } : {}),
2389
- plugins: cached.plugins,
2390
- ...(cached.pluginMetadata
2391
- ? { pluginMetadata: cached.pluginMetadata }
2392
- : {}),
2393
- });
2669
+ this.sendCodexQueueState(ws, msg.sessionId, session);
2394
2670
  }
2671
+ this.sendCachedCommands(ws, msg.sessionId, session);
2395
2672
  }
2396
2673
  else {
2397
2674
  this.send(ws, {
@@ -2403,8 +2680,40 @@ export class BridgeWebSocketServer {
2403
2680
  }
2404
2681
  case "get_history_delta": {
2405
2682
  const session = this.sessionManager.get(msg.sessionId);
2406
- const result = this.sessionManager.getHistorySince(msg.sessionId, msg.sinceSeq);
2407
- if (session && result) {
2683
+ if (session?.provider === "codex") {
2684
+ const result = this.sessionManager.getHistorySince(msg.sessionId, msg.sinceSeq);
2685
+ if (!result) {
2686
+ this.send(ws, {
2687
+ type: "error",
2688
+ message: `Session ${msg.sessionId} not found`,
2689
+ });
2690
+ break;
2691
+ }
2692
+ if (this.shouldResetCodexHistoryDelta(session, msg.sinceSeq, result.kind)) {
2693
+ await this.sendCodexCanonicalHistorySnapshot(ws, msg.sessionId, session);
2694
+ break;
2695
+ }
2696
+ this.send(ws, {
2697
+ type: result.kind === "snapshot" ? "history_snapshot" : "history_delta",
2698
+ sessionId: msg.sessionId,
2699
+ fromSeq: result.fromSeq,
2700
+ toSeq: result.toSeq,
2701
+ messages: result.entries,
2702
+ status: session.status,
2703
+ ...(result.kind === "snapshot" ? { reason: result.reason } : {}),
2704
+ });
2705
+ this.sendCodexQueueState(ws, msg.sessionId, session);
2706
+ break;
2707
+ }
2708
+ if (session) {
2709
+ const result = this.sessionManager.getHistorySince(msg.sessionId, msg.sinceSeq);
2710
+ if (!result) {
2711
+ this.send(ws, {
2712
+ type: "error",
2713
+ message: `Session ${msg.sessionId} not found`,
2714
+ });
2715
+ break;
2716
+ }
2408
2717
  if (session.pastMessages && session.pastMessages.length > 0) {
2409
2718
  const splitPastHistory = await this.splitPastHistoryMessages(session);
2410
2719
  if (splitPastHistory.pastMessages.length > 0) {
@@ -2427,31 +2736,6 @@ export class BridgeWebSocketServer {
2427
2736
  status: session.status,
2428
2737
  ...(result.kind === "snapshot" ? { reason: result.reason } : {}),
2429
2738
  });
2430
- if (session.provider === "codex") {
2431
- const item = session.codexQueuedInput;
2432
- this.sendConversationQueue(ws, {
2433
- type: "conversation_queue",
2434
- sessionId: msg.sessionId,
2435
- limit: 1,
2436
- items: item
2437
- ? [
2438
- {
2439
- itemId: item.itemId,
2440
- text: item.text,
2441
- createdAt: item.createdAt,
2442
- ...(item.updatedAt ? { updatedAt: item.updatedAt } : {}),
2443
- ...(item.imageCount
2444
- ? { imageCount: item.imageCount }
2445
- : {}),
2446
- ...(item.skills?.length ? { skills: item.skills } : {}),
2447
- ...(item.mentions?.length
2448
- ? { mentions: item.mentions }
2449
- : {}),
2450
- },
2451
- ]
2452
- : [],
2453
- });
2454
- }
2455
2739
  }
2456
2740
  else {
2457
2741
  this.send(ws, {
@@ -4528,21 +4812,37 @@ export class BridgeWebSocketServer {
4528
4812
  (await this.createStandaloneCodexProcess(msg.projectPath));
4529
4813
  const isStandalone = process !== this.getActiveCodexProcess();
4530
4814
  try {
4531
- const result = await process.listThreads({
4532
- limit: limit + offset,
4533
- cwd: msg.projectPath,
4534
- searchTerm: msg.searchQuery,
4535
- });
4536
4815
  const archivedIds = this.archiveStore.archivedIds();
4537
- const visibleThreads = result.data
4538
- .filter((thread) => !archivedIds.has(thread.id))
4539
- .filter((thread) => !msg.namedOnly || !!thread.name)
4540
- .slice(offset, offset + limit);
4541
- const indexedById = await getCodexSessionIndexMetadata(visibleThreads.map((thread) => thread.id));
4542
- const sessions = visibleThreads.map((thread) => codexThreadToRecentSession(thread, indexedById.get(thread.id)));
4816
+ const visibleThreads = [];
4817
+ let cursor;
4818
+ let hasServerMore = false;
4819
+ const targetCount = offset + limit;
4820
+ do {
4821
+ const request = {
4822
+ limit: Math.max(limit, 1),
4823
+ cwd: msg.projectPath,
4824
+ searchTerm: msg.searchQuery,
4825
+ sourceKinds: CODEX_RECENT_THREAD_SOURCE_KINDS,
4826
+ };
4827
+ if (cursor != null)
4828
+ request.cursor = cursor;
4829
+ const result = await process.listThreads(request);
4830
+ for (const thread of result.data) {
4831
+ if (archivedIds.has(thread.id))
4832
+ continue;
4833
+ if (msg.namedOnly && !thread.name)
4834
+ continue;
4835
+ visibleThreads.push(thread);
4836
+ }
4837
+ cursor = result.nextCursor;
4838
+ hasServerMore = cursor != null;
4839
+ } while (visibleThreads.length < targetCount && cursor != null);
4840
+ const pageThreads = visibleThreads.slice(offset, offset + limit);
4841
+ const indexedById = await getCodexSessionIndexMetadata(pageThreads.map((thread) => thread.id));
4842
+ const sessions = pageThreads.map((thread) => codexThreadToRecentSession(thread, indexedById.get(thread.id)));
4543
4843
  return {
4544
4844
  sessions,
4545
- hasMore: result.nextCursor != null,
4845
+ hasMore: hasServerMore || visibleThreads.length > offset + limit,
4546
4846
  };
4547
4847
  }
4548
4848
  finally {
@@ -4750,8 +5050,13 @@ export class BridgeWebSocketServer {
4750
5050
  return true;
4751
5051
  return (this.clientSupportedServerMessages.get(ws)?.has(type) ?? false);
4752
5052
  }
4753
- hasInputConflictSince(sessionId, baseSeq) {
4754
- const delta = this.sessionManager.getHistorySince(sessionId, baseSeq);
5053
+ hasInputConflictSince(session, baseSeq) {
5054
+ if (session.provider === "codex" &&
5055
+ typeof session.codexCanonicalHistoryRevision === "number" &&
5056
+ baseSeq < session.codexCanonicalHistoryRevision) {
5057
+ return true;
5058
+ }
5059
+ const delta = this.sessionManager.getHistorySince(session.id, baseSeq);
4755
5060
  if (!delta)
4756
5061
  return true;
4757
5062
  if (delta.kind === "snapshot")
@@ -4770,6 +5075,51 @@ export class BridgeWebSocketServer {
4770
5075
  return false;
4771
5076
  });
4772
5077
  }
5078
+ sendCodexQueueState(ws, sessionId, session) {
5079
+ const item = session.codexQueuedInput;
5080
+ this.sendConversationQueue(ws, {
5081
+ type: "conversation_queue",
5082
+ sessionId,
5083
+ limit: 1,
5084
+ items: item
5085
+ ? [
5086
+ {
5087
+ itemId: item.itemId,
5088
+ text: item.text,
5089
+ createdAt: item.createdAt,
5090
+ ...(item.updatedAt ? { updatedAt: item.updatedAt } : {}),
5091
+ ...(item.imageCount ? { imageCount: item.imageCount } : {}),
5092
+ ...(item.skills?.length ? { skills: item.skills } : {}),
5093
+ ...(item.mentions?.length ? { mentions: item.mentions } : {}),
5094
+ },
5095
+ ]
5096
+ : [],
5097
+ });
5098
+ }
5099
+ sendCachedCommands(ws, sessionId, session) {
5100
+ // Restore command metadata when the original init/supported_commands event
5101
+ // has fallen out of the bounded in-memory history.
5102
+ const cached = this.sessionManager.getCachedCommands(session.projectPath);
5103
+ if (!cached ||
5104
+ (cached.slashCommands.length === 0 &&
5105
+ cached.skills.length === 0 &&
5106
+ cached.apps.length === 0 &&
5107
+ cached.plugins.length === 0)) {
5108
+ return;
5109
+ }
5110
+ this.send(ws, {
5111
+ type: "system",
5112
+ subtype: "supported_commands",
5113
+ sessionId,
5114
+ slashCommands: cached.slashCommands,
5115
+ skills: cached.skills,
5116
+ ...(cached.skillMetadata ? { skillMetadata: cached.skillMetadata } : {}),
5117
+ apps: cached.apps,
5118
+ ...(cached.appMetadata ? { appMetadata: cached.appMetadata } : {}),
5119
+ plugins: cached.plugins,
5120
+ ...(cached.pluginMetadata ? { pluginMetadata: cached.pluginMetadata } : {}),
5121
+ });
5122
+ }
4773
5123
  sendConversationQueue(ws, msg) {
4774
5124
  if (!this.shouldSendToClient(ws, msg))
4775
5125
  return;