@ccpocket/bridge 1.69.1 → 1.69.4
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/image-store.d.ts +13 -1
- package/dist/image-store.js +69 -12
- package/dist/image-store.js.map +1 -1
- package/dist/parser.d.ts +16 -0
- package/dist/parser.js +12 -0
- package/dist/parser.js.map +1 -1
- package/dist/resume-metrics.d.ts +20 -0
- package/dist/resume-metrics.js +58 -0
- package/dist/resume-metrics.js.map +1 -0
- package/dist/session.d.ts +6 -0
- package/dist/session.js.map +1 -1
- package/dist/sessions-index.d.ts +2 -0
- package/dist/sessions-index.js +5 -0
- package/dist/sessions-index.js.map +1 -1
- package/dist/websocket.d.ts +12 -0
- package/dist/websocket.js +388 -43
- package/dist/websocket.js.map +1 -1
- package/package.json +1 -1
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;
|
|
@@ -594,7 +598,7 @@ export class BridgeWebSocketServer {
|
|
|
594
598
|
return { roots: [...normalized.values()] };
|
|
595
599
|
}
|
|
596
600
|
buildSessionCreatedMessage(params) {
|
|
597
|
-
const { sessionId, provider, projectPath, session, permissionMode, executionMode, planMode, approvalsReviewer, codexPermissionsMode, sandboxMode, slashCommands, skills, skillMetadata, apps, appMetadata, plugins, pluginMetadata, sourceSessionId, } = params;
|
|
601
|
+
const { sessionId, provider, projectPath, session, permissionMode, executionMode, planMode, approvalsReviewer, codexPermissionsMode, sandboxMode, slashCommands, skills, skillMetadata, apps, appMetadata, plugins, pluginMetadata, sourceSessionId, resumeRequestId, } = params;
|
|
598
602
|
const derivedCodexSettings = provider === "codex"
|
|
599
603
|
? withDerivedCodexPermissionsMode(session?.codexSettings)
|
|
600
604
|
: session?.codexSettings;
|
|
@@ -689,6 +693,7 @@ export class BridgeWebSocketServer {
|
|
|
689
693
|
}
|
|
690
694
|
: {}),
|
|
691
695
|
...(sourceSessionId ? { sourceSessionId } : {}),
|
|
696
|
+
...(resumeRequestId ? { resumeRequestId } : {}),
|
|
692
697
|
};
|
|
693
698
|
if (provider === "codex" && derivedCodexSettings) {
|
|
694
699
|
if (derivedCodexSettings.model !== undefined) {
|
|
@@ -980,7 +985,9 @@ export class BridgeWebSocketServer {
|
|
|
980
985
|
const threadId = this.codexThreadIdForSession(session);
|
|
981
986
|
if (!threadId)
|
|
982
987
|
return null;
|
|
983
|
-
const history =
|
|
988
|
+
const history = session.codexInitialHistoryPending
|
|
989
|
+
? (session.pastMessages ?? [])
|
|
990
|
+
: await this.getCodexThreadHistoryFromRpc(threadId, session.projectPath, session.process);
|
|
984
991
|
session.claudeSessionId = threadId;
|
|
985
992
|
const messages = await this.codexHistoryToServerMessages(session, history);
|
|
986
993
|
const entries = messages.map((message, index) => ({
|
|
@@ -988,6 +995,7 @@ export class BridgeWebSocketServer {
|
|
|
988
995
|
message,
|
|
989
996
|
}));
|
|
990
997
|
this.applyCodexCanonicalHistoryBaseline(session, history, entries);
|
|
998
|
+
session.codexInitialHistoryPending = false;
|
|
991
999
|
return entries;
|
|
992
1000
|
}
|
|
993
1001
|
applyCodexCanonicalHistoryBaseline(session, history, canonicalEntries) {
|
|
@@ -1556,6 +1564,11 @@ export class BridgeWebSocketServer {
|
|
|
1556
1564
|
}
|
|
1557
1565
|
close() {
|
|
1558
1566
|
console.log("[ws] Shutting down...");
|
|
1567
|
+
for (const operation of this.resumeOperations.values()) {
|
|
1568
|
+
if (operation.timeout)
|
|
1569
|
+
clearTimeout(operation.timeout);
|
|
1570
|
+
}
|
|
1571
|
+
this.resumeOperations.clear();
|
|
1559
1572
|
this.flushAllDeltaBatches();
|
|
1560
1573
|
this.sessionManager.destroyAll();
|
|
1561
1574
|
this.flushAllDeltaBatches();
|
|
@@ -3144,6 +3157,55 @@ export class BridgeWebSocketServer {
|
|
|
3144
3157
|
this.send(ws, {
|
|
3145
3158
|
type: "error",
|
|
3146
3159
|
message: `Session ${msg.sessionId} not found`,
|
|
3160
|
+
errorCode: "session_not_found",
|
|
3161
|
+
sessionId: msg.sessionId,
|
|
3162
|
+
});
|
|
3163
|
+
}
|
|
3164
|
+
break;
|
|
3165
|
+
}
|
|
3166
|
+
case "resolve_session_link": {
|
|
3167
|
+
const provider = msg.provider ?? "claude";
|
|
3168
|
+
const activeSession = this.sessionManager
|
|
3169
|
+
.list()
|
|
3170
|
+
.find((session) => session.provider === provider &&
|
|
3171
|
+
(session.id === msg.sessionId ||
|
|
3172
|
+
session.claudeSessionId === msg.sessionId));
|
|
3173
|
+
if (activeSession) {
|
|
3174
|
+
this.send(ws, {
|
|
3175
|
+
type: "session_link_resolution",
|
|
3176
|
+
requestId: msg.requestId,
|
|
3177
|
+
sourceSessionId: msg.sessionId,
|
|
3178
|
+
status: "live",
|
|
3179
|
+
bridgeSessionId: activeSession.id,
|
|
3180
|
+
provider,
|
|
3181
|
+
});
|
|
3182
|
+
break;
|
|
3183
|
+
}
|
|
3184
|
+
try {
|
|
3185
|
+
const { sessions } = await getAllRecentSessions({
|
|
3186
|
+
limit: 1,
|
|
3187
|
+
provider,
|
|
3188
|
+
sessionId: msg.sessionId,
|
|
3189
|
+
archivedSessionIds: this.archiveStore.archivedIds(),
|
|
3190
|
+
});
|
|
3191
|
+
const recentSession = sessions[0];
|
|
3192
|
+
this.send(ws, {
|
|
3193
|
+
type: "session_link_resolution",
|
|
3194
|
+
requestId: msg.requestId,
|
|
3195
|
+
sourceSessionId: msg.sessionId,
|
|
3196
|
+
status: recentSession ? "recent" : "unavailable",
|
|
3197
|
+
provider,
|
|
3198
|
+
...(recentSession ? { recentSession } : {}),
|
|
3199
|
+
});
|
|
3200
|
+
}
|
|
3201
|
+
catch (err) {
|
|
3202
|
+
console.error("[ws] Failed to resolve session link:", err);
|
|
3203
|
+
this.send(ws, {
|
|
3204
|
+
type: "session_link_resolution",
|
|
3205
|
+
requestId: msg.requestId,
|
|
3206
|
+
sourceSessionId: msg.sessionId,
|
|
3207
|
+
status: "unavailable",
|
|
3208
|
+
provider,
|
|
3147
3209
|
});
|
|
3148
3210
|
}
|
|
3149
3211
|
break;
|
|
@@ -3397,13 +3459,20 @@ export class BridgeWebSocketServer {
|
|
|
3397
3459
|
break;
|
|
3398
3460
|
}
|
|
3399
3461
|
case "resume_session": {
|
|
3462
|
+
const resumeStartedAt = Date.now();
|
|
3400
3463
|
console.log(`[ws] resume_session: sessionId=${msg.sessionId} projectPath=${msg.projectPath} provider=${msg.provider ?? "claude"}`);
|
|
3401
3464
|
const resumeProjectPath = resolvePlatformPath(msg.projectPath, this.platform);
|
|
3465
|
+
const provider = msg.provider ?? "claude";
|
|
3402
3466
|
if (!this.isPathAllowed(resumeProjectPath)) {
|
|
3467
|
+
this.sendResumeFailed(ws, {
|
|
3468
|
+
provider,
|
|
3469
|
+
sourceSessionId: msg.sessionId,
|
|
3470
|
+
projectPath: resumeProjectPath,
|
|
3471
|
+
resumeRequestId: msg.resumeRequestId,
|
|
3472
|
+
});
|
|
3403
3473
|
this.send(ws, this.buildPathNotAllowedError(msg.projectPath));
|
|
3404
3474
|
break;
|
|
3405
3475
|
}
|
|
3406
|
-
const provider = msg.provider ?? "claude";
|
|
3407
3476
|
const normalizedCodexPermissionsMode = provider === "codex"
|
|
3408
3477
|
? normalizeCodexPermissionsMode(msg.codexPermissionsMode)
|
|
3409
3478
|
: undefined;
|
|
@@ -3449,6 +3518,12 @@ export class BridgeWebSocketServer {
|
|
|
3449
3518
|
: undefined;
|
|
3450
3519
|
const additionalWritableRoots = this.normalizeAdditionalWritableRoots(msg.additionalWritableRoots, effectiveProjectPath);
|
|
3451
3520
|
if (additionalWritableRoots.deniedRoot) {
|
|
3521
|
+
this.sendResumeFailed(ws, {
|
|
3522
|
+
provider,
|
|
3523
|
+
sourceSessionId: sessionRefId,
|
|
3524
|
+
projectPath: effectiveProjectPath,
|
|
3525
|
+
resumeRequestId: msg.resumeRequestId,
|
|
3526
|
+
});
|
|
3452
3527
|
this.send(ws, this.buildPathNotAllowedError(additionalWritableRoots.deniedRoot));
|
|
3453
3528
|
break;
|
|
3454
3529
|
}
|
|
@@ -3467,8 +3542,27 @@ export class BridgeWebSocketServer {
|
|
|
3467
3542
|
};
|
|
3468
3543
|
}
|
|
3469
3544
|
}
|
|
3545
|
+
const resumeOperation = this.beginResumeOperation({
|
|
3546
|
+
ws,
|
|
3547
|
+
provider: "codex",
|
|
3548
|
+
sourceSessionId: sessionRefId,
|
|
3549
|
+
projectPath: effectiveProjectPath,
|
|
3550
|
+
request: msg,
|
|
3551
|
+
});
|
|
3552
|
+
if (!resumeOperation.isOwner)
|
|
3553
|
+
break;
|
|
3554
|
+
let historyMetrics = summarizeResumeHistory([]);
|
|
3555
|
+
let historyLoadMs = 0;
|
|
3556
|
+
let historyLoaded = false;
|
|
3557
|
+
let sessionCreateMs = 0;
|
|
3558
|
+
let nameLoadMs = 0;
|
|
3559
|
+
const historyStartedAt = Date.now();
|
|
3470
3560
|
try {
|
|
3471
3561
|
const pastMessages = await this.getCodexThreadHistory(sessionRefId, effectiveProjectPath);
|
|
3562
|
+
historyLoadMs = Date.now() - historyStartedAt;
|
|
3563
|
+
historyLoaded = true;
|
|
3564
|
+
historyMetrics = summarizeResumeHistory(pastMessages);
|
|
3565
|
+
const createStartedAt = Date.now();
|
|
3472
3566
|
const sessionId = this.sessionManager.create(effectiveProjectPath, undefined, pastMessages, worktreeOpts, "codex", this.withCodexAutoReviewPolicy({
|
|
3473
3567
|
threadId: sessionRefId,
|
|
3474
3568
|
profile: effectiveProfile,
|
|
@@ -3494,10 +3588,19 @@ export class BridgeWebSocketServer {
|
|
|
3494
3588
|
? "plan"
|
|
3495
3589
|
: "default",
|
|
3496
3590
|
}));
|
|
3591
|
+
sessionCreateMs = Date.now() - createStartedAt;
|
|
3497
3592
|
const createdSession = this.sessionManager.get(sessionId);
|
|
3593
|
+
if (createdSession) {
|
|
3594
|
+
// get_history immediately follows session_created on the app.
|
|
3595
|
+
// Reuse the canonical history loaded above instead of issuing a
|
|
3596
|
+
// second thread/read for the same restored session.
|
|
3597
|
+
createdSession.codexInitialHistoryPending = true;
|
|
3598
|
+
}
|
|
3498
3599
|
const cached = this.sessionManager.getCachedCommands("codex", createdSession?.worktreePath ?? effectiveProjectPath);
|
|
3600
|
+
const nameStartedAt = Date.now();
|
|
3499
3601
|
await this.loadAndSetSessionName(createdSession, "codex", effectiveProjectPath, sessionRefId);
|
|
3500
|
-
|
|
3602
|
+
nameLoadMs = Date.now() - nameStartedAt;
|
|
3603
|
+
const createdMessage = this.buildSessionCreatedMessage({
|
|
3501
3604
|
sessionId,
|
|
3502
3605
|
provider: "codex",
|
|
3503
3606
|
projectPath: effectiveProjectPath,
|
|
@@ -3510,6 +3613,7 @@ export class BridgeWebSocketServer {
|
|
|
3510
3613
|
permissionMode: legacyPermissionMode,
|
|
3511
3614
|
executionMode,
|
|
3512
3615
|
planMode,
|
|
3616
|
+
resumeRequestId: msg.resumeRequestId,
|
|
3513
3617
|
...(cached
|
|
3514
3618
|
? {
|
|
3515
3619
|
slashCommands: cached.slashCommands,
|
|
@@ -3527,7 +3631,11 @@ export class BridgeWebSocketServer {
|
|
|
3527
3631
|
: {}),
|
|
3528
3632
|
}
|
|
3529
3633
|
: {}),
|
|
3530
|
-
})
|
|
3634
|
+
});
|
|
3635
|
+
if (!this.completeResumeOperation(resumeOperation.key, resumeOperation.operationId, sessionId, createdMessage)) {
|
|
3636
|
+
this.sessionManager.destroy(sessionId);
|
|
3637
|
+
break;
|
|
3638
|
+
}
|
|
3531
3639
|
this.broadcastSessionList();
|
|
3532
3640
|
this.debugEvents.set(sessionId, []);
|
|
3533
3641
|
this.recordDebugEvent(sessionId, {
|
|
@@ -3537,29 +3645,36 @@ export class BridgeWebSocketServer {
|
|
|
3537
3645
|
detail: `provider=codex thread=${sessionRefId}`,
|
|
3538
3646
|
});
|
|
3539
3647
|
this.projectHistory?.addProject(effectiveProjectPath);
|
|
3648
|
+
console.info(formatResumePerformanceLog({
|
|
3649
|
+
provider: "codex",
|
|
3650
|
+
sourceSessionId: sessionRefId,
|
|
3651
|
+
outcome: "success",
|
|
3652
|
+
...historyMetrics,
|
|
3653
|
+
historyLoadMs,
|
|
3654
|
+
sessionCreateMs,
|
|
3655
|
+
nameLoadMs,
|
|
3656
|
+
totalMs: Date.now() - resumeStartedAt,
|
|
3657
|
+
}));
|
|
3540
3658
|
}
|
|
3541
3659
|
catch (err) {
|
|
3542
|
-
|
|
3543
|
-
|
|
3544
|
-
|
|
3545
|
-
|
|
3660
|
+
if (!historyLoaded) {
|
|
3661
|
+
historyLoadMs = Date.now() - historyStartedAt;
|
|
3662
|
+
}
|
|
3663
|
+
console.info(formatResumePerformanceLog({
|
|
3664
|
+
provider: "codex",
|
|
3665
|
+
sourceSessionId: sessionRefId,
|
|
3666
|
+
outcome: "failed",
|
|
3667
|
+
...historyMetrics,
|
|
3668
|
+
historyLoadMs,
|
|
3669
|
+
sessionCreateMs,
|
|
3670
|
+
nameLoadMs,
|
|
3671
|
+
totalMs: Date.now() - resumeStartedAt,
|
|
3672
|
+
}));
|
|
3673
|
+
this.failResumeOperation(resumeOperation.key, resumeOperation.operationId, `Failed to load Codex session history: ${err}`);
|
|
3546
3674
|
}
|
|
3547
3675
|
break;
|
|
3548
3676
|
}
|
|
3549
3677
|
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
3678
|
// Look up worktree mapping for this Claude session
|
|
3564
3679
|
const wtMapping = this.worktreeStore.get(claudeSessionId);
|
|
3565
3680
|
let worktreeOpts;
|
|
@@ -3579,8 +3694,26 @@ export class BridgeWebSocketServer {
|
|
|
3579
3694
|
};
|
|
3580
3695
|
}
|
|
3581
3696
|
}
|
|
3697
|
+
const resumeOperation = this.beginResumeOperation({
|
|
3698
|
+
ws,
|
|
3699
|
+
provider: "claude",
|
|
3700
|
+
sourceSessionId: claudeSessionId,
|
|
3701
|
+
projectPath: resumeProjectPath,
|
|
3702
|
+
request: msg,
|
|
3703
|
+
});
|
|
3704
|
+
if (!resumeOperation.isOwner)
|
|
3705
|
+
break;
|
|
3706
|
+
const historyStartedAt = Date.now();
|
|
3707
|
+
let historyMetrics = summarizeResumeHistory([]);
|
|
3708
|
+
let historyLoadMs = 0;
|
|
3709
|
+
let historyLoaded = false;
|
|
3710
|
+
let sessionCreateMs = 0;
|
|
3582
3711
|
getSessionHistory(claudeSessionId)
|
|
3583
3712
|
.then((pastMessages) => {
|
|
3713
|
+
historyLoadMs = Date.now() - historyStartedAt;
|
|
3714
|
+
historyLoaded = true;
|
|
3715
|
+
historyMetrics = summarizeResumeHistory(pastMessages);
|
|
3716
|
+
const createStartedAt = Date.now();
|
|
3584
3717
|
const { sessionId, permissionMode: effectivePermissionMode, executionMode: effectiveExecutionMode, planMode: effectivePlanMode, usedFallback: autoFallbackUsed, } = this.createClaudeSessionWithFallback({
|
|
3585
3718
|
projectPath: resumeProjectPath,
|
|
3586
3719
|
options: {
|
|
@@ -3600,10 +3733,12 @@ export class BridgeWebSocketServer {
|
|
|
3600
3733
|
pastMessages,
|
|
3601
3734
|
worktreeOptions: worktreeOpts,
|
|
3602
3735
|
});
|
|
3736
|
+
sessionCreateMs = Date.now() - createStartedAt;
|
|
3603
3737
|
const createdSession = this.sessionManager.get(sessionId);
|
|
3604
3738
|
const cached = this.sessionManager.getCachedCommands("claude", createdSession?.worktreePath ?? resumeProjectPath);
|
|
3739
|
+
const nameStartedAt = Date.now();
|
|
3605
3740
|
const finishResume = () => {
|
|
3606
|
-
|
|
3741
|
+
const createdMessage = {
|
|
3607
3742
|
...this.buildSessionCreatedMessage({
|
|
3608
3743
|
sessionId,
|
|
3609
3744
|
provider: "claude",
|
|
@@ -3613,6 +3748,7 @@ export class BridgeWebSocketServer {
|
|
|
3613
3748
|
executionMode: effectiveExecutionMode,
|
|
3614
3749
|
planMode: effectivePlanMode,
|
|
3615
3750
|
sandboxMode: msg.sandboxMode,
|
|
3751
|
+
resumeRequestId: msg.resumeRequestId,
|
|
3616
3752
|
...(cached
|
|
3617
3753
|
? {
|
|
3618
3754
|
slashCommands: cached.slashCommands,
|
|
@@ -3632,16 +3768,25 @@ export class BridgeWebSocketServer {
|
|
|
3632
3768
|
: {}),
|
|
3633
3769
|
}),
|
|
3634
3770
|
claudeSessionId,
|
|
3635
|
-
}
|
|
3636
|
-
|
|
3637
|
-
|
|
3638
|
-
|
|
3639
|
-
void this.handleClientMessage({ ...input, sessionId }, ws);
|
|
3771
|
+
};
|
|
3772
|
+
if (!this.completeResumeOperation(resumeOperation.key, resumeOperation.operationId, sessionId, createdMessage)) {
|
|
3773
|
+
this.sessionManager.destroy(sessionId);
|
|
3774
|
+
return;
|
|
3640
3775
|
}
|
|
3641
3776
|
this.broadcastSessionList();
|
|
3642
3777
|
if (autoFallbackUsed) {
|
|
3643
3778
|
this.sendTip(ws, sessionId, "auto_mode_fallback_default", createdSession);
|
|
3644
3779
|
}
|
|
3780
|
+
console.info(formatResumePerformanceLog({
|
|
3781
|
+
provider: "claude",
|
|
3782
|
+
sourceSessionId: claudeSessionId,
|
|
3783
|
+
outcome: "success",
|
|
3784
|
+
...historyMetrics,
|
|
3785
|
+
historyLoadMs,
|
|
3786
|
+
sessionCreateMs,
|
|
3787
|
+
nameLoadMs: Date.now() - nameStartedAt,
|
|
3788
|
+
totalMs: Date.now() - resumeStartedAt,
|
|
3789
|
+
}));
|
|
3645
3790
|
};
|
|
3646
3791
|
void this.loadAndSetSessionName(createdSession, "claude", resumeProjectPath, claudeSessionId).then(finishResume, (err) => {
|
|
3647
3792
|
console.error("[ws] Failed to load resumed session name:", err);
|
|
@@ -3657,22 +3802,20 @@ export class BridgeWebSocketServer {
|
|
|
3657
3802
|
this.projectHistory?.addProject(resumeProjectPath);
|
|
3658
3803
|
})
|
|
3659
3804
|
.catch((err) => {
|
|
3660
|
-
|
|
3661
|
-
|
|
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
|
-
}
|
|
3805
|
+
if (!historyLoaded) {
|
|
3806
|
+
historyLoadMs = Date.now() - historyStartedAt;
|
|
3671
3807
|
}
|
|
3672
|
-
|
|
3673
|
-
|
|
3674
|
-
|
|
3675
|
-
|
|
3808
|
+
console.info(formatResumePerformanceLog({
|
|
3809
|
+
provider: "claude",
|
|
3810
|
+
sourceSessionId: claudeSessionId,
|
|
3811
|
+
outcome: "failed",
|
|
3812
|
+
...historyMetrics,
|
|
3813
|
+
historyLoadMs,
|
|
3814
|
+
sessionCreateMs,
|
|
3815
|
+
nameLoadMs: 0,
|
|
3816
|
+
totalMs: Date.now() - resumeStartedAt,
|
|
3817
|
+
}));
|
|
3818
|
+
this.failResumeOperation(resumeOperation.key, resumeOperation.operationId, `Failed to load session history: ${err}`);
|
|
3676
3819
|
});
|
|
3677
3820
|
break;
|
|
3678
3821
|
}
|
|
@@ -4980,6 +5123,208 @@ export class BridgeWebSocketServer {
|
|
|
4980
5123
|
clearPendingClaudeResumeInputs(ws) {
|
|
4981
5124
|
this.pendingClaudeResumeInputs.get(ws)?.clear();
|
|
4982
5125
|
this.pendingClaudeResumeInputs.delete(ws);
|
|
5126
|
+
for (const operation of this.resumeOperations.values()) {
|
|
5127
|
+
operation.waiters.delete(ws);
|
|
5128
|
+
}
|
|
5129
|
+
}
|
|
5130
|
+
resumeOperationKey(provider, sourceSessionId) {
|
|
5131
|
+
return `${provider}:${sourceSessionId}`;
|
|
5132
|
+
}
|
|
5133
|
+
resumeRequestFingerprint(msg) {
|
|
5134
|
+
return JSON.stringify({
|
|
5135
|
+
provider: msg.provider ?? "claude",
|
|
5136
|
+
sessionId: msg.sessionId,
|
|
5137
|
+
projectPath: msg.projectPath,
|
|
5138
|
+
permissionMode: msg.permissionMode,
|
|
5139
|
+
executionMode: msg.executionMode,
|
|
5140
|
+
approvalPolicy: msg.approvalPolicy,
|
|
5141
|
+
approvalsReviewer: msg.approvalsReviewer,
|
|
5142
|
+
codexPermissionsMode: msg.codexPermissionsMode,
|
|
5143
|
+
planMode: msg.planMode,
|
|
5144
|
+
sandboxMode: msg.sandboxMode,
|
|
5145
|
+
model: msg.model,
|
|
5146
|
+
effort: msg.effort,
|
|
5147
|
+
maxTurns: msg.maxTurns,
|
|
5148
|
+
maxBudgetUsd: msg.maxBudgetUsd,
|
|
5149
|
+
fallbackModel: msg.fallbackModel,
|
|
5150
|
+
forkSession: msg.forkSession ?? false,
|
|
5151
|
+
persistSession: msg.persistSession,
|
|
5152
|
+
profile: msg.profile,
|
|
5153
|
+
modelReasoningEffort: msg.modelReasoningEffort,
|
|
5154
|
+
serviceTier: msg.serviceTier,
|
|
5155
|
+
networkAccessEnabled: msg.networkAccessEnabled,
|
|
5156
|
+
webSearchMode: msg.webSearchMode,
|
|
5157
|
+
additionalWritableRoots: [...(msg.additionalWritableRoots ?? [])].sort(),
|
|
5158
|
+
resumeRequestId: msg.resumeRequestId,
|
|
5159
|
+
});
|
|
5160
|
+
}
|
|
5161
|
+
clearResumeOperation(key, operation) {
|
|
5162
|
+
if (operation.timeout)
|
|
5163
|
+
clearTimeout(operation.timeout);
|
|
5164
|
+
if (this.resumeOperations.get(key) === operation) {
|
|
5165
|
+
this.resumeOperations.delete(key);
|
|
5166
|
+
}
|
|
5167
|
+
}
|
|
5168
|
+
ensurePendingClaudeResume(ws, sourceSessionId) {
|
|
5169
|
+
let pendingResumes = this.pendingClaudeResumeInputs.get(ws);
|
|
5170
|
+
if (!pendingResumes) {
|
|
5171
|
+
pendingResumes = new Map();
|
|
5172
|
+
this.pendingClaudeResumeInputs.set(ws, pendingResumes);
|
|
5173
|
+
}
|
|
5174
|
+
if (!pendingResumes.has(sourceSessionId)) {
|
|
5175
|
+
pendingResumes.set(sourceSessionId, []);
|
|
5176
|
+
}
|
|
5177
|
+
}
|
|
5178
|
+
beginResumeOperation(params) {
|
|
5179
|
+
const { ws, provider, sourceSessionId, projectPath, request } = params;
|
|
5180
|
+
const key = this.resumeOperationKey(provider, sourceSessionId);
|
|
5181
|
+
const fingerprint = this.resumeRequestFingerprint(request);
|
|
5182
|
+
let operation = this.resumeOperations.get(key);
|
|
5183
|
+
if (operation?.completed &&
|
|
5184
|
+
(!this.sessionManager.get(operation.completed.sessionId) ||
|
|
5185
|
+
Date.now() - operation.completed.completedAt >
|
|
5186
|
+
RESUME_COMPLETED_TTL_MS ||
|
|
5187
|
+
operation.fingerprint !== fingerprint ||
|
|
5188
|
+
request.forkSession === true)) {
|
|
5189
|
+
this.clearResumeOperation(key, operation);
|
|
5190
|
+
operation = undefined;
|
|
5191
|
+
}
|
|
5192
|
+
if (operation &&
|
|
5193
|
+
!operation.completed &&
|
|
5194
|
+
operation.fingerprint !== fingerprint) {
|
|
5195
|
+
this.sendResumeFailed(ws, {
|
|
5196
|
+
provider,
|
|
5197
|
+
sourceSessionId,
|
|
5198
|
+
projectPath,
|
|
5199
|
+
resumeRequestId: request.resumeRequestId,
|
|
5200
|
+
});
|
|
5201
|
+
this.send(ws, {
|
|
5202
|
+
type: "error",
|
|
5203
|
+
message: "This session is already being restored with different settings. Wait for it to finish, then try again.",
|
|
5204
|
+
});
|
|
5205
|
+
return { key, operationId: operation.id, isOwner: false };
|
|
5206
|
+
}
|
|
5207
|
+
this.send(ws, {
|
|
5208
|
+
type: "system",
|
|
5209
|
+
subtype: "session_resume_started",
|
|
5210
|
+
sourceSessionId,
|
|
5211
|
+
provider,
|
|
5212
|
+
projectPath,
|
|
5213
|
+
...(request.resumeRequestId
|
|
5214
|
+
? { resumeRequestId: request.resumeRequestId }
|
|
5215
|
+
: {}),
|
|
5216
|
+
});
|
|
5217
|
+
if (provider === "claude") {
|
|
5218
|
+
this.ensurePendingClaudeResume(ws, sourceSessionId);
|
|
5219
|
+
}
|
|
5220
|
+
if (operation) {
|
|
5221
|
+
if (operation.completed) {
|
|
5222
|
+
this.send(ws, operation.completed.message);
|
|
5223
|
+
this.flushPendingClaudeResumeInputs(ws, sourceSessionId, operation.completed.sessionId);
|
|
5224
|
+
}
|
|
5225
|
+
else {
|
|
5226
|
+
operation.waiters.add(ws);
|
|
5227
|
+
}
|
|
5228
|
+
return { key, operationId: operation.id, isOwner: false };
|
|
5229
|
+
}
|
|
5230
|
+
const operationId = randomUUID();
|
|
5231
|
+
const newOperation = {
|
|
5232
|
+
id: operationId,
|
|
5233
|
+
provider,
|
|
5234
|
+
sourceSessionId,
|
|
5235
|
+
projectPath,
|
|
5236
|
+
resumeRequestId: request.resumeRequestId,
|
|
5237
|
+
fingerprint,
|
|
5238
|
+
waiters: new Set([ws]),
|
|
5239
|
+
};
|
|
5240
|
+
const timeout = setTimeout(() => {
|
|
5241
|
+
this.failResumeOperation(key, operationId, "Session restore is taking longer than expected. Please reconnect and try again.");
|
|
5242
|
+
}, RESUME_OPERATION_TIMEOUT_MS);
|
|
5243
|
+
timeout.unref?.();
|
|
5244
|
+
newOperation.timeout = timeout;
|
|
5245
|
+
this.resumeOperations.set(key, newOperation);
|
|
5246
|
+
return { key, operationId, isOwner: true };
|
|
5247
|
+
}
|
|
5248
|
+
completeResumeOperation(key, operationId, sessionId, message) {
|
|
5249
|
+
const operation = this.resumeOperations.get(key);
|
|
5250
|
+
if (!operation || operation.id !== operationId)
|
|
5251
|
+
return false;
|
|
5252
|
+
if (operation.timeout)
|
|
5253
|
+
clearTimeout(operation.timeout);
|
|
5254
|
+
operation.completed = {
|
|
5255
|
+
sessionId,
|
|
5256
|
+
message,
|
|
5257
|
+
completedAt: Date.now(),
|
|
5258
|
+
};
|
|
5259
|
+
for (const waiter of operation.waiters) {
|
|
5260
|
+
this.send(waiter, message);
|
|
5261
|
+
this.flushPendingClaudeResumeInputs(waiter, operation.sourceSessionId, sessionId);
|
|
5262
|
+
}
|
|
5263
|
+
operation.waiters.clear();
|
|
5264
|
+
const timeout = setTimeout(() => {
|
|
5265
|
+
this.clearResumeOperation(key, operation);
|
|
5266
|
+
}, RESUME_COMPLETED_TTL_MS);
|
|
5267
|
+
timeout.unref?.();
|
|
5268
|
+
operation.timeout = timeout;
|
|
5269
|
+
this.pruneCompletedResumeOperations();
|
|
5270
|
+
return true;
|
|
5271
|
+
}
|
|
5272
|
+
failResumeOperation(key, operationId, message) {
|
|
5273
|
+
const operation = this.resumeOperations.get(key);
|
|
5274
|
+
if (!operation || operation.id !== operationId)
|
|
5275
|
+
return;
|
|
5276
|
+
this.clearResumeOperation(key, operation);
|
|
5277
|
+
for (const waiter of operation.waiters) {
|
|
5278
|
+
this.rejectPendingClaudeResumeInputs(waiter, operation.sourceSessionId);
|
|
5279
|
+
this.sendResumeFailed(waiter, operation);
|
|
5280
|
+
this.send(waiter, { type: "error", message });
|
|
5281
|
+
}
|
|
5282
|
+
}
|
|
5283
|
+
sendResumeFailed(ws, resume) {
|
|
5284
|
+
this.send(ws, {
|
|
5285
|
+
type: "system",
|
|
5286
|
+
subtype: "session_resume_failed",
|
|
5287
|
+
provider: resume.provider,
|
|
5288
|
+
sourceSessionId: resume.sourceSessionId,
|
|
5289
|
+
projectPath: resume.projectPath,
|
|
5290
|
+
...(resume.resumeRequestId
|
|
5291
|
+
? { resumeRequestId: resume.resumeRequestId }
|
|
5292
|
+
: {}),
|
|
5293
|
+
});
|
|
5294
|
+
}
|
|
5295
|
+
flushPendingClaudeResumeInputs(ws, sourceSessionId, sessionId) {
|
|
5296
|
+
const pendingResumes = this.pendingClaudeResumeInputs.get(ws);
|
|
5297
|
+
const queuedInputs = pendingResumes?.get(sourceSessionId) ?? [];
|
|
5298
|
+
pendingResumes?.delete(sourceSessionId);
|
|
5299
|
+
for (const input of queuedInputs) {
|
|
5300
|
+
void this.handleClientMessage({ ...input, sessionId }, ws);
|
|
5301
|
+
}
|
|
5302
|
+
}
|
|
5303
|
+
rejectPendingClaudeResumeInputs(ws, sourceSessionId) {
|
|
5304
|
+
const pendingResumes = this.pendingClaudeResumeInputs.get(ws);
|
|
5305
|
+
const queuedInputs = pendingResumes?.get(sourceSessionId) ?? [];
|
|
5306
|
+
pendingResumes?.delete(sourceSessionId);
|
|
5307
|
+
for (const input of queuedInputs) {
|
|
5308
|
+
if (!input.clientMessageId)
|
|
5309
|
+
continue;
|
|
5310
|
+
this.send(ws, {
|
|
5311
|
+
type: "input_rejected",
|
|
5312
|
+
sessionId: sourceSessionId,
|
|
5313
|
+
clientMessageId: input.clientMessageId,
|
|
5314
|
+
reason: "Session resume failed",
|
|
5315
|
+
});
|
|
5316
|
+
}
|
|
5317
|
+
}
|
|
5318
|
+
pruneCompletedResumeOperations() {
|
|
5319
|
+
const completed = [...this.resumeOperations.entries()]
|
|
5320
|
+
.filter((entry) => entry[1].completed)
|
|
5321
|
+
.sort((a, b) => (a[1].completed?.completedAt ?? 0) -
|
|
5322
|
+
(b[1].completed?.completedAt ?? 0));
|
|
5323
|
+
while (completed.length > 100) {
|
|
5324
|
+
const oldest = completed.shift();
|
|
5325
|
+
if (oldest)
|
|
5326
|
+
this.clearResumeOperation(oldest[0], oldest[1]);
|
|
5327
|
+
}
|
|
4983
5328
|
}
|
|
4984
5329
|
/**
|
|
4985
5330
|
* Load the saved session name from CLI storage and set it on the SessionInfo.
|