@ccpocket/bridge 1.69.1 → 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
@@ -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 { 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",
@@ -491,6 +494,7 @@ export class BridgeWebSocketServer {
491
494
  platform;
492
495
  clientSupportedServerMessages = new WeakMap();
493
496
  pendingClaudeResumeInputs = new WeakMap();
497
+ resumeOperations = new Map();
494
498
  constructor(options) {
495
499
  const { server, apiKey, allowedDirs, imageStore, galleryStore, projectHistory, debugTraceStore, recordingStore, firebaseAuth, promptHistoryBackup, promptHistoryStore, platform, fileListMaxEntries, fileListMaxBytes, deltaBatchMs, deltaBatchMaxChars, } = options;
496
500
  this.apiKey = apiKey ?? null;
@@ -980,7 +984,9 @@ export class BridgeWebSocketServer {
980
984
  const threadId = this.codexThreadIdForSession(session);
981
985
  if (!threadId)
982
986
  return null;
983
- 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);
984
990
  session.claudeSessionId = threadId;
985
991
  const messages = await this.codexHistoryToServerMessages(session, history);
986
992
  const entries = messages.map((message, index) => ({
@@ -988,6 +994,7 @@ export class BridgeWebSocketServer {
988
994
  message,
989
995
  }));
990
996
  this.applyCodexCanonicalHistoryBaseline(session, history, entries);
997
+ session.codexInitialHistoryPending = false;
991
998
  return entries;
992
999
  }
993
1000
  applyCodexCanonicalHistoryBaseline(session, history, canonicalEntries) {
@@ -1556,6 +1563,11 @@ export class BridgeWebSocketServer {
1556
1563
  }
1557
1564
  close() {
1558
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();
1559
1571
  this.flushAllDeltaBatches();
1560
1572
  this.sessionManager.destroyAll();
1561
1573
  this.flushAllDeltaBatches();
@@ -3397,13 +3409,19 @@ export class BridgeWebSocketServer {
3397
3409
  break;
3398
3410
  }
3399
3411
  case "resume_session": {
3412
+ const resumeStartedAt = Date.now();
3400
3413
  console.log(`[ws] resume_session: sessionId=${msg.sessionId} projectPath=${msg.projectPath} provider=${msg.provider ?? "claude"}`);
3401
3414
  const resumeProjectPath = resolvePlatformPath(msg.projectPath, this.platform);
3415
+ const provider = msg.provider ?? "claude";
3402
3416
  if (!this.isPathAllowed(resumeProjectPath)) {
3417
+ this.sendResumeFailed(ws, {
3418
+ provider,
3419
+ sourceSessionId: msg.sessionId,
3420
+ projectPath: resumeProjectPath,
3421
+ });
3403
3422
  this.send(ws, this.buildPathNotAllowedError(msg.projectPath));
3404
3423
  break;
3405
3424
  }
3406
- const provider = msg.provider ?? "claude";
3407
3425
  const normalizedCodexPermissionsMode = provider === "codex"
3408
3426
  ? normalizeCodexPermissionsMode(msg.codexPermissionsMode)
3409
3427
  : undefined;
@@ -3449,6 +3467,11 @@ export class BridgeWebSocketServer {
3449
3467
  : undefined;
3450
3468
  const additionalWritableRoots = this.normalizeAdditionalWritableRoots(msg.additionalWritableRoots, effectiveProjectPath);
3451
3469
  if (additionalWritableRoots.deniedRoot) {
3470
+ this.sendResumeFailed(ws, {
3471
+ provider,
3472
+ sourceSessionId: sessionRefId,
3473
+ projectPath: effectiveProjectPath,
3474
+ });
3452
3475
  this.send(ws, this.buildPathNotAllowedError(additionalWritableRoots.deniedRoot));
3453
3476
  break;
3454
3477
  }
@@ -3467,8 +3490,27 @@ export class BridgeWebSocketServer {
3467
3490
  };
3468
3491
  }
3469
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();
3470
3508
  try {
3471
3509
  const pastMessages = await this.getCodexThreadHistory(sessionRefId, effectiveProjectPath);
3510
+ historyLoadMs = Date.now() - historyStartedAt;
3511
+ historyLoaded = true;
3512
+ historyMetrics = summarizeResumeHistory(pastMessages);
3513
+ const createStartedAt = Date.now();
3472
3514
  const sessionId = this.sessionManager.create(effectiveProjectPath, undefined, pastMessages, worktreeOpts, "codex", this.withCodexAutoReviewPolicy({
3473
3515
  threadId: sessionRefId,
3474
3516
  profile: effectiveProfile,
@@ -3494,10 +3536,19 @@ export class BridgeWebSocketServer {
3494
3536
  ? "plan"
3495
3537
  : "default",
3496
3538
  }));
3539
+ sessionCreateMs = Date.now() - createStartedAt;
3497
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
+ }
3498
3547
  const cached = this.sessionManager.getCachedCommands("codex", createdSession?.worktreePath ?? effectiveProjectPath);
3548
+ const nameStartedAt = Date.now();
3499
3549
  await this.loadAndSetSessionName(createdSession, "codex", effectiveProjectPath, sessionRefId);
3500
- this.send(ws, this.buildSessionCreatedMessage({
3550
+ nameLoadMs = Date.now() - nameStartedAt;
3551
+ const createdMessage = this.buildSessionCreatedMessage({
3501
3552
  sessionId,
3502
3553
  provider: "codex",
3503
3554
  projectPath: effectiveProjectPath,
@@ -3527,7 +3578,11 @@ export class BridgeWebSocketServer {
3527
3578
  : {}),
3528
3579
  }
3529
3580
  : {}),
3530
- }));
3581
+ });
3582
+ if (!this.completeResumeOperation(resumeOperation.key, resumeOperation.operationId, sessionId, createdMessage)) {
3583
+ this.sessionManager.destroy(sessionId);
3584
+ break;
3585
+ }
3531
3586
  this.broadcastSessionList();
3532
3587
  this.debugEvents.set(sessionId, []);
3533
3588
  this.recordDebugEvent(sessionId, {
@@ -3537,29 +3592,36 @@ export class BridgeWebSocketServer {
3537
3592
  detail: `provider=codex thread=${sessionRefId}`,
3538
3593
  });
3539
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
+ }));
3540
3605
  }
3541
3606
  catch (err) {
3542
- this.send(ws, {
3543
- type: "error",
3544
- message: `Failed to load Codex session history: ${err}`,
3545
- });
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}`);
3546
3621
  }
3547
3622
  break;
3548
3623
  }
3549
3624
  const claudeSessionId = sessionRefId;
3550
- let pendingResumes = this.pendingClaudeResumeInputs.get(ws);
3551
- if (!pendingResumes) {
3552
- pendingResumes = new Map();
3553
- this.pendingClaudeResumeInputs.set(ws, pendingResumes);
3554
- }
3555
- if (pendingResumes.has(claudeSessionId)) {
3556
- this.send(ws, {
3557
- type: "error",
3558
- message: `Session resume already in progress: ${claudeSessionId}`,
3559
- });
3560
- break;
3561
- }
3562
- pendingResumes.set(claudeSessionId, []);
3563
3625
  // Look up worktree mapping for this Claude session
3564
3626
  const wtMapping = this.worktreeStore.get(claudeSessionId);
3565
3627
  let worktreeOpts;
@@ -3579,8 +3641,26 @@ export class BridgeWebSocketServer {
3579
3641
  };
3580
3642
  }
3581
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;
3582
3658
  getSessionHistory(claudeSessionId)
3583
3659
  .then((pastMessages) => {
3660
+ historyLoadMs = Date.now() - historyStartedAt;
3661
+ historyLoaded = true;
3662
+ historyMetrics = summarizeResumeHistory(pastMessages);
3663
+ const createStartedAt = Date.now();
3584
3664
  const { sessionId, permissionMode: effectivePermissionMode, executionMode: effectiveExecutionMode, planMode: effectivePlanMode, usedFallback: autoFallbackUsed, } = this.createClaudeSessionWithFallback({
3585
3665
  projectPath: resumeProjectPath,
3586
3666
  options: {
@@ -3600,10 +3680,12 @@ export class BridgeWebSocketServer {
3600
3680
  pastMessages,
3601
3681
  worktreeOptions: worktreeOpts,
3602
3682
  });
3683
+ sessionCreateMs = Date.now() - createStartedAt;
3603
3684
  const createdSession = this.sessionManager.get(sessionId);
3604
3685
  const cached = this.sessionManager.getCachedCommands("claude", createdSession?.worktreePath ?? resumeProjectPath);
3686
+ const nameStartedAt = Date.now();
3605
3687
  const finishResume = () => {
3606
- this.send(ws, {
3688
+ const createdMessage = {
3607
3689
  ...this.buildSessionCreatedMessage({
3608
3690
  sessionId,
3609
3691
  provider: "claude",
@@ -3632,16 +3714,25 @@ export class BridgeWebSocketServer {
3632
3714
  : {}),
3633
3715
  }),
3634
3716
  claudeSessionId,
3635
- });
3636
- const queuedInputs = pendingResumes.get(claudeSessionId) ?? [];
3637
- pendingResumes.delete(claudeSessionId);
3638
- for (const input of queuedInputs) {
3639
- void this.handleClientMessage({ ...input, sessionId }, ws);
3717
+ };
3718
+ if (!this.completeResumeOperation(resumeOperation.key, resumeOperation.operationId, sessionId, createdMessage)) {
3719
+ this.sessionManager.destroy(sessionId);
3720
+ return;
3640
3721
  }
3641
3722
  this.broadcastSessionList();
3642
3723
  if (autoFallbackUsed) {
3643
3724
  this.sendTip(ws, sessionId, "auto_mode_fallback_default", createdSession);
3644
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
+ }));
3645
3736
  };
3646
3737
  void this.loadAndSetSessionName(createdSession, "claude", resumeProjectPath, claudeSessionId).then(finishResume, (err) => {
3647
3738
  console.error("[ws] Failed to load resumed session name:", err);
@@ -3657,22 +3748,20 @@ export class BridgeWebSocketServer {
3657
3748
  this.projectHistory?.addProject(resumeProjectPath);
3658
3749
  })
3659
3750
  .catch((err) => {
3660
- const queuedInputs = pendingResumes.get(claudeSessionId) ?? [];
3661
- pendingResumes.delete(claudeSessionId);
3662
- for (const input of queuedInputs) {
3663
- if (input.clientMessageId) {
3664
- this.send(ws, {
3665
- type: "input_rejected",
3666
- sessionId: claudeSessionId,
3667
- clientMessageId: input.clientMessageId,
3668
- reason: "Session resume failed",
3669
- });
3670
- }
3751
+ if (!historyLoaded) {
3752
+ historyLoadMs = Date.now() - historyStartedAt;
3671
3753
  }
3672
- this.send(ws, {
3673
- type: "error",
3674
- message: `Failed to load session history: ${err}`,
3675
- });
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}`);
3676
3765
  });
3677
3766
  break;
3678
3767
  }
@@ -4980,6 +5069,199 @@ export class BridgeWebSocketServer {
4980
5069
  clearPendingClaudeResumeInputs(ws) {
4981
5070
  this.pendingClaudeResumeInputs.get(ws)?.clear();
4982
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
+ }
4983
5265
  }
4984
5266
  /**
4985
5267
  * Load the saved session name from CLI storage and set it on the SessionInfo.