@ccpocket/bridge 1.43.1 → 1.44.1

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 { randomUUID } from "node:crypto";
1
2
  import { execFile, execFileSync } from "node:child_process";
2
3
  import { existsSync } from "node:fs";
3
4
  import { lstat, readFile, readlink, stat, unlink } from "node:fs/promises";
@@ -178,6 +179,7 @@ export class BridgeWebSocketServer {
178
179
  failSetPermissionMode = envFlagEnabled("BRIDGE_FAIL_SET_PERMISSION_MODE");
179
180
  failSetSandboxMode = envFlagEnabled("BRIDGE_FAIL_SET_SANDBOX_MODE");
180
181
  platform;
182
+ clientSupportedServerMessages = new WeakMap();
181
183
  constructor(options) {
182
184
  const { server, apiKey, allowedDirs, imageStore, galleryStore, projectHistory, debugTraceStore, recordingStore, firebaseAuth, promptHistoryBackup, platform, } = options;
183
185
  this.apiKey = apiKey ?? null;
@@ -477,6 +479,10 @@ export class BridgeWebSocketServer {
477
479
  });
478
480
  }
479
481
  async handleClientMessage(msg, ws) {
482
+ if (msg.type === "client_capabilities") {
483
+ this.clientSupportedServerMessages.set(ws, new Set(msg.supportedServerMessages ?? []));
484
+ return;
485
+ }
480
486
  const incomingSessionId = this.extractSessionIdFromClientMessage(msg);
481
487
  const isActiveRuntimeSession = incomingSessionId != null &&
482
488
  this.sessionManager.get(incomingSessionId) != null;
@@ -667,16 +673,8 @@ export class BridgeWebSocketServer {
667
673
  return;
668
674
  }
669
675
  const text = msg.text;
670
- // Codex: reject if the process is not waiting for input (turn-based, no internal queue)
671
- if (session.provider === "codex" &&
672
- !session.process.isWaitingForInput) {
673
- this.send(ws, {
674
- type: "input_rejected",
675
- sessionId: session.id,
676
- reason: "Process is busy",
677
- });
678
- break;
679
- }
676
+ const codexSkills = msg.skills ?? (msg.skill ? [msg.skill] : []);
677
+ const codexMentions = msg.mentions ?? [];
680
678
  // Snapshot busy state before dispatch. We prefer the actual enqueue
681
679
  // result returned by SdkProcess sendInput* below, but keep this as a
682
680
  // fallback for test doubles and async paths.
@@ -709,6 +707,50 @@ export class BridgeWebSocketServer {
709
707
  if (imageRefs.length === 0)
710
708
  imageRefs = undefined;
711
709
  }
710
+ if (session.provider === "codex" &&
711
+ !session.process.isWaitingForInput) {
712
+ if (session.codexQueuedInput) {
713
+ this.send(ws, {
714
+ type: "input_rejected",
715
+ sessionId: session.id,
716
+ reason: "Queue is full",
717
+ });
718
+ break;
719
+ }
720
+ const queued = this.sessionManager.queueCodexInput(session.id, {
721
+ itemId: randomUUID(),
722
+ text,
723
+ createdAt: new Date().toISOString(),
724
+ ...(images.length > 0 ? { imageCount: images.length, images } : {}),
725
+ ...(imageRefs ? { imageRefs } : {}),
726
+ ...(codexSkills.length > 0 ? { skills: codexSkills } : {}),
727
+ ...(codexMentions.length > 0 ? { mentions: codexMentions } : {}),
728
+ });
729
+ if (!queued) {
730
+ this.send(ws, {
731
+ type: "input_rejected",
732
+ sessionId: session.id,
733
+ reason: "Queue is full",
734
+ });
735
+ break;
736
+ }
737
+ if (images.length > 0 && this.galleryStore && session.projectPath) {
738
+ for (const img of images) {
739
+ this.galleryStore
740
+ .addImageFromBase64(img.base64, img.mimeType, session.projectPath, msg.sessionId)
741
+ .catch((err) => {
742
+ console.warn(`[ws] Failed to persist queued image to gallery: ${err}`);
743
+ });
744
+ }
745
+ }
746
+ this.send(ws, {
747
+ type: "input_ack",
748
+ sessionId: session.id,
749
+ queued: true,
750
+ });
751
+ this.broadcastSessionList();
752
+ break;
753
+ }
712
754
  session.history.push({
713
755
  type: "user_input",
714
756
  text,
@@ -734,13 +776,11 @@ export class BridgeWebSocketServer {
734
776
  queued: false,
735
777
  });
736
778
  const codexProc = session.process;
737
- const skills = msg.skills ?? (msg.skill ? [msg.skill] : []);
738
- const mentions = msg.mentions ?? [];
739
779
  if (images.length > 0) {
740
780
  codexProc.sendInputStructured(text, {
741
781
  images,
742
- skills,
743
- mentions,
782
+ skills: codexSkills,
783
+ mentions: codexMentions,
744
784
  });
745
785
  }
746
786
  else if (msg.imageId && this.galleryStore) {
@@ -750,22 +790,31 @@ export class BridgeWebSocketServer {
750
790
  if (imageData) {
751
791
  codexProc.sendInputStructured(text, {
752
792
  images: [imageData],
753
- skills,
754
- mentions,
793
+ skills: codexSkills,
794
+ mentions: codexMentions,
755
795
  });
756
796
  }
757
797
  else {
758
798
  console.warn(`[ws] Image not found: ${msg.imageId}`);
759
- codexProc.sendInputStructured(text, { skills, mentions });
799
+ codexProc.sendInputStructured(text, {
800
+ skills: codexSkills,
801
+ mentions: codexMentions,
802
+ });
760
803
  }
761
804
  })
762
805
  .catch((err) => {
763
806
  console.error(`[ws] Failed to load image: ${err}`);
764
- codexProc.sendInputStructured(text, { skills, mentions });
807
+ codexProc.sendInputStructured(text, {
808
+ skills: codexSkills,
809
+ mentions: codexMentions,
810
+ });
765
811
  });
766
812
  }
767
- else if (skills.length > 0 || mentions.length > 0) {
768
- codexProc.sendInputStructured(text, { skills, mentions });
813
+ else if (codexSkills.length > 0 || codexMentions.length > 0) {
814
+ codexProc.sendInputStructured(text, {
815
+ skills: codexSkills,
816
+ mentions: codexMentions,
817
+ });
769
818
  }
770
819
  else {
771
820
  codexProc.sendInput(text);
@@ -841,6 +890,69 @@ export class BridgeWebSocketServer {
841
890
  }
842
891
  break;
843
892
  }
893
+ case "update_queued_input": {
894
+ const session = this.resolveSession(msg.sessionId);
895
+ if (!session || session.provider !== "codex") {
896
+ this.send(ws, { type: "error", message: "No active Codex session." });
897
+ return;
898
+ }
899
+ if (!msg.text.trim()) {
900
+ this.send(ws, {
901
+ type: "error",
902
+ message: "Queued message cannot be empty.",
903
+ });
904
+ return;
905
+ }
906
+ const success = this.sessionManager.updateCodexQueuedInput(session.id, msg.itemId, msg.text, { skills: msg.skills ?? [], mentions: msg.mentions ?? [] });
907
+ if (!success) {
908
+ this.send(ws, {
909
+ type: "error",
910
+ message: "Queued message not found.",
911
+ errorCode: "queued_input_not_found",
912
+ });
913
+ return;
914
+ }
915
+ this.broadcastSessionList();
916
+ break;
917
+ }
918
+ case "cancel_queued_input": {
919
+ const session = this.resolveSession(msg.sessionId);
920
+ if (!session || session.provider !== "codex") {
921
+ this.send(ws, { type: "error", message: "No active Codex session." });
922
+ return;
923
+ }
924
+ const success = this.sessionManager.cancelCodexQueuedInput(session.id, msg.itemId);
925
+ if (!success) {
926
+ this.send(ws, {
927
+ type: "error",
928
+ message: "Queued message not found.",
929
+ errorCode: "queued_input_not_found",
930
+ });
931
+ return;
932
+ }
933
+ this.broadcastSessionList();
934
+ break;
935
+ }
936
+ case "steer_queued_input": {
937
+ const session = this.resolveSession(msg.sessionId);
938
+ if (!session || session.provider !== "codex") {
939
+ this.send(ws, { type: "error", message: "No active Codex session." });
940
+ return;
941
+ }
942
+ const result = await this.sessionManager.steerCodexQueuedInput(session.id, msg.itemId);
943
+ if (!result.ok) {
944
+ this.send(ws, {
945
+ type: "error",
946
+ message: result.error,
947
+ errorCode: result.error === "Queued message not found."
948
+ ? "queued_input_not_found"
949
+ : "queued_input_steer_failed",
950
+ });
951
+ return;
952
+ }
953
+ this.broadcastSessionList();
954
+ break;
955
+ }
844
956
  case "push_register": {
845
957
  const locale = normalizePushLocale(msg.locale);
846
958
  const privacyMode = msg.privacyMode === true;
@@ -1478,6 +1590,31 @@ export class BridgeWebSocketServer {
1478
1590
  status: session.status,
1479
1591
  sessionId: msg.sessionId,
1480
1592
  });
1593
+ if (session.provider === "codex") {
1594
+ const item = session.codexQueuedInput;
1595
+ this.sendConversationQueue(ws, {
1596
+ type: "conversation_queue",
1597
+ sessionId: msg.sessionId,
1598
+ limit: 1,
1599
+ items: item
1600
+ ? [
1601
+ {
1602
+ itemId: item.itemId,
1603
+ text: item.text,
1604
+ createdAt: item.createdAt,
1605
+ ...(item.updatedAt ? { updatedAt: item.updatedAt } : {}),
1606
+ ...(item.imageCount
1607
+ ? { imageCount: item.imageCount }
1608
+ : {}),
1609
+ ...(item.skills?.length ? { skills: item.skills } : {}),
1610
+ ...(item.mentions?.length
1611
+ ? { mentions: item.mentions }
1612
+ : {}),
1613
+ },
1614
+ ]
1615
+ : [],
1616
+ });
1617
+ }
1481
1618
  // Send cached slash commands so the client can restore them even when
1482
1619
  // the original init/supported_commands message was evicted from the
1483
1620
  // in-memory history (MAX_HISTORY_PER_SESSION overflow).
@@ -3049,6 +3186,8 @@ export class BridgeWebSocketServer {
3049
3186
  const data = JSON.stringify({ ...msg, sessionId });
3050
3187
  for (const client of this.wss.clients) {
3051
3188
  if (client.readyState === WebSocket.OPEN) {
3189
+ if (!this.shouldSendToClient(client, msg))
3190
+ continue;
3052
3191
  client.send(data);
3053
3192
  }
3054
3193
  }
@@ -3371,11 +3510,27 @@ export class BridgeWebSocketServer {
3371
3510
  const data = JSON.stringify(msg);
3372
3511
  for (const client of this.wss.clients) {
3373
3512
  if (client.readyState === WebSocket.OPEN) {
3513
+ if (!this.shouldSendToClient(client, msg))
3514
+ continue;
3374
3515
  client.send(data);
3375
3516
  }
3376
3517
  }
3377
3518
  }
3519
+ shouldSendToClient(ws, msg) {
3520
+ if (msg.type !== "conversation_queue")
3521
+ return true;
3522
+ return (this.clientSupportedServerMessages
3523
+ .get(ws)
3524
+ ?.has("conversation_queue") ?? false);
3525
+ }
3526
+ sendConversationQueue(ws, msg) {
3527
+ if (!this.shouldSendToClient(ws, msg))
3528
+ return;
3529
+ this.send(ws, msg);
3530
+ }
3378
3531
  send(ws, msg) {
3532
+ if (!this.shouldSendToClient(ws, msg))
3533
+ return;
3379
3534
  const sessionId = this.extractSessionIdFromServerMessage(msg);
3380
3535
  if (sessionId) {
3381
3536
  this.recordDebugEvent(sessionId, {