@ccpocket/bridge 1.68.1 → 1.69.0

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
@@ -5,7 +5,7 @@ import { lstat, readFile, readlink, stat, unlink } from "node:fs/promises";
5
5
  import { resolve, extname } from "node:path";
6
6
  import { promisify } from "node:util";
7
7
  import { WebSocketServer, WebSocket } from "ws";
8
- import { SessionManager, } from "./session.js";
8
+ import { SessionManager, MAX_HISTORY_PER_SESSION, } from "./session.js";
9
9
  import { SdkProcess, listAvailableClaudeModels, } from "./sdk-process.js";
10
10
  import { CodexProcess, } from "./codex-process.js";
11
11
  import { stopManagedCodexAppServers } from "./codex-transport.js";
@@ -90,8 +90,12 @@ const CODEX_USER_TURN_UUID_RE = /^codex:user-turn:(\d+)$/;
90
90
  const OPT_IN_SERVER_MESSAGES = new Set([
91
91
  "conversation_queue",
92
92
  "goal_state",
93
+ "guardian_approval",
93
94
  "prompt_history_status",
94
95
  ]);
96
+ function isRecord(value) {
97
+ return typeof value === "object" && value !== null && !Array.isArray(value);
98
+ }
95
99
  function parseCodexUserTurnOrdinal(uuid) {
96
100
  if (!uuid)
97
101
  return null;
@@ -979,89 +983,110 @@ export class BridgeWebSocketServer {
979
983
  message,
980
984
  }));
981
985
  this.applyCodexCanonicalHistoryBaseline(session, history, entries);
982
- return [...entries, ...session.historyEntries];
986
+ return entries;
983
987
  }
984
988
  applyCodexCanonicalHistoryBaseline(session, history, canonicalEntries) {
985
- const liveEntries = session.historyEntries.map((entry) => ({
989
+ const orderedRevision = session.codexOrderedHistoryRevision ?? 0;
990
+ const liveEntries = [
991
+ ...(session.codexOrderedHistoryEntries ?? []),
992
+ ...session.historyEntries.filter((entry) => entry.seq > orderedRevision),
993
+ ].map((entry) => ({
986
994
  seq: entry.seq,
987
995
  message: entry.message,
988
996
  }));
989
- const canonicalKeys = new Set();
997
+ const latestUserInput = session.codexLatestUserInput;
998
+ const latestUserKeys = latestUserInput
999
+ ? this.codexHistoryMessageIdentityKeys(latestUserInput)
1000
+ : [];
1001
+ if (latestUserInput &&
1002
+ !liveEntries.some((entry) => entry.message === latestUserInput ||
1003
+ (entry.message.type === "user_input" &&
1004
+ this.codexHistoryMessageIdentityKeys(entry.message).some((key) => latestUserKeys.includes(key))))) {
1005
+ const historySeq = latestUserInput
1006
+ .historySeq;
1007
+ liveEntries.unshift({
1008
+ seq: typeof historySeq === "number" ? historySeq : 0,
1009
+ message: latestUserInput,
1010
+ });
1011
+ }
990
1012
  const canonicalUserUuids = new Set();
991
1013
  for (const entry of canonicalEntries) {
992
- for (const key of this.codexHistoryMessageIdentityKeys(entry.message)) {
993
- canonicalKeys.add(key);
994
- }
995
1014
  const message = entry.message;
996
1015
  if (message.type === "user_input" && message.userMessageUuid) {
997
1016
  canonicalUserUuids.add(message.userMessageUuid);
998
1017
  }
999
1018
  }
1000
1019
  this.seedCodexCanonicalUserTurnUuidMap(session, history);
1001
- let nextSeq = canonicalEntries.at(-1)?.seq ?? 0;
1002
- const canonicalAssistantContentsByUser = new Map();
1003
- let canonicalUserKey = null;
1004
- for (const entry of canonicalEntries) {
1005
- if (entry.message.type === "user_input") {
1006
- canonicalUserKey = entry.message.userMessageUuid
1007
- ? `user:${entry.message.userMessageUuid}`
1008
- : null;
1009
- continue;
1020
+ const merged = [];
1021
+ let canonicalCursor = 0;
1022
+ let hasCanonicalAnchor = false;
1023
+ let canMatchAssistantContent = false;
1024
+ const appendCanonicalUntil = (endExclusive) => {
1025
+ while (canonicalCursor < endExclusive) {
1026
+ const message = canonicalEntries[canonicalCursor].message;
1027
+ merged.push({ message, retained: false });
1028
+ canonicalCursor += 1;
1029
+ hasCanonicalAnchor = true;
1030
+ if (message.type === "user_input") {
1031
+ canMatchAssistantContent = true;
1032
+ }
1010
1033
  }
1011
- if (entry.message.type !== "assistant" || !canonicalUserKey)
1012
- continue;
1013
- const contents = canonicalAssistantContentsByUser.get(canonicalUserKey);
1014
- const contentKey = this.historyValueKey(entry.message.message.content);
1015
- if (contents)
1016
- contents.push(contentKey);
1017
- else
1018
- canonicalAssistantContentsByUser.set(canonicalUserKey, [contentKey]);
1019
- }
1020
- let pendingCanonicalAssistantContents = [];
1021
- const retainedMessages = [];
1022
- const retainedEntries = [];
1023
- for (const entry of liveEntries) {
1024
- if (!this.shouldRetainCodexLiveHistoryMessage(entry.message))
1025
- continue;
1026
- const keys = this.codexHistoryMessageIdentityKeys(entry.message);
1027
- const matchesCanonical = keys.some((key) => canonicalKeys.has(key));
1028
- if (entry.message.type === "user_input") {
1029
- const userKey = entry.message.userMessageUuid
1030
- ? `user:${entry.message.userMessageUuid}`
1031
- : null;
1032
- pendingCanonicalAssistantContents =
1033
- matchesCanonical && userKey
1034
- ? [...(canonicalAssistantContentsByUser.get(userKey) ?? [])]
1035
- : [];
1036
- }
1037
- if (entry.message.type === "assistant") {
1038
- const contentKey = this.historyValueKey(entry.message.message.content);
1039
- const contentIndex = pendingCanonicalAssistantContents.indexOf(contentKey);
1040
- if (matchesCanonical) {
1041
- if (contentIndex !== -1) {
1042
- pendingCanonicalAssistantContents.splice(contentIndex, 1);
1043
- }
1044
- continue;
1034
+ };
1035
+ for (let liveIndex = 0; liveIndex < liveEntries.length; liveIndex++) {
1036
+ const liveEntry = liveEntries[liveIndex];
1037
+ const matchIndex = this.findCodexCanonicalMatchIndex(canonicalEntries, liveEntry.message, canonicalCursor, canMatchAssistantContent);
1038
+ if (matchIndex === -1) {
1039
+ let nextMatchIndex = -1;
1040
+ for (let nextLiveIndex = liveIndex + 1; nextLiveIndex < liveEntries.length; nextLiveIndex++) {
1041
+ nextMatchIndex = this.findCodexCanonicalMatchIndex(canonicalEntries, liveEntries[nextLiveIndex].message, canonicalCursor, canMatchAssistantContent);
1042
+ if (nextMatchIndex !== -1)
1043
+ break;
1045
1044
  }
1046
- const matchesCanonicalTurnAssistant = contentIndex !== -1;
1047
- if (matchesCanonicalTurnAssistant) {
1048
- pendingCanonicalAssistantContents.splice(contentIndex, 1);
1045
+ if (nextMatchIndex !== -1) {
1046
+ appendCanonicalUntil(nextMatchIndex);
1047
+ }
1048
+ else if (!hasCanonicalAnchor) {
1049
+ appendCanonicalUntil(canonicalEntries.length);
1050
+ }
1051
+ if (this.shouldRetainCodexLiveHistoryMessage(liveEntry.message)) {
1052
+ merged.push({ message: liveEntry.message, retained: true });
1049
1053
  }
1050
- if (matchesCanonicalTurnAssistant)
1051
- continue;
1052
- }
1053
- if (matchesCanonical)
1054
1054
  continue;
1055
- const seq = ++nextSeq;
1056
- entry.message.historySeq = seq;
1057
- retainedMessages.push(entry.message);
1058
- retainedEntries.push({ seq, message: entry.message });
1055
+ }
1056
+ appendCanonicalUntil(matchIndex + 1);
1059
1057
  }
1058
+ appendCanonicalUntil(canonicalEntries.length);
1059
+ const retainedMessages = [];
1060
+ const retainedEntries = [];
1061
+ let canonicalRevision = 0;
1062
+ const previousRevision = session.historyRevision;
1063
+ const requiresNewBaselineRevision = previousRevision > 0;
1064
+ const targetRevision = requiresNewBaselineRevision
1065
+ ? Math.max(merged.length, previousRevision + 1)
1066
+ : merged.length;
1067
+ const sequenceOffset = targetRevision - merged.length;
1068
+ const mergedEntries = merged.map(({ message, retained }, index) => {
1069
+ const seq = sequenceOffset + index + 1;
1070
+ if (retained) {
1071
+ message.historySeq = seq;
1072
+ retainedMessages.push(message);
1073
+ retainedEntries.push({ seq, message });
1074
+ }
1075
+ else {
1076
+ canonicalRevision = seq;
1077
+ }
1078
+ return { seq, message };
1079
+ });
1080
+ canonicalEntries.splice(0, canonicalEntries.length, ...mergedEntries);
1081
+ session.codexOrderedHistoryEntries =
1082
+ this.buildCodexOrderedHistoryWindow(mergedEntries);
1083
+ session.codexOrderedHistoryRevision = targetRevision;
1060
1084
  session.pastMessages = history;
1061
1085
  session.history = retainedMessages;
1062
1086
  session.historyEntries = retainedEntries;
1063
- session.historyRevision = nextSeq;
1064
- session.codexCanonicalHistoryRevision = canonicalEntries.at(-1)?.seq ?? 0;
1087
+ session.historyRevision = targetRevision;
1088
+ session.codexCanonicalHistoryRevision = canonicalRevision;
1089
+ session.codexHistoryResetRevision = targetRevision;
1065
1090
  session.historyLowWatermark =
1066
1091
  retainedEntries[0]?.seq ?? session.historyRevision + 1;
1067
1092
  if (session.pendingCodexUserEchoUuids) {
@@ -1075,6 +1100,52 @@ export class BridgeWebSocketServer {
1075
1100
  }
1076
1101
  }
1077
1102
  }
1103
+ buildCodexOrderedHistoryWindow(entries) {
1104
+ if (entries.length <= MAX_HISTORY_PER_SESSION) {
1105
+ return entries.map((entry) => ({
1106
+ seq: entry.seq,
1107
+ message: entry.message,
1108
+ }));
1109
+ }
1110
+ const tail = entries.slice(-MAX_HISTORY_PER_SESSION);
1111
+ if (tail.some((entry) => entry.message.type === "user_input")) {
1112
+ return tail;
1113
+ }
1114
+ let latestUser;
1115
+ for (let index = entries.length - 1; index >= 0; index--) {
1116
+ if (entries[index].message.type === "user_input") {
1117
+ latestUser = entries[index];
1118
+ break;
1119
+ }
1120
+ }
1121
+ return latestUser
1122
+ ? [latestUser, ...entries.slice(-(MAX_HISTORY_PER_SESSION - 1))]
1123
+ : tail;
1124
+ }
1125
+ findCodexCanonicalMatchIndex(canonicalEntries, liveMessage, start, allowAssistantContentMatch) {
1126
+ const liveKeys = this.codexHistoryMessageIdentityKeys(liveMessage);
1127
+ if (liveKeys.length > 0) {
1128
+ for (let index = start; index < canonicalEntries.length; index++) {
1129
+ const canonicalKeys = this.codexHistoryMessageIdentityKeys(canonicalEntries[index].message);
1130
+ if (liveKeys.some((key) => canonicalKeys.includes(key)))
1131
+ return index;
1132
+ }
1133
+ }
1134
+ if (!allowAssistantContentMatch || liveMessage.type !== "assistant") {
1135
+ return -1;
1136
+ }
1137
+ const liveContent = this.historyValueKey(liveMessage.message.content);
1138
+ for (let index = start; index < canonicalEntries.length; index++) {
1139
+ const canonicalMessage = canonicalEntries[index].message;
1140
+ if (canonicalMessage.type === "user_input")
1141
+ break;
1142
+ if (canonicalMessage.type === "assistant" &&
1143
+ this.historyValueKey(canonicalMessage.message.content) === liveContent) {
1144
+ return index;
1145
+ }
1146
+ }
1147
+ return -1;
1148
+ }
1078
1149
  seedCodexCanonicalUserTurnUuidMap(session, history) {
1079
1150
  if (session.provider !== "codex")
1080
1151
  return;
@@ -1151,8 +1222,8 @@ export class BridgeWebSocketServer {
1151
1222
  this.send(ws, {
1152
1223
  type: "history_snapshot",
1153
1224
  sessionId,
1154
- fromSeq: entries[0]?.seq ?? 1,
1155
- toSeq: entries.at(-1)?.seq ?? 0,
1225
+ fromSeq: entries[0]?.seq ?? session.historyRevision + 1,
1226
+ toSeq: session.historyRevision,
1156
1227
  messages: entries,
1157
1228
  status: session.status,
1158
1229
  reason: "reset",
@@ -1205,9 +1276,11 @@ export class BridgeWebSocketServer {
1205
1276
  shouldResetCodexHistoryDelta(session, sinceSeq, resultKind) {
1206
1277
  if (!this.codexThreadIdForSession(session))
1207
1278
  return false;
1208
- if (typeof session.codexCanonicalHistoryRevision !== "number")
1279
+ const resetRevision = session.codexHistoryResetRevision ??
1280
+ session.codexCanonicalHistoryRevision;
1281
+ if (typeof resetRevision !== "number")
1209
1282
  return true;
1210
- if (sinceSeq < session.codexCanonicalHistoryRevision)
1283
+ if (sinceSeq < resetRevision)
1211
1284
  return true;
1212
1285
  return resultKind === "snapshot";
1213
1286
  }
@@ -5147,14 +5220,18 @@ export class BridgeWebSocketServer {
5147
5220
  }
5148
5221
  }
5149
5222
  broadcastSessionMessageNow(sessionId, msg, exclude) {
5150
- const data = JSON.stringify({ ...msg, sessionId });
5151
5223
  for (const client of this.wss.clients) {
5152
5224
  if (client === exclude)
5153
5225
  continue;
5154
5226
  if (client.readyState === WebSocket.OPEN) {
5155
- if (!this.shouldSendToClient(client, msg))
5227
+ const outboundMsg = {
5228
+ ...msg,
5229
+ sessionId,
5230
+ };
5231
+ const compatibleMsg = this.prepareServerMessageForClient(client, outboundMsg);
5232
+ if (!compatibleMsg)
5156
5233
  continue;
5157
- client.send(data);
5234
+ client.send(JSON.stringify(compatibleMsg));
5158
5235
  }
5159
5236
  }
5160
5237
  }
@@ -5657,15 +5734,39 @@ export class BridgeWebSocketServer {
5657
5734
  }
5658
5735
  }
5659
5736
  broadcast(msg) {
5660
- const data = JSON.stringify(msg);
5661
5737
  for (const client of this.wss.clients) {
5662
5738
  if (client.readyState === WebSocket.OPEN) {
5663
- if (!this.shouldSendToClient(client, msg))
5739
+ const compatibleMsg = this.prepareServerMessageForClient(client, msg);
5740
+ if (!compatibleMsg)
5664
5741
  continue;
5665
- client.send(data);
5742
+ client.send(JSON.stringify(compatibleMsg));
5666
5743
  }
5667
5744
  }
5668
5745
  }
5746
+ prepareServerMessageForClient(ws, msg) {
5747
+ if (!this.shouldSendToClient(ws, msg))
5748
+ return null;
5749
+ if (!("messages" in msg) || !Array.isArray(msg.messages))
5750
+ return msg;
5751
+ const messages = msg.messages;
5752
+ if (msg.type === "history") {
5753
+ return {
5754
+ ...msg,
5755
+ messages: messages.filter((message) => isRecord(message) && this.shouldSendToClient(ws, message)),
5756
+ };
5757
+ }
5758
+ if (msg.type === "history_delta" || msg.type === "history_snapshot") {
5759
+ return {
5760
+ ...msg,
5761
+ messages: messages.filter((entry) => {
5762
+ if (!isRecord(entry) || !isRecord(entry.message))
5763
+ return true;
5764
+ return this.shouldSendToClient(ws, entry.message);
5765
+ }),
5766
+ };
5767
+ }
5768
+ return msg;
5769
+ }
5669
5770
  shouldSendToClient(ws, msg) {
5670
5771
  const type = typeof msg.type === "string" ? msg.type : "";
5671
5772
  if (!OPT_IN_SERVER_MESSAGES.has(type))
@@ -5673,9 +5774,11 @@ export class BridgeWebSocketServer {
5673
5774
  return (this.clientSupportedServerMessages.get(ws)?.has(type) ?? false);
5674
5775
  }
5675
5776
  hasInputConflictSince(session, baseSeq) {
5777
+ const codexResetRevision = session.codexHistoryResetRevision ??
5778
+ session.codexCanonicalHistoryRevision;
5676
5779
  if (session.provider === "codex" &&
5677
- typeof session.codexCanonicalHistoryRevision === "number" &&
5678
- baseSeq < session.codexCanonicalHistoryRevision) {
5780
+ typeof codexResetRevision === "number" &&
5781
+ baseSeq < codexResetRevision) {
5679
5782
  return true;
5680
5783
  }
5681
5784
  const delta = this.sessionManager.getHistorySince(session.id, baseSeq);
@@ -5757,19 +5860,20 @@ export class BridgeWebSocketServer {
5757
5860
  this.send(ws, msg);
5758
5861
  }
5759
5862
  send(ws, msg) {
5760
- if (!this.shouldSendToClient(ws, msg))
5863
+ const compatibleMsg = this.prepareServerMessageForClient(ws, msg);
5864
+ if (!compatibleMsg)
5761
5865
  return;
5762
- const sessionId = this.extractSessionIdFromServerMessage(msg);
5866
+ const sessionId = this.extractSessionIdFromServerMessage(compatibleMsg);
5763
5867
  if (sessionId) {
5764
5868
  this.recordDebugEvent(sessionId, {
5765
5869
  direction: "outgoing",
5766
5870
  channel: "ws",
5767
- type: String(msg.type ?? "unknown"),
5768
- detail: this.summarizeOutboundMessage(msg),
5871
+ type: String(compatibleMsg.type ?? "unknown"),
5872
+ detail: this.summarizeOutboundMessage(compatibleMsg),
5769
5873
  });
5770
5874
  }
5771
5875
  if (ws.readyState === WebSocket.OPEN) {
5772
- ws.send(JSON.stringify(msg));
5876
+ ws.send(JSON.stringify(compatibleMsg));
5773
5877
  }
5774
5878
  }
5775
5879
  /** Broadcast a gallery_new_image message to all connected clients. */