@ccpocket/bridge 1.63.0 → 1.63.3
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/codex-process.d.ts +3 -0
- package/dist/codex-process.js +6 -0
- package/dist/codex-process.js.map +1 -1
- package/dist/parser.d.ts +2 -0
- package/dist/parser.js.map +1 -1
- package/dist/session.d.ts +5 -0
- package/dist/session.js +41 -0
- package/dist/session.js.map +1 -1
- package/dist/sessions-index.d.ts +2 -0
- package/dist/sessions-index.js +48 -5
- package/dist/sessions-index.js.map +1 -1
- package/dist/websocket.d.ts +21 -0
- package/dist/websocket.js +555 -137
- package/dist/websocket.js.map +1 -1
- package/package.json +1 -1
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",
|
|
@@ -357,6 +362,28 @@ function codexThreadToRecentSession(thread, indexed) {
|
|
|
357
362
|
...(indexed?.codexSettings ? { codexSettings: indexed.codexSettings } : {}),
|
|
358
363
|
};
|
|
359
364
|
}
|
|
365
|
+
function recentSessionModifiedTime(session) {
|
|
366
|
+
const modified = session?.modified;
|
|
367
|
+
return typeof modified === "string" ? new Date(modified).getTime() || 0 : 0;
|
|
368
|
+
}
|
|
369
|
+
function recentSessionDedupeKey(session, fallbackIndex) {
|
|
370
|
+
const value = session;
|
|
371
|
+
return typeof value.provider === "string" && typeof value.sessionId === "string"
|
|
372
|
+
? `${value.provider}:${value.sessionId}`
|
|
373
|
+
: `unknown:${fallbackIndex}`;
|
|
374
|
+
}
|
|
375
|
+
function mergeRecentSessionPages(sessions) {
|
|
376
|
+
const seen = new Set();
|
|
377
|
+
const merged = [];
|
|
378
|
+
for (const session of sessions) {
|
|
379
|
+
const key = recentSessionDedupeKey(session, merged.length);
|
|
380
|
+
if (seen.has(key))
|
|
381
|
+
continue;
|
|
382
|
+
seen.add(key);
|
|
383
|
+
merged.push(session);
|
|
384
|
+
}
|
|
385
|
+
return merged.sort((a, b) => recentSessionModifiedTime(b) - recentSessionModifiedTime(a));
|
|
386
|
+
}
|
|
360
387
|
export class BridgeWebSocketServer {
|
|
361
388
|
static MAX_DEBUG_EVENTS = 800;
|
|
362
389
|
static MAX_HISTORY_SUMMARY_ITEMS = 300;
|
|
@@ -849,37 +876,8 @@ export class BridgeWebSocketServer {
|
|
|
849
876
|
pastMessages.push(raw);
|
|
850
877
|
continue;
|
|
851
878
|
}
|
|
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
879
|
const content = typeof msg.content === "string" ? msg.content : "";
|
|
860
|
-
|
|
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
|
-
: [];
|
|
880
|
+
const images = await this.registerPastToolResultImages(session, msg);
|
|
883
881
|
pastMessages.push({
|
|
884
882
|
role: "tool_result",
|
|
885
883
|
toolUseId: typeof msg.toolUseId === "string"
|
|
@@ -887,13 +885,308 @@ export class BridgeWebSocketServer {
|
|
|
887
885
|
: `past-tool-result-${pastMessages.length}`,
|
|
888
886
|
content,
|
|
889
887
|
...(typeof msg.toolName === "string" ? { toolName: msg.toolName } : {}),
|
|
890
|
-
...(
|
|
891
|
-
? { images: [...existingImages, ...images] }
|
|
892
|
-
: {}),
|
|
888
|
+
...(images.length > 0 ? { images } : {}),
|
|
893
889
|
});
|
|
894
890
|
}
|
|
895
891
|
return { pastMessages, historyMessages };
|
|
896
892
|
}
|
|
893
|
+
codexThreadIdForSession(session) {
|
|
894
|
+
if (session.provider !== "codex")
|
|
895
|
+
return undefined;
|
|
896
|
+
if (session.claudeSessionId)
|
|
897
|
+
return session.claudeSessionId;
|
|
898
|
+
if (session.process instanceof CodexProcess) {
|
|
899
|
+
return session.process.sessionId ?? undefined;
|
|
900
|
+
}
|
|
901
|
+
return undefined;
|
|
902
|
+
}
|
|
903
|
+
async codexCanonicalHistoryEntries(session) {
|
|
904
|
+
const threadId = this.codexThreadIdForSession(session);
|
|
905
|
+
if (!threadId)
|
|
906
|
+
return null;
|
|
907
|
+
const history = await this.getCodexThreadHistory(threadId, session.projectPath);
|
|
908
|
+
session.claudeSessionId = threadId;
|
|
909
|
+
const messages = await this.codexHistoryToServerMessages(session, history);
|
|
910
|
+
const entries = messages.map((message, index) => ({
|
|
911
|
+
seq: index + 1,
|
|
912
|
+
message,
|
|
913
|
+
}));
|
|
914
|
+
this.applyCodexCanonicalHistoryBaseline(session, history, entries);
|
|
915
|
+
return [...entries, ...session.historyEntries];
|
|
916
|
+
}
|
|
917
|
+
applyCodexCanonicalHistoryBaseline(session, history, canonicalEntries) {
|
|
918
|
+
const liveEntries = session.historyEntries.map((entry) => ({
|
|
919
|
+
seq: entry.seq,
|
|
920
|
+
message: entry.message,
|
|
921
|
+
}));
|
|
922
|
+
const canonicalKeys = new Set();
|
|
923
|
+
const canonicalUserUuids = new Set();
|
|
924
|
+
for (const entry of canonicalEntries) {
|
|
925
|
+
for (const key of this.codexHistoryMessageIdentityKeys(entry.message)) {
|
|
926
|
+
canonicalKeys.add(key);
|
|
927
|
+
}
|
|
928
|
+
const message = entry.message;
|
|
929
|
+
if (message.type === "user_input" && message.userMessageUuid) {
|
|
930
|
+
canonicalUserUuids.add(message.userMessageUuid);
|
|
931
|
+
}
|
|
932
|
+
}
|
|
933
|
+
this.seedCodexCanonicalUserTurnUuidMap(session, history);
|
|
934
|
+
let nextSeq = canonicalEntries.at(-1)?.seq ?? 0;
|
|
935
|
+
const retainedMessages = [];
|
|
936
|
+
const retainedEntries = [];
|
|
937
|
+
for (const entry of liveEntries) {
|
|
938
|
+
if (!this.shouldRetainCodexLiveHistoryMessage(entry.message))
|
|
939
|
+
continue;
|
|
940
|
+
const keys = this.codexHistoryMessageIdentityKeys(entry.message);
|
|
941
|
+
if (keys.some((key) => canonicalKeys.has(key)))
|
|
942
|
+
continue;
|
|
943
|
+
const seq = ++nextSeq;
|
|
944
|
+
entry.message.historySeq = seq;
|
|
945
|
+
retainedMessages.push(entry.message);
|
|
946
|
+
retainedEntries.push({ seq, message: entry.message });
|
|
947
|
+
}
|
|
948
|
+
session.pastMessages = history;
|
|
949
|
+
session.history = retainedMessages;
|
|
950
|
+
session.historyEntries = retainedEntries;
|
|
951
|
+
session.historyRevision = nextSeq;
|
|
952
|
+
session.codexCanonicalHistoryRevision = canonicalEntries.at(-1)?.seq ?? 0;
|
|
953
|
+
session.historyLowWatermark =
|
|
954
|
+
retainedEntries[0]?.seq ?? session.historyRevision + 1;
|
|
955
|
+
if (session.pendingCodexUserEchoUuids) {
|
|
956
|
+
for (const uuid of canonicalUserUuids) {
|
|
957
|
+
session.pendingCodexUserEchoUuids.delete(uuid);
|
|
958
|
+
}
|
|
959
|
+
for (const uuid of [...session.pendingCodexUserEchoUuids]) {
|
|
960
|
+
const stillLive = retainedMessages.some((message) => message.type === "user_input" && message.userMessageUuid === uuid);
|
|
961
|
+
if (!stillLive)
|
|
962
|
+
session.pendingCodexUserEchoUuids.delete(uuid);
|
|
963
|
+
}
|
|
964
|
+
}
|
|
965
|
+
}
|
|
966
|
+
seedCodexCanonicalUserTurnUuidMap(session, history) {
|
|
967
|
+
if (session.provider !== "codex")
|
|
968
|
+
return;
|
|
969
|
+
for (const message of history) {
|
|
970
|
+
if (message.role !== "user" ||
|
|
971
|
+
!message.rawItemId ||
|
|
972
|
+
!message.uuid) {
|
|
973
|
+
continue;
|
|
974
|
+
}
|
|
975
|
+
session.codexUserTurnUuidByRawId ??= new Map();
|
|
976
|
+
session.codexUserTurnUuidByRawId.set(message.rawItemId, message.uuid);
|
|
977
|
+
}
|
|
978
|
+
}
|
|
979
|
+
shouldRetainCodexLiveHistoryMessage(message) {
|
|
980
|
+
return !(message.type === "system" &&
|
|
981
|
+
message.subtype === "tip");
|
|
982
|
+
}
|
|
983
|
+
codexHistoryMessageIdentityKeys(message) {
|
|
984
|
+
if (message.type === "user_input") {
|
|
985
|
+
return message.userMessageUuid ? [`user:${message.userMessageUuid}`] : [];
|
|
986
|
+
}
|
|
987
|
+
if (message.type === "assistant") {
|
|
988
|
+
const assistantId = message.messageUuid ?? message.message.id;
|
|
989
|
+
if (assistantId)
|
|
990
|
+
return [`assistant:${assistantId}`];
|
|
991
|
+
return [
|
|
992
|
+
`assistant-content:${this.historyValueKey(message.message.content)}`,
|
|
993
|
+
];
|
|
994
|
+
}
|
|
995
|
+
if (message.type === "tool_result") {
|
|
996
|
+
if (message.toolUseId) {
|
|
997
|
+
return [`tool-result:${message.toolUseId}:${message.toolName ?? ""}`];
|
|
998
|
+
}
|
|
999
|
+
return [
|
|
1000
|
+
`tool-result-content:${message.toolName ?? ""}:${message.content}`,
|
|
1001
|
+
];
|
|
1002
|
+
}
|
|
1003
|
+
return [];
|
|
1004
|
+
}
|
|
1005
|
+
historyValueKey(value) {
|
|
1006
|
+
try {
|
|
1007
|
+
return JSON.stringify(value);
|
|
1008
|
+
}
|
|
1009
|
+
catch {
|
|
1010
|
+
return String(value);
|
|
1011
|
+
}
|
|
1012
|
+
}
|
|
1013
|
+
async sendCodexCanonicalHistorySnapshot(ws, sessionId, session, options = {}) {
|
|
1014
|
+
try {
|
|
1015
|
+
const entries = await this.codexCanonicalHistoryEntries(session);
|
|
1016
|
+
if (!entries)
|
|
1017
|
+
return false;
|
|
1018
|
+
this.send(ws, {
|
|
1019
|
+
type: "history_snapshot",
|
|
1020
|
+
sessionId,
|
|
1021
|
+
fromSeq: entries[0]?.seq ?? 1,
|
|
1022
|
+
toSeq: entries.at(-1)?.seq ?? 0,
|
|
1023
|
+
messages: entries,
|
|
1024
|
+
status: session.status,
|
|
1025
|
+
reason: "reset",
|
|
1026
|
+
});
|
|
1027
|
+
this.sendCodexQueueState(ws, sessionId, session);
|
|
1028
|
+
if (options.includeCachedCommands) {
|
|
1029
|
+
this.sendCachedCommands(ws, sessionId, session);
|
|
1030
|
+
}
|
|
1031
|
+
return true;
|
|
1032
|
+
}
|
|
1033
|
+
catch (err) {
|
|
1034
|
+
this.send(ws, {
|
|
1035
|
+
type: "error",
|
|
1036
|
+
message: `Failed to read Codex thread history: ${err instanceof Error ? err.message : String(err)}`,
|
|
1037
|
+
});
|
|
1038
|
+
return true;
|
|
1039
|
+
}
|
|
1040
|
+
}
|
|
1041
|
+
async sendCodexCanonicalLegacyHistory(ws, sessionId, session) {
|
|
1042
|
+
try {
|
|
1043
|
+
const entries = await this.codexCanonicalHistoryEntries(session);
|
|
1044
|
+
if (!entries)
|
|
1045
|
+
return false;
|
|
1046
|
+
this.send(ws, {
|
|
1047
|
+
type: "history",
|
|
1048
|
+
messages: entries.map((entry) => entry.message),
|
|
1049
|
+
sessionId,
|
|
1050
|
+
});
|
|
1051
|
+
this.send(ws, {
|
|
1052
|
+
type: "status",
|
|
1053
|
+
status: session.status,
|
|
1054
|
+
sessionId,
|
|
1055
|
+
});
|
|
1056
|
+
this.sendCodexQueueState(ws, sessionId, session);
|
|
1057
|
+
this.sendCachedCommands(ws, sessionId, session);
|
|
1058
|
+
return true;
|
|
1059
|
+
}
|
|
1060
|
+
catch (err) {
|
|
1061
|
+
this.send(ws, {
|
|
1062
|
+
type: "error",
|
|
1063
|
+
message: `Failed to read Codex thread history: ${err instanceof Error ? err.message : String(err)}`,
|
|
1064
|
+
});
|
|
1065
|
+
return true;
|
|
1066
|
+
}
|
|
1067
|
+
}
|
|
1068
|
+
shouldResetCodexHistoryDelta(session, sinceSeq, resultKind) {
|
|
1069
|
+
if (!this.codexThreadIdForSession(session))
|
|
1070
|
+
return false;
|
|
1071
|
+
if (typeof session.codexCanonicalHistoryRevision !== "number")
|
|
1072
|
+
return true;
|
|
1073
|
+
if (sinceSeq < session.codexCanonicalHistoryRevision)
|
|
1074
|
+
return true;
|
|
1075
|
+
return resultKind === "snapshot";
|
|
1076
|
+
}
|
|
1077
|
+
async codexHistoryToServerMessages(session, history) {
|
|
1078
|
+
const messages = [];
|
|
1079
|
+
for (const item of history) {
|
|
1080
|
+
const converted = await this.codexHistoryMessageToServerMessage(session, item);
|
|
1081
|
+
if (converted)
|
|
1082
|
+
messages.push(converted);
|
|
1083
|
+
}
|
|
1084
|
+
return messages;
|
|
1085
|
+
}
|
|
1086
|
+
async codexHistoryMessageToServerMessage(session, item) {
|
|
1087
|
+
if (item.role === "user") {
|
|
1088
|
+
const images = await this.registerPastUserMessageImages(session, item);
|
|
1089
|
+
const text = this.sessionHistoryText(item.content);
|
|
1090
|
+
if (!text && item.imageCount == null && images.length === 0)
|
|
1091
|
+
return null;
|
|
1092
|
+
return {
|
|
1093
|
+
type: "user_input",
|
|
1094
|
+
text,
|
|
1095
|
+
...(item.uuid ? { userMessageUuid: item.uuid } : {}),
|
|
1096
|
+
...(item.isMeta ? { isMeta: true } : {}),
|
|
1097
|
+
...(item.imageCount != null || images.length > 0
|
|
1098
|
+
? { imageCount: Math.max(item.imageCount ?? 0, images.length) }
|
|
1099
|
+
: {}),
|
|
1100
|
+
...(item.timestamp ? { timestamp: item.timestamp } : {}),
|
|
1101
|
+
...(images.length > 0 ? { images } : {}),
|
|
1102
|
+
};
|
|
1103
|
+
}
|
|
1104
|
+
if (item.role === "assistant") {
|
|
1105
|
+
const content = this.sessionHistoryAssistantContent(item.content);
|
|
1106
|
+
if (content.length === 0)
|
|
1107
|
+
return null;
|
|
1108
|
+
const messageId = item.uuid ??
|
|
1109
|
+
this.sessionHistorySingleToolUseId(item.content) ??
|
|
1110
|
+
randomUUID();
|
|
1111
|
+
return {
|
|
1112
|
+
type: "assistant",
|
|
1113
|
+
message: {
|
|
1114
|
+
id: messageId,
|
|
1115
|
+
role: "assistant",
|
|
1116
|
+
content,
|
|
1117
|
+
model: session.codexSettings?.model ?? "",
|
|
1118
|
+
},
|
|
1119
|
+
...(item.uuid ? { messageUuid: item.uuid } : {}),
|
|
1120
|
+
};
|
|
1121
|
+
}
|
|
1122
|
+
const images = await this.registerPastToolResultImages(session, item);
|
|
1123
|
+
const content = this.sessionHistoryText(item.content);
|
|
1124
|
+
if (!content && images.length === 0)
|
|
1125
|
+
return null;
|
|
1126
|
+
return {
|
|
1127
|
+
type: "tool_result",
|
|
1128
|
+
toolUseId: item.toolUseId ?? item.uuid ?? `codex-history-tool-${randomUUID()}`,
|
|
1129
|
+
content,
|
|
1130
|
+
...(item.toolName ? { toolName: item.toolName } : {}),
|
|
1131
|
+
...(images.length > 0 ? { images } : {}),
|
|
1132
|
+
};
|
|
1133
|
+
}
|
|
1134
|
+
sessionHistoryText(content) {
|
|
1135
|
+
if (typeof content === "string")
|
|
1136
|
+
return content;
|
|
1137
|
+
if (!Array.isArray(content))
|
|
1138
|
+
return "";
|
|
1139
|
+
return content
|
|
1140
|
+
.map((item) => {
|
|
1141
|
+
if (item.type === "text" && typeof item.text === "string") {
|
|
1142
|
+
return item.text;
|
|
1143
|
+
}
|
|
1144
|
+
if (item.type === "thinking" && typeof item.thinking === "string") {
|
|
1145
|
+
return item.thinking;
|
|
1146
|
+
}
|
|
1147
|
+
return "";
|
|
1148
|
+
})
|
|
1149
|
+
.filter((text) => text.length > 0)
|
|
1150
|
+
.join("\n");
|
|
1151
|
+
}
|
|
1152
|
+
sessionHistoryAssistantContent(content) {
|
|
1153
|
+
if (typeof content === "string") {
|
|
1154
|
+
return content.trim().length > 0
|
|
1155
|
+
? [{ type: "text", text: content }]
|
|
1156
|
+
: [];
|
|
1157
|
+
}
|
|
1158
|
+
if (!Array.isArray(content))
|
|
1159
|
+
return [];
|
|
1160
|
+
const items = [];
|
|
1161
|
+
for (const item of content) {
|
|
1162
|
+
if (item.type === "text" && typeof item.text === "string") {
|
|
1163
|
+
items.push({ type: "text", text: item.text });
|
|
1164
|
+
}
|
|
1165
|
+
else if (item.type === "thinking" &&
|
|
1166
|
+
typeof item.thinking === "string") {
|
|
1167
|
+
items.push({ type: "thinking", thinking: item.thinking });
|
|
1168
|
+
}
|
|
1169
|
+
else if (item.type === "tool_use" &&
|
|
1170
|
+
typeof item.id === "string" &&
|
|
1171
|
+
typeof item.name === "string") {
|
|
1172
|
+
items.push({
|
|
1173
|
+
type: "tool_use",
|
|
1174
|
+
id: item.id,
|
|
1175
|
+
name: item.name,
|
|
1176
|
+
input: item.input ?? {},
|
|
1177
|
+
});
|
|
1178
|
+
}
|
|
1179
|
+
}
|
|
1180
|
+
return items;
|
|
1181
|
+
}
|
|
1182
|
+
sessionHistorySingleToolUseId(content) {
|
|
1183
|
+
if (!Array.isArray(content) || content.length !== 1)
|
|
1184
|
+
return undefined;
|
|
1185
|
+
const item = content[0];
|
|
1186
|
+
return item.type === "tool_use" && typeof item.id === "string"
|
|
1187
|
+
? item.id
|
|
1188
|
+
: undefined;
|
|
1189
|
+
}
|
|
897
1190
|
async registerPastUserMessageImages(session, msg) {
|
|
898
1191
|
if (!this.imageStore)
|
|
899
1192
|
return [];
|
|
@@ -901,6 +1194,12 @@ export class BridgeWebSocketServer {
|
|
|
901
1194
|
? msg.images
|
|
902
1195
|
: [];
|
|
903
1196
|
const refs = [...existingImages];
|
|
1197
|
+
if (Array.isArray(msg.imagePaths)) {
|
|
1198
|
+
const paths = msg.imagePaths.filter((path) => typeof path === "string" && path.length > 0);
|
|
1199
|
+
if (paths.length > 0) {
|
|
1200
|
+
refs.push(...(await this.imageStore.registerImages(paths, session.projectPath)));
|
|
1201
|
+
}
|
|
1202
|
+
}
|
|
904
1203
|
if (Array.isArray(msg.imageBase64)) {
|
|
905
1204
|
for (const image of msg.imageBase64) {
|
|
906
1205
|
const rawImage = image;
|
|
@@ -932,6 +1231,43 @@ export class BridgeWebSocketServer {
|
|
|
932
1231
|
}
|
|
933
1232
|
return refs;
|
|
934
1233
|
}
|
|
1234
|
+
async registerPastToolResultImages(session, msg) {
|
|
1235
|
+
const existingImages = Array.isArray(msg.images)
|
|
1236
|
+
? msg.images
|
|
1237
|
+
: [];
|
|
1238
|
+
if (!this.imageStore)
|
|
1239
|
+
return [...existingImages];
|
|
1240
|
+
const paths = new Set();
|
|
1241
|
+
if (Array.isArray(msg.imagePaths)) {
|
|
1242
|
+
for (const path of msg.imagePaths) {
|
|
1243
|
+
if (typeof path === "string" && path.length > 0)
|
|
1244
|
+
paths.add(path);
|
|
1245
|
+
}
|
|
1246
|
+
}
|
|
1247
|
+
const content = typeof msg.content === "string" ? msg.content : "";
|
|
1248
|
+
if (content) {
|
|
1249
|
+
for (const path of this.imageStore.extractImagePaths(content)) {
|
|
1250
|
+
paths.add(path);
|
|
1251
|
+
}
|
|
1252
|
+
}
|
|
1253
|
+
const refs = [...existingImages];
|
|
1254
|
+
if (paths.size > 0) {
|
|
1255
|
+
refs.push(...(await this.imageStore.registerImages([...paths], session.projectPath)));
|
|
1256
|
+
}
|
|
1257
|
+
if (Array.isArray(msg.imageBase64)) {
|
|
1258
|
+
for (const image of msg.imageBase64) {
|
|
1259
|
+
const rawImage = image;
|
|
1260
|
+
if (typeof rawImage.data !== "string" ||
|
|
1261
|
+
typeof rawImage.mimeType !== "string") {
|
|
1262
|
+
continue;
|
|
1263
|
+
}
|
|
1264
|
+
const ref = this.imageStore.registerFromBase64(rawImage.data, rawImage.mimeType);
|
|
1265
|
+
if (ref)
|
|
1266
|
+
refs.push(ref);
|
|
1267
|
+
}
|
|
1268
|
+
}
|
|
1269
|
+
return refs;
|
|
1270
|
+
}
|
|
935
1271
|
async getCodexThreadHistoryFromRpc(threadId, projectPath) {
|
|
936
1272
|
const activeProcess = this.getActiveCodexProcess();
|
|
937
1273
|
const process = activeProcess ?? (await this.createStandaloneCodexProcess(projectPath));
|
|
@@ -940,23 +1276,27 @@ export class BridgeWebSocketServer {
|
|
|
940
1276
|
const thread = await process.readThread(threadId, true);
|
|
941
1277
|
return codexThreadToSessionHistory(thread);
|
|
942
1278
|
}
|
|
1279
|
+
catch (err) {
|
|
1280
|
+
if (this.isCodexThreadNotMaterializedError(err))
|
|
1281
|
+
return [];
|
|
1282
|
+
throw err;
|
|
1283
|
+
}
|
|
943
1284
|
finally {
|
|
944
1285
|
if (isStandalone) {
|
|
945
1286
|
process.stop();
|
|
946
1287
|
}
|
|
947
1288
|
}
|
|
948
1289
|
}
|
|
1290
|
+
isCodexThreadNotMaterializedError(err) {
|
|
1291
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
1292
|
+
return (message.includes("is not materialized yet") &&
|
|
1293
|
+
message.includes("includeTurns is unavailable before first user message"));
|
|
1294
|
+
}
|
|
949
1295
|
async getCodexThreadHistory(threadId, projectPath) {
|
|
950
1296
|
if (!this.getActiveCodexProcess() && process.env.NODE_ENV === "test") {
|
|
951
1297
|
return getCodexSessionHistory(threadId);
|
|
952
1298
|
}
|
|
953
|
-
|
|
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
|
-
}
|
|
1299
|
+
return this.getCodexThreadHistoryFromRpc(threadId, projectPath);
|
|
960
1300
|
}
|
|
961
1301
|
codexHistoryFromThreadOrFallback(params) {
|
|
962
1302
|
const messages = codexThreadToSessionHistory(params.thread);
|
|
@@ -1319,7 +1659,7 @@ export class BridgeWebSocketServer {
|
|
|
1319
1659
|
}
|
|
1320
1660
|
if (clientMessageId &&
|
|
1321
1661
|
baseSeq !== undefined &&
|
|
1322
|
-
this.hasInputConflictSince(session
|
|
1662
|
+
this.hasInputConflictSince(session, baseSeq)) {
|
|
1323
1663
|
this.send(ws, {
|
|
1324
1664
|
type: "input_rejected",
|
|
1325
1665
|
sessionId: session.id,
|
|
@@ -2319,6 +2659,12 @@ export class BridgeWebSocketServer {
|
|
|
2319
2659
|
case "get_history": {
|
|
2320
2660
|
const session = this.sessionManager.get(msg.sessionId);
|
|
2321
2661
|
if (session) {
|
|
2662
|
+
if (session.provider === "codex") {
|
|
2663
|
+
const handled = await this.sendCodexCanonicalLegacyHistory(ws, msg.sessionId, session);
|
|
2664
|
+
if (handled) {
|
|
2665
|
+
break;
|
|
2666
|
+
}
|
|
2667
|
+
}
|
|
2322
2668
|
const splitPastHistory = session.pastMessages && session.pastMessages.length > 0
|
|
2323
2669
|
? await this.splitPastHistoryMessages(session)
|
|
2324
2670
|
: { pastMessages: [], historyMessages: [] };
|
|
@@ -2342,56 +2688,9 @@ export class BridgeWebSocketServer {
|
|
|
2342
2688
|
sessionId: msg.sessionId,
|
|
2343
2689
|
});
|
|
2344
2690
|
if (session.provider === "codex") {
|
|
2345
|
-
|
|
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
|
-
});
|
|
2691
|
+
this.sendCodexQueueState(ws, msg.sessionId, session);
|
|
2394
2692
|
}
|
|
2693
|
+
this.sendCachedCommands(ws, msg.sessionId, session);
|
|
2395
2694
|
}
|
|
2396
2695
|
else {
|
|
2397
2696
|
this.send(ws, {
|
|
@@ -2403,8 +2702,40 @@ export class BridgeWebSocketServer {
|
|
|
2403
2702
|
}
|
|
2404
2703
|
case "get_history_delta": {
|
|
2405
2704
|
const session = this.sessionManager.get(msg.sessionId);
|
|
2406
|
-
|
|
2407
|
-
|
|
2705
|
+
if (session?.provider === "codex") {
|
|
2706
|
+
const result = this.sessionManager.getHistorySince(msg.sessionId, msg.sinceSeq);
|
|
2707
|
+
if (!result) {
|
|
2708
|
+
this.send(ws, {
|
|
2709
|
+
type: "error",
|
|
2710
|
+
message: `Session ${msg.sessionId} not found`,
|
|
2711
|
+
});
|
|
2712
|
+
break;
|
|
2713
|
+
}
|
|
2714
|
+
if (this.shouldResetCodexHistoryDelta(session, msg.sinceSeq, result.kind)) {
|
|
2715
|
+
await this.sendCodexCanonicalHistorySnapshot(ws, msg.sessionId, session);
|
|
2716
|
+
break;
|
|
2717
|
+
}
|
|
2718
|
+
this.send(ws, {
|
|
2719
|
+
type: result.kind === "snapshot" ? "history_snapshot" : "history_delta",
|
|
2720
|
+
sessionId: msg.sessionId,
|
|
2721
|
+
fromSeq: result.fromSeq,
|
|
2722
|
+
toSeq: result.toSeq,
|
|
2723
|
+
messages: result.entries,
|
|
2724
|
+
status: session.status,
|
|
2725
|
+
...(result.kind === "snapshot" ? { reason: result.reason } : {}),
|
|
2726
|
+
});
|
|
2727
|
+
this.sendCodexQueueState(ws, msg.sessionId, session);
|
|
2728
|
+
break;
|
|
2729
|
+
}
|
|
2730
|
+
if (session) {
|
|
2731
|
+
const result = this.sessionManager.getHistorySince(msg.sessionId, msg.sinceSeq);
|
|
2732
|
+
if (!result) {
|
|
2733
|
+
this.send(ws, {
|
|
2734
|
+
type: "error",
|
|
2735
|
+
message: `Session ${msg.sessionId} not found`,
|
|
2736
|
+
});
|
|
2737
|
+
break;
|
|
2738
|
+
}
|
|
2408
2739
|
if (session.pastMessages && session.pastMessages.length > 0) {
|
|
2409
2740
|
const splitPastHistory = await this.splitPastHistoryMessages(session);
|
|
2410
2741
|
if (splitPastHistory.pastMessages.length > 0) {
|
|
@@ -2427,31 +2758,6 @@ export class BridgeWebSocketServer {
|
|
|
2427
2758
|
status: session.status,
|
|
2428
2759
|
...(result.kind === "snapshot" ? { reason: result.reason } : {}),
|
|
2429
2760
|
});
|
|
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
2761
|
}
|
|
2456
2762
|
else {
|
|
2457
2763
|
this.send(ws, {
|
|
@@ -4318,12 +4624,10 @@ export class BridgeWebSocketServer {
|
|
|
4318
4624
|
}
|
|
4319
4625
|
async listRecentSessions(msg) {
|
|
4320
4626
|
if (msg.provider === "codex") {
|
|
4321
|
-
|
|
4322
|
-
|
|
4323
|
-
|
|
4324
|
-
|
|
4325
|
-
console.warn(`[ws] Codex thread/list failed, falling back to rollout scan: ${err}`);
|
|
4326
|
-
}
|
|
4627
|
+
return this.listRecentCodexSessions(msg);
|
|
4628
|
+
}
|
|
4629
|
+
if (!msg.provider) {
|
|
4630
|
+
return this.listRecentAllProviderSessions(msg);
|
|
4327
4631
|
}
|
|
4328
4632
|
return getAllRecentSessions({
|
|
4329
4633
|
limit: msg.limit,
|
|
@@ -4335,6 +4639,54 @@ export class BridgeWebSocketServer {
|
|
|
4335
4639
|
archivedSessionIds: this.archiveStore.archivedIds(),
|
|
4336
4640
|
});
|
|
4337
4641
|
}
|
|
4642
|
+
async listRecentAllProviderSessions(msg) {
|
|
4643
|
+
const limit = msg.limit ?? 20;
|
|
4644
|
+
const offset = msg.offset ?? 0;
|
|
4645
|
+
const sourceLimit = offset + limit;
|
|
4646
|
+
const [scanResult, codexResult] = await Promise.all([
|
|
4647
|
+
getAllRecentSessions({
|
|
4648
|
+
limit: sourceLimit,
|
|
4649
|
+
offset: 0,
|
|
4650
|
+
projectPath: msg.projectPath,
|
|
4651
|
+
namedOnly: msg.namedOnly,
|
|
4652
|
+
searchQuery: msg.searchQuery,
|
|
4653
|
+
archivedSessionIds: this.archiveStore.archivedIds(),
|
|
4654
|
+
}),
|
|
4655
|
+
this.listRecentCodexSessions({
|
|
4656
|
+
...msg,
|
|
4657
|
+
provider: "codex",
|
|
4658
|
+
limit: sourceLimit,
|
|
4659
|
+
offset: 0,
|
|
4660
|
+
}),
|
|
4661
|
+
]);
|
|
4662
|
+
const merged = mergeRecentSessionPages([
|
|
4663
|
+
...codexResult.sessions,
|
|
4664
|
+
...scanResult.sessions,
|
|
4665
|
+
]);
|
|
4666
|
+
return {
|
|
4667
|
+
sessions: merged.slice(offset, offset + limit),
|
|
4668
|
+
hasMore: merged.length > offset + limit ||
|
|
4669
|
+
scanResult.hasMore ||
|
|
4670
|
+
codexResult.hasMore,
|
|
4671
|
+
};
|
|
4672
|
+
}
|
|
4673
|
+
async listRecentCodexSessions(msg) {
|
|
4674
|
+
try {
|
|
4675
|
+
return await this.listRecentCodexThreads(msg);
|
|
4676
|
+
}
|
|
4677
|
+
catch (err) {
|
|
4678
|
+
console.warn(`[ws] Codex thread/list failed, falling back to rollout scan: ${err}`);
|
|
4679
|
+
return getAllRecentSessions({
|
|
4680
|
+
limit: msg.limit,
|
|
4681
|
+
offset: msg.offset,
|
|
4682
|
+
projectPath: msg.projectPath,
|
|
4683
|
+
provider: "codex",
|
|
4684
|
+
namedOnly: msg.namedOnly,
|
|
4685
|
+
searchQuery: msg.searchQuery,
|
|
4686
|
+
archivedSessionIds: this.archiveStore.archivedIds(),
|
|
4687
|
+
});
|
|
4688
|
+
}
|
|
4689
|
+
}
|
|
4338
4690
|
async refreshCodexModels(projectPath) {
|
|
4339
4691
|
if (this.codexModelsRequest)
|
|
4340
4692
|
return this.codexModelsRequest;
|
|
@@ -4528,21 +4880,37 @@ export class BridgeWebSocketServer {
|
|
|
4528
4880
|
(await this.createStandaloneCodexProcess(msg.projectPath));
|
|
4529
4881
|
const isStandalone = process !== this.getActiveCodexProcess();
|
|
4530
4882
|
try {
|
|
4531
|
-
const result = await process.listThreads({
|
|
4532
|
-
limit: limit + offset,
|
|
4533
|
-
cwd: msg.projectPath,
|
|
4534
|
-
searchTerm: msg.searchQuery,
|
|
4535
|
-
});
|
|
4536
4883
|
const archivedIds = this.archiveStore.archivedIds();
|
|
4537
|
-
const visibleThreads =
|
|
4538
|
-
|
|
4539
|
-
|
|
4540
|
-
|
|
4541
|
-
|
|
4542
|
-
|
|
4884
|
+
const visibleThreads = [];
|
|
4885
|
+
let cursor;
|
|
4886
|
+
let hasServerMore = false;
|
|
4887
|
+
const targetCount = offset + limit;
|
|
4888
|
+
do {
|
|
4889
|
+
const request = {
|
|
4890
|
+
limit: Math.max(limit, 1),
|
|
4891
|
+
cwd: msg.projectPath,
|
|
4892
|
+
searchTerm: msg.searchQuery,
|
|
4893
|
+
sourceKinds: CODEX_RECENT_THREAD_SOURCE_KINDS,
|
|
4894
|
+
};
|
|
4895
|
+
if (cursor != null)
|
|
4896
|
+
request.cursor = cursor;
|
|
4897
|
+
const result = await process.listThreads(request);
|
|
4898
|
+
for (const thread of result.data) {
|
|
4899
|
+
if (archivedIds.has(thread.id))
|
|
4900
|
+
continue;
|
|
4901
|
+
if (msg.namedOnly && !thread.name)
|
|
4902
|
+
continue;
|
|
4903
|
+
visibleThreads.push(thread);
|
|
4904
|
+
}
|
|
4905
|
+
cursor = result.nextCursor;
|
|
4906
|
+
hasServerMore = cursor != null;
|
|
4907
|
+
} while (visibleThreads.length < targetCount && cursor != null);
|
|
4908
|
+
const pageThreads = visibleThreads.slice(offset, offset + limit);
|
|
4909
|
+
const indexedById = await getCodexSessionIndexMetadata(pageThreads.map((thread) => thread.id));
|
|
4910
|
+
const sessions = pageThreads.map((thread) => codexThreadToRecentSession(thread, indexedById.get(thread.id)));
|
|
4543
4911
|
return {
|
|
4544
4912
|
sessions,
|
|
4545
|
-
hasMore:
|
|
4913
|
+
hasMore: hasServerMore || visibleThreads.length > offset + limit,
|
|
4546
4914
|
};
|
|
4547
4915
|
}
|
|
4548
4916
|
finally {
|
|
@@ -4750,8 +5118,13 @@ export class BridgeWebSocketServer {
|
|
|
4750
5118
|
return true;
|
|
4751
5119
|
return (this.clientSupportedServerMessages.get(ws)?.has(type) ?? false);
|
|
4752
5120
|
}
|
|
4753
|
-
hasInputConflictSince(
|
|
4754
|
-
|
|
5121
|
+
hasInputConflictSince(session, baseSeq) {
|
|
5122
|
+
if (session.provider === "codex" &&
|
|
5123
|
+
typeof session.codexCanonicalHistoryRevision === "number" &&
|
|
5124
|
+
baseSeq < session.codexCanonicalHistoryRevision) {
|
|
5125
|
+
return true;
|
|
5126
|
+
}
|
|
5127
|
+
const delta = this.sessionManager.getHistorySince(session.id, baseSeq);
|
|
4755
5128
|
if (!delta)
|
|
4756
5129
|
return true;
|
|
4757
5130
|
if (delta.kind === "snapshot")
|
|
@@ -4770,6 +5143,51 @@ export class BridgeWebSocketServer {
|
|
|
4770
5143
|
return false;
|
|
4771
5144
|
});
|
|
4772
5145
|
}
|
|
5146
|
+
sendCodexQueueState(ws, sessionId, session) {
|
|
5147
|
+
const item = session.codexQueuedInput;
|
|
5148
|
+
this.sendConversationQueue(ws, {
|
|
5149
|
+
type: "conversation_queue",
|
|
5150
|
+
sessionId,
|
|
5151
|
+
limit: 1,
|
|
5152
|
+
items: item
|
|
5153
|
+
? [
|
|
5154
|
+
{
|
|
5155
|
+
itemId: item.itemId,
|
|
5156
|
+
text: item.text,
|
|
5157
|
+
createdAt: item.createdAt,
|
|
5158
|
+
...(item.updatedAt ? { updatedAt: item.updatedAt } : {}),
|
|
5159
|
+
...(item.imageCount ? { imageCount: item.imageCount } : {}),
|
|
5160
|
+
...(item.skills?.length ? { skills: item.skills } : {}),
|
|
5161
|
+
...(item.mentions?.length ? { mentions: item.mentions } : {}),
|
|
5162
|
+
},
|
|
5163
|
+
]
|
|
5164
|
+
: [],
|
|
5165
|
+
});
|
|
5166
|
+
}
|
|
5167
|
+
sendCachedCommands(ws, sessionId, session) {
|
|
5168
|
+
// Restore command metadata when the original init/supported_commands event
|
|
5169
|
+
// has fallen out of the bounded in-memory history.
|
|
5170
|
+
const cached = this.sessionManager.getCachedCommands(session.projectPath);
|
|
5171
|
+
if (!cached ||
|
|
5172
|
+
(cached.slashCommands.length === 0 &&
|
|
5173
|
+
cached.skills.length === 0 &&
|
|
5174
|
+
cached.apps.length === 0 &&
|
|
5175
|
+
cached.plugins.length === 0)) {
|
|
5176
|
+
return;
|
|
5177
|
+
}
|
|
5178
|
+
this.send(ws, {
|
|
5179
|
+
type: "system",
|
|
5180
|
+
subtype: "supported_commands",
|
|
5181
|
+
sessionId,
|
|
5182
|
+
slashCommands: cached.slashCommands,
|
|
5183
|
+
skills: cached.skills,
|
|
5184
|
+
...(cached.skillMetadata ? { skillMetadata: cached.skillMetadata } : {}),
|
|
5185
|
+
apps: cached.apps,
|
|
5186
|
+
...(cached.appMetadata ? { appMetadata: cached.appMetadata } : {}),
|
|
5187
|
+
plugins: cached.plugins,
|
|
5188
|
+
...(cached.pluginMetadata ? { pluginMetadata: cached.pluginMetadata } : {}),
|
|
5189
|
+
});
|
|
5190
|
+
}
|
|
4773
5191
|
sendConversationQueue(ws, msg) {
|
|
4774
5192
|
if (!this.shouldSendToClient(ws, msg))
|
|
4775
5193
|
return;
|