@ccpocket/bridge 1.68.0 → 1.68.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
@@ -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";
@@ -979,89 +979,110 @@ export class BridgeWebSocketServer {
979
979
  message,
980
980
  }));
981
981
  this.applyCodexCanonicalHistoryBaseline(session, history, entries);
982
- return [...entries, ...session.historyEntries];
982
+ return entries;
983
983
  }
984
984
  applyCodexCanonicalHistoryBaseline(session, history, canonicalEntries) {
985
- const liveEntries = session.historyEntries.map((entry) => ({
985
+ const orderedRevision = session.codexOrderedHistoryRevision ?? 0;
986
+ const liveEntries = [
987
+ ...(session.codexOrderedHistoryEntries ?? []),
988
+ ...session.historyEntries.filter((entry) => entry.seq > orderedRevision),
989
+ ].map((entry) => ({
986
990
  seq: entry.seq,
987
991
  message: entry.message,
988
992
  }));
989
- const canonicalKeys = new Set();
993
+ const latestUserInput = session.codexLatestUserInput;
994
+ const latestUserKeys = latestUserInput
995
+ ? this.codexHistoryMessageIdentityKeys(latestUserInput)
996
+ : [];
997
+ if (latestUserInput &&
998
+ !liveEntries.some((entry) => entry.message === latestUserInput ||
999
+ (entry.message.type === "user_input" &&
1000
+ this.codexHistoryMessageIdentityKeys(entry.message).some((key) => latestUserKeys.includes(key))))) {
1001
+ const historySeq = latestUserInput
1002
+ .historySeq;
1003
+ liveEntries.unshift({
1004
+ seq: typeof historySeq === "number" ? historySeq : 0,
1005
+ message: latestUserInput,
1006
+ });
1007
+ }
990
1008
  const canonicalUserUuids = new Set();
991
1009
  for (const entry of canonicalEntries) {
992
- for (const key of this.codexHistoryMessageIdentityKeys(entry.message)) {
993
- canonicalKeys.add(key);
994
- }
995
1010
  const message = entry.message;
996
1011
  if (message.type === "user_input" && message.userMessageUuid) {
997
1012
  canonicalUserUuids.add(message.userMessageUuid);
998
1013
  }
999
1014
  }
1000
1015
  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;
1016
+ const merged = [];
1017
+ let canonicalCursor = 0;
1018
+ let hasCanonicalAnchor = false;
1019
+ let canMatchAssistantContent = false;
1020
+ const appendCanonicalUntil = (endExclusive) => {
1021
+ while (canonicalCursor < endExclusive) {
1022
+ const message = canonicalEntries[canonicalCursor].message;
1023
+ merged.push({ message, retained: false });
1024
+ canonicalCursor += 1;
1025
+ hasCanonicalAnchor = true;
1026
+ if (message.type === "user_input") {
1027
+ canMatchAssistantContent = true;
1028
+ }
1010
1029
  }
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;
1030
+ };
1031
+ for (let liveIndex = 0; liveIndex < liveEntries.length; liveIndex++) {
1032
+ const liveEntry = liveEntries[liveIndex];
1033
+ const matchIndex = this.findCodexCanonicalMatchIndex(canonicalEntries, liveEntry.message, canonicalCursor, canMatchAssistantContent);
1034
+ if (matchIndex === -1) {
1035
+ let nextMatchIndex = -1;
1036
+ for (let nextLiveIndex = liveIndex + 1; nextLiveIndex < liveEntries.length; nextLiveIndex++) {
1037
+ nextMatchIndex = this.findCodexCanonicalMatchIndex(canonicalEntries, liveEntries[nextLiveIndex].message, canonicalCursor, canMatchAssistantContent);
1038
+ if (nextMatchIndex !== -1)
1039
+ break;
1045
1040
  }
1046
- const matchesCanonicalTurnAssistant = contentIndex !== -1;
1047
- if (matchesCanonicalTurnAssistant) {
1048
- pendingCanonicalAssistantContents.splice(contentIndex, 1);
1041
+ if (nextMatchIndex !== -1) {
1042
+ appendCanonicalUntil(nextMatchIndex);
1043
+ }
1044
+ else if (!hasCanonicalAnchor) {
1045
+ appendCanonicalUntil(canonicalEntries.length);
1046
+ }
1047
+ if (this.shouldRetainCodexLiveHistoryMessage(liveEntry.message)) {
1048
+ merged.push({ message: liveEntry.message, retained: true });
1049
1049
  }
1050
- if (matchesCanonicalTurnAssistant)
1051
- continue;
1052
- }
1053
- if (matchesCanonical)
1054
1050
  continue;
1055
- const seq = ++nextSeq;
1056
- entry.message.historySeq = seq;
1057
- retainedMessages.push(entry.message);
1058
- retainedEntries.push({ seq, message: entry.message });
1051
+ }
1052
+ appendCanonicalUntil(matchIndex + 1);
1059
1053
  }
1054
+ appendCanonicalUntil(canonicalEntries.length);
1055
+ const retainedMessages = [];
1056
+ const retainedEntries = [];
1057
+ let canonicalRevision = 0;
1058
+ const previousRevision = session.historyRevision;
1059
+ const requiresNewBaselineRevision = previousRevision > 0;
1060
+ const targetRevision = requiresNewBaselineRevision
1061
+ ? Math.max(merged.length, previousRevision + 1)
1062
+ : merged.length;
1063
+ const sequenceOffset = targetRevision - merged.length;
1064
+ const mergedEntries = merged.map(({ message, retained }, index) => {
1065
+ const seq = sequenceOffset + index + 1;
1066
+ if (retained) {
1067
+ message.historySeq = seq;
1068
+ retainedMessages.push(message);
1069
+ retainedEntries.push({ seq, message });
1070
+ }
1071
+ else {
1072
+ canonicalRevision = seq;
1073
+ }
1074
+ return { seq, message };
1075
+ });
1076
+ canonicalEntries.splice(0, canonicalEntries.length, ...mergedEntries);
1077
+ session.codexOrderedHistoryEntries =
1078
+ this.buildCodexOrderedHistoryWindow(mergedEntries);
1079
+ session.codexOrderedHistoryRevision = targetRevision;
1060
1080
  session.pastMessages = history;
1061
1081
  session.history = retainedMessages;
1062
1082
  session.historyEntries = retainedEntries;
1063
- session.historyRevision = nextSeq;
1064
- session.codexCanonicalHistoryRevision = canonicalEntries.at(-1)?.seq ?? 0;
1083
+ session.historyRevision = targetRevision;
1084
+ session.codexCanonicalHistoryRevision = canonicalRevision;
1085
+ session.codexHistoryResetRevision = targetRevision;
1065
1086
  session.historyLowWatermark =
1066
1087
  retainedEntries[0]?.seq ?? session.historyRevision + 1;
1067
1088
  if (session.pendingCodexUserEchoUuids) {
@@ -1075,6 +1096,52 @@ export class BridgeWebSocketServer {
1075
1096
  }
1076
1097
  }
1077
1098
  }
1099
+ buildCodexOrderedHistoryWindow(entries) {
1100
+ if (entries.length <= MAX_HISTORY_PER_SESSION) {
1101
+ return entries.map((entry) => ({
1102
+ seq: entry.seq,
1103
+ message: entry.message,
1104
+ }));
1105
+ }
1106
+ const tail = entries.slice(-MAX_HISTORY_PER_SESSION);
1107
+ if (tail.some((entry) => entry.message.type === "user_input")) {
1108
+ return tail;
1109
+ }
1110
+ let latestUser;
1111
+ for (let index = entries.length - 1; index >= 0; index--) {
1112
+ if (entries[index].message.type === "user_input") {
1113
+ latestUser = entries[index];
1114
+ break;
1115
+ }
1116
+ }
1117
+ return latestUser
1118
+ ? [latestUser, ...entries.slice(-(MAX_HISTORY_PER_SESSION - 1))]
1119
+ : tail;
1120
+ }
1121
+ findCodexCanonicalMatchIndex(canonicalEntries, liveMessage, start, allowAssistantContentMatch) {
1122
+ const liveKeys = this.codexHistoryMessageIdentityKeys(liveMessage);
1123
+ if (liveKeys.length > 0) {
1124
+ for (let index = start; index < canonicalEntries.length; index++) {
1125
+ const canonicalKeys = this.codexHistoryMessageIdentityKeys(canonicalEntries[index].message);
1126
+ if (liveKeys.some((key) => canonicalKeys.includes(key)))
1127
+ return index;
1128
+ }
1129
+ }
1130
+ if (!allowAssistantContentMatch || liveMessage.type !== "assistant") {
1131
+ return -1;
1132
+ }
1133
+ const liveContent = this.historyValueKey(liveMessage.message.content);
1134
+ for (let index = start; index < canonicalEntries.length; index++) {
1135
+ const canonicalMessage = canonicalEntries[index].message;
1136
+ if (canonicalMessage.type === "user_input")
1137
+ break;
1138
+ if (canonicalMessage.type === "assistant" &&
1139
+ this.historyValueKey(canonicalMessage.message.content) === liveContent) {
1140
+ return index;
1141
+ }
1142
+ }
1143
+ return -1;
1144
+ }
1078
1145
  seedCodexCanonicalUserTurnUuidMap(session, history) {
1079
1146
  if (session.provider !== "codex")
1080
1147
  return;
@@ -1151,8 +1218,8 @@ export class BridgeWebSocketServer {
1151
1218
  this.send(ws, {
1152
1219
  type: "history_snapshot",
1153
1220
  sessionId,
1154
- fromSeq: entries[0]?.seq ?? 1,
1155
- toSeq: entries.at(-1)?.seq ?? 0,
1221
+ fromSeq: entries[0]?.seq ?? session.historyRevision + 1,
1222
+ toSeq: session.historyRevision,
1156
1223
  messages: entries,
1157
1224
  status: session.status,
1158
1225
  reason: "reset",
@@ -1205,9 +1272,11 @@ export class BridgeWebSocketServer {
1205
1272
  shouldResetCodexHistoryDelta(session, sinceSeq, resultKind) {
1206
1273
  if (!this.codexThreadIdForSession(session))
1207
1274
  return false;
1208
- if (typeof session.codexCanonicalHistoryRevision !== "number")
1275
+ const resetRevision = session.codexHistoryResetRevision ??
1276
+ session.codexCanonicalHistoryRevision;
1277
+ if (typeof resetRevision !== "number")
1209
1278
  return true;
1210
- if (sinceSeq < session.codexCanonicalHistoryRevision)
1279
+ if (sinceSeq < resetRevision)
1211
1280
  return true;
1212
1281
  return resultKind === "snapshot";
1213
1282
  }
@@ -1620,9 +1689,6 @@ export class BridgeWebSocketServer {
1620
1689
  this.send(ws, this.buildPathNotAllowedError(additionalWritableRoots.deniedRoot));
1621
1690
  break;
1622
1691
  }
1623
- const cached = provider === "claude"
1624
- ? this.sessionManager.getCachedCommands(projectPath)
1625
- : undefined;
1626
1692
  const { sessionId, permissionMode: effectivePermissionMode, executionMode: effectiveExecutionMode, planMode: effectivePlanMode, usedFallback: autoFallbackUsed, } = provider === "claude"
1627
1693
  ? this.createClaudeSessionWithFallback({
1628
1694
  projectPath,
@@ -1687,6 +1753,7 @@ export class BridgeWebSocketServer {
1687
1753
  usedFallback: false,
1688
1754
  };
1689
1755
  const createdSession = this.sessionManager.get(sessionId);
1756
+ const cached = this.sessionManager.getCachedCommands(provider, createdSession?.worktreePath ?? projectPath);
1690
1757
  // Load saved session name from CLI storage (for resumed sessions)
1691
1758
  void this.loadAndSetSessionName(createdSession, provider, projectPath, msg.sessionId).then(() => {
1692
1759
  this.send(ws, this.buildSessionCreatedMessage({
@@ -3380,6 +3447,7 @@ export class BridgeWebSocketServer {
3380
3447
  : "default",
3381
3448
  });
3382
3449
  const createdSession = this.sessionManager.get(sessionId);
3450
+ const cached = this.sessionManager.getCachedCommands("codex", createdSession?.worktreePath ?? effectiveProjectPath);
3383
3451
  await this.loadAndSetSessionName(createdSession, "codex", effectiveProjectPath, sessionRefId);
3384
3452
  this.send(ws, this.buildSessionCreatedMessage({
3385
3453
  sessionId,
@@ -3394,6 +3462,23 @@ export class BridgeWebSocketServer {
3394
3462
  permissionMode: legacyPermissionMode,
3395
3463
  executionMode,
3396
3464
  planMode,
3465
+ ...(cached
3466
+ ? {
3467
+ slashCommands: cached.slashCommands,
3468
+ skills: cached.skills,
3469
+ ...(cached.skillMetadata
3470
+ ? { skillMetadata: cached.skillMetadata }
3471
+ : {}),
3472
+ apps: cached.apps,
3473
+ ...(cached.appMetadata
3474
+ ? { appMetadata: cached.appMetadata }
3475
+ : {}),
3476
+ plugins: cached.plugins,
3477
+ ...(cached.pluginMetadata
3478
+ ? { pluginMetadata: cached.pluginMetadata }
3479
+ : {}),
3480
+ }
3481
+ : {}),
3397
3482
  }));
3398
3483
  this.broadcastSessionList();
3399
3484
  this.debugEvents.set(sessionId, []);
@@ -3414,7 +3499,6 @@ export class BridgeWebSocketServer {
3414
3499
  break;
3415
3500
  }
3416
3501
  const claudeSessionId = sessionRefId;
3417
- const cached = this.sessionManager.getCachedCommands(resumeProjectPath);
3418
3502
  let pendingResumes = this.pendingClaudeResumeInputs.get(ws);
3419
3503
  if (!pendingResumes) {
3420
3504
  pendingResumes = new Map();
@@ -3469,6 +3553,7 @@ export class BridgeWebSocketServer {
3469
3553
  worktreeOptions: worktreeOpts,
3470
3554
  });
3471
3555
  const createdSession = this.sessionManager.get(sessionId);
3556
+ const cached = this.sessionManager.getCachedCommands("claude", createdSession?.worktreePath ?? resumeProjectPath);
3472
3557
  const finishResume = () => {
3473
3558
  this.send(ws, {
3474
3559
  ...this.buildSessionCreatedMessage({
@@ -5657,9 +5742,11 @@ export class BridgeWebSocketServer {
5657
5742
  return (this.clientSupportedServerMessages.get(ws)?.has(type) ?? false);
5658
5743
  }
5659
5744
  hasInputConflictSince(session, baseSeq) {
5745
+ const codexResetRevision = session.codexHistoryResetRevision ??
5746
+ session.codexCanonicalHistoryRevision;
5660
5747
  if (session.provider === "codex" &&
5661
- typeof session.codexCanonicalHistoryRevision === "number" &&
5662
- baseSeq < session.codexCanonicalHistoryRevision) {
5748
+ typeof codexResetRevision === "number" &&
5749
+ baseSeq < codexResetRevision) {
5663
5750
  return true;
5664
5751
  }
5665
5752
  const delta = this.sessionManager.getHistorySince(session.id, baseSeq);
@@ -5714,7 +5801,7 @@ export class BridgeWebSocketServer {
5714
5801
  sendCachedCommands(ws, sessionId, session) {
5715
5802
  // Restore command metadata when the original init/supported_commands event
5716
5803
  // has fallen out of the bounded in-memory history.
5717
- const cached = this.sessionManager.getCachedCommands(session.projectPath);
5804
+ const cached = this.sessionManager.getCachedCommands(session.provider, session.worktreePath ?? session.projectPath);
5718
5805
  if (!cached ||
5719
5806
  (cached.slashCommands.length === 0 &&
5720
5807
  cached.skills.length === 0 &&