@ccpocket/bridge 1.86.2 → 1.88.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
@@ -1,3 +1,4 @@
1
+ import { performanceMessage } from "./performance-mode.js";
1
2
  import { createHash, randomUUID } from "node:crypto";
2
3
  import { execFile, execFileSync } from "node:child_process";
3
4
  import { existsSync } from "node:fs";
@@ -104,6 +105,7 @@ const CODEX_USER_TURN_UUID_RE = /^codex:user-turn:(\d+)$/;
104
105
  const OPT_IN_SERVER_MESSAGES = new Set([
105
106
  "conversation_queue",
106
107
  "goal_state",
108
+ "codex_recovery_state",
107
109
  "guardian_approval",
108
110
  "prompt_history_status",
109
111
  "projects",
@@ -571,6 +573,16 @@ export class BridgeWebSocketServer {
571
573
  deltaBatchMaxChars;
572
574
  deltaBatches = new Map();
573
575
  platform;
576
+ deliveryPreferences = new WeakMap();
577
+ activityTimes = new WeakMap();
578
+ performanceEnabled(ws, msg) {
579
+ const prefs = this.deliveryPreferences.get(ws);
580
+ if (!prefs || typeof msg.sessionId !== "string")
581
+ return false;
582
+ return Object.hasOwn(prefs.sessions, msg.sessionId)
583
+ ? prefs.sessions[msg.sessionId]
584
+ : prefs.enabled;
585
+ }
574
586
  clientSupportedServerMessages = new WeakMap();
575
587
  clientProtocolVersions = new WeakMap();
576
588
  rejectedProtocolClients = new WeakSet();
@@ -618,7 +630,15 @@ export class BridgeWebSocketServer {
618
630
  else {
619
631
  console.log("[ws] Push relay enabled (Firebase Anonymous Auth)");
620
632
  }
621
- this.wss = new WebSocketServer({ server });
633
+ this.wss = new WebSocketServer({
634
+ server,
635
+ perMessageDeflate: {
636
+ serverNoContextTakeover: true,
637
+ clientNoContextTakeover: true,
638
+ threshold: 1024,
639
+ zlibDeflateOptions: { level: 3 },
640
+ },
641
+ });
622
642
  this.sessionManager = new SessionManager((sessionId, msg) => {
623
643
  this.broadcastSessionMessage(sessionId, msg);
624
644
  }, imageStore, galleryStore,
@@ -1954,6 +1974,13 @@ export class BridgeWebSocketServer {
1954
1974
  }
1955
1975
  this.clientProtocolVersions.set(ws, selectedProtocolVersion);
1956
1976
  this.clientSupportedServerMessages.set(ws, new Set(msg.supportedServerMessages ?? []));
1977
+ this.deliveryPreferences.set(ws, {
1978
+ enabled: msg.performanceMode ?? false,
1979
+ sessions: msg.sessionPerformanceModes ?? {},
1980
+ });
1981
+ if (msg.deliveryRevision !== undefined) {
1982
+ this.send(ws, { type: "performance_mode_state", deliveryRevision: msg.deliveryRevision });
1983
+ }
1957
1984
  this.sendPromptHistoryStatus(ws);
1958
1985
  return;
1959
1986
  }
@@ -3104,6 +3131,21 @@ export class BridgeWebSocketServer {
3104
3131
  console.log(`[ws] set_codex_speed(codex): serviceTier=${serviceTier}`);
3105
3132
  break;
3106
3133
  }
3134
+ case "set_codex_recovery":
3135
+ case "cancel_codex_recovery": {
3136
+ const session = this.resolveSession(msg.sessionId);
3137
+ if (!session || session.provider !== "codex") {
3138
+ this.send(ws, { type: "error", sessionId: msg.sessionId, errorCode: "codex_recovery_unsupported", message: "Automatic recovery requires an active Codex session." });
3139
+ break;
3140
+ }
3141
+ const process = session.process;
3142
+ if (msg.type === "set_codex_recovery")
3143
+ process.setRecoveryEnabled(msg.enabled);
3144
+ else
3145
+ process.cancelRecovery();
3146
+ this.send(ws, { type: "codex_recovery_state", sessionId: session.id, recovery: process.getRecoveryState() });
3147
+ break;
3148
+ }
3107
3149
  case "get_goal": {
3108
3150
  const session = this.resolveSession(msg.sessionId);
3109
3151
  if (!session || session.provider !== "codex") {
@@ -6549,6 +6591,7 @@ export class BridgeWebSocketServer {
6549
6591
  protocolVersion: BRIDGE_PROTOCOL_MAX_VERSION,
6550
6592
  minimumProtocolVersion: BRIDGE_PROTOCOL_MIN_VERSION,
6551
6593
  protocolCapabilities: [
6594
+ "performance_mode_v1",
6552
6595
  "project_request_correlation_v1",
6553
6596
  "session_context_v1",
6554
6597
  ],
@@ -6587,6 +6630,7 @@ export class BridgeWebSocketServer {
6587
6630
  protocolVersion: BRIDGE_PROTOCOL_MAX_VERSION,
6588
6631
  minimumProtocolVersion: BRIDGE_PROTOCOL_MIN_VERSION,
6589
6632
  protocolCapabilities: [
6633
+ "performance_mode_v1",
6590
6634
  "project_request_correlation_v1",
6591
6635
  "session_context_v1",
6592
6636
  ],
@@ -6620,12 +6664,17 @@ export class BridgeWebSocketServer {
6620
6664
  broadcastSessionMessage(sessionId, msg, exclude) {
6621
6665
  if (this.shouldBatchDelta(msg, exclude)) {
6622
6666
  this.trackSessionMessage(sessionId, msg);
6623
- const chunks = this.splitDeltaText(msg.text);
6667
+ let chunks;
6624
6668
  for (const client of this.wss.clients) {
6625
6669
  if (client.readyState !== WebSocket.OPEN)
6626
6670
  continue;
6627
6671
  if (!this.shouldSendToClient(client, msg))
6628
6672
  continue;
6673
+ if (msg.type === "thinking_delta" && this.performanceEnabled(client, { sessionId })) {
6674
+ this.send(client, { ...msg, sessionId });
6675
+ continue;
6676
+ }
6677
+ chunks ??= this.splitDeltaText(msg.text);
6629
6678
  this.queueDeltaForClient(client, sessionId, msg.type, chunks);
6630
6679
  }
6631
6680
  return;
@@ -6717,7 +6766,7 @@ export class BridgeWebSocketServer {
6717
6766
  if (client.readyState !== WebSocket.OPEN)
6718
6767
  return;
6719
6768
  for (const msg of batch.messages) {
6720
- client.send(JSON.stringify({ ...msg, sessionId }));
6769
+ this.send(client, { ...msg, sessionId });
6721
6770
  }
6722
6771
  }
6723
6772
  discardClientDeltaBatches(client) {
@@ -6858,9 +6907,9 @@ export class BridgeWebSocketServer {
6858
6907
  }
6859
6908
  }
6860
6909
  async listWorkspaceFilteredCodexThreads(msg, matchesWorkspace, limit) {
6861
- const activeProcess = this.getActiveCodexProcess();
6862
- const process = activeProcess ?? (await this.createStandaloneCodexProcess(undefined));
6863
- const isStandalone = activeProcess === null;
6910
+ // A busy app-server can be blocked on a large thread/read or a running turn.
6911
+ // Discovery must remain independent so one session cannot hide the list.
6912
+ const process = await this.createStandaloneCodexProcess(undefined);
6864
6913
  try {
6865
6914
  const archivedIds = this.archiveStore.archivedIds();
6866
6915
  const matchingThreads = [];
@@ -6892,12 +6941,19 @@ export class BridgeWebSocketServer {
6892
6941
  }
6893
6942
  cursor = result.nextCursor;
6894
6943
  } while (matchingThreads.length < limit && cursor != null);
6895
- const indexedById = await getCodexSessionIndexMetadata(matchingThreads.map((thread) => thread.id));
6896
- return matchingThreads.map((thread) => this.enrichRecentSessionWorkspace(codexThreadToRecentSession(thread, indexedById.get(thread.id))));
6944
+ const [indexedById, threadNames] = await Promise.all([
6945
+ getCodexSessionIndexMetadata(matchingThreads.map((thread) => thread.id)),
6946
+ loadCodexSessionNames(),
6947
+ ]);
6948
+ return matchingThreads.map((thread) => this.enrichRecentSessionWorkspace({
6949
+ ...codexThreadToRecentSession(thread, indexedById.get(thread.id)),
6950
+ ...(threadNames.get(thread.id)
6951
+ ? { name: threadNames.get(thread.id) }
6952
+ : {}),
6953
+ }));
6897
6954
  }
6898
6955
  finally {
6899
- if (isStandalone)
6900
- process.stop();
6956
+ process.stop();
6901
6957
  }
6902
6958
  }
6903
6959
  async listRecentSessionsRaw(msg) {
@@ -7319,9 +7375,7 @@ export class BridgeWebSocketServer {
7319
7375
  async listRecentCodexThreads(msg) {
7320
7376
  const limit = msg.limit ?? 20;
7321
7377
  const offset = msg.offset ?? 0;
7322
- const process = this.getActiveCodexProcess() ??
7323
- (await this.createStandaloneCodexProcess(msg.projectPath));
7324
- const isStandalone = process !== this.getActiveCodexProcess();
7378
+ const process = await this.createStandaloneCodexProcess(msg.projectPath);
7325
7379
  try {
7326
7380
  const archivedIds = this.archiveStore.archivedIds();
7327
7381
  const visibleThreads = [];
@@ -7349,17 +7403,23 @@ export class BridgeWebSocketServer {
7349
7403
  hasServerMore = cursor != null;
7350
7404
  } while (visibleThreads.length < targetCount && cursor != null);
7351
7405
  const pageThreads = visibleThreads.slice(offset, offset + limit);
7352
- const indexedById = await getCodexSessionIndexMetadata(pageThreads.map((thread) => thread.id));
7353
- const sessions = pageThreads.map((thread) => codexThreadToRecentSession(thread, indexedById.get(thread.id)));
7406
+ const [indexedById, threadNames] = await Promise.all([
7407
+ getCodexSessionIndexMetadata(pageThreads.map((thread) => thread.id)),
7408
+ loadCodexSessionNames(),
7409
+ ]);
7410
+ const sessions = pageThreads.map((thread) => ({
7411
+ ...codexThreadToRecentSession(thread, indexedById.get(thread.id)),
7412
+ ...(threadNames.get(thread.id)
7413
+ ? { name: threadNames.get(thread.id) }
7414
+ : {}),
7415
+ }));
7354
7416
  return {
7355
7417
  sessions,
7356
7418
  hasMore: hasServerMore || visibleThreads.length > offset + limit,
7357
7419
  };
7358
7420
  }
7359
7421
  finally {
7360
- if (isStandalone) {
7361
- process.stop();
7362
- }
7422
+ process.stop();
7363
7423
  }
7364
7424
  }
7365
7425
  async createStandaloneCodexProcess(projectPath) {
@@ -7623,6 +7683,28 @@ export class BridgeWebSocketServer {
7623
7683
  prepareServerMessageForClient(ws, msg) {
7624
7684
  if (!this.shouldSendToClient(ws, msg))
7625
7685
  return null;
7686
+ if (this.performanceEnabled(ws, msg)) {
7687
+ const projected = performanceMessage(msg);
7688
+ if (projected) {
7689
+ msg = projected;
7690
+ }
7691
+ else {
7692
+ const sessionId = msg.sessionId;
7693
+ const times = this.activityTimes.get(ws) ?? new Map();
7694
+ this.activityTimes.set(ws, times);
7695
+ const now = Date.now();
7696
+ if (now - (times.get(sessionId) ?? 0) < 1000)
7697
+ return null;
7698
+ times.set(sessionId, now);
7699
+ const historySeq = msg.historySeq;
7700
+ return {
7701
+ type: "session_activity",
7702
+ sessionId,
7703
+ historySeq: typeof historySeq === "number" ? historySeq : undefined,
7704
+ at: new Date(now).toISOString(),
7705
+ };
7706
+ }
7707
+ }
7626
7708
  if (!("messages" in msg) || !Array.isArray(msg.messages))
7627
7709
  return msg;
7628
7710
  const messages = msg.messages;
@@ -7699,6 +7781,9 @@ export class BridgeWebSocketServer {
7699
7781
  });
7700
7782
  }
7701
7783
  sendCodexGoalState(ws, sessionId, session) {
7784
+ const recovery = session.process.getRecoveryState?.();
7785
+ if (recovery)
7786
+ this.send(ws, { type: "codex_recovery_state", sessionId, recovery });
7702
7787
  if (session.codexGoal === undefined)
7703
7788
  return;
7704
7789
  this.send(ws, {