@ccpocket/bridge 1.75.0 → 1.77.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
@@ -11,6 +11,7 @@ import { codexErrorMessage, CodexRpcError, CodexProcess, } from "./codex-process
11
11
  import { stopManagedCodexAppServers } from "./codex-transport.js";
12
12
  import { parseClientMessage, } from "./parser.js";
13
13
  import { getAllRecentSessions, getCodexSessionHistory, getSessionHistory, codexUserTurnUuid, codexThreadToSessionHistory, findSessionsByClaudeIds, extractMessageImages, getClaudeSessionName, getCodexSessionIndexMetadata, loadCodexSessionNames, renameClaudeSession, renameCodexSession, saveCodexSessionProfile, } from "./sessions-index.js";
14
+ import { isSafeUploadFileName, UploadStoreError, } from "./upload-store.js";
14
15
  import { formatResumePerformanceLog, summarizeResumeHistory, } from "./resume-metrics.js";
15
16
  import { ArchiveStore } from "./archive-store.js";
16
17
  import { WorktreeStore } from "./worktree-store.js";
@@ -26,6 +27,12 @@ import { getPackageVersion } from "./version.js";
26
27
  import { isPathWithinAllowedDirectory, resolvePlatformPath, resolvePlatformPathFrom, } from "./path-utils.js";
27
28
  import { DirectoryListingError, listAllowedDirectories, } from "./directory-listing.js";
28
29
  import { deriveCodexPermissionsMode, normalizeCodexPermissionsMode, withDerivedCodexPermissionsMode, } from "./codex-permissions.js";
30
+ function projectRequestMetadata(request) {
31
+ return {
32
+ projectPath: request.projectPath,
33
+ ...(request.requestId ? { requestId: request.requestId } : {}),
34
+ };
35
+ }
29
36
  const RESUME_OPERATION_TIMEOUT_MS = 5 * 60 * 1000;
30
37
  const RESUME_COMPLETED_TTL_MS = 30 * 1000;
31
38
  // ---- Available model lists (delivered to clients via session_list) ----
@@ -483,6 +490,7 @@ export class BridgeWebSocketServer {
483
490
  static DEFAULT_FILE_LIST_MAX_ENTRIES = 5000;
484
491
  static DEFAULT_FILE_LIST_MAX_BYTES = 512 * 1024;
485
492
  static DEFAULT_FILE_DOWNLOAD_MAX_BYTES = 512 * 1024 * 1024;
493
+ static DEFAULT_FILE_UPLOAD_MAX_BYTES = 512 * 1024 * 1024;
486
494
  static DEFAULT_DELTA_BATCH_MS = 100;
487
495
  static DEFAULT_DELTA_BATCH_MAX_CHARS = 4096;
488
496
  wss;
@@ -491,6 +499,7 @@ export class BridgeWebSocketServer {
491
499
  allowedDirs;
492
500
  imageStore;
493
501
  mediaStore;
502
+ uploadStore;
494
503
  galleryStore;
495
504
  projectHistory;
496
505
  debugTraceStore;
@@ -535,6 +544,7 @@ export class BridgeWebSocketServer {
535
544
  fileListMaxEntries;
536
545
  fileListMaxBytes;
537
546
  fileDownloadMaxBytes;
547
+ fileUploadMaxBytes;
538
548
  deltaBatchMs;
539
549
  deltaBatchMaxChars;
540
550
  deltaBatches = new Map();
@@ -543,11 +553,12 @@ export class BridgeWebSocketServer {
543
553
  pendingClaudeResumeInputs = new WeakMap();
544
554
  resumeOperations = new Map();
545
555
  constructor(options) {
546
- const { server, apiKey, allowedDirs, imageStore, mediaStore, galleryStore, projectHistory, debugTraceStore, recordingStore, firebaseAuth, promptHistoryBackup, promptHistoryStore, platform, fileListMaxEntries, fileListMaxBytes, fileDownloadMaxBytes, deltaBatchMs, deltaBatchMaxChars, } = options;
556
+ const { server, apiKey, allowedDirs, imageStore, mediaStore, uploadStore, galleryStore, projectHistory, debugTraceStore, recordingStore, firebaseAuth, promptHistoryBackup, promptHistoryStore, platform, fileListMaxEntries, fileListMaxBytes, fileDownloadMaxBytes, fileUploadMaxBytes, deltaBatchMs, deltaBatchMaxChars, } = options;
547
557
  this.apiKey = apiKey ?? null;
548
558
  this.allowedDirs = allowedDirs ?? [];
549
559
  this.imageStore = imageStore ?? null;
550
560
  this.mediaStore = mediaStore ?? null;
561
+ this.uploadStore = uploadStore ?? null;
551
562
  this.galleryStore = galleryStore ?? null;
552
563
  this.projectHistory = projectHistory ?? null;
553
564
  this.debugTraceStore = debugTraceStore ?? new DebugTraceStore();
@@ -560,6 +571,7 @@ export class BridgeWebSocketServer {
560
571
  this.fileListMaxEntries = normalizePositiveLimit(fileListMaxEntries, positiveEnvInt("BRIDGE_FILE_LIST_MAX_ENTRIES", BridgeWebSocketServer.DEFAULT_FILE_LIST_MAX_ENTRIES));
561
572
  this.fileListMaxBytes = normalizePositiveLimit(fileListMaxBytes, positiveEnvInt("BRIDGE_FILE_LIST_MAX_BYTES", BridgeWebSocketServer.DEFAULT_FILE_LIST_MAX_BYTES));
562
573
  this.fileDownloadMaxBytes = normalizePositiveLimit(fileDownloadMaxBytes, positiveEnvInt("BRIDGE_FILE_DOWNLOAD_MAX_SIZE_MB", BridgeWebSocketServer.DEFAULT_FILE_DOWNLOAD_MAX_BYTES / 1024 / 1024) * 1024 * 1024);
574
+ this.fileUploadMaxBytes = normalizePositiveLimit(fileUploadMaxBytes, positiveEnvInt("BRIDGE_FILE_UPLOAD_MAX_SIZE_MB", BridgeWebSocketServer.DEFAULT_FILE_UPLOAD_MAX_BYTES / 1024 / 1024) * 1024 * 1024);
563
575
  this.deltaBatchMs = normalizeNonNegativeLimit(deltaBatchMs, nonNegativeEnvInt("BRIDGE_DELTA_BATCH_MS", BridgeWebSocketServer.DEFAULT_DELTA_BATCH_MS));
564
576
  this.deltaBatchMaxChars = normalizePositiveLimit(deltaBatchMaxChars, positiveEnvInt("BRIDGE_DELTA_BATCH_MAX_CHARS", BridgeWebSocketServer.DEFAULT_DELTA_BATCH_MAX_CHARS));
565
577
  this.archiveStore = new ArchiveStore();
@@ -714,12 +726,85 @@ export class BridgeWebSocketServer {
714
726
  this.sendFileDownloadError(ws, request, "file_download_failed", "Unable to prepare the file download.");
715
727
  }
716
728
  }
729
+ sendFileUploadError(ws, requestId, errorCode, message, path) {
730
+ this.send(ws, { type: "error", errorCode, message, requestId, path });
731
+ }
732
+ async prepareFileUpload(ws, request) {
733
+ const pathApi = this.platform === "win32" ? win32 : posix;
734
+ const projectPath = pathApi.resolve(request.projectPath);
735
+ if (!this.uploadStore ||
736
+ pathApi.isAbsolute(request.directoryPath) ||
737
+ !isSafeUploadFileName(request.fileName)) {
738
+ this.sendFileUploadError(ws, request.requestId, !this.uploadStore ? "file_upload_unavailable" : "file_upload_not_allowed", !this.uploadStore
739
+ ? "File uploads are unavailable on this Bridge."
740
+ : "The upload destination or file name is not allowed.", request.directoryPath);
741
+ return;
742
+ }
743
+ if (request.sizeBytes > this.fileUploadMaxBytes) {
744
+ this.sendFileUploadError(ws, request.requestId, "file_upload_too_large", `File is too large to upload. Maximum size is ${Math.ceil(this.fileUploadMaxBytes / 1024 / 1024)} MB.`, request.fileName);
745
+ return;
746
+ }
747
+ const requestedDirectory = pathApi.resolve(projectPath, request.directoryPath || ".");
748
+ if (!this.isPathAllowed(projectPath) ||
749
+ !isPathWithinAllowedDirectory(requestedDirectory, projectPath, this.platform)) {
750
+ this.sendFileUploadError(ws, request.requestId, "file_upload_not_allowed", "The upload destination is outside the current project.", request.directoryPath);
751
+ return;
752
+ }
753
+ try {
754
+ const canonicalProject = await realpath(projectPath);
755
+ const canonicalDirectory = await realpath(requestedDirectory);
756
+ const directoryStat = await stat(canonicalDirectory);
757
+ if (!directoryStat.isDirectory() ||
758
+ !isPathWithinAllowedDirectory(canonicalDirectory, canonicalProject, this.platform) ||
759
+ !(await this.isCanonicalPathAllowed(canonicalDirectory))) {
760
+ throw new UploadStoreError("file_upload_directory_changed", "The upload destination is not allowed.");
761
+ }
762
+ const ref = await this.uploadStore.register({
763
+ directoryPath: canonicalDirectory,
764
+ relativeDirectoryPath: pathApi
765
+ .relative(projectPath, requestedDirectory)
766
+ .split(pathApi.sep)
767
+ .join("/"),
768
+ fileName: request.fileName,
769
+ sizeBytes: request.sizeBytes,
770
+ conflictPolicy: request.conflictPolicy,
771
+ });
772
+ this.send(ws, {
773
+ type: "file_upload_ready",
774
+ requestId: request.requestId,
775
+ fileName: request.fileName,
776
+ sizeBytes: request.sizeBytes,
777
+ uploadUrl: ref.url,
778
+ uploadToken: ref.token,
779
+ });
780
+ }
781
+ catch (error) {
782
+ const known = error instanceof UploadStoreError ? error : null;
783
+ this.sendFileUploadError(ws, request.requestId, known?.code ?? "file_upload_directory_not_found", known?.message ?? "The upload destination was not found or is not allowed.", request.directoryPath);
784
+ }
785
+ }
786
+ async finalizeFileUpload(ws, request) {
787
+ if (!this.uploadStore) {
788
+ this.sendFileUploadError(ws, request.requestId, "file_upload_unavailable", "File uploads are unavailable on this Bridge.");
789
+ return;
790
+ }
791
+ try {
792
+ const result = await this.uploadStore.finalize(request.uploadToken, request.sha256);
793
+ this.send(ws, { type: "file_upload_complete", requestId: request.requestId, ...result });
794
+ }
795
+ catch (error) {
796
+ const known = error instanceof UploadStoreError ? error : null;
797
+ this.sendFileUploadError(ws, request.requestId, known?.code ?? "file_upload_failed", known?.message ?? "Unable to finish the file upload.");
798
+ }
799
+ }
717
800
  /** Build a user-friendly error for disallowed project paths. */
718
- buildPathNotAllowedError(projectPath) {
801
+ buildPathNotAllowedError(projectPath, requestId) {
719
802
  return {
720
803
  type: "error",
721
804
  message: `⚠ Project path not allowed\n\n"${projectPath}" is not in the allowed directories.\n\nFix: Update BRIDGE_ALLOWED_DIRS on the Bridge server to include this path.`,
722
805
  errorCode: "path_not_allowed",
806
+ path: projectPath,
807
+ requestId,
723
808
  };
724
809
  }
725
810
  sendToolActionError(ws, message, error) {
@@ -750,7 +835,7 @@ export class BridgeWebSocketServer {
750
835
  return { roots: [...normalized.values()] };
751
836
  }
752
837
  buildSessionCreatedMessage(params) {
753
- const { sessionId, provider, projectPath, session, permissionMode, executionMode, planMode, approvalsReviewer, codexPermissionsMode, sandboxMode, slashCommands, skills, skillMetadata, apps, appMetadata, plugins, pluginMetadata, sourceSessionId, resumeRequestId, } = params;
838
+ const { sessionId, provider, projectPath, session, permissionMode, executionMode, planMode, approvalsReviewer, codexPermissionsMode, sandboxMode, slashCommands, skills, skillMetadata, apps, appMetadata, plugins, pluginMetadata, sourceSessionId, resumeRequestId, requestId, } = params;
754
839
  const derivedCodexSettings = provider === "codex"
755
840
  ? withDerivedCodexPermissionsMode(session?.codexSettings)
756
841
  : session?.codexSettings;
@@ -846,6 +931,7 @@ export class BridgeWebSocketServer {
846
931
  : {}),
847
932
  ...(sourceSessionId ? { sourceSessionId } : {}),
848
933
  ...(resumeRequestId ? { resumeRequestId } : {}),
934
+ ...(requestId ? { requestId } : {}),
849
935
  };
850
936
  if (provider === "codex" && derivedCodexSettings) {
851
937
  if (derivedCodexSettings.model !== undefined) {
@@ -880,6 +966,7 @@ export class BridgeWebSocketServer {
880
966
  if (mode !== "conversation") {
881
967
  this.send(ws, {
882
968
  type: "rewind_result",
969
+ sessionId,
883
970
  success: false,
884
971
  mode,
885
972
  error: "Codex only supports conversation rewind",
@@ -890,6 +977,7 @@ export class BridgeWebSocketServer {
890
977
  if (!session) {
891
978
  this.send(ws, {
892
979
  type: "rewind_result",
980
+ sessionId,
893
981
  success: false,
894
982
  mode,
895
983
  error: `Session ${sessionId} not found`,
@@ -901,6 +989,7 @@ export class BridgeWebSocketServer {
901
989
  typeof codexProcess.rollbackThread !== "function") {
902
990
  this.send(ws, {
903
991
  type: "rewind_result",
992
+ sessionId,
904
993
  success: false,
905
994
  mode,
906
995
  error: "Session is not a Codex session",
@@ -910,6 +999,7 @@ export class BridgeWebSocketServer {
910
999
  if (session.status !== "idle" || (codexProcess.status ?? session.status) !== "idle") {
911
1000
  this.send(ws, {
912
1001
  type: "rewind_result",
1002
+ sessionId,
913
1003
  success: false,
914
1004
  mode,
915
1005
  error: "Cannot rewind while Codex is running",
@@ -919,6 +1009,7 @@ export class BridgeWebSocketServer {
919
1009
  if (session.codexQueuedInput) {
920
1010
  this.send(ws, {
921
1011
  type: "rewind_result",
1012
+ sessionId,
922
1013
  success: false,
923
1014
  mode,
924
1015
  error: "Cannot rewind while Codex has queued input",
@@ -930,6 +1021,7 @@ export class BridgeWebSocketServer {
930
1021
  if (targetOrdinal === null || targetOrdinal > totalUserTurns) {
931
1022
  this.send(ws, {
932
1023
  type: "rewind_result",
1024
+ sessionId,
933
1025
  success: false,
934
1026
  mode,
935
1027
  error: "Invalid Codex rewind target",
@@ -940,6 +1032,7 @@ export class BridgeWebSocketServer {
940
1032
  if (numTurns <= 0) {
941
1033
  this.send(ws, {
942
1034
  type: "rewind_result",
1035
+ sessionId,
943
1036
  success: false,
944
1037
  mode,
945
1038
  error: "Invalid Codex rewind target",
@@ -950,6 +1043,7 @@ export class BridgeWebSocketServer {
950
1043
  if (!threadId) {
951
1044
  this.send(ws, {
952
1045
  type: "rewind_result",
1046
+ sessionId,
953
1047
  success: false,
954
1048
  mode,
955
1049
  error: "No Codex thread ID available for rewind",
@@ -978,6 +1072,7 @@ export class BridgeWebSocketServer {
978
1072
  const newSession = this.sessionManager.get(newSessionId);
979
1073
  this.send(ws, {
980
1074
  type: "rewind_result",
1075
+ sessionId,
981
1076
  success: true,
982
1077
  mode,
983
1078
  });
@@ -1811,7 +1906,11 @@ export class BridgeWebSocketServer {
1811
1906
  case "start": {
1812
1907
  const projectPath = resolvePlatformPath(msg.projectPath, this.platform);
1813
1908
  if (!this.isPathAllowed(projectPath)) {
1814
- this.send(ws, this.buildPathNotAllowedError(msg.projectPath));
1909
+ this.send(ws, {
1910
+ ...this.buildPathNotAllowedError(msg.projectPath),
1911
+ requestId: msg.requestId,
1912
+ path: msg.projectPath,
1913
+ });
1815
1914
  break;
1816
1915
  }
1817
1916
  try {
@@ -1856,6 +1955,8 @@ export class BridgeWebSocketServer {
1856
1955
  !(await this.validateCodexProfile(msg.profile, projectPath))) {
1857
1956
  this.send(ws, {
1858
1957
  type: "error",
1958
+ requestId: msg.requestId,
1959
+ path: projectPath,
1859
1960
  message: `Codex profile not found: ${msg.profile}`,
1860
1961
  });
1861
1962
  break;
@@ -1865,7 +1966,11 @@ export class BridgeWebSocketServer {
1865
1966
  ? this.normalizeAdditionalWritableRoots(msg.additionalWritableRoots, projectPath)
1866
1967
  : {};
1867
1968
  if (additionalWritableRoots.deniedRoot) {
1868
- this.send(ws, this.buildPathNotAllowedError(additionalWritableRoots.deniedRoot));
1969
+ this.send(ws, {
1970
+ ...this.buildPathNotAllowedError(additionalWritableRoots.deniedRoot),
1971
+ requestId: msg.requestId,
1972
+ path: projectPath,
1973
+ });
1869
1974
  break;
1870
1975
  }
1871
1976
  const { sessionId, permissionMode: effectivePermissionMode, executionMode: effectiveExecutionMode, planMode: effectivePlanMode, usedFallback: autoFallbackUsed, } = provider === "claude"
@@ -1951,6 +2056,7 @@ export class BridgeWebSocketServer {
1951
2056
  : msg.sandboxMode,
1952
2057
  codexPermissionsMode: createdSession?.codexSettings?.codexPermissionsMode,
1953
2058
  approvalsReviewer: createdSession?.codexSettings?.approvalsReviewer,
2059
+ requestId: msg.requestId,
1954
2060
  ...(cached
1955
2061
  ? {
1956
2062
  slashCommands: cached.slashCommands,
@@ -2002,6 +2108,8 @@ export class BridgeWebSocketServer {
2002
2108
  console.error(`[ws] Failed to start session:`, err);
2003
2109
  this.send(ws, {
2004
2110
  type: "error",
2111
+ requestId: msg.requestId,
2112
+ path: msg.projectPath,
2005
2113
  message: `Failed to start session: ${err.message}`,
2006
2114
  });
2007
2115
  }
@@ -2019,6 +2127,7 @@ export class BridgeWebSocketServer {
2019
2127
  }
2020
2128
  this.send(ws, {
2021
2129
  type: "error",
2130
+ sessionId: msg.sessionId,
2022
2131
  message: "No active session. Send 'start' first.",
2023
2132
  });
2024
2133
  return;
@@ -2332,12 +2441,17 @@ export class BridgeWebSocketServer {
2332
2441
  case "update_queued_input": {
2333
2442
  const session = this.resolveSession(msg.sessionId);
2334
2443
  if (!session || session.provider !== "codex") {
2335
- this.send(ws, { type: "error", message: "No active Codex session." });
2444
+ this.send(ws, {
2445
+ type: "error",
2446
+ sessionId: msg.sessionId,
2447
+ message: "No active Codex session.",
2448
+ });
2336
2449
  return;
2337
2450
  }
2338
2451
  if (!msg.text.trim()) {
2339
2452
  this.send(ws, {
2340
2453
  type: "error",
2454
+ sessionId: msg.sessionId,
2341
2455
  message: "Queued message cannot be empty.",
2342
2456
  });
2343
2457
  return;
@@ -2346,6 +2460,7 @@ export class BridgeWebSocketServer {
2346
2460
  if (!success) {
2347
2461
  this.send(ws, {
2348
2462
  type: "error",
2463
+ sessionId: msg.sessionId,
2349
2464
  message: "Queued message not found.",
2350
2465
  errorCode: "queued_input_not_found",
2351
2466
  });
@@ -2357,13 +2472,18 @@ export class BridgeWebSocketServer {
2357
2472
  case "cancel_queued_input": {
2358
2473
  const session = this.resolveSession(msg.sessionId);
2359
2474
  if (!session || session.provider !== "codex") {
2360
- this.send(ws, { type: "error", message: "No active Codex session." });
2475
+ this.send(ws, {
2476
+ type: "error",
2477
+ sessionId: msg.sessionId,
2478
+ message: "No active Codex session.",
2479
+ });
2361
2480
  return;
2362
2481
  }
2363
2482
  const success = this.sessionManager.cancelCodexQueuedInput(session.id, msg.itemId);
2364
2483
  if (!success) {
2365
2484
  this.send(ws, {
2366
2485
  type: "error",
2486
+ sessionId: msg.sessionId,
2367
2487
  message: "Queued message not found.",
2368
2488
  errorCode: "queued_input_not_found",
2369
2489
  });
@@ -2375,13 +2495,18 @@ export class BridgeWebSocketServer {
2375
2495
  case "steer_queued_input": {
2376
2496
  const session = this.resolveSession(msg.sessionId);
2377
2497
  if (!session || session.provider !== "codex") {
2378
- this.send(ws, { type: "error", message: "No active Codex session." });
2498
+ this.send(ws, {
2499
+ type: "error",
2500
+ sessionId: msg.sessionId,
2501
+ message: "No active Codex session.",
2502
+ });
2379
2503
  return;
2380
2504
  }
2381
2505
  const result = await this.sessionManager.steerCodexQueuedInput(session.id, msg.itemId);
2382
2506
  if (!result.ok) {
2383
2507
  this.send(ws, {
2384
2508
  type: "error",
2509
+ sessionId: msg.sessionId,
2385
2510
  message: result.error,
2386
2511
  errorCode: result.error === "Queued message not found."
2387
2512
  ? "queued_input_not_found"
@@ -2482,6 +2607,7 @@ export class BridgeWebSocketServer {
2482
2607
  if (this.failSetPermissionMode) {
2483
2608
  this.send(ws, {
2484
2609
  type: "error",
2610
+ sessionId: msg.sessionId,
2485
2611
  message: "Failed to set permission mode: forced test failure",
2486
2612
  errorCode: "set_permission_mode_rejected",
2487
2613
  });
@@ -2489,7 +2615,11 @@ export class BridgeWebSocketServer {
2489
2615
  }
2490
2616
  const session = this.resolveSession(msg.sessionId);
2491
2617
  if (!session) {
2492
- this.send(ws, { type: "error", message: "No active session." });
2618
+ this.send(ws, {
2619
+ type: "error",
2620
+ sessionId: msg.sessionId,
2621
+ message: "No active session.",
2622
+ });
2493
2623
  return;
2494
2624
  }
2495
2625
  if (session.provider === "codex") {
@@ -2739,6 +2869,7 @@ export class BridgeWebSocketServer {
2739
2869
  .catch((err) => {
2740
2870
  this.send(ws, {
2741
2871
  type: "error",
2872
+ sessionId: msg.sessionId,
2742
2873
  message: `Failed to restart session for permission mode change: ${err}`,
2743
2874
  });
2744
2875
  });
@@ -2751,6 +2882,7 @@ export class BridgeWebSocketServer {
2751
2882
  isClaudeAutoModeUnavailableError(err)) {
2752
2883
  this.send(ws, {
2753
2884
  type: "error",
2885
+ sessionId: msg.sessionId,
2754
2886
  message: "Auto mode is unavailable in this environment. Keeping the current permission mode.",
2755
2887
  errorCode: "auto_mode_unavailable",
2756
2888
  });
@@ -2758,6 +2890,7 @@ export class BridgeWebSocketServer {
2758
2890
  }
2759
2891
  this.send(ws, {
2760
2892
  type: "error",
2893
+ sessionId: msg.sessionId,
2761
2894
  message: `Failed to set permission mode: ${errorMessageOf(err)}`,
2762
2895
  });
2763
2896
  });
@@ -2766,12 +2899,17 @@ export class BridgeWebSocketServer {
2766
2899
  case "set_codex_model": {
2767
2900
  const session = this.resolveSession(msg.sessionId);
2768
2901
  if (!session) {
2769
- this.send(ws, { type: "error", message: "No active session." });
2902
+ this.send(ws, {
2903
+ type: "error",
2904
+ sessionId: msg.sessionId,
2905
+ message: "No active session.",
2906
+ });
2770
2907
  return;
2771
2908
  }
2772
2909
  if (session.provider !== "codex") {
2773
2910
  this.send(ws, {
2774
2911
  type: "error",
2912
+ sessionId: msg.sessionId,
2775
2913
  message: "Model switching is only supported for Codex sessions.",
2776
2914
  errorCode: "set_codex_model_unsupported",
2777
2915
  });
@@ -2781,6 +2919,7 @@ export class BridgeWebSocketServer {
2781
2919
  if (!model) {
2782
2920
  this.send(ws, {
2783
2921
  type: "error",
2922
+ sessionId: msg.sessionId,
2784
2923
  message: `Invalid Codex model: ${msg.model}`,
2785
2924
  errorCode: "set_codex_model_rejected",
2786
2925
  });
@@ -2827,6 +2966,7 @@ export class BridgeWebSocketServer {
2827
2966
  if (!session || session.provider !== "codex") {
2828
2967
  this.send(ws, {
2829
2968
  type: "error",
2969
+ sessionId: msg.sessionId,
2830
2970
  message: "No active Codex session.",
2831
2971
  errorCode: "set_codex_speed_unsupported",
2832
2972
  });
@@ -2858,6 +2998,7 @@ export class BridgeWebSocketServer {
2858
2998
  if (!session || session.provider !== "codex") {
2859
2999
  this.send(ws, {
2860
3000
  type: "error",
3001
+ sessionId: msg.sessionId,
2861
3002
  message: "Goal lookup is only supported for active Codex sessions.",
2862
3003
  errorCode: "goal_get_unsupported",
2863
3004
  });
@@ -2871,6 +3012,7 @@ export class BridgeWebSocketServer {
2871
3012
  catch (err) {
2872
3013
  this.send(ws, {
2873
3014
  type: "error",
3015
+ sessionId: msg.sessionId,
2874
3016
  message: `Failed to get goal: ${errorMessageOf(err)}`,
2875
3017
  errorCode: "goal_get_failed",
2876
3018
  });
@@ -2882,6 +3024,7 @@ export class BridgeWebSocketServer {
2882
3024
  if (!session || session.provider !== "codex") {
2883
3025
  this.send(ws, {
2884
3026
  type: "error",
3027
+ sessionId: msg.sessionId,
2885
3028
  message: "Goal updates are only supported for active Codex sessions.",
2886
3029
  errorCode: "goal_set_unsupported",
2887
3030
  });
@@ -2903,6 +3046,7 @@ export class BridgeWebSocketServer {
2903
3046
  catch (err) {
2904
3047
  this.send(ws, {
2905
3048
  type: "error",
3049
+ sessionId: msg.sessionId,
2906
3050
  message: `Failed to set goal: ${errorMessageOf(err)}`,
2907
3051
  errorCode: "goal_set_failed",
2908
3052
  });
@@ -2914,6 +3058,7 @@ export class BridgeWebSocketServer {
2914
3058
  if (!session || session.provider !== "codex") {
2915
3059
  this.send(ws, {
2916
3060
  type: "error",
3061
+ sessionId: msg.sessionId,
2917
3062
  message: "Goal clearing is only supported for active Codex sessions.",
2918
3063
  errorCode: "goal_clear_unsupported",
2919
3064
  });
@@ -2930,6 +3075,7 @@ export class BridgeWebSocketServer {
2930
3075
  catch (err) {
2931
3076
  this.send(ws, {
2932
3077
  type: "error",
3078
+ sessionId: msg.sessionId,
2933
3079
  message: `Failed to clear goal: ${errorMessageOf(err)}`,
2934
3080
  errorCode: "goal_clear_failed",
2935
3081
  });
@@ -2940,6 +3086,7 @@ export class BridgeWebSocketServer {
2940
3086
  if (this.failSetSandboxMode) {
2941
3087
  this.send(ws, {
2942
3088
  type: "error",
3089
+ sessionId: msg.sessionId,
2943
3090
  message: "Failed to set sandbox mode: forced test failure",
2944
3091
  errorCode: "set_sandbox_mode_rejected",
2945
3092
  });
@@ -2947,12 +3094,17 @@ export class BridgeWebSocketServer {
2947
3094
  }
2948
3095
  const session = this.resolveSession(msg.sessionId);
2949
3096
  if (!session) {
2950
- this.send(ws, { type: "error", message: "No active session." });
3097
+ this.send(ws, {
3098
+ type: "error",
3099
+ sessionId: msg.sessionId,
3100
+ message: "No active session.",
3101
+ });
2951
3102
  return;
2952
3103
  }
2953
3104
  if (msg.sandboxMode !== "on" && msg.sandboxMode !== "off") {
2954
3105
  this.send(ws, {
2955
3106
  type: "error",
3107
+ sessionId: msg.sessionId,
2956
3108
  message: `Invalid sandbox mode: ${msg.sandboxMode}`,
2957
3109
  });
2958
3110
  return;
@@ -3140,6 +3292,7 @@ export class BridgeWebSocketServer {
3140
3292
  .catch((err) => {
3141
3293
  this.send(ws, {
3142
3294
  type: "error",
3295
+ sessionId: msg.sessionId,
3143
3296
  message: `Failed to restart session for sandbox mode change: ${err}`,
3144
3297
  });
3145
3298
  });
@@ -3311,6 +3464,7 @@ export class BridgeWebSocketServer {
3311
3464
  else {
3312
3465
  this.send(ws, {
3313
3466
  type: "error",
3467
+ sessionId: msg.sessionId,
3314
3468
  message: `Session ${msg.sessionId} not found`,
3315
3469
  });
3316
3470
  }
@@ -3420,6 +3574,7 @@ export class BridgeWebSocketServer {
3420
3574
  if (!result) {
3421
3575
  this.send(ws, {
3422
3576
  type: "error",
3577
+ sessionId: msg.sessionId,
3423
3578
  message: `Session ${msg.sessionId} not found`,
3424
3579
  });
3425
3580
  break;
@@ -3447,6 +3602,7 @@ export class BridgeWebSocketServer {
3447
3602
  if (!result) {
3448
3603
  this.send(ws, {
3449
3604
  type: "error",
3605
+ sessionId: msg.sessionId,
3450
3606
  message: `Session ${msg.sessionId} not found`,
3451
3607
  });
3452
3608
  break;
@@ -3477,6 +3633,7 @@ export class BridgeWebSocketServer {
3477
3633
  else {
3478
3634
  this.send(ws, {
3479
3635
  type: "error",
3636
+ sessionId: msg.sessionId,
3480
3637
  message: `Session ${msg.sessionId} not found`,
3481
3638
  });
3482
3639
  }
@@ -3507,6 +3664,7 @@ export class BridgeWebSocketServer {
3507
3664
  else {
3508
3665
  this.send(ws, {
3509
3666
  type: "error",
3667
+ sessionId: msg.sessionId,
3510
3668
  message: `Session ${msg.sessionId} not found`,
3511
3669
  });
3512
3670
  }
@@ -3517,6 +3675,7 @@ export class BridgeWebSocketServer {
3517
3675
  if (!session) {
3518
3676
  this.send(ws, {
3519
3677
  type: "error",
3678
+ sessionId: msg.sessionId,
3520
3679
  message: `Session ${msg.sessionId} not found`,
3521
3680
  });
3522
3681
  return;
@@ -4091,7 +4250,11 @@ export class BridgeWebSocketServer {
4091
4250
  case "interrupt": {
4092
4251
  const session = this.resolveSession(msg.sessionId);
4093
4252
  if (!session) {
4094
- this.send(ws, { type: "error", message: "No active session." });
4253
+ this.send(ws, {
4254
+ type: "error",
4255
+ sessionId: msg.sessionId,
4256
+ message: "No active session.",
4257
+ });
4095
4258
  return;
4096
4259
  }
4097
4260
  session.process.interrupt();
@@ -4136,13 +4299,30 @@ export class BridgeWebSocketServer {
4136
4299
  void this.prepareFileDownload(ws, msg);
4137
4300
  break;
4138
4301
  }
4302
+ case "prepare_file_upload": {
4303
+ void this.prepareFileUpload(ws, msg);
4304
+ break;
4305
+ }
4306
+ case "finalize_file_upload": {
4307
+ void this.finalizeFileUpload(ws, msg);
4308
+ break;
4309
+ }
4310
+ case "cancel_file_upload": {
4311
+ if (this.uploadStore)
4312
+ void this.uploadStore.cancel(msg.uploadToken);
4313
+ break;
4314
+ }
4139
4315
  case "read_file":
4140
4316
  case "read_media_file": {
4317
+ const responseMetadata = {
4318
+ ...projectRequestMetadata(msg),
4319
+ filePath: msg.filePath,
4320
+ };
4141
4321
  const absPath = resolve(msg.projectPath, msg.filePath);
4142
4322
  if (!this.isPathAllowed(absPath)) {
4143
4323
  this.send(ws, {
4144
4324
  type: "file_content",
4145
- filePath: msg.filePath,
4325
+ ...responseMetadata,
4146
4326
  content: "",
4147
4327
  error: "Path not allowed",
4148
4328
  });
@@ -4153,7 +4333,7 @@ export class BridgeWebSocketServer {
4153
4333
  if (!existsSync(absPath)) {
4154
4334
  this.send(ws, {
4155
4335
  type: "file_content",
4156
- filePath: msg.filePath,
4336
+ ...responseMetadata,
4157
4337
  content: "",
4158
4338
  error: "File not found",
4159
4339
  });
@@ -4175,7 +4355,7 @@ export class BridgeWebSocketServer {
4175
4355
  catch {
4176
4356
  this.send(ws, {
4177
4357
  type: "file_content",
4178
- filePath: msg.filePath,
4358
+ ...responseMetadata,
4179
4359
  content: "",
4180
4360
  error: targetPath.length > 0
4181
4361
  ? `This symbolic link points to a missing target: ${targetPath}`
@@ -4186,7 +4366,7 @@ export class BridgeWebSocketServer {
4186
4366
  if (resolvedTargetStat.isDirectory()) {
4187
4367
  this.send(ws, {
4188
4368
  type: "file_content",
4189
- filePath: msg.filePath,
4369
+ ...responseMetadata,
4190
4370
  content: "",
4191
4371
  error: targetPath.length > 0
4192
4372
  ? `This symbolic link points to a directory (${targetPath}). Open the target directory instead.`
@@ -4198,7 +4378,7 @@ export class BridgeWebSocketServer {
4198
4378
  else if (fileStat.isDirectory()) {
4199
4379
  this.send(ws, {
4200
4380
  type: "file_content",
4201
- filePath: msg.filePath,
4381
+ ...responseMetadata,
4202
4382
  content: "",
4203
4383
  error: "This path is a directory. Open a file instead.",
4204
4384
  });
@@ -4211,7 +4391,7 @@ export class BridgeWebSocketServer {
4211
4391
  if (!(await this.isCanonicalPathAllowed(canonicalPath))) {
4212
4392
  this.send(ws, {
4213
4393
  type: "file_content",
4214
- filePath: msg.filePath,
4394
+ ...responseMetadata,
4215
4395
  content: "",
4216
4396
  error: "Path not allowed",
4217
4397
  });
@@ -4223,7 +4403,7 @@ export class BridgeWebSocketServer {
4223
4403
  if (!this.mediaStore) {
4224
4404
  this.send(ws, {
4225
4405
  type: "file_content",
4226
- filePath: msg.filePath,
4406
+ ...responseMetadata,
4227
4407
  kind: mediaType.kind,
4228
4408
  content: "",
4229
4409
  mimeType: mediaType.mimeType,
@@ -4235,7 +4415,7 @@ export class BridgeWebSocketServer {
4235
4415
  const ref = await this.mediaStore.register(canonicalPath, mediaType.mimeType, resolvedFileStat.size);
4236
4416
  this.send(ws, {
4237
4417
  type: "file_content",
4238
- filePath: msg.filePath,
4418
+ ...responseMetadata,
4239
4419
  kind: mediaType.kind,
4240
4420
  content: "",
4241
4421
  mimeType: ref.mimeType,
@@ -4247,7 +4427,7 @@ export class BridgeWebSocketServer {
4247
4427
  if (msg.type === "read_media_file") {
4248
4428
  this.send(ws, {
4249
4429
  type: "file_content",
4250
- filePath: msg.filePath,
4430
+ ...responseMetadata,
4251
4431
  content: "",
4252
4432
  error: "Unsupported media file type.",
4253
4433
  });
@@ -4258,7 +4438,7 @@ export class BridgeWebSocketServer {
4258
4438
  if (resolvedFileStat.size > BridgeWebSocketServer.MAX_IMAGE_SIZE) {
4259
4439
  this.send(ws, {
4260
4440
  type: "file_content",
4261
- filePath: msg.filePath,
4441
+ ...responseMetadata,
4262
4442
  kind: "image",
4263
4443
  content: "",
4264
4444
  mimeType,
@@ -4270,7 +4450,7 @@ export class BridgeWebSocketServer {
4270
4450
  const buf = await readFile(absPath);
4271
4451
  this.send(ws, {
4272
4452
  type: "file_content",
4273
- filePath: msg.filePath,
4453
+ ...responseMetadata,
4274
4454
  kind: "image",
4275
4455
  content: "",
4276
4456
  base64: buf.toString("base64"),
@@ -4326,7 +4506,7 @@ export class BridgeWebSocketServer {
4326
4506
  : raw;
4327
4507
  this.send(ws, {
4328
4508
  type: "file_content",
4329
- filePath: msg.filePath,
4509
+ ...responseMetadata,
4330
4510
  kind: "text",
4331
4511
  content,
4332
4512
  language,
@@ -4337,7 +4517,7 @@ export class BridgeWebSocketServer {
4337
4517
  catch (err) {
4338
4518
  this.send(ws, {
4339
4519
  type: "file_content",
4340
- filePath: msg.filePath,
4520
+ ...responseMetadata,
4341
4521
  content: "",
4342
4522
  error: `Failed to read file: ${err}`,
4343
4523
  });
@@ -4347,7 +4527,12 @@ export class BridgeWebSocketServer {
4347
4527
  }
4348
4528
  case "list_files": {
4349
4529
  if (!this.isPathAllowed(msg.projectPath)) {
4350
- this.send(ws, this.buildPathNotAllowedError(msg.projectPath));
4530
+ this.send(ws, {
4531
+ type: "file_list",
4532
+ ...projectRequestMetadata(msg),
4533
+ files: [],
4534
+ error: this.buildPathNotAllowedError(msg.projectPath, msg.requestId).message,
4535
+ });
4351
4536
  break;
4352
4537
  }
4353
4538
  void (async () => {
@@ -4358,6 +4543,7 @@ export class BridgeWebSocketServer {
4358
4543
  });
4359
4544
  this.send(ws, {
4360
4545
  type: "file_list",
4546
+ ...projectRequestMetadata(msg),
4361
4547
  files: result.files,
4362
4548
  totalFiles: result.totalFiles,
4363
4549
  truncated: result.truncated,
@@ -4366,8 +4552,10 @@ export class BridgeWebSocketServer {
4366
4552
  catch (err) {
4367
4553
  const message = err instanceof Error ? err.message : String(err);
4368
4554
  this.send(ws, {
4369
- type: "error",
4370
- message: `Failed to list files: ${message}`,
4555
+ type: "file_list",
4556
+ ...projectRequestMetadata(msg),
4557
+ files: [],
4558
+ error: `Failed to list files: ${message}`,
4371
4559
  });
4372
4560
  }
4373
4561
  })();
@@ -4457,7 +4645,15 @@ export class BridgeWebSocketServer {
4457
4645
  }
4458
4646
  case "get_diff": {
4459
4647
  if (!this.isPathAllowed(msg.projectPath)) {
4460
- this.send(ws, this.buildPathNotAllowedError(msg.projectPath));
4648
+ this.send(ws, {
4649
+ type: "diff_result",
4650
+ ...projectRequestMetadata(msg),
4651
+ staged: msg.staged === true,
4652
+ diff: "",
4653
+ error: this.buildPathNotAllowedError(msg.projectPath, msg.requestId)
4654
+ .message,
4655
+ errorCode: "path_not_allowed",
4656
+ });
4461
4657
  break;
4462
4658
  }
4463
4659
  this.collectGitDiff(msg.projectPath, ({ diff, error }) => {
@@ -4465,6 +4661,8 @@ export class BridgeWebSocketServer {
4465
4661
  if (/not a git repository/i.test(error)) {
4466
4662
  this.send(ws, {
4467
4663
  type: "diff_result",
4664
+ ...projectRequestMetadata(msg),
4665
+ staged: msg.staged === true,
4468
4666
  diff: "",
4469
4667
  error: "This project is not a git repository",
4470
4668
  errorCode: "git_not_available",
@@ -4473,6 +4671,8 @@ export class BridgeWebSocketServer {
4473
4671
  else {
4474
4672
  this.send(ws, {
4475
4673
  type: "diff_result",
4674
+ ...projectRequestMetadata(msg),
4675
+ staged: msg.staged === true,
4476
4676
  diff: "",
4477
4677
  error: `Failed to get diff: ${error}`,
4478
4678
  });
@@ -4481,10 +4681,21 @@ export class BridgeWebSocketServer {
4481
4681
  }
4482
4682
  void this.collectImageChanges(msg.projectPath, diff).then((imageChanges) => {
4483
4683
  if (imageChanges.length > 0) {
4484
- this.send(ws, { type: "diff_result", diff, imageChanges });
4684
+ this.send(ws, {
4685
+ type: "diff_result",
4686
+ ...projectRequestMetadata(msg),
4687
+ staged: msg.staged === true,
4688
+ diff,
4689
+ imageChanges,
4690
+ });
4485
4691
  }
4486
4692
  else {
4487
- this.send(ws, { type: "diff_result", diff });
4693
+ this.send(ws, {
4694
+ type: "diff_result",
4695
+ ...projectRequestMetadata(msg),
4696
+ staged: msg.staged === true,
4697
+ diff,
4698
+ });
4488
4699
  }
4489
4700
  });
4490
4701
  }, msg.staged === true
@@ -4497,7 +4708,13 @@ export class BridgeWebSocketServer {
4497
4708
  case "get_diff_image": {
4498
4709
  if (!this.isPathAllowed(msg.projectPath) ||
4499
4710
  !this.isPathAllowed(resolve(msg.projectPath, msg.filePath))) {
4500
- this.send(ws, { type: "error", message: `Path not allowed` });
4711
+ this.send(ws, {
4712
+ type: "diff_image_result",
4713
+ ...projectRequestMetadata(msg),
4714
+ filePath: msg.filePath,
4715
+ version: msg.version,
4716
+ error: "Path not allowed",
4717
+ });
4501
4718
  break;
4502
4719
  }
4503
4720
  if (msg.version === "both") {
@@ -4510,6 +4727,7 @@ export class BridgeWebSocketServer {
4510
4727
  const errors = [oldResult.error, newResult.error].filter(Boolean);
4511
4728
  this.send(ws, {
4512
4729
  type: "diff_image_result",
4730
+ ...projectRequestMetadata(msg),
4513
4731
  filePath: msg.filePath,
4514
4732
  version: "both",
4515
4733
  oldBase64: oldResult.base64,
@@ -4530,6 +4748,7 @@ export class BridgeWebSocketServer {
4530
4748
  const result = await this.loadDiffImageAsync(msg.projectPath, msg.filePath, version);
4531
4749
  this.send(ws, {
4532
4750
  type: "diff_image_result",
4751
+ ...projectRequestMetadata(msg),
4533
4752
  filePath: msg.filePath,
4534
4753
  version,
4535
4754
  ...result,
@@ -4544,25 +4763,42 @@ export class BridgeWebSocketServer {
4544
4763
  }
4545
4764
  case "list_worktrees": {
4546
4765
  if (!this.isPathAllowed(msg.projectPath)) {
4547
- this.send(ws, this.buildPathNotAllowedError(msg.projectPath));
4766
+ this.send(ws, {
4767
+ type: "worktree_list",
4768
+ ...projectRequestMetadata(msg),
4769
+ worktrees: [],
4770
+ error: this.buildPathNotAllowedError(msg.projectPath, msg.requestId).message,
4771
+ });
4548
4772
  break;
4549
4773
  }
4550
4774
  try {
4551
4775
  const worktrees = listWorktrees(msg.projectPath);
4552
4776
  const mainBranch = getMainBranch(msg.projectPath);
4553
- this.send(ws, { type: "worktree_list", worktrees, mainBranch });
4777
+ this.send(ws, {
4778
+ type: "worktree_list",
4779
+ ...projectRequestMetadata(msg),
4780
+ worktrees,
4781
+ mainBranch,
4782
+ });
4554
4783
  }
4555
4784
  catch (err) {
4556
4785
  this.send(ws, {
4557
- type: "error",
4558
- message: `Failed to list worktrees: ${err}`,
4786
+ type: "worktree_list",
4787
+ ...projectRequestMetadata(msg),
4788
+ worktrees: [],
4789
+ error: `Failed to list worktrees: ${err}`,
4559
4790
  });
4560
4791
  }
4561
4792
  break;
4562
4793
  }
4563
4794
  case "remove_worktree": {
4564
4795
  if (!this.isPathAllowed(msg.projectPath)) {
4565
- this.send(ws, this.buildPathNotAllowedError(msg.projectPath));
4796
+ this.send(ws, {
4797
+ type: "worktree_removed",
4798
+ ...projectRequestMetadata(msg),
4799
+ worktreePath: msg.worktreePath,
4800
+ error: this.buildPathNotAllowedError(msg.projectPath, msg.requestId).message,
4801
+ });
4566
4802
  break;
4567
4803
  }
4568
4804
  try {
@@ -4570,13 +4806,16 @@ export class BridgeWebSocketServer {
4570
4806
  this.worktreeStore.deleteByWorktreePath(msg.worktreePath);
4571
4807
  this.send(ws, {
4572
4808
  type: "worktree_removed",
4809
+ ...projectRequestMetadata(msg),
4573
4810
  worktreePath: msg.worktreePath,
4574
4811
  });
4575
4812
  }
4576
4813
  catch (err) {
4577
4814
  this.send(ws, {
4578
- type: "error",
4579
- message: `Failed to remove worktree: ${err}`,
4815
+ type: "worktree_removed",
4816
+ ...projectRequestMetadata(msg),
4817
+ worktreePath: msg.worktreePath,
4818
+ error: `Failed to remove worktree: ${err}`,
4580
4819
  });
4581
4820
  }
4582
4821
  break;
@@ -4584,7 +4823,12 @@ export class BridgeWebSocketServer {
4584
4823
  // ---- Git Operations (Phase 1-3) ----
4585
4824
  case "git_stage": {
4586
4825
  if (!this.isPathAllowed(msg.projectPath)) {
4587
- this.send(ws, this.buildPathNotAllowedError(msg.projectPath));
4826
+ this.send(ws, {
4827
+ type: "git_stage_result",
4828
+ ...projectRequestMetadata(msg),
4829
+ success: false,
4830
+ error: this.buildPathNotAllowedError(msg.projectPath, msg.requestId).message,
4831
+ });
4588
4832
  break;
4589
4833
  }
4590
4834
  try {
@@ -4592,11 +4836,16 @@ export class BridgeWebSocketServer {
4592
4836
  stageFiles(msg.projectPath, msg.files);
4593
4837
  if (msg.hunks?.length)
4594
4838
  stageHunks(msg.projectPath, msg.hunks);
4595
- this.send(ws, { type: "git_stage_result", success: true });
4839
+ this.send(ws, {
4840
+ type: "git_stage_result",
4841
+ ...projectRequestMetadata(msg),
4842
+ success: true,
4843
+ });
4596
4844
  }
4597
4845
  catch (err) {
4598
4846
  this.send(ws, {
4599
4847
  type: "git_stage_result",
4848
+ ...projectRequestMetadata(msg),
4600
4849
  success: false,
4601
4850
  error: String(err),
4602
4851
  });
@@ -4605,16 +4854,26 @@ export class BridgeWebSocketServer {
4605
4854
  }
4606
4855
  case "git_unstage": {
4607
4856
  if (!this.isPathAllowed(msg.projectPath)) {
4608
- this.send(ws, this.buildPathNotAllowedError(msg.projectPath));
4857
+ this.send(ws, {
4858
+ type: "git_unstage_result",
4859
+ ...projectRequestMetadata(msg),
4860
+ success: false,
4861
+ error: this.buildPathNotAllowedError(msg.projectPath, msg.requestId).message,
4862
+ });
4609
4863
  break;
4610
4864
  }
4611
4865
  try {
4612
4866
  unstageFiles(msg.projectPath, msg.files ?? []);
4613
- this.send(ws, { type: "git_unstage_result", success: true });
4867
+ this.send(ws, {
4868
+ type: "git_unstage_result",
4869
+ ...projectRequestMetadata(msg),
4870
+ success: true,
4871
+ });
4614
4872
  }
4615
4873
  catch (err) {
4616
4874
  this.send(ws, {
4617
4875
  type: "git_unstage_result",
4876
+ ...projectRequestMetadata(msg),
4618
4877
  success: false,
4619
4878
  error: String(err),
4620
4879
  });
@@ -4623,16 +4882,26 @@ export class BridgeWebSocketServer {
4623
4882
  }
4624
4883
  case "git_unstage_hunks": {
4625
4884
  if (!this.isPathAllowed(msg.projectPath)) {
4626
- this.send(ws, this.buildPathNotAllowedError(msg.projectPath));
4885
+ this.send(ws, {
4886
+ type: "git_unstage_hunks_result",
4887
+ ...projectRequestMetadata(msg),
4888
+ success: false,
4889
+ error: this.buildPathNotAllowedError(msg.projectPath, msg.requestId).message,
4890
+ });
4627
4891
  break;
4628
4892
  }
4629
4893
  try {
4630
4894
  unstageHunks(msg.projectPath, msg.hunks);
4631
- this.send(ws, { type: "git_unstage_hunks_result", success: true });
4895
+ this.send(ws, {
4896
+ type: "git_unstage_hunks_result",
4897
+ ...projectRequestMetadata(msg),
4898
+ success: true,
4899
+ });
4632
4900
  }
4633
4901
  catch (err) {
4634
4902
  this.send(ws, {
4635
4903
  type: "git_unstage_hunks_result",
4904
+ ...projectRequestMetadata(msg),
4636
4905
  success: false,
4637
4906
  error: String(err),
4638
4907
  });
@@ -4641,7 +4910,12 @@ export class BridgeWebSocketServer {
4641
4910
  }
4642
4911
  case "git_commit": {
4643
4912
  if (!this.isPathAllowed(msg.projectPath)) {
4644
- this.send(ws, this.buildPathNotAllowedError(msg.projectPath));
4913
+ this.send(ws, {
4914
+ type: "git_commit_result",
4915
+ ...projectRequestMetadata(msg),
4916
+ success: false,
4917
+ error: this.buildPathNotAllowedError(msg.projectPath, msg.requestId).message,
4918
+ });
4645
4919
  break;
4646
4920
  }
4647
4921
  const session = msg.sessionId
@@ -4675,6 +4949,7 @@ export class BridgeWebSocketServer {
4675
4949
  const result = gitCommit(msg.projectPath, message);
4676
4950
  this.send(ws, {
4677
4951
  type: "git_commit_result",
4952
+ ...projectRequestMetadata(msg),
4678
4953
  success: true,
4679
4954
  commitHash: result.hash,
4680
4955
  message: result.message,
@@ -4683,6 +4958,7 @@ export class BridgeWebSocketServer {
4683
4958
  catch (err) {
4684
4959
  this.send(ws, {
4685
4960
  type: "git_commit_result",
4961
+ ...projectRequestMetadata(msg),
4686
4962
  success: false,
4687
4963
  error: err instanceof Error ? err.message : String(err),
4688
4964
  });
@@ -4691,19 +4967,26 @@ export class BridgeWebSocketServer {
4691
4967
  }
4692
4968
  case "git_push": {
4693
4969
  if (!this.isPathAllowed(msg.projectPath)) {
4694
- this.send(ws, this.buildPathNotAllowedError(msg.projectPath));
4970
+ this.send(ws, {
4971
+ type: "git_push_result",
4972
+ ...projectRequestMetadata(msg),
4973
+ success: false,
4974
+ error: this.buildPathNotAllowedError(msg.projectPath, msg.requestId).message,
4975
+ });
4695
4976
  break;
4696
4977
  }
4697
4978
  try {
4698
4979
  gitPush(msg.projectPath);
4699
4980
  this.send(ws, {
4700
4981
  type: "git_push_result",
4982
+ ...projectRequestMetadata(msg),
4701
4983
  success: true,
4702
4984
  });
4703
4985
  }
4704
4986
  catch (err) {
4705
4987
  this.send(ws, {
4706
4988
  type: "git_push_result",
4989
+ ...projectRequestMetadata(msg),
4707
4990
  success: false,
4708
4991
  error: String(err),
4709
4992
  });
@@ -4712,13 +4995,22 @@ export class BridgeWebSocketServer {
4712
4995
  }
4713
4996
  case "git_branches": {
4714
4997
  if (!this.isPathAllowed(msg.projectPath)) {
4715
- this.send(ws, this.buildPathNotAllowedError(msg.projectPath));
4998
+ this.send(ws, {
4999
+ type: "git_branches_result",
5000
+ ...projectRequestMetadata(msg),
5001
+ current: "",
5002
+ branches: [],
5003
+ checkedOutBranches: [],
5004
+ remoteStatusByBranch: {},
5005
+ error: this.buildPathNotAllowedError(msg.projectPath, msg.requestId).message,
5006
+ });
4716
5007
  break;
4717
5008
  }
4718
5009
  try {
4719
5010
  const result = listBranches(msg.projectPath);
4720
5011
  this.send(ws, {
4721
5012
  type: "git_branches_result",
5013
+ ...projectRequestMetadata(msg),
4722
5014
  current: result.current,
4723
5015
  branches: result.branches,
4724
5016
  checkedOutBranches: result.checkedOutBranches,
@@ -4728,6 +5020,7 @@ export class BridgeWebSocketServer {
4728
5020
  catch (err) {
4729
5021
  this.send(ws, {
4730
5022
  type: "git_branches_result",
5023
+ ...projectRequestMetadata(msg),
4731
5024
  current: "",
4732
5025
  branches: [],
4733
5026
  remoteStatusByBranch: {},
@@ -4738,16 +5031,26 @@ export class BridgeWebSocketServer {
4738
5031
  }
4739
5032
  case "git_create_branch": {
4740
5033
  if (!this.isPathAllowed(msg.projectPath)) {
4741
- this.send(ws, this.buildPathNotAllowedError(msg.projectPath));
5034
+ this.send(ws, {
5035
+ type: "git_create_branch_result",
5036
+ ...projectRequestMetadata(msg),
5037
+ success: false,
5038
+ error: this.buildPathNotAllowedError(msg.projectPath, msg.requestId).message,
5039
+ });
4742
5040
  break;
4743
5041
  }
4744
5042
  try {
4745
5043
  createBranch(msg.projectPath, msg.name, msg.checkout);
4746
- this.send(ws, { type: "git_create_branch_result", success: true });
5044
+ this.send(ws, {
5045
+ type: "git_create_branch_result",
5046
+ ...projectRequestMetadata(msg),
5047
+ success: true,
5048
+ });
4747
5049
  }
4748
5050
  catch (err) {
4749
5051
  this.send(ws, {
4750
5052
  type: "git_create_branch_result",
5053
+ ...projectRequestMetadata(msg),
4751
5054
  success: false,
4752
5055
  error: String(err),
4753
5056
  });
@@ -4756,16 +5059,26 @@ export class BridgeWebSocketServer {
4756
5059
  }
4757
5060
  case "git_checkout_branch": {
4758
5061
  if (!this.isPathAllowed(msg.projectPath)) {
4759
- this.send(ws, this.buildPathNotAllowedError(msg.projectPath));
5062
+ this.send(ws, {
5063
+ type: "git_checkout_branch_result",
5064
+ ...projectRequestMetadata(msg),
5065
+ success: false,
5066
+ error: this.buildPathNotAllowedError(msg.projectPath, msg.requestId).message,
5067
+ });
4760
5068
  break;
4761
5069
  }
4762
5070
  try {
4763
5071
  checkoutBranch(msg.projectPath, msg.branch);
4764
- this.send(ws, { type: "git_checkout_branch_result", success: true });
5072
+ this.send(ws, {
5073
+ type: "git_checkout_branch_result",
5074
+ ...projectRequestMetadata(msg),
5075
+ success: true,
5076
+ });
4765
5077
  }
4766
5078
  catch (err) {
4767
5079
  this.send(ws, {
4768
5080
  type: "git_checkout_branch_result",
5081
+ ...projectRequestMetadata(msg),
4769
5082
  success: false,
4770
5083
  error: String(err),
4771
5084
  });
@@ -4774,16 +5087,26 @@ export class BridgeWebSocketServer {
4774
5087
  }
4775
5088
  case "git_revert_file": {
4776
5089
  if (!this.isPathAllowed(msg.projectPath)) {
4777
- this.send(ws, this.buildPathNotAllowedError(msg.projectPath));
5090
+ this.send(ws, {
5091
+ type: "git_revert_file_result",
5092
+ ...projectRequestMetadata(msg),
5093
+ success: false,
5094
+ error: this.buildPathNotAllowedError(msg.projectPath, msg.requestId).message,
5095
+ });
4778
5096
  break;
4779
5097
  }
4780
5098
  try {
4781
5099
  revertFiles(msg.projectPath, msg.files);
4782
- this.send(ws, { type: "git_revert_file_result", success: true });
5100
+ this.send(ws, {
5101
+ type: "git_revert_file_result",
5102
+ ...projectRequestMetadata(msg),
5103
+ success: true,
5104
+ });
4783
5105
  }
4784
5106
  catch (err) {
4785
5107
  this.send(ws, {
4786
5108
  type: "git_revert_file_result",
5109
+ ...projectRequestMetadata(msg),
4787
5110
  success: false,
4788
5111
  error: String(err),
4789
5112
  });
@@ -4792,16 +5115,26 @@ export class BridgeWebSocketServer {
4792
5115
  }
4793
5116
  case "git_revert_hunks": {
4794
5117
  if (!this.isPathAllowed(msg.projectPath)) {
4795
- this.send(ws, this.buildPathNotAllowedError(msg.projectPath));
5118
+ this.send(ws, {
5119
+ type: "git_revert_hunks_result",
5120
+ ...projectRequestMetadata(msg),
5121
+ success: false,
5122
+ error: this.buildPathNotAllowedError(msg.projectPath, msg.requestId).message,
5123
+ });
4796
5124
  break;
4797
5125
  }
4798
5126
  try {
4799
5127
  revertHunks(msg.projectPath, msg.hunks);
4800
- this.send(ws, { type: "git_revert_hunks_result", success: true });
5128
+ this.send(ws, {
5129
+ type: "git_revert_hunks_result",
5130
+ ...projectRequestMetadata(msg),
5131
+ success: true,
5132
+ });
4801
5133
  }
4802
5134
  catch (err) {
4803
5135
  this.send(ws, {
4804
5136
  type: "git_revert_hunks_result",
5137
+ ...projectRequestMetadata(msg),
4805
5138
  success: false,
4806
5139
  error: String(err),
4807
5140
  });
@@ -4810,16 +5143,26 @@ export class BridgeWebSocketServer {
4810
5143
  }
4811
5144
  case "git_fetch": {
4812
5145
  if (!this.isPathAllowed(msg.projectPath)) {
4813
- this.send(ws, this.buildPathNotAllowedError(msg.projectPath));
5146
+ this.send(ws, {
5147
+ type: "git_fetch_result",
5148
+ ...projectRequestMetadata(msg),
5149
+ success: false,
5150
+ error: this.buildPathNotAllowedError(msg.projectPath, msg.requestId).message,
5151
+ });
4814
5152
  break;
4815
5153
  }
4816
5154
  try {
4817
5155
  gitFetch(msg.projectPath);
4818
- this.send(ws, { type: "git_fetch_result", success: true });
5156
+ this.send(ws, {
5157
+ type: "git_fetch_result",
5158
+ ...projectRequestMetadata(msg),
5159
+ success: true,
5160
+ });
4819
5161
  }
4820
5162
  catch (err) {
4821
5163
  this.send(ws, {
4822
5164
  type: "git_fetch_result",
5165
+ ...projectRequestMetadata(msg),
4823
5166
  success: false,
4824
5167
  error: String(err),
4825
5168
  });
@@ -4828,7 +5171,12 @@ export class BridgeWebSocketServer {
4828
5171
  }
4829
5172
  case "git_pull": {
4830
5173
  if (!this.isPathAllowed(msg.projectPath)) {
4831
- this.send(ws, this.buildPathNotAllowedError(msg.projectPath));
5174
+ this.send(ws, {
5175
+ type: "git_pull_result",
5176
+ ...projectRequestMetadata(msg),
5177
+ success: false,
5178
+ error: this.buildPathNotAllowedError(msg.projectPath, msg.requestId).message,
5179
+ });
4832
5180
  break;
4833
5181
  }
4834
5182
  try {
@@ -4836,6 +5184,7 @@ export class BridgeWebSocketServer {
4836
5184
  if (result.success) {
4837
5185
  this.send(ws, {
4838
5186
  type: "git_pull_result",
5187
+ ...projectRequestMetadata(msg),
4839
5188
  success: true,
4840
5189
  message: result.message,
4841
5190
  });
@@ -4843,6 +5192,7 @@ export class BridgeWebSocketServer {
4843
5192
  else {
4844
5193
  this.send(ws, {
4845
5194
  type: "git_pull_result",
5195
+ ...projectRequestMetadata(msg),
4846
5196
  success: false,
4847
5197
  error: result.message,
4848
5198
  });
@@ -4851,6 +5201,7 @@ export class BridgeWebSocketServer {
4851
5201
  catch (err) {
4852
5202
  this.send(ws, {
4853
5203
  type: "git_pull_result",
5204
+ ...projectRequestMetadata(msg),
4854
5205
  success: false,
4855
5206
  error: String(err),
4856
5207
  });
@@ -4861,6 +5212,7 @@ export class BridgeWebSocketServer {
4861
5212
  if (!this.isPathAllowed(msg.projectPath)) {
4862
5213
  this.send(ws, {
4863
5214
  type: "git_status_result",
5215
+ requestId: msg.requestId,
4864
5216
  sessionId: msg.sessionId,
4865
5217
  projectPath: msg.projectPath,
4866
5218
  hasUncommittedChanges: false,
@@ -4882,6 +5234,7 @@ export class BridgeWebSocketServer {
4882
5234
  });
4883
5235
  this.send(ws, {
4884
5236
  type: "git_status_result",
5237
+ requestId: msg.requestId,
4885
5238
  sessionId: msg.sessionId,
4886
5239
  projectPath: msg.projectPath,
4887
5240
  hasUncommittedChanges: result.hasUncommittedChanges,
@@ -4900,6 +5253,7 @@ export class BridgeWebSocketServer {
4900
5253
  catch (err) {
4901
5254
  this.send(ws, {
4902
5255
  type: "git_status_result",
5256
+ requestId: msg.requestId,
4903
5257
  sessionId: msg.sessionId,
4904
5258
  projectPath: msg.projectPath,
4905
5259
  hasUncommittedChanges: false,
@@ -4918,13 +5272,22 @@ export class BridgeWebSocketServer {
4918
5272
  }
4919
5273
  case "git_remote_status": {
4920
5274
  if (!this.isPathAllowed(msg.projectPath)) {
4921
- this.send(ws, this.buildPathNotAllowedError(msg.projectPath));
5275
+ this.send(ws, {
5276
+ type: "git_remote_status_result",
5277
+ ...projectRequestMetadata(msg),
5278
+ ahead: 0,
5279
+ behind: 0,
5280
+ branch: "",
5281
+ hasUpstream: false,
5282
+ error: this.buildPathNotAllowedError(msg.projectPath, msg.requestId).message,
5283
+ });
4922
5284
  break;
4923
5285
  }
4924
5286
  try {
4925
5287
  const result = gitRemoteStatus(msg.projectPath);
4926
5288
  this.send(ws, {
4927
5289
  type: "git_remote_status_result",
5290
+ ...projectRequestMetadata(msg),
4928
5291
  ahead: result.ahead,
4929
5292
  behind: result.behind,
4930
5293
  branch: result.branch,
@@ -4934,6 +5297,7 @@ export class BridgeWebSocketServer {
4934
5297
  catch (err) {
4935
5298
  this.send(ws, {
4936
5299
  type: "git_remote_status_result",
5300
+ ...projectRequestMetadata(msg),
4937
5301
  ahead: 0,
4938
5302
  behind: 0,
4939
5303
  branch: "",
@@ -4947,6 +5311,7 @@ export class BridgeWebSocketServer {
4947
5311
  if (!session) {
4948
5312
  this.send(ws, {
4949
5313
  type: "rewind_preview",
5314
+ sessionId: msg.sessionId,
4950
5315
  canRewind: false,
4951
5316
  error: `Session ${msg.sessionId} not found`,
4952
5317
  });
@@ -4955,6 +5320,7 @@ export class BridgeWebSocketServer {
4955
5320
  if (session.provider === "codex") {
4956
5321
  this.send(ws, {
4957
5322
  type: "rewind_preview",
5323
+ sessionId: msg.sessionId,
4958
5324
  canRewind: false,
4959
5325
  error: "Codex rewind does not restore files",
4960
5326
  });
@@ -4965,6 +5331,7 @@ export class BridgeWebSocketServer {
4965
5331
  .then((result) => {
4966
5332
  this.send(ws, {
4967
5333
  type: "rewind_preview",
5334
+ sessionId: msg.sessionId,
4968
5335
  canRewind: result.canRewind,
4969
5336
  filesChanged: result.filesChanged,
4970
5337
  insertions: result.insertions,
@@ -4975,6 +5342,7 @@ export class BridgeWebSocketServer {
4975
5342
  .catch((err) => {
4976
5343
  this.send(ws, {
4977
5344
  type: "rewind_preview",
5345
+ sessionId: msg.sessionId,
4978
5346
  canRewind: false,
4979
5347
  error: `Dry run failed: ${err}`,
4980
5348
  });
@@ -4986,6 +5354,7 @@ export class BridgeWebSocketServer {
4986
5354
  if (!session) {
4987
5355
  this.send(ws, {
4988
5356
  type: "rewind_result",
5357
+ sessionId: msg.sessionId,
4989
5358
  success: false,
4990
5359
  mode: msg.mode,
4991
5360
  error: `Session ${msg.sessionId} not found`,
@@ -4996,6 +5365,7 @@ export class BridgeWebSocketServer {
4996
5365
  const errMsg = err instanceof Error ? err.message : String(err);
4997
5366
  this.send(ws, {
4998
5367
  type: "rewind_result",
5368
+ sessionId: msg.sessionId,
4999
5369
  success: false,
5000
5370
  mode: msg.mode,
5001
5371
  error: errMsg,
@@ -5014,6 +5384,7 @@ export class BridgeWebSocketServer {
5014
5384
  if (result.canRewind) {
5015
5385
  this.send(ws, {
5016
5386
  type: "rewind_result",
5387
+ sessionId: msg.sessionId,
5017
5388
  success: true,
5018
5389
  mode: "code",
5019
5390
  });
@@ -5021,6 +5392,7 @@ export class BridgeWebSocketServer {
5021
5392
  else {
5022
5393
  this.send(ws, {
5023
5394
  type: "rewind_result",
5395
+ sessionId: msg.sessionId,
5024
5396
  success: false,
5025
5397
  mode: "code",
5026
5398
  error: result.error ?? "Cannot rewind files",
@@ -5036,6 +5408,7 @@ export class BridgeWebSocketServer {
5036
5408
  this.sessionManager.rewindConversation(msg.sessionId, msg.targetUuid, (newSessionId) => {
5037
5409
  this.send(ws, {
5038
5410
  type: "rewind_result",
5411
+ sessionId: msg.sessionId,
5039
5412
  success: true,
5040
5413
  mode: "conversation",
5041
5414
  });
@@ -5067,6 +5440,7 @@ export class BridgeWebSocketServer {
5067
5440
  if (!result.canRewind) {
5068
5441
  this.send(ws, {
5069
5442
  type: "rewind_result",
5443
+ sessionId: msg.sessionId,
5070
5444
  success: false,
5071
5445
  mode: "both",
5072
5446
  error: result.error ?? "Cannot rewind files",
@@ -5078,6 +5452,7 @@ export class BridgeWebSocketServer {
5078
5452
  this.sessionManager.rewindConversation(msg.sessionId, msg.targetUuid, (newSessionId) => {
5079
5453
  this.send(ws, {
5080
5454
  type: "rewind_result",
5455
+ sessionId: msg.sessionId,
5081
5456
  success: true,
5082
5457
  mode: "both",
5083
5458
  });
@@ -5109,6 +5484,7 @@ export class BridgeWebSocketServer {
5109
5484
  const errMsg = err instanceof Error ? err.message : String(err);
5110
5485
  this.send(ws, {
5111
5486
  type: "error",
5487
+ sessionId: msg.sessionId,
5112
5488
  message: errMsg,
5113
5489
  errorCode: "fork_failed",
5114
5490
  });
@@ -5154,6 +5530,8 @@ export class BridgeWebSocketServer {
5154
5530
  const info = this.galleryStore.metaToInfo(meta);
5155
5531
  this.send(ws, {
5156
5532
  type: "screenshot_result",
5533
+ ...projectRequestMetadata(msg),
5534
+ sessionId: msg.sessionId,
5157
5535
  success: true,
5158
5536
  image: info,
5159
5537
  });
@@ -5163,6 +5541,8 @@ export class BridgeWebSocketServer {
5163
5541
  }
5164
5542
  this.send(ws, {
5165
5543
  type: "screenshot_result",
5544
+ ...projectRequestMetadata(msg),
5545
+ sessionId: msg.sessionId,
5166
5546
  success: false,
5167
5547
  error: "Failed to save screenshot to gallery",
5168
5548
  });
@@ -5175,6 +5555,8 @@ export class BridgeWebSocketServer {
5175
5555
  .catch((err) => {
5176
5556
  this.send(ws, {
5177
5557
  type: "screenshot_result",
5558
+ ...projectRequestMetadata(msg),
5559
+ sessionId: msg.sessionId,
5178
5560
  success: false,
5179
5561
  error: err instanceof Error ? err.message : String(err),
5180
5562
  });
@@ -5727,6 +6109,7 @@ export class BridgeWebSocketServer {
5727
6109
  defaultCodexProfile: this.defaultCodexProfile,
5728
6110
  codexAutoReviewDisabled: this.codexAutoReviewDisabled,
5729
6111
  bridgeVersion: getPackageVersion(),
6112
+ protocolCapabilities: ["project_request_correlation_v1"],
5730
6113
  });
5731
6114
  }
5732
6115
  sendPromptHistoryStatus(ws) {
@@ -5759,6 +6142,7 @@ export class BridgeWebSocketServer {
5759
6142
  defaultCodexProfile: this.defaultCodexProfile,
5760
6143
  codexAutoReviewDisabled: this.codexAutoReviewDisabled,
5761
6144
  bridgeVersion: getPackageVersion(),
6145
+ protocolCapabilities: ["project_request_correlation_v1"],
5762
6146
  });
5763
6147
  }
5764
6148
  broadcastPromptHistoryStatus() {
@@ -6650,7 +7034,7 @@ export class BridgeWebSocketServer {
6650
7034
  if (!compatibleMsg)
6651
7035
  return;
6652
7036
  const sessionId = this.extractSessionIdFromServerMessage(compatibleMsg);
6653
- if (sessionId) {
7037
+ if (sessionId && this.debugEvents.has(sessionId)) {
6654
7038
  this.recordDebugEvent(sessionId, {
6655
7039
  direction: "outgoing",
6656
7040
  channel: "ws",