@ccpocket/bridge 1.62.1 → 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",
@@ -213,6 +218,14 @@ function normalizeCodexPermissionsMode(value) {
213
218
  return undefined;
214
219
  }
215
220
  }
221
+ function sanitizeCodexModel(model) {
222
+ if (typeof model !== "string")
223
+ return undefined;
224
+ const normalized = model.trim();
225
+ if (!normalized || normalized === "codex")
226
+ return undefined;
227
+ return normalized;
228
+ }
216
229
  function codexSettingsFromPermissionsMode(mode) {
217
230
  switch (mode) {
218
231
  case "default":
@@ -841,37 +854,8 @@ export class BridgeWebSocketServer {
841
854
  pastMessages.push(raw);
842
855
  continue;
843
856
  }
844
- const paths = new Set();
845
- if (Array.isArray(msg.imagePaths)) {
846
- for (const path of msg.imagePaths) {
847
- if (typeof path === "string" && path.length > 0)
848
- paths.add(path);
849
- }
850
- }
851
857
  const content = typeof msg.content === "string" ? msg.content : "";
852
- if (this.imageStore && content) {
853
- for (const path of this.imageStore.extractImagePaths(content)) {
854
- paths.add(path);
855
- }
856
- }
857
- const images = this.imageStore && paths.size > 0
858
- ? await this.imageStore.registerImages([...paths], session.projectPath)
859
- : [];
860
- if (this.imageStore && Array.isArray(msg.imageBase64)) {
861
- for (const image of msg.imageBase64) {
862
- const rawImage = image;
863
- if (typeof rawImage.data !== "string"
864
- || typeof rawImage.mimeType !== "string") {
865
- continue;
866
- }
867
- const ref = this.imageStore.registerFromBase64(rawImage.data, rawImage.mimeType);
868
- if (ref)
869
- images.push(ref);
870
- }
871
- }
872
- const existingImages = Array.isArray(msg.images)
873
- ? msg.images
874
- : [];
858
+ const images = await this.registerPastToolResultImages(session, msg);
875
859
  pastMessages.push({
876
860
  role: "tool_result",
877
861
  toolUseId: typeof msg.toolUseId === "string"
@@ -879,13 +863,308 @@ export class BridgeWebSocketServer {
879
863
  : `past-tool-result-${pastMessages.length}`,
880
864
  content,
881
865
  ...(typeof msg.toolName === "string" ? { toolName: msg.toolName } : {}),
882
- ...(existingImages.length > 0 || images.length > 0
883
- ? { images: [...existingImages, ...images] }
884
- : {}),
866
+ ...(images.length > 0 ? { images } : {}),
885
867
  });
886
868
  }
887
869
  return { pastMessages, historyMessages };
888
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
+ }
889
1168
  async registerPastUserMessageImages(session, msg) {
890
1169
  if (!this.imageStore)
891
1170
  return [];
@@ -893,6 +1172,12 @@ export class BridgeWebSocketServer {
893
1172
  ? msg.images
894
1173
  : [];
895
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
+ }
896
1181
  if (Array.isArray(msg.imageBase64)) {
897
1182
  for (const image of msg.imageBase64) {
898
1183
  const rawImage = image;
@@ -924,6 +1209,43 @@ export class BridgeWebSocketServer {
924
1209
  }
925
1210
  return refs;
926
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
+ }
927
1249
  async getCodexThreadHistoryFromRpc(threadId, projectPath) {
928
1250
  const activeProcess = this.getActiveCodexProcess();
929
1251
  const process = activeProcess ?? (await this.createStandaloneCodexProcess(projectPath));
@@ -932,23 +1254,27 @@ export class BridgeWebSocketServer {
932
1254
  const thread = await process.readThread(threadId, true);
933
1255
  return codexThreadToSessionHistory(thread);
934
1256
  }
1257
+ catch (err) {
1258
+ if (this.isCodexThreadNotMaterializedError(err))
1259
+ return [];
1260
+ throw err;
1261
+ }
935
1262
  finally {
936
1263
  if (isStandalone) {
937
1264
  process.stop();
938
1265
  }
939
1266
  }
940
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
+ }
941
1273
  async getCodexThreadHistory(threadId, projectPath) {
942
1274
  if (!this.getActiveCodexProcess() && process.env.NODE_ENV === "test") {
943
1275
  return getCodexSessionHistory(threadId);
944
1276
  }
945
- try {
946
- return await this.getCodexThreadHistoryFromRpc(threadId, projectPath);
947
- }
948
- catch (err) {
949
- console.warn(`[ws] thread/read failed for ${threadId}; falling back to JSONL: ${err instanceof Error ? err.message : String(err)}`);
950
- return getCodexSessionHistory(threadId);
951
- }
1277
+ return this.getCodexThreadHistoryFromRpc(threadId, projectPath);
952
1278
  }
953
1279
  codexHistoryFromThreadOrFallback(params) {
954
1280
  const messages = codexThreadToSessionHistory(params.thread);
@@ -1311,7 +1637,7 @@ export class BridgeWebSocketServer {
1311
1637
  }
1312
1638
  if (clientMessageId &&
1313
1639
  baseSeq !== undefined &&
1314
- this.hasInputConflictSince(session.id, baseSeq)) {
1640
+ this.hasInputConflictSince(session, baseSeq)) {
1315
1641
  this.send(ws, {
1316
1642
  type: "input_rejected",
1317
1643
  sessionId: session.id,
@@ -1917,6 +2243,65 @@ export class BridgeWebSocketServer {
1917
2243
  });
1918
2244
  break;
1919
2245
  }
2246
+ case "set_codex_model": {
2247
+ const session = this.resolveSession(msg.sessionId);
2248
+ if (!session) {
2249
+ this.send(ws, { type: "error", message: "No active session." });
2250
+ return;
2251
+ }
2252
+ if (session.provider !== "codex") {
2253
+ this.send(ws, {
2254
+ type: "error",
2255
+ message: "Model switching is only supported for Codex sessions.",
2256
+ errorCode: "set_codex_model_unsupported",
2257
+ });
2258
+ break;
2259
+ }
2260
+ const model = sanitizeCodexModel(msg.model);
2261
+ if (!model) {
2262
+ this.send(ws, {
2263
+ type: "error",
2264
+ message: `Invalid Codex model: ${msg.model}`,
2265
+ errorCode: "set_codex_model_rejected",
2266
+ });
2267
+ break;
2268
+ }
2269
+ const modelReasoningEffort = msg.modelReasoningEffort;
2270
+ const currentModel = sanitizeCodexModel(session.codexSettings?.model);
2271
+ const currentEffort = session.codexSettings?.modelReasoningEffort;
2272
+ if (model === currentModel && modelReasoningEffort === currentEffort) {
2273
+ break;
2274
+ }
2275
+ const process = session.process;
2276
+ process.setModel(model, modelReasoningEffort);
2277
+ session.codexSettings = {
2278
+ ...(session.codexSettings ?? {}),
2279
+ model,
2280
+ ...(modelReasoningEffort !== undefined
2281
+ ? { modelReasoningEffort }
2282
+ : {}),
2283
+ };
2284
+ session.lastActivityAt = new Date();
2285
+ this.broadcast({
2286
+ type: "system",
2287
+ subtype: "set_codex_model",
2288
+ sessionId: session.id,
2289
+ provider: "codex",
2290
+ model,
2291
+ ...(modelReasoningEffort !== undefined
2292
+ ? { modelReasoningEffort }
2293
+ : {}),
2294
+ });
2295
+ this.broadcastSessionList();
2296
+ this.recordDebugEvent(session.id, {
2297
+ direction: "internal",
2298
+ channel: "bridge",
2299
+ type: "codex_model_changed",
2300
+ detail: `model=${model} effort=${modelReasoningEffort ?? ""}`,
2301
+ });
2302
+ console.log(`[ws] set_codex_model(codex): model=${model} effort=${modelReasoningEffort ?? ""}`);
2303
+ break;
2304
+ }
1920
2305
  case "set_sandbox_mode": {
1921
2306
  if (this.failSetSandboxMode) {
1922
2307
  this.send(ws, {
@@ -2252,6 +2637,12 @@ export class BridgeWebSocketServer {
2252
2637
  case "get_history": {
2253
2638
  const session = this.sessionManager.get(msg.sessionId);
2254
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
+ }
2255
2646
  const splitPastHistory = session.pastMessages && session.pastMessages.length > 0
2256
2647
  ? await this.splitPastHistoryMessages(session)
2257
2648
  : { pastMessages: [], historyMessages: [] };
@@ -2275,56 +2666,9 @@ export class BridgeWebSocketServer {
2275
2666
  sessionId: msg.sessionId,
2276
2667
  });
2277
2668
  if (session.provider === "codex") {
2278
- const item = session.codexQueuedInput;
2279
- this.sendConversationQueue(ws, {
2280
- type: "conversation_queue",
2281
- sessionId: msg.sessionId,
2282
- limit: 1,
2283
- items: item
2284
- ? [
2285
- {
2286
- itemId: item.itemId,
2287
- text: item.text,
2288
- createdAt: item.createdAt,
2289
- ...(item.updatedAt ? { updatedAt: item.updatedAt } : {}),
2290
- ...(item.imageCount
2291
- ? { imageCount: item.imageCount }
2292
- : {}),
2293
- ...(item.skills?.length ? { skills: item.skills } : {}),
2294
- ...(item.mentions?.length
2295
- ? { mentions: item.mentions }
2296
- : {}),
2297
- },
2298
- ]
2299
- : [],
2300
- });
2301
- }
2302
- // Send cached slash commands so the client can restore them even when
2303
- // the original init/supported_commands message was evicted from the
2304
- // in-memory history (MAX_HISTORY_PER_SESSION overflow).
2305
- const cached = this.sessionManager.getCachedCommands(session.projectPath);
2306
- if (cached &&
2307
- (cached.slashCommands.length > 0 ||
2308
- cached.skills.length > 0 ||
2309
- cached.apps.length > 0 ||
2310
- cached.plugins.length > 0)) {
2311
- this.send(ws, {
2312
- type: "system",
2313
- subtype: "supported_commands",
2314
- sessionId: msg.sessionId,
2315
- slashCommands: cached.slashCommands,
2316
- skills: cached.skills,
2317
- ...(cached.skillMetadata
2318
- ? { skillMetadata: cached.skillMetadata }
2319
- : {}),
2320
- apps: cached.apps,
2321
- ...(cached.appMetadata ? { appMetadata: cached.appMetadata } : {}),
2322
- plugins: cached.plugins,
2323
- ...(cached.pluginMetadata
2324
- ? { pluginMetadata: cached.pluginMetadata }
2325
- : {}),
2326
- });
2669
+ this.sendCodexQueueState(ws, msg.sessionId, session);
2327
2670
  }
2671
+ this.sendCachedCommands(ws, msg.sessionId, session);
2328
2672
  }
2329
2673
  else {
2330
2674
  this.send(ws, {
@@ -2336,8 +2680,40 @@ export class BridgeWebSocketServer {
2336
2680
  }
2337
2681
  case "get_history_delta": {
2338
2682
  const session = this.sessionManager.get(msg.sessionId);
2339
- const result = this.sessionManager.getHistorySince(msg.sessionId, msg.sinceSeq);
2340
- 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
+ }
2341
2717
  if (session.pastMessages && session.pastMessages.length > 0) {
2342
2718
  const splitPastHistory = await this.splitPastHistoryMessages(session);
2343
2719
  if (splitPastHistory.pastMessages.length > 0) {
@@ -2360,31 +2736,6 @@ export class BridgeWebSocketServer {
2360
2736
  status: session.status,
2361
2737
  ...(result.kind === "snapshot" ? { reason: result.reason } : {}),
2362
2738
  });
2363
- if (session.provider === "codex") {
2364
- const item = session.codexQueuedInput;
2365
- this.sendConversationQueue(ws, {
2366
- type: "conversation_queue",
2367
- sessionId: msg.sessionId,
2368
- limit: 1,
2369
- items: item
2370
- ? [
2371
- {
2372
- itemId: item.itemId,
2373
- text: item.text,
2374
- createdAt: item.createdAt,
2375
- ...(item.updatedAt ? { updatedAt: item.updatedAt } : {}),
2376
- ...(item.imageCount
2377
- ? { imageCount: item.imageCount }
2378
- : {}),
2379
- ...(item.skills?.length ? { skills: item.skills } : {}),
2380
- ...(item.mentions?.length
2381
- ? { mentions: item.mentions }
2382
- : {}),
2383
- },
2384
- ]
2385
- : [],
2386
- });
2387
- }
2388
2739
  }
2389
2740
  else {
2390
2741
  this.send(ws, {
@@ -4461,21 +4812,37 @@ export class BridgeWebSocketServer {
4461
4812
  (await this.createStandaloneCodexProcess(msg.projectPath));
4462
4813
  const isStandalone = process !== this.getActiveCodexProcess();
4463
4814
  try {
4464
- const result = await process.listThreads({
4465
- limit: limit + offset,
4466
- cwd: msg.projectPath,
4467
- searchTerm: msg.searchQuery,
4468
- });
4469
4815
  const archivedIds = this.archiveStore.archivedIds();
4470
- const visibleThreads = result.data
4471
- .filter((thread) => !archivedIds.has(thread.id))
4472
- .filter((thread) => !msg.namedOnly || !!thread.name)
4473
- .slice(offset, offset + limit);
4474
- const indexedById = await getCodexSessionIndexMetadata(visibleThreads.map((thread) => thread.id));
4475
- 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)));
4476
4843
  return {
4477
4844
  sessions,
4478
- hasMore: result.nextCursor != null,
4845
+ hasMore: hasServerMore || visibleThreads.length > offset + limit,
4479
4846
  };
4480
4847
  }
4481
4848
  finally {
@@ -4683,8 +5050,13 @@ export class BridgeWebSocketServer {
4683
5050
  return true;
4684
5051
  return (this.clientSupportedServerMessages.get(ws)?.has(type) ?? false);
4685
5052
  }
4686
- hasInputConflictSince(sessionId, baseSeq) {
4687
- 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);
4688
5060
  if (!delta)
4689
5061
  return true;
4690
5062
  if (delta.kind === "snapshot")
@@ -4703,6 +5075,51 @@ export class BridgeWebSocketServer {
4703
5075
  return false;
4704
5076
  });
4705
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
+ }
4706
5123
  sendConversationQueue(ws, msg) {
4707
5124
  if (!this.shouldSendToClient(ws, msg))
4708
5125
  return;