@ccpocket/bridge 1.91.3 → 1.92.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
@@ -984,6 +984,7 @@ export class BridgeWebSocketServer {
984
984
  }
985
985
  : {}),
986
986
  ...(sourceSessionId ? { sourceSessionId } : {}),
987
+ ...(params.forked ? { forked: true } : {}),
987
988
  ...(resumeRequestId ? { resumeRequestId } : {}),
988
989
  ...(requestId ? { requestId } : {}),
989
990
  };
@@ -1060,7 +1061,7 @@ export class BridgeWebSocketServer {
1060
1061
  });
1061
1062
  return;
1062
1063
  }
1063
- if (session.codexQueuedInput) {
1064
+ if (session.queuedInput) {
1064
1065
  this.send(ws, {
1065
1066
  type: "rewind_result",
1066
1067
  sessionId,
@@ -1145,6 +1146,79 @@ export class BridgeWebSocketServer {
1145
1146
  }));
1146
1147
  this.sendSessionList(ws);
1147
1148
  }
1149
+ /**
1150
+ * Branch a Claude conversation into a new session that ends at the given
1151
+ * assistant message. The source session keeps running unchanged.
1152
+ */
1153
+ async forkClaudeSession(ws, sessionId, targetUuid) {
1154
+ const fail = (message) => {
1155
+ this.send(ws, {
1156
+ type: "error",
1157
+ sessionId,
1158
+ message,
1159
+ errorCode: "fork_failed",
1160
+ });
1161
+ };
1162
+ const session = this.sessionManager.get(sessionId);
1163
+ if (!session || session.provider !== "claude") {
1164
+ fail(`Session ${sessionId} not found`);
1165
+ return;
1166
+ }
1167
+ const claudeSessionId = session.claudeSessionId;
1168
+ if (!claudeSessionId) {
1169
+ fail("Session has no Claude session ID yet");
1170
+ return;
1171
+ }
1172
+ const sdkProcess = session.process;
1173
+ if (session.status !== "idle" || (sdkProcess.status ?? "idle") !== "idle") {
1174
+ fail("Cannot fork while Claude is running");
1175
+ return;
1176
+ }
1177
+ if (session.queuedInput) {
1178
+ fail("Cannot fork while Claude has queued input");
1179
+ return;
1180
+ }
1181
+ const history = await getSessionHistory(claudeSessionId);
1182
+ const targetIndex = history.findIndex((message) => message.role === "assistant" && message.uuid === targetUuid);
1183
+ if (targetIndex < 0) {
1184
+ fail("Fork target not found in the Claude transcript");
1185
+ return;
1186
+ }
1187
+ const pastMessages = history.slice(0, targetIndex + 1);
1188
+ const workspace = this.workspaceForRuntimeSession(session);
1189
+ const worktreeOpts = session.worktreePath
1190
+ ? {
1191
+ existingWorktreePath: session.worktreePath,
1192
+ worktreeBranch: session.worktreeBranch,
1193
+ }
1194
+ : undefined;
1195
+ const settings = sdkProcess.settings ?? {};
1196
+ const permissionMode = sdkProcess.permissionMode;
1197
+ const newSessionId = this.sessionManager.create(session.projectPath, {
1198
+ sessionId: claudeSessionId,
1199
+ forkSession: true,
1200
+ resumeSessionAt: targetUuid,
1201
+ ...(permissionMode ? { permissionMode } : {}),
1202
+ ...(settings.model ? { model: settings.model } : {}),
1203
+ ...(settings.effort ? { effort: settings.effort } : {}),
1204
+ ...(settings.fastMode ? { fastMode: true } : {}),
1205
+ ...(session.sandboxEnabled !== undefined
1206
+ ? { sandboxEnabled: session.sandboxEnabled }
1207
+ : {}),
1208
+ }, pastMessages, worktreeOpts, "claude");
1209
+ this.attachWorkspaceToRuntimeSession(newSessionId, workspace);
1210
+ const newSession = this.sessionManager.get(newSessionId);
1211
+ this.send(ws, this.buildSessionCreatedMessage({
1212
+ sessionId: newSessionId,
1213
+ provider: "claude",
1214
+ projectPath: session.projectPath,
1215
+ session: newSession,
1216
+ permissionMode,
1217
+ sourceSessionId: sessionId,
1218
+ forked: true,
1219
+ }));
1220
+ this.sendSessionList(ws);
1221
+ }
1148
1222
  async forkCodexSession(ws, sessionId, targetUuid) {
1149
1223
  const session = this.sessionManager.get(sessionId);
1150
1224
  if (!session) {
@@ -1173,7 +1247,7 @@ export class BridgeWebSocketServer {
1173
1247
  });
1174
1248
  return;
1175
1249
  }
1176
- if (session.codexQueuedInput) {
1250
+ if (session.queuedInput) {
1177
1251
  this.send(ws, {
1178
1252
  type: "error",
1179
1253
  message: "Cannot fork while Codex has queued input",
@@ -1222,6 +1296,7 @@ export class BridgeWebSocketServer {
1222
1296
  approvalsReviewer: newSession?.codexSettings?.approvalsReviewer,
1223
1297
  sandboxMode: newSession?.codexSettings?.sandboxMode,
1224
1298
  sourceSessionId: sessionId,
1299
+ forked: true,
1225
1300
  }));
1226
1301
  this.sendSessionList(ws);
1227
1302
  }
@@ -1509,6 +1584,26 @@ export class BridgeWebSocketServer {
1509
1584
  return String(value);
1510
1585
  }
1511
1586
  }
1587
+ claudeSettingsMessage(sessionId, session) {
1588
+ const settings = session.provider === "claude"
1589
+ ? (session.process
1590
+ .settings ?? {})
1591
+ : {};
1592
+ return {
1593
+ type: "system",
1594
+ subtype: "claude_settings",
1595
+ sessionId,
1596
+ provider: "claude",
1597
+ ...(settings.model ? { model: settings.model } : {}),
1598
+ ...(settings.effort ? { effort: settings.effort } : {}),
1599
+ fastMode: settings.fastMode === true,
1600
+ };
1601
+ }
1602
+ sendClaudeCurrentSettings(ws, sessionId, session) {
1603
+ if (session.provider !== "claude")
1604
+ return;
1605
+ this.send(ws, this.claudeSettingsMessage(sessionId, session));
1606
+ }
1512
1607
  sendCodexCurrentSettings(ws, sessionId, session) {
1513
1608
  const process = session.process instanceof CodexProcess
1514
1609
  ? session.process
@@ -1545,7 +1640,7 @@ export class BridgeWebSocketServer {
1545
1640
  reason: "reset",
1546
1641
  });
1547
1642
  this.sendCodexCurrentSettings(ws, sessionId, session);
1548
- this.sendCodexQueueState(ws, sessionId, session);
1643
+ this.sendQueueState(ws, sessionId, session);
1549
1644
  this.sendCodexGoalState(ws, sessionId, session);
1550
1645
  if (options.includeCachedCommands) {
1551
1646
  this.sendCachedCommands(ws, sessionId, session);
@@ -1576,7 +1671,7 @@ export class BridgeWebSocketServer {
1576
1671
  status: session.status,
1577
1672
  sessionId,
1578
1673
  });
1579
- this.sendCodexQueueState(ws, sessionId, session);
1674
+ this.sendQueueState(ws, sessionId, session);
1580
1675
  this.sendCodexGoalState(ws, sessionId, session);
1581
1676
  this.sendCachedCommands(ws, sessionId, session);
1582
1677
  return true;
@@ -2103,6 +2198,7 @@ export class BridgeWebSocketServer {
2103
2198
  permissionMode: claudePermissionMode,
2104
2199
  model: msg.model,
2105
2200
  effort: msg.effort,
2201
+ fastMode: msg.claudeFastMode,
2106
2202
  maxTurns: msg.maxTurns,
2107
2203
  maxBudgetUsd: msg.maxBudgetUsd,
2108
2204
  fallbackModel: msg.fallbackModel,
@@ -2321,29 +2417,47 @@ export class BridgeWebSocketServer {
2321
2417
  });
2322
2418
  break;
2323
2419
  }
2324
- if (session.provider === "codex" &&
2325
- !session.process.isWaitingForInput) {
2326
- if (session.codexQueuedInput) {
2327
- this.send(ws, {
2328
- type: "input_rejected",
2329
- sessionId: session.id,
2330
- ...(clientMessageId ? { clientMessageId } : {}),
2331
- reason: "Queue is full",
2332
- });
2333
- break;
2334
- }
2335
- const queued = this.sessionManager.queueCodexInput(session.id, {
2420
+ const delivery = msg.delivery;
2421
+ const sdkTurnActive = session.provider === "claude"
2422
+ ? session.process
2423
+ .isTurnActive
2424
+ : undefined;
2425
+ const turnActive = session.provider === "codex"
2426
+ ? !session.process.isWaitingForInput
2427
+ : typeof sdkTurnActive === "boolean"
2428
+ ? sdkTurnActive
2429
+ : isAgentBusySnapshot;
2430
+ // Older clients omit `delivery`: Codex queues and Claude keeps the
2431
+ // legacy interrupt-and-send path below.
2432
+ const holdForDelivery = turnActive &&
2433
+ (delivery !== undefined || session.provider === "codex");
2434
+ if (holdForDelivery) {
2435
+ const pendingInput = {
2336
2436
  itemId: randomUUID(),
2337
2437
  ...(clientMessageId ? { clientMessageId } : {}),
2338
2438
  text,
2339
2439
  createdAt: new Date().toISOString(),
2340
- userMessageUuid: nextCodexUserTurnUuid(session),
2440
+ ...(session.provider === "codex"
2441
+ ? { userMessageUuid: nextCodexUserTurnUuid(session) }
2442
+ : {}),
2341
2443
  ...(images.length > 0 ? { imageCount: images.length, images } : {}),
2342
2444
  ...(imageRefs ? { imageRefs } : {}),
2343
2445
  ...(codexSkills.length > 0 ? { skills: codexSkills } : {}),
2344
2446
  ...(codexMentions.length > 0 ? { mentions: codexMentions } : {}),
2345
- });
2346
- if (!queued) {
2447
+ };
2448
+ let outcome;
2449
+ if (delivery === "steer") {
2450
+ outcome = await this.sessionManager.steerInput(session.id, pendingInput);
2451
+ }
2452
+ else if (session.queuedInput) {
2453
+ outcome = "rejected";
2454
+ }
2455
+ else {
2456
+ outcome = this.sessionManager.queueInput(session.id, pendingInput)
2457
+ ? "queued"
2458
+ : "rejected";
2459
+ }
2460
+ if (outcome === "rejected") {
2347
2461
  this.send(ws, {
2348
2462
  type: "input_rejected",
2349
2463
  sessionId: session.id,
@@ -2366,7 +2480,7 @@ export class BridgeWebSocketServer {
2366
2480
  sessionId: session.id,
2367
2481
  ...(clientMessageId ? { clientMessageId } : {}),
2368
2482
  acceptedSeq: session.historyRevision,
2369
- queued: true,
2483
+ queued: outcome === "queued",
2370
2484
  });
2371
2485
  this.broadcastSessionList();
2372
2486
  break;
@@ -2580,11 +2694,11 @@ export class BridgeWebSocketServer {
2580
2694
  }
2581
2695
  case "update_queued_input": {
2582
2696
  const session = this.resolveSession(msg.sessionId);
2583
- if (!session || session.provider !== "codex") {
2697
+ if (!session) {
2584
2698
  this.send(ws, {
2585
2699
  type: "error",
2586
2700
  sessionId: msg.sessionId,
2587
- message: "No active Codex session.",
2701
+ message: "No active session.",
2588
2702
  });
2589
2703
  return;
2590
2704
  }
@@ -2596,7 +2710,7 @@ export class BridgeWebSocketServer {
2596
2710
  });
2597
2711
  return;
2598
2712
  }
2599
- const success = this.sessionManager.updateCodexQueuedInput(session.id, msg.itemId, msg.text, { skills: msg.skills ?? [], mentions: msg.mentions ?? [] });
2713
+ const success = this.sessionManager.updateQueuedInput(session.id, msg.itemId, msg.text, { skills: msg.skills ?? [], mentions: msg.mentions ?? [] });
2600
2714
  if (!success) {
2601
2715
  this.send(ws, {
2602
2716
  type: "error",
@@ -2611,15 +2725,15 @@ export class BridgeWebSocketServer {
2611
2725
  }
2612
2726
  case "cancel_queued_input": {
2613
2727
  const session = this.resolveSession(msg.sessionId);
2614
- if (!session || session.provider !== "codex") {
2728
+ if (!session) {
2615
2729
  this.send(ws, {
2616
2730
  type: "error",
2617
2731
  sessionId: msg.sessionId,
2618
- message: "No active Codex session.",
2732
+ message: "No active session.",
2619
2733
  });
2620
2734
  return;
2621
2735
  }
2622
- const success = this.sessionManager.cancelCodexQueuedInput(session.id, msg.itemId);
2736
+ const success = this.sessionManager.cancelQueuedInput(session.id, msg.itemId);
2623
2737
  if (!success) {
2624
2738
  this.send(ws, {
2625
2739
  type: "error",
@@ -2634,15 +2748,15 @@ export class BridgeWebSocketServer {
2634
2748
  }
2635
2749
  case "steer_queued_input": {
2636
2750
  const session = this.resolveSession(msg.sessionId);
2637
- if (!session || session.provider !== "codex") {
2751
+ if (!session) {
2638
2752
  this.send(ws, {
2639
2753
  type: "error",
2640
2754
  sessionId: msg.sessionId,
2641
- message: "No active Codex session.",
2755
+ message: "No active session.",
2642
2756
  });
2643
2757
  return;
2644
2758
  }
2645
- const result = await this.sessionManager.steerCodexQueuedInput(session.id, msg.itemId);
2759
+ const result = await this.sessionManager.steerQueuedInput(session.id, msg.itemId);
2646
2760
  if (!result.ok) {
2647
2761
  this.send(ws, {
2648
2762
  type: "error",
@@ -3139,6 +3253,43 @@ export class BridgeWebSocketServer {
3139
3253
  console.log(`[ws] set_codex_speed(codex): serviceTier=${serviceTier}`);
3140
3254
  break;
3141
3255
  }
3256
+ case "set_claude_settings": {
3257
+ const session = this.resolveSession(msg.sessionId);
3258
+ if (!session || session.provider !== "claude") {
3259
+ this.send(ws, {
3260
+ type: "error",
3261
+ sessionId: msg.sessionId,
3262
+ message: "No active Claude session.",
3263
+ errorCode: "set_claude_settings_unsupported",
3264
+ });
3265
+ break;
3266
+ }
3267
+ const claudeProcess = session.process;
3268
+ try {
3269
+ await claudeProcess.updateSettings({
3270
+ ...(msg.model !== undefined ? { model: msg.model.trim() } : {}),
3271
+ ...(msg.effort !== undefined ? { effort: msg.effort } : {}),
3272
+ ...(msg.fastMode !== undefined ? { fastMode: msg.fastMode } : {}),
3273
+ });
3274
+ }
3275
+ catch (err) {
3276
+ this.send(ws, {
3277
+ type: "error",
3278
+ sessionId: msg.sessionId,
3279
+ message: `Failed to change Claude settings: ${err instanceof Error ? err.message : String(err)}`,
3280
+ errorCode: "set_claude_settings_rejected",
3281
+ });
3282
+ // Re-sync the requester with the settings still in effect.
3283
+ this.sendClaudeCurrentSettings(ws, session.id, session);
3284
+ break;
3285
+ }
3286
+ session.lastActivityAt = new Date();
3287
+ this.broadcastSessionMessage(session.id, this.claudeSettingsMessage(session.id, session));
3288
+ this.broadcastSessionList();
3289
+ const settings = claudeProcess.settings;
3290
+ console.log(`[ws] set_claude_settings: model=${settings.model ?? "default"} effort=${settings.effort ?? ""} fast=${settings.fastMode === true}`);
3291
+ break;
3292
+ }
3142
3293
  case "set_codex_recovery":
3143
3294
  case "cancel_codex_recovery": {
3144
3295
  const session = this.resolveSession(msg.sessionId);
@@ -3677,13 +3828,16 @@ export class BridgeWebSocketServer {
3677
3828
  if (session.provider === "codex") {
3678
3829
  this.sendCodexCurrentSettings(ws, msg.sessionId, session);
3679
3830
  }
3831
+ else {
3832
+ this.sendClaudeCurrentSettings(ws, msg.sessionId, session);
3833
+ }
3680
3834
  this.send(ws, {
3681
3835
  type: "status",
3682
3836
  status: session.status,
3683
3837
  sessionId: msg.sessionId,
3684
3838
  });
3839
+ this.sendQueueState(ws, msg.sessionId, session);
3685
3840
  if (session.provider === "codex") {
3686
- this.sendCodexQueueState(ws, msg.sessionId, session);
3687
3841
  this.sendCodexGoalState(ws, msg.sessionId, session);
3688
3842
  }
3689
3843
  this.sendCachedCommands(ws, msg.sessionId, session);
@@ -3790,7 +3944,7 @@ export class BridgeWebSocketServer {
3790
3944
  ...(result.kind === "snapshot" ? { reason: result.reason } : {}),
3791
3945
  });
3792
3946
  this.sendCodexCurrentSettings(ws, msg.sessionId, session);
3793
- this.sendCodexQueueState(ws, msg.sessionId, session);
3947
+ this.sendQueueState(ws, msg.sessionId, session);
3794
3948
  this.sendCodexGoalState(ws, msg.sessionId, session);
3795
3949
  break;
3796
3950
  }
@@ -3826,6 +3980,8 @@ export class BridgeWebSocketServer {
3826
3980
  status: session.status,
3827
3981
  ...(result.kind === "snapshot" ? { reason: result.reason } : {}),
3828
3982
  });
3983
+ this.sendClaudeCurrentSettings(ws, msg.sessionId, session);
3984
+ this.sendQueueState(ws, msg.sessionId, session);
3829
3985
  }
3830
3986
  else {
3831
3987
  this.send(ws, {
@@ -4374,6 +4530,7 @@ export class BridgeWebSocketServer {
4374
4530
  permissionMode: claudePermissionMode,
4375
4531
  model: msg.model,
4376
4532
  effort: msg.effort,
4533
+ fastMode: msg.claudeFastMode,
4377
4534
  maxTurns: msg.maxTurns,
4378
4535
  maxBudgetUsd: msg.maxBudgetUsd,
4379
4536
  fallbackModel: msg.fallbackModel,
@@ -4578,7 +4735,7 @@ export class BridgeWebSocketServer {
4578
4735
  if (msg.type === "trust_project") {
4579
4736
  if (msg.sessionId) {
4580
4737
  const session = this.sessionManager.get(msg.sessionId);
4581
- if (!session || session.provider !== "codex" || session.codexQueuedInput ||
4738
+ if (!session || session.provider !== "codex" || session.queuedInput ||
4582
4739
  await realpath(session.worktreePath ?? session.projectPath) !== projectPath) {
4583
4740
  throw new Error("Session is unavailable or has queued input");
4584
4741
  }
@@ -5988,7 +6145,11 @@ export class BridgeWebSocketServer {
5988
6145
  break;
5989
6146
  }
5990
6147
  case "fork": {
5991
- this.forkCodexSession(ws, msg.sessionId, msg.targetUuid).catch((err) => {
6148
+ const forkSource = this.sessionManager.get(msg.sessionId);
6149
+ const fork = forkSource?.provider === "claude"
6150
+ ? this.forkClaudeSession(ws, msg.sessionId, msg.targetUuid)
6151
+ : this.forkCodexSession(ws, msg.sessionId, msg.targetUuid);
6152
+ fork.catch((err) => {
5992
6153
  const errMsg = err instanceof Error ? err.message : String(err);
5993
6154
  this.send(ws, {
5994
6155
  type: "error",
@@ -6346,6 +6507,7 @@ export class BridgeWebSocketServer {
6346
6507
  sandboxMode: msg.sandboxMode,
6347
6508
  model: msg.model,
6348
6509
  effort: msg.effort,
6510
+ fastMode: msg.claudeFastMode,
6349
6511
  maxTurns: msg.maxTurns,
6350
6512
  maxBudgetUsd: msg.maxBudgetUsd,
6351
6513
  fallbackModel: msg.fallbackModel,
@@ -6655,6 +6817,9 @@ export class BridgeWebSocketServer {
6655
6817
  "performance_mode_v1",
6656
6818
  "project_request_correlation_v1",
6657
6819
  "session_context_v1",
6820
+ "input_delivery_v1",
6821
+ "claude_session_settings_v1",
6822
+ "claude_fork_v1",
6658
6823
  ],
6659
6824
  });
6660
6825
  }
@@ -6696,6 +6861,9 @@ export class BridgeWebSocketServer {
6696
6861
  "performance_mode_v1",
6697
6862
  "project_request_correlation_v1",
6698
6863
  "session_context_v1",
6864
+ "input_delivery_v1",
6865
+ "claude_session_settings_v1",
6866
+ "claude_fork_v1",
6699
6867
  ],
6700
6868
  });
6701
6869
  }
@@ -7304,6 +7472,7 @@ export class BridgeWebSocketServer {
7304
7472
  value: model.model,
7305
7473
  ...(model.displayName ? { displayName: model.displayName } : {}),
7306
7474
  ...(model.description ? { description: model.description } : {}),
7475
+ ...(model.supportsFastMode ? { supportsFastMode: true } : {}),
7307
7476
  }));
7308
7477
  this.claudeModelEfforts = Object.fromEntries(models.map((model) => [model.model, model.effortLevels]));
7309
7478
  }
@@ -7828,8 +7997,8 @@ export class BridgeWebSocketServer {
7828
7997
  return false;
7829
7998
  });
7830
7999
  }
7831
- sendCodexQueueState(ws, sessionId, session) {
7832
- const item = session.codexQueuedInput;
8000
+ sendQueueState(ws, sessionId, session) {
8001
+ const item = session.queuedInput;
7833
8002
  this.sendConversationQueue(ws, {
7834
8003
  type: "conversation_queue",
7835
8004
  sessionId,