@ccpocket/bridge 1.69.0 → 1.69.3

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
@@ -7,10 +7,11 @@ import { promisify } from "node:util";
7
7
  import { WebSocketServer, WebSocket } from "ws";
8
8
  import { SessionManager, MAX_HISTORY_PER_SESSION, } from "./session.js";
9
9
  import { SdkProcess, listAvailableClaudeModels, } from "./sdk-process.js";
10
- import { CodexProcess, } from "./codex-process.js";
10
+ import { codexErrorMessage, CodexRpcError, CodexProcess, } from "./codex-process.js";
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 { formatResumePerformanceLog, summarizeResumeHistory, } from "./resume-metrics.js";
14
15
  import { ArchiveStore } from "./archive-store.js";
15
16
  import { WorktreeStore } from "./worktree-store.js";
16
17
  import { listWorktrees, removeWorktree, worktreeExists, getMainBranch, } from "./worktree.js";
@@ -24,6 +25,8 @@ import { fetchAllUsage } from "./usage.js";
24
25
  import { getPackageVersion } from "./version.js";
25
26
  import { isPathWithinAllowedDirectory, resolvePlatformPath, resolvePlatformPathFrom, } from "./path-utils.js";
26
27
  import { deriveCodexPermissionsMode, normalizeCodexPermissionsMode, withDerivedCodexPermissionsMode, } from "./codex-permissions.js";
28
+ const RESUME_OPERATION_TIMEOUT_MS = 5 * 60 * 1000;
29
+ const RESUME_COMPLETED_TTL_MS = 30 * 1000;
27
30
  // ---- Available model lists (delivered to clients via session_list) ----
28
31
  const FALLBACK_CLAUDE_MODELS = [
29
32
  "claude-opus-4-7",
@@ -243,6 +246,9 @@ function normalizeCodexApprovalPolicy(value) {
243
246
  return "on-request";
244
247
  }
245
248
  }
249
+ function isCodexAutoReviewApprovalsReviewer(value) {
250
+ return value === "auto_review" || value === "guardian_subagent";
251
+ }
246
252
  function sanitizeCodexModel(model) {
247
253
  if (typeof model !== "string")
248
254
  return undefined;
@@ -457,6 +463,8 @@ export class BridgeWebSocketServer {
457
463
  archiveStore;
458
464
  codexProfiles = [];
459
465
  defaultCodexProfile;
466
+ codexAutoReviewDisabled = false;
467
+ codexAutoReviewPolicyLoaded = false;
460
468
  codexMetadataRequest = null;
461
469
  lastConnectMetadataRefreshAt = null;
462
470
  claudeModels = FALLBACK_CLAUDE_MODELS;
@@ -486,6 +494,7 @@ export class BridgeWebSocketServer {
486
494
  platform;
487
495
  clientSupportedServerMessages = new WeakMap();
488
496
  pendingClaudeResumeInputs = new WeakMap();
497
+ resumeOperations = new Map();
489
498
  constructor(options) {
490
499
  const { server, apiKey, allowedDirs, imageStore, galleryStore, projectHistory, debugTraceStore, recordingStore, firebaseAuth, promptHistoryBackup, promptHistoryStore, platform, fileListMaxEntries, fileListMaxBytes, deltaBatchMs, deltaBatchMaxChars, } = options;
491
500
  this.apiKey = apiKey ?? null;
@@ -809,10 +818,10 @@ export class BridgeWebSocketServer {
809
818
  fallback: buildCodexHistoryPrefix(session, targetOrdinal - 1),
810
819
  });
811
820
  this.destroySession(sessionId);
812
- const newSessionId = this.sessionManager.create(projectPath, undefined, pastMessages, worktreeOpts, "codex", {
821
+ const newSessionId = this.sessionManager.create(projectPath, undefined, pastMessages, worktreeOpts, "codex", this.withCodexAutoReviewPolicy({
813
822
  ...(codexSettings ?? {}),
814
823
  threadId,
815
- });
824
+ }));
816
825
  const newSession = this.sessionManager.get(newSessionId);
817
826
  this.send(ws, {
818
827
  type: "rewind_result",
@@ -824,8 +833,8 @@ export class BridgeWebSocketServer {
824
833
  provider: "codex",
825
834
  projectPath,
826
835
  session: newSession,
827
- approvalsReviewer: codexSettings?.approvalsReviewer,
828
- sandboxMode: codexSettings?.sandboxMode,
836
+ approvalsReviewer: newSession?.codexSettings?.approvalsReviewer,
837
+ sandboxMode: newSession?.codexSettings?.sandboxMode,
829
838
  sourceSessionId: sessionId,
830
839
  }));
831
840
  this.sendSessionList(ws);
@@ -896,18 +905,18 @@ export class BridgeWebSocketServer {
896
905
  expectedUserTurns: targetOrdinal,
897
906
  fallback: buildCodexHistoryPrefix(session, targetOrdinal),
898
907
  });
899
- const newSessionId = this.sessionManager.create(projectPath, undefined, pastMessages, worktreeOpts, "codex", {
908
+ const newSessionId = this.sessionManager.create(projectPath, undefined, pastMessages, worktreeOpts, "codex", this.withCodexAutoReviewPolicy({
900
909
  ...(codexSettings ?? {}),
901
910
  threadId: forkedThreadId,
902
- });
911
+ }));
903
912
  const newSession = this.sessionManager.get(newSessionId);
904
913
  this.send(ws, this.buildSessionCreatedMessage({
905
914
  sessionId: newSessionId,
906
915
  provider: "codex",
907
916
  projectPath,
908
917
  session: newSession,
909
- approvalsReviewer: codexSettings?.approvalsReviewer,
910
- sandboxMode: codexSettings?.sandboxMode,
918
+ approvalsReviewer: newSession?.codexSettings?.approvalsReviewer,
919
+ sandboxMode: newSession?.codexSettings?.sandboxMode,
911
920
  sourceSessionId: sessionId,
912
921
  }));
913
922
  this.sendSessionList(ws);
@@ -975,7 +984,9 @@ export class BridgeWebSocketServer {
975
984
  const threadId = this.codexThreadIdForSession(session);
976
985
  if (!threadId)
977
986
  return null;
978
- const history = await this.getCodexThreadHistoryFromRpc(threadId, session.projectPath, session.process);
987
+ const history = session.codexInitialHistoryPending
988
+ ? (session.pastMessages ?? [])
989
+ : await this.getCodexThreadHistoryFromRpc(threadId, session.projectPath, session.process);
979
990
  session.claudeSessionId = threadId;
980
991
  const messages = await this.codexHistoryToServerMessages(session, history);
981
992
  const entries = messages.map((message, index) => ({
@@ -983,6 +994,7 @@ export class BridgeWebSocketServer {
983
994
  message,
984
995
  }));
985
996
  this.applyCodexCanonicalHistoryBaseline(session, history, entries);
997
+ session.codexInitialHistoryPending = false;
986
998
  return entries;
987
999
  }
988
1000
  applyCodexCanonicalHistoryBaseline(session, history, canonicalEntries) {
@@ -1551,6 +1563,11 @@ export class BridgeWebSocketServer {
1551
1563
  }
1552
1564
  close() {
1553
1565
  console.log("[ws] Shutting down...");
1566
+ for (const operation of this.resumeOperations.values()) {
1567
+ if (operation.timeout)
1568
+ clearTimeout(operation.timeout);
1569
+ }
1570
+ this.resumeOperations.clear();
1554
1571
  this.flushAllDeltaBatches();
1555
1572
  this.sessionManager.destroyAll();
1556
1573
  this.flushAllDeltaBatches();
@@ -1645,9 +1662,13 @@ export class BridgeWebSocketServer {
1645
1662
  }
1646
1663
  try {
1647
1664
  const provider = msg.provider ?? "claude";
1648
- const requestedCodexPermissionsMode = provider === "codex"
1665
+ const normalizedCodexPermissionsMode = provider === "codex"
1649
1666
  ? normalizeCodexPermissionsMode(msg.codexPermissionsMode)
1650
1667
  : undefined;
1668
+ const requestedCodexPermissionsMode = this.codexAutoReviewDisabled &&
1669
+ normalizedCodexPermissionsMode === "autoReview"
1670
+ ? "default"
1671
+ : normalizedCodexPermissionsMode;
1651
1672
  const codexPermissionSettings = requestedCodexPermissionsMode
1652
1673
  ? codexSettingsFromPermissionsMode(requestedCodexPermissionsMode)
1653
1674
  : undefined;
@@ -1723,7 +1744,7 @@ export class BridgeWebSocketServer {
1723
1744
  useWorktree: msg.useWorktree,
1724
1745
  worktreeBranch: msg.worktreeBranch,
1725
1746
  existingWorktreePath: msg.existingWorktreePath,
1726
- }, provider, {
1747
+ }, provider, this.withCodexAutoReviewPolicy({
1727
1748
  profile: msg.profile,
1728
1749
  approvalPolicy: codexPermissionSettings
1729
1750
  ? codexPermissionSettings.approvalPolicy
@@ -1731,9 +1752,8 @@ export class BridgeWebSocketServer {
1731
1752
  normalizeCodexApprovalPolicy(executionMode === "fullAccess"
1732
1753
  ? "never"
1733
1754
  : "on-request")),
1734
- approvalsReviewer: codexPermissionSettings
1735
- ? codexPermissionSettings.approvalsReviewer
1736
- : msg.approvalsReviewer,
1755
+ approvalsReviewer: codexPermissionSettings?.approvalsReviewer ??
1756
+ msg.approvalsReviewer,
1737
1757
  codexPermissionsMode: codexPermissionSettings?.codexPermissionsMode,
1738
1758
  sandboxMode: codexPermissionSettings
1739
1759
  ? codexPermissionSettings.sandboxMode
@@ -1750,7 +1770,7 @@ export class BridgeWebSocketServer {
1750
1770
  collaborationMode: planMode
1751
1771
  ? "plan"
1752
1772
  : "default",
1753
- }),
1773
+ })),
1754
1774
  permissionMode: claudePermissionMode,
1755
1775
  executionMode,
1756
1776
  planMode,
@@ -2290,7 +2310,15 @@ export class BridgeWebSocketServer {
2290
2310
  // Permission mode for Codex requires a session restart (like sandbox mode).
2291
2311
  // approvalPolicy and collaborationMode are thread-level settings that
2292
2312
  // only take effect reliably at thread/start or thread/resume time.
2293
- const requestedCodexPermissionsMode = normalizeCodexPermissionsMode(msg.codexPermissionsMode);
2313
+ const normalizedCodexPermissionsMode = normalizeCodexPermissionsMode(msg.codexPermissionsMode);
2314
+ const requestedCodexPermissionsMode = this.codexAutoReviewDisabled &&
2315
+ normalizedCodexPermissionsMode === "autoReview"
2316
+ ? "default"
2317
+ : normalizedCodexPermissionsMode;
2318
+ const requestedApprovalsReviewer = this.codexAutoReviewDisabled &&
2319
+ isCodexAutoReviewApprovalsReviewer(msg.approvalsReviewer)
2320
+ ? "user"
2321
+ : msg.approvalsReviewer;
2294
2322
  const codexPermissionSettings = requestedCodexPermissionsMode
2295
2323
  ? codexSettingsFromPermissionsMode(requestedCodexPermissionsMode)
2296
2324
  : undefined;
@@ -2328,23 +2356,28 @@ export class BridgeWebSocketServer {
2328
2356
  const newSandboxMode = codexPermissionSettings
2329
2357
  ? codexPermissionSettings.sandboxMode
2330
2358
  : currentSandboxMode;
2331
- const newPermissionsMode = codexPermissionSettings?.codexPermissionsMode ??
2359
+ const configuredReviewer = requestedCodexPermissionsMode === "custom"
2360
+ ? undefined
2361
+ : (codexPermissionSettings?.approvalsReviewer ??
2362
+ requestedApprovalsReviewer ??
2363
+ currentReviewer);
2364
+ const newReviewer = this.codexAutoReviewDisabled
2365
+ ? "user"
2366
+ : configuredReviewer;
2367
+ const derivedPermissionsMode = codexPermissionSettings?.codexPermissionsMode ??
2332
2368
  (collaborationOnlyChange ? currentPermissionsMode : undefined) ??
2333
2369
  deriveCodexPermissionsMode({
2334
2370
  approvalPolicy: newApproval,
2335
- approvalsReviewer: codexPermissionSettings?.approvalsReviewer ??
2336
- msg.approvalsReviewer ??
2337
- currentReviewer,
2371
+ approvalsReviewer: newReviewer,
2338
2372
  sandboxMode: newSandboxMode,
2339
2373
  });
2374
+ const newPermissionsMode = this.codexAutoReviewDisabled &&
2375
+ derivedPermissionsMode === "autoReview"
2376
+ ? "default"
2377
+ : derivedPermissionsMode;
2340
2378
  const newCollaboration = planMode
2341
2379
  ? "plan"
2342
2380
  : "default";
2343
- const newReviewer = requestedCodexPermissionsMode === "custom"
2344
- ? undefined
2345
- : (codexPermissionSettings?.approvalsReviewer ??
2346
- msg.approvalsReviewer ??
2347
- currentReviewer);
2348
2381
  const currentCollaboration = process.collaborationMode;
2349
2382
  if (newApproval === currentApproval &&
2350
2383
  newReviewer === currentReviewer &&
@@ -2355,7 +2388,9 @@ export class BridgeWebSocketServer {
2355
2388
  }
2356
2389
  const canApplyModeInPlace = session.status === "idle" &&
2357
2390
  requestedCodexPermissionsMode !== "custom" &&
2358
- newSandboxMode === currentSandboxMode;
2391
+ newSandboxMode === currentSandboxMode &&
2392
+ (this.codexAutoReviewPolicyLoaded ||
2393
+ !isCodexAutoReviewApprovalsReviewer(newReviewer));
2359
2394
  if (canApplyModeInPlace) {
2360
2395
  const process = session.process;
2361
2396
  if (newApproval && newApproval !== currentApproval) {
@@ -2411,7 +2446,7 @@ export class BridgeWebSocketServer {
2411
2446
  if (!threadId || !hasUserMessages) {
2412
2447
  const newId = this.sessionManager.create(projectPath, undefined, undefined, worktreePath
2413
2448
  ? { existingWorktreePath: worktreePath, worktreeBranch }
2414
- : undefined, "codex", {
2449
+ : undefined, "codex", this.withCodexAutoReviewPolicy({
2415
2450
  approvalPolicy: newApproval,
2416
2451
  approvalsReviewer: newReviewer,
2417
2452
  codexPermissionsMode: newPermissionsMode,
@@ -2422,7 +2457,7 @@ export class BridgeWebSocketServer {
2422
2457
  networkAccessEnabled: oldSettings.networkAccessEnabled,
2423
2458
  webSearchMode: oldSettings.webSearchMode,
2424
2459
  collaborationMode: newCollaboration,
2425
- });
2460
+ }));
2426
2461
  const newSession = this.sessionManager.get(newId);
2427
2462
  if (newSession && sessionName)
2428
2463
  newSession.name = sessionName;
@@ -2471,7 +2506,7 @@ export class BridgeWebSocketServer {
2471
2506
  }
2472
2507
  this.getCodexThreadHistory(threadId, effectiveProjectPath)
2473
2508
  .then((pastMessages) => {
2474
- const newId = this.sessionManager.create(effectiveProjectPath, undefined, pastMessages, worktreeOpts, "codex", {
2509
+ const newId = this.sessionManager.create(effectiveProjectPath, undefined, pastMessages, worktreeOpts, "codex", this.withCodexAutoReviewPolicy({
2475
2510
  threadId,
2476
2511
  approvalPolicy: newApproval,
2477
2512
  approvalsReviewer: newReviewer,
@@ -2483,7 +2518,7 @@ export class BridgeWebSocketServer {
2483
2518
  networkAccessEnabled: oldSettings.networkAccessEnabled,
2484
2519
  webSearchMode: oldSettings.webSearchMode,
2485
2520
  collaborationMode: newCollaboration,
2486
- });
2521
+ }));
2487
2522
  const newSession = this.sessionManager.get(newId);
2488
2523
  if (newSession && sessionName) {
2489
2524
  newSession.name = sessionName;
@@ -2822,8 +2857,10 @@ export class BridgeWebSocketServer {
2822
2857
  // "no rollout found for thread id".)
2823
2858
  const newId = this.sessionManager.create(projectPath, undefined, undefined, worktreePath
2824
2859
  ? { existingWorktreePath: worktreePath, worktreeBranch }
2825
- : undefined, "codex", {
2860
+ : undefined, "codex", this.withCodexAutoReviewPolicy({
2826
2861
  approvalPolicy: oldSettings.approvalPolicy,
2862
+ approvalsReviewer: oldSettings.approvalsReviewer,
2863
+ codexPermissionsMode: oldSettings.codexPermissionsMode,
2827
2864
  sandboxMode: newSandboxMode,
2828
2865
  model: oldSettings.model,
2829
2866
  modelReasoningEffort: oldSettings.modelReasoningEffort,
@@ -2831,7 +2868,7 @@ export class BridgeWebSocketServer {
2831
2868
  networkAccessEnabled: oldSettings.networkAccessEnabled,
2832
2869
  webSearchMode: oldSettings.webSearchMode,
2833
2870
  collaborationMode,
2834
- });
2871
+ }));
2835
2872
  const newSession = this.sessionManager.get(newId);
2836
2873
  if (newSession && sessionName)
2837
2874
  newSession.name = sessionName;
@@ -2873,9 +2910,11 @@ export class BridgeWebSocketServer {
2873
2910
  }
2874
2911
  this.getCodexThreadHistory(threadId, effectiveProjectPath)
2875
2912
  .then((pastMessages) => {
2876
- const newId = this.sessionManager.create(effectiveProjectPath, undefined, pastMessages, worktreeOpts, "codex", {
2913
+ const newId = this.sessionManager.create(effectiveProjectPath, undefined, pastMessages, worktreeOpts, "codex", this.withCodexAutoReviewPolicy({
2877
2914
  threadId,
2878
2915
  approvalPolicy: oldSettings.approvalPolicy,
2916
+ approvalsReviewer: oldSettings.approvalsReviewer,
2917
+ codexPermissionsMode: oldSettings.codexPermissionsMode,
2879
2918
  sandboxMode: newSandboxMode,
2880
2919
  model: oldSettings.model,
2881
2920
  modelReasoningEffort: oldSettings.modelReasoningEffort,
@@ -2883,7 +2922,7 @@ export class BridgeWebSocketServer {
2883
2922
  networkAccessEnabled: oldSettings.networkAccessEnabled,
2884
2923
  webSearchMode: oldSettings.webSearchMode,
2885
2924
  collaborationMode,
2886
- });
2925
+ }));
2887
2926
  // Restore session name
2888
2927
  const newSession = this.sessionManager.get(newId);
2889
2928
  if (newSession && sessionName) {
@@ -3320,25 +3359,39 @@ export class BridgeWebSocketServer {
3320
3359
  }
3321
3360
  case "archive_session": {
3322
3361
  const { sessionId, provider, projectPath } = msg;
3323
- this.archiveStore
3324
- .archive(sessionId, provider, projectPath)
3325
- .then(() => {
3326
- // For Codex sessions, also call thread/archive RPC (best-effort).
3327
- // Requires a running Codex app-server process; skip if none active.
3362
+ const archiveProjectPath = resolvePlatformPath(projectPath, this.platform);
3363
+ if (!this.isPathAllowed(archiveProjectPath)) {
3364
+ const pathError = this.buildPathNotAllowedError(projectPath);
3365
+ this.send(ws, {
3366
+ type: "archive_result",
3367
+ sessionId,
3368
+ success: false,
3369
+ error: pathError.message,
3370
+ });
3371
+ break;
3372
+ }
3373
+ void (async () => {
3328
3374
  if (provider === "codex") {
3329
- const activeSessions = this.sessionManager.list();
3330
- const codexSession = activeSessions.find((s) => s.provider === "codex");
3331
- if (codexSession) {
3332
- const session = this.sessionManager.get(codexSession.id);
3333
- if (session) {
3334
- session.process
3335
- .archiveThread(sessionId)
3336
- .catch((err) => {
3337
- console.warn(`[ws] Codex thread/archive failed (non-fatal): ${err}`);
3338
- });
3375
+ const activeProcess = this.getActiveCodexProcess();
3376
+ const codexProcess = activeProcess ??
3377
+ (await this.createStandaloneCodexProcess(archiveProjectPath));
3378
+ try {
3379
+ await codexProcess.archiveThread(sessionId);
3380
+ }
3381
+ catch (err) {
3382
+ if (!(err instanceof CodexRpcError && err.code === -32601)) {
3383
+ throw err;
3339
3384
  }
3385
+ console.warn("[ws] thread/archive unsupported; using local archive marker");
3386
+ }
3387
+ finally {
3388
+ if (!activeProcess)
3389
+ codexProcess.stop();
3340
3390
  }
3341
3391
  }
3392
+ await this.archiveStore.archive(sessionId, provider, archiveProjectPath);
3393
+ })()
3394
+ .then(() => {
3342
3395
  this.send(ws, {
3343
3396
  type: "archive_result",
3344
3397
  sessionId,
@@ -3350,22 +3403,32 @@ export class BridgeWebSocketServer {
3350
3403
  type: "archive_result",
3351
3404
  sessionId,
3352
3405
  success: false,
3353
- error: String(err),
3406
+ error: codexErrorMessage(err),
3354
3407
  });
3355
3408
  });
3356
3409
  break;
3357
3410
  }
3358
3411
  case "resume_session": {
3412
+ const resumeStartedAt = Date.now();
3359
3413
  console.log(`[ws] resume_session: sessionId=${msg.sessionId} projectPath=${msg.projectPath} provider=${msg.provider ?? "claude"}`);
3360
3414
  const resumeProjectPath = resolvePlatformPath(msg.projectPath, this.platform);
3415
+ const provider = msg.provider ?? "claude";
3361
3416
  if (!this.isPathAllowed(resumeProjectPath)) {
3417
+ this.sendResumeFailed(ws, {
3418
+ provider,
3419
+ sourceSessionId: msg.sessionId,
3420
+ projectPath: resumeProjectPath,
3421
+ });
3362
3422
  this.send(ws, this.buildPathNotAllowedError(msg.projectPath));
3363
3423
  break;
3364
3424
  }
3365
- const provider = msg.provider ?? "claude";
3366
- const requestedCodexPermissionsMode = provider === "codex"
3425
+ const normalizedCodexPermissionsMode = provider === "codex"
3367
3426
  ? normalizeCodexPermissionsMode(msg.codexPermissionsMode)
3368
3427
  : undefined;
3428
+ const requestedCodexPermissionsMode = this.codexAutoReviewDisabled &&
3429
+ normalizedCodexPermissionsMode === "autoReview"
3430
+ ? "default"
3431
+ : normalizedCodexPermissionsMode;
3369
3432
  const codexPermissionSettings = requestedCodexPermissionsMode
3370
3433
  ? codexSettingsFromPermissionsMode(requestedCodexPermissionsMode)
3371
3434
  : undefined;
@@ -3404,6 +3467,11 @@ export class BridgeWebSocketServer {
3404
3467
  : undefined;
3405
3468
  const additionalWritableRoots = this.normalizeAdditionalWritableRoots(msg.additionalWritableRoots, effectiveProjectPath);
3406
3469
  if (additionalWritableRoots.deniedRoot) {
3470
+ this.sendResumeFailed(ws, {
3471
+ provider,
3472
+ sourceSessionId: sessionRefId,
3473
+ projectPath: effectiveProjectPath,
3474
+ });
3407
3475
  this.send(ws, this.buildPathNotAllowedError(additionalWritableRoots.deniedRoot));
3408
3476
  break;
3409
3477
  }
@@ -3422,18 +3490,36 @@ export class BridgeWebSocketServer {
3422
3490
  };
3423
3491
  }
3424
3492
  }
3493
+ const resumeOperation = this.beginResumeOperation({
3494
+ ws,
3495
+ provider: "codex",
3496
+ sourceSessionId: sessionRefId,
3497
+ projectPath: effectiveProjectPath,
3498
+ request: msg,
3499
+ });
3500
+ if (!resumeOperation.isOwner)
3501
+ break;
3502
+ let historyMetrics = summarizeResumeHistory([]);
3503
+ let historyLoadMs = 0;
3504
+ let historyLoaded = false;
3505
+ let sessionCreateMs = 0;
3506
+ let nameLoadMs = 0;
3507
+ const historyStartedAt = Date.now();
3425
3508
  try {
3426
3509
  const pastMessages = await this.getCodexThreadHistory(sessionRefId, effectiveProjectPath);
3427
- const sessionId = this.sessionManager.create(effectiveProjectPath, undefined, pastMessages, worktreeOpts, "codex", {
3510
+ historyLoadMs = Date.now() - historyStartedAt;
3511
+ historyLoaded = true;
3512
+ historyMetrics = summarizeResumeHistory(pastMessages);
3513
+ const createStartedAt = Date.now();
3514
+ const sessionId = this.sessionManager.create(effectiveProjectPath, undefined, pastMessages, worktreeOpts, "codex", this.withCodexAutoReviewPolicy({
3428
3515
  threadId: sessionRefId,
3429
3516
  profile: effectiveProfile,
3430
3517
  approvalPolicy: codexPermissionSettings
3431
3518
  ? codexPermissionSettings.approvalPolicy
3432
3519
  : (codexApprovalPolicy ??
3433
3520
  normalizeCodexApprovalPolicy(executionMode === "fullAccess" ? "never" : "on-request")),
3434
- approvalsReviewer: codexPermissionSettings
3435
- ? codexPermissionSettings.approvalsReviewer
3436
- : msg.approvalsReviewer,
3521
+ approvalsReviewer: codexPermissionSettings?.approvalsReviewer ??
3522
+ msg.approvalsReviewer,
3437
3523
  codexPermissionsMode: codexPermissionSettings?.codexPermissionsMode,
3438
3524
  sandboxMode: codexPermissionSettings
3439
3525
  ? codexPermissionSettings.sandboxMode
@@ -3449,11 +3535,20 @@ export class BridgeWebSocketServer {
3449
3535
  collaborationMode: planMode
3450
3536
  ? "plan"
3451
3537
  : "default",
3452
- });
3538
+ }));
3539
+ sessionCreateMs = Date.now() - createStartedAt;
3453
3540
  const createdSession = this.sessionManager.get(sessionId);
3541
+ if (createdSession) {
3542
+ // get_history immediately follows session_created on the app.
3543
+ // Reuse the canonical history loaded above instead of issuing a
3544
+ // second thread/read for the same restored session.
3545
+ createdSession.codexInitialHistoryPending = true;
3546
+ }
3454
3547
  const cached = this.sessionManager.getCachedCommands("codex", createdSession?.worktreePath ?? effectiveProjectPath);
3548
+ const nameStartedAt = Date.now();
3455
3549
  await this.loadAndSetSessionName(createdSession, "codex", effectiveProjectPath, sessionRefId);
3456
- this.send(ws, this.buildSessionCreatedMessage({
3550
+ nameLoadMs = Date.now() - nameStartedAt;
3551
+ const createdMessage = this.buildSessionCreatedMessage({
3457
3552
  sessionId,
3458
3553
  provider: "codex",
3459
3554
  projectPath: effectiveProjectPath,
@@ -3483,7 +3578,11 @@ export class BridgeWebSocketServer {
3483
3578
  : {}),
3484
3579
  }
3485
3580
  : {}),
3486
- }));
3581
+ });
3582
+ if (!this.completeResumeOperation(resumeOperation.key, resumeOperation.operationId, sessionId, createdMessage)) {
3583
+ this.sessionManager.destroy(sessionId);
3584
+ break;
3585
+ }
3487
3586
  this.broadcastSessionList();
3488
3587
  this.debugEvents.set(sessionId, []);
3489
3588
  this.recordDebugEvent(sessionId, {
@@ -3493,29 +3592,36 @@ export class BridgeWebSocketServer {
3493
3592
  detail: `provider=codex thread=${sessionRefId}`,
3494
3593
  });
3495
3594
  this.projectHistory?.addProject(effectiveProjectPath);
3595
+ console.info(formatResumePerformanceLog({
3596
+ provider: "codex",
3597
+ sourceSessionId: sessionRefId,
3598
+ outcome: "success",
3599
+ ...historyMetrics,
3600
+ historyLoadMs,
3601
+ sessionCreateMs,
3602
+ nameLoadMs,
3603
+ totalMs: Date.now() - resumeStartedAt,
3604
+ }));
3496
3605
  }
3497
3606
  catch (err) {
3498
- this.send(ws, {
3499
- type: "error",
3500
- message: `Failed to load Codex session history: ${err}`,
3501
- });
3607
+ if (!historyLoaded) {
3608
+ historyLoadMs = Date.now() - historyStartedAt;
3609
+ }
3610
+ console.info(formatResumePerformanceLog({
3611
+ provider: "codex",
3612
+ sourceSessionId: sessionRefId,
3613
+ outcome: "failed",
3614
+ ...historyMetrics,
3615
+ historyLoadMs,
3616
+ sessionCreateMs,
3617
+ nameLoadMs,
3618
+ totalMs: Date.now() - resumeStartedAt,
3619
+ }));
3620
+ this.failResumeOperation(resumeOperation.key, resumeOperation.operationId, `Failed to load Codex session history: ${err}`);
3502
3621
  }
3503
3622
  break;
3504
3623
  }
3505
3624
  const claudeSessionId = sessionRefId;
3506
- let pendingResumes = this.pendingClaudeResumeInputs.get(ws);
3507
- if (!pendingResumes) {
3508
- pendingResumes = new Map();
3509
- this.pendingClaudeResumeInputs.set(ws, pendingResumes);
3510
- }
3511
- if (pendingResumes.has(claudeSessionId)) {
3512
- this.send(ws, {
3513
- type: "error",
3514
- message: `Session resume already in progress: ${claudeSessionId}`,
3515
- });
3516
- break;
3517
- }
3518
- pendingResumes.set(claudeSessionId, []);
3519
3625
  // Look up worktree mapping for this Claude session
3520
3626
  const wtMapping = this.worktreeStore.get(claudeSessionId);
3521
3627
  let worktreeOpts;
@@ -3535,8 +3641,26 @@ export class BridgeWebSocketServer {
3535
3641
  };
3536
3642
  }
3537
3643
  }
3644
+ const resumeOperation = this.beginResumeOperation({
3645
+ ws,
3646
+ provider: "claude",
3647
+ sourceSessionId: claudeSessionId,
3648
+ projectPath: resumeProjectPath,
3649
+ request: msg,
3650
+ });
3651
+ if (!resumeOperation.isOwner)
3652
+ break;
3653
+ const historyStartedAt = Date.now();
3654
+ let historyMetrics = summarizeResumeHistory([]);
3655
+ let historyLoadMs = 0;
3656
+ let historyLoaded = false;
3657
+ let sessionCreateMs = 0;
3538
3658
  getSessionHistory(claudeSessionId)
3539
3659
  .then((pastMessages) => {
3660
+ historyLoadMs = Date.now() - historyStartedAt;
3661
+ historyLoaded = true;
3662
+ historyMetrics = summarizeResumeHistory(pastMessages);
3663
+ const createStartedAt = Date.now();
3540
3664
  const { sessionId, permissionMode: effectivePermissionMode, executionMode: effectiveExecutionMode, planMode: effectivePlanMode, usedFallback: autoFallbackUsed, } = this.createClaudeSessionWithFallback({
3541
3665
  projectPath: resumeProjectPath,
3542
3666
  options: {
@@ -3556,10 +3680,12 @@ export class BridgeWebSocketServer {
3556
3680
  pastMessages,
3557
3681
  worktreeOptions: worktreeOpts,
3558
3682
  });
3683
+ sessionCreateMs = Date.now() - createStartedAt;
3559
3684
  const createdSession = this.sessionManager.get(sessionId);
3560
3685
  const cached = this.sessionManager.getCachedCommands("claude", createdSession?.worktreePath ?? resumeProjectPath);
3686
+ const nameStartedAt = Date.now();
3561
3687
  const finishResume = () => {
3562
- this.send(ws, {
3688
+ const createdMessage = {
3563
3689
  ...this.buildSessionCreatedMessage({
3564
3690
  sessionId,
3565
3691
  provider: "claude",
@@ -3588,16 +3714,25 @@ export class BridgeWebSocketServer {
3588
3714
  : {}),
3589
3715
  }),
3590
3716
  claudeSessionId,
3591
- });
3592
- const queuedInputs = pendingResumes.get(claudeSessionId) ?? [];
3593
- pendingResumes.delete(claudeSessionId);
3594
- for (const input of queuedInputs) {
3595
- void this.handleClientMessage({ ...input, sessionId }, ws);
3717
+ };
3718
+ if (!this.completeResumeOperation(resumeOperation.key, resumeOperation.operationId, sessionId, createdMessage)) {
3719
+ this.sessionManager.destroy(sessionId);
3720
+ return;
3596
3721
  }
3597
3722
  this.broadcastSessionList();
3598
3723
  if (autoFallbackUsed) {
3599
3724
  this.sendTip(ws, sessionId, "auto_mode_fallback_default", createdSession);
3600
3725
  }
3726
+ console.info(formatResumePerformanceLog({
3727
+ provider: "claude",
3728
+ sourceSessionId: claudeSessionId,
3729
+ outcome: "success",
3730
+ ...historyMetrics,
3731
+ historyLoadMs,
3732
+ sessionCreateMs,
3733
+ nameLoadMs: Date.now() - nameStartedAt,
3734
+ totalMs: Date.now() - resumeStartedAt,
3735
+ }));
3601
3736
  };
3602
3737
  void this.loadAndSetSessionName(createdSession, "claude", resumeProjectPath, claudeSessionId).then(finishResume, (err) => {
3603
3738
  console.error("[ws] Failed to load resumed session name:", err);
@@ -3613,22 +3748,20 @@ export class BridgeWebSocketServer {
3613
3748
  this.projectHistory?.addProject(resumeProjectPath);
3614
3749
  })
3615
3750
  .catch((err) => {
3616
- const queuedInputs = pendingResumes.get(claudeSessionId) ?? [];
3617
- pendingResumes.delete(claudeSessionId);
3618
- for (const input of queuedInputs) {
3619
- if (input.clientMessageId) {
3620
- this.send(ws, {
3621
- type: "input_rejected",
3622
- sessionId: claudeSessionId,
3623
- clientMessageId: input.clientMessageId,
3624
- reason: "Session resume failed",
3625
- });
3626
- }
3751
+ if (!historyLoaded) {
3752
+ historyLoadMs = Date.now() - historyStartedAt;
3627
3753
  }
3628
- this.send(ws, {
3629
- type: "error",
3630
- message: `Failed to load session history: ${err}`,
3631
- });
3754
+ console.info(formatResumePerformanceLog({
3755
+ provider: "claude",
3756
+ sourceSessionId: claudeSessionId,
3757
+ outcome: "failed",
3758
+ ...historyMetrics,
3759
+ historyLoadMs,
3760
+ sessionCreateMs,
3761
+ nameLoadMs: 0,
3762
+ totalMs: Date.now() - resumeStartedAt,
3763
+ }));
3764
+ this.failResumeOperation(resumeOperation.key, resumeOperation.operationId, `Failed to load session history: ${err}`);
3632
3765
  });
3633
3766
  break;
3634
3767
  }
@@ -4936,6 +5069,199 @@ export class BridgeWebSocketServer {
4936
5069
  clearPendingClaudeResumeInputs(ws) {
4937
5070
  this.pendingClaudeResumeInputs.get(ws)?.clear();
4938
5071
  this.pendingClaudeResumeInputs.delete(ws);
5072
+ for (const operation of this.resumeOperations.values()) {
5073
+ operation.waiters.delete(ws);
5074
+ }
5075
+ }
5076
+ resumeOperationKey(provider, sourceSessionId) {
5077
+ return `${provider}:${sourceSessionId}`;
5078
+ }
5079
+ resumeRequestFingerprint(msg) {
5080
+ return JSON.stringify({
5081
+ provider: msg.provider ?? "claude",
5082
+ sessionId: msg.sessionId,
5083
+ projectPath: msg.projectPath,
5084
+ permissionMode: msg.permissionMode,
5085
+ executionMode: msg.executionMode,
5086
+ approvalPolicy: msg.approvalPolicy,
5087
+ approvalsReviewer: msg.approvalsReviewer,
5088
+ codexPermissionsMode: msg.codexPermissionsMode,
5089
+ planMode: msg.planMode,
5090
+ sandboxMode: msg.sandboxMode,
5091
+ model: msg.model,
5092
+ effort: msg.effort,
5093
+ maxTurns: msg.maxTurns,
5094
+ maxBudgetUsd: msg.maxBudgetUsd,
5095
+ fallbackModel: msg.fallbackModel,
5096
+ forkSession: msg.forkSession ?? false,
5097
+ persistSession: msg.persistSession,
5098
+ profile: msg.profile,
5099
+ modelReasoningEffort: msg.modelReasoningEffort,
5100
+ serviceTier: msg.serviceTier,
5101
+ networkAccessEnabled: msg.networkAccessEnabled,
5102
+ webSearchMode: msg.webSearchMode,
5103
+ additionalWritableRoots: [...(msg.additionalWritableRoots ?? [])].sort(),
5104
+ });
5105
+ }
5106
+ clearResumeOperation(key, operation) {
5107
+ if (operation.timeout)
5108
+ clearTimeout(operation.timeout);
5109
+ if (this.resumeOperations.get(key) === operation) {
5110
+ this.resumeOperations.delete(key);
5111
+ }
5112
+ }
5113
+ ensurePendingClaudeResume(ws, sourceSessionId) {
5114
+ let pendingResumes = this.pendingClaudeResumeInputs.get(ws);
5115
+ if (!pendingResumes) {
5116
+ pendingResumes = new Map();
5117
+ this.pendingClaudeResumeInputs.set(ws, pendingResumes);
5118
+ }
5119
+ if (!pendingResumes.has(sourceSessionId)) {
5120
+ pendingResumes.set(sourceSessionId, []);
5121
+ }
5122
+ }
5123
+ beginResumeOperation(params) {
5124
+ const { ws, provider, sourceSessionId, projectPath, request } = params;
5125
+ const key = this.resumeOperationKey(provider, sourceSessionId);
5126
+ const fingerprint = this.resumeRequestFingerprint(request);
5127
+ let operation = this.resumeOperations.get(key);
5128
+ if (operation?.completed &&
5129
+ (!this.sessionManager.get(operation.completed.sessionId) ||
5130
+ Date.now() - operation.completed.completedAt >
5131
+ RESUME_COMPLETED_TTL_MS ||
5132
+ operation.fingerprint !== fingerprint ||
5133
+ request.forkSession === true)) {
5134
+ this.clearResumeOperation(key, operation);
5135
+ operation = undefined;
5136
+ }
5137
+ if (operation &&
5138
+ !operation.completed &&
5139
+ operation.fingerprint !== fingerprint) {
5140
+ this.sendResumeFailed(ws, {
5141
+ provider,
5142
+ sourceSessionId,
5143
+ projectPath,
5144
+ });
5145
+ this.send(ws, {
5146
+ type: "error",
5147
+ message: "This session is already being restored with different settings. Wait for it to finish, then try again.",
5148
+ });
5149
+ return { key, operationId: operation.id, isOwner: false };
5150
+ }
5151
+ this.send(ws, {
5152
+ type: "system",
5153
+ subtype: "session_resume_started",
5154
+ sourceSessionId,
5155
+ provider,
5156
+ projectPath,
5157
+ });
5158
+ if (provider === "claude") {
5159
+ this.ensurePendingClaudeResume(ws, sourceSessionId);
5160
+ }
5161
+ if (operation) {
5162
+ if (operation.completed) {
5163
+ this.send(ws, operation.completed.message);
5164
+ this.flushPendingClaudeResumeInputs(ws, sourceSessionId, operation.completed.sessionId);
5165
+ }
5166
+ else {
5167
+ operation.waiters.add(ws);
5168
+ }
5169
+ return { key, operationId: operation.id, isOwner: false };
5170
+ }
5171
+ const operationId = randomUUID();
5172
+ const newOperation = {
5173
+ id: operationId,
5174
+ provider,
5175
+ sourceSessionId,
5176
+ projectPath,
5177
+ fingerprint,
5178
+ waiters: new Set([ws]),
5179
+ };
5180
+ const timeout = setTimeout(() => {
5181
+ this.failResumeOperation(key, operationId, "Session restore is taking longer than expected. Please reconnect and try again.");
5182
+ }, RESUME_OPERATION_TIMEOUT_MS);
5183
+ timeout.unref?.();
5184
+ newOperation.timeout = timeout;
5185
+ this.resumeOperations.set(key, newOperation);
5186
+ return { key, operationId, isOwner: true };
5187
+ }
5188
+ completeResumeOperation(key, operationId, sessionId, message) {
5189
+ const operation = this.resumeOperations.get(key);
5190
+ if (!operation || operation.id !== operationId)
5191
+ return false;
5192
+ if (operation.timeout)
5193
+ clearTimeout(operation.timeout);
5194
+ operation.completed = {
5195
+ sessionId,
5196
+ message,
5197
+ completedAt: Date.now(),
5198
+ };
5199
+ for (const waiter of operation.waiters) {
5200
+ this.send(waiter, message);
5201
+ this.flushPendingClaudeResumeInputs(waiter, operation.sourceSessionId, sessionId);
5202
+ }
5203
+ operation.waiters.clear();
5204
+ const timeout = setTimeout(() => {
5205
+ this.clearResumeOperation(key, operation);
5206
+ }, RESUME_COMPLETED_TTL_MS);
5207
+ timeout.unref?.();
5208
+ operation.timeout = timeout;
5209
+ this.pruneCompletedResumeOperations();
5210
+ return true;
5211
+ }
5212
+ failResumeOperation(key, operationId, message) {
5213
+ const operation = this.resumeOperations.get(key);
5214
+ if (!operation || operation.id !== operationId)
5215
+ return;
5216
+ this.clearResumeOperation(key, operation);
5217
+ for (const waiter of operation.waiters) {
5218
+ this.rejectPendingClaudeResumeInputs(waiter, operation.sourceSessionId);
5219
+ this.sendResumeFailed(waiter, operation);
5220
+ this.send(waiter, { type: "error", message });
5221
+ }
5222
+ }
5223
+ sendResumeFailed(ws, resume) {
5224
+ this.send(ws, {
5225
+ type: "system",
5226
+ subtype: "session_resume_failed",
5227
+ provider: resume.provider,
5228
+ sourceSessionId: resume.sourceSessionId,
5229
+ projectPath: resume.projectPath,
5230
+ });
5231
+ }
5232
+ flushPendingClaudeResumeInputs(ws, sourceSessionId, sessionId) {
5233
+ const pendingResumes = this.pendingClaudeResumeInputs.get(ws);
5234
+ const queuedInputs = pendingResumes?.get(sourceSessionId) ?? [];
5235
+ pendingResumes?.delete(sourceSessionId);
5236
+ for (const input of queuedInputs) {
5237
+ void this.handleClientMessage({ ...input, sessionId }, ws);
5238
+ }
5239
+ }
5240
+ rejectPendingClaudeResumeInputs(ws, sourceSessionId) {
5241
+ const pendingResumes = this.pendingClaudeResumeInputs.get(ws);
5242
+ const queuedInputs = pendingResumes?.get(sourceSessionId) ?? [];
5243
+ pendingResumes?.delete(sourceSessionId);
5244
+ for (const input of queuedInputs) {
5245
+ if (!input.clientMessageId)
5246
+ continue;
5247
+ this.send(ws, {
5248
+ type: "input_rejected",
5249
+ sessionId: sourceSessionId,
5250
+ clientMessageId: input.clientMessageId,
5251
+ reason: "Session resume failed",
5252
+ });
5253
+ }
5254
+ }
5255
+ pruneCompletedResumeOperations() {
5256
+ const completed = [...this.resumeOperations.entries()]
5257
+ .filter((entry) => entry[1].completed)
5258
+ .sort((a, b) => (a[1].completed?.completedAt ?? 0) -
5259
+ (b[1].completed?.completedAt ?? 0));
5260
+ while (completed.length > 100) {
5261
+ const oldest = completed.shift();
5262
+ if (oldest)
5263
+ this.clearResumeOperation(oldest[0], oldest[1]);
5264
+ }
4939
5265
  }
4940
5266
  /**
4941
5267
  * Load the saved session name from CLI storage and set it on the SessionInfo.
@@ -5033,6 +5359,7 @@ export class BridgeWebSocketServer {
5033
5359
  codexModelServiceTiers: this.codexModelServiceTiers,
5034
5360
  codexProfiles: this.codexProfiles,
5035
5361
  defaultCodexProfile: this.defaultCodexProfile,
5362
+ codexAutoReviewDisabled: this.codexAutoReviewDisabled,
5036
5363
  bridgeVersion: getPackageVersion(),
5037
5364
  });
5038
5365
  }
@@ -5064,6 +5391,7 @@ export class BridgeWebSocketServer {
5064
5391
  codexModelServiceTiers: this.codexModelServiceTiers,
5065
5392
  codexProfiles: this.codexProfiles,
5066
5393
  defaultCodexProfile: this.defaultCodexProfile,
5394
+ codexAutoReviewDisabled: this.codexAutoReviewDisabled,
5067
5395
  bridgeVersion: getPackageVersion(),
5068
5396
  });
5069
5397
  }
@@ -5312,6 +5640,7 @@ export class BridgeWebSocketServer {
5312
5640
  console.warn(`[ws] Failed to load Codex metadata: ${err}`);
5313
5641
  this.codexProfiles = [];
5314
5642
  this.defaultCodexProfile = undefined;
5643
+ this.codexAutoReviewPolicyLoaded = false;
5315
5644
  this.applyFallbackCodexModels();
5316
5645
  this.broadcastSessionList();
5317
5646
  })
@@ -5324,9 +5653,10 @@ export class BridgeWebSocketServer {
5324
5653
  const activeProcess = this.getActiveCodexProcess();
5325
5654
  const codexProcess = activeProcess ?? (await this.createStandaloneCodexProcess(projectPath));
5326
5655
  try {
5327
- const [profileResult, modelResult] = await Promise.allSettled([
5656
+ const [profileResult, modelResult, requirementsResult] = await Promise.allSettled([
5328
5657
  codexProcess.readProfileConfig(projectPath),
5329
5658
  this.readCodexModels(codexProcess),
5659
+ this.readCodexAutoReviewDisabled(codexProcess),
5330
5660
  ]);
5331
5661
  if (profileResult.status === "fulfilled") {
5332
5662
  this.codexProfiles = profileResult.value.profiles;
@@ -5346,6 +5676,14 @@ export class BridgeWebSocketServer {
5346
5676
  }
5347
5677
  this.applyFallbackCodexModels();
5348
5678
  }
5679
+ if (requirementsResult.status === "fulfilled") {
5680
+ this.codexAutoReviewDisabled = requirementsResult.value;
5681
+ this.codexAutoReviewPolicyLoaded = true;
5682
+ }
5683
+ else {
5684
+ console.warn(`[ws] Failed to load Codex config requirements: ${requirementsResult.reason}`);
5685
+ this.codexAutoReviewPolicyLoaded = false;
5686
+ }
5349
5687
  this.broadcastSessionList();
5350
5688
  }
5351
5689
  finally {
@@ -5409,6 +5747,14 @@ export class BridgeWebSocketServer {
5409
5747
  supportedServiceTiers: fallbackCodexServiceTiers(model),
5410
5748
  }));
5411
5749
  }
5750
+ async readCodexAutoReviewDisabled(codexProcess) {
5751
+ const requirementsSource = codexProcess;
5752
+ if (typeof requirementsSource.readConfigRequirements !== "function") {
5753
+ return false;
5754
+ }
5755
+ return (await requirementsSource.readConfigRequirements())
5756
+ .autoReviewDisabled;
5757
+ }
5412
5758
  applyCodexModels(models) {
5413
5759
  this.codexModels = models.map((model) => model.model);
5414
5760
  this.codexModelReasoningEfforts = Object.fromEntries(models.map((model) => [
@@ -5487,6 +5833,19 @@ export class BridgeWebSocketServer {
5487
5833
  ? session.process
5488
5834
  : null;
5489
5835
  }
5836
+ withCodexAutoReviewPolicy(options) {
5837
+ const disableAutoReview = this.codexAutoReviewDisabled;
5838
+ return {
5839
+ ...options,
5840
+ ...(disableAutoReview ? { approvalsReviewer: "user" } : {}),
5841
+ ...(disableAutoReview && options.codexPermissionsMode === "autoReview"
5842
+ ? { codexPermissionsMode: "default" }
5843
+ : {}),
5844
+ autoReviewDisabledByPolicy: this.codexAutoReviewPolicyLoaded
5845
+ ? disableAutoReview
5846
+ : null,
5847
+ };
5848
+ }
5490
5849
  getActiveClaudeProcess() {
5491
5850
  const summary = this.sessionManager
5492
5851
  .list()