@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/codex-process.d.ts +25 -0
- package/dist/codex-process.js +148 -23
- package/dist/codex-process.js.map +1 -1
- 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/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.js +4 -0
- package/dist/sessions-index.js.map +1 -1
- package/dist/websocket.d.ts +16 -0
- package/dist/websocket.js +458 -99
- package/dist/websocket.js.map +1 -1
- package/package.json +1 -1
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 =
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
|
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:
|
|
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.
|
|
3324
|
-
|
|
3325
|
-
.
|
|
3326
|
-
|
|
3327
|
-
|
|
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
|
|
3330
|
-
const
|
|
3331
|
-
|
|
3332
|
-
|
|
3333
|
-
|
|
3334
|
-
|
|
3335
|
-
|
|
3336
|
-
|
|
3337
|
-
|
|
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:
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
3499
|
-
|
|
3500
|
-
|
|
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
|
-
|
|
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
|
-
|
|
3593
|
-
|
|
3594
|
-
|
|
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
|
-
|
|
3617
|
-
|
|
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
|
-
|
|
3629
|
-
|
|
3630
|
-
|
|
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()
|