@ccpocket/bridge 1.63.5 → 1.64.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +5 -1
- package/dist/bridge-port.d.ts +2 -0
- package/dist/bridge-port.js +20 -0
- package/dist/bridge-port.js.map +1 -0
- package/dist/cli-args.js +3 -0
- package/dist/cli-args.js.map +1 -1
- package/dist/cli.js +8 -2
- package/dist/cli.js.map +1 -1
- package/dist/codex-permissions.d.ts +10 -0
- package/dist/codex-permissions.js +62 -0
- package/dist/codex-permissions.js.map +1 -0
- package/dist/codex-process.d.ts +1 -1
- package/dist/codex-process.js +8 -2
- package/dist/codex-process.js.map +1 -1
- package/dist/git-operations.d.ts +13 -0
- package/dist/git-operations.js +171 -3
- package/dist/git-operations.js.map +1 -1
- package/dist/index.js +22 -16
- package/dist/index.js.map +1 -1
- package/dist/parser.d.ts +2 -0
- package/dist/parser.js +6 -3
- package/dist/parser.js.map +1 -1
- package/dist/path-utils.d.ts +1 -0
- package/dist/path-utils.js +16 -0
- package/dist/path-utils.js.map +1 -1
- package/dist/sdk-process.d.ts +16 -1
- package/dist/sdk-process.js +126 -23
- package/dist/sdk-process.js.map +1 -1
- package/dist/server-listen.d.ts +2 -0
- package/dist/server-listen.js +23 -0
- package/dist/server-listen.js.map +1 -0
- package/dist/session.js +18 -4
- package/dist/session.js.map +1 -1
- package/dist/sessions-index.js +96 -11
- package/dist/sessions-index.js.map +1 -1
- package/dist/setup-launchd.js +3 -2
- package/dist/setup-launchd.js.map +1 -1
- package/dist/setup-systemd.js +3 -2
- package/dist/setup-systemd.js.map +1 -1
- package/dist/websocket.d.ts +32 -5
- package/dist/websocket.js +432 -150
- package/dist/websocket.js.map +1 -1
- package/package.json +1 -1
package/dist/websocket.js
CHANGED
|
@@ -14,7 +14,7 @@ import { getAllRecentSessions, getCodexSessionHistory, getSessionHistory, codexU
|
|
|
14
14
|
import { ArchiveStore } from "./archive-store.js";
|
|
15
15
|
import { WorktreeStore } from "./worktree-store.js";
|
|
16
16
|
import { listWorktrees, removeWorktree, worktreeExists, getMainBranch, } from "./worktree.js";
|
|
17
|
-
import { stageFiles, stageHunks, unstageFiles, unstageHunks, gitCommit, gitPush,
|
|
17
|
+
import { stageFiles, stageHunks, unstageFiles, unstageHunks, gitCommit, gitPush, listProjectFilesAndDirectoriesForClient, listBranches, createBranch, checkoutBranch, revertFiles, revertHunks, gitFetch, gitPull, gitRemoteStatus, gitStatus, } from "./git-operations.js";
|
|
18
18
|
import { generateCommitMessage } from "./git-assist.js";
|
|
19
19
|
import { listWindows, takeScreenshot } from "./screenshot.js";
|
|
20
20
|
import { DebugTraceStore } from "./debug-trace-store.js";
|
|
@@ -23,6 +23,7 @@ import { normalizePushLocale, t } from "./push-i18n.js";
|
|
|
23
23
|
import { fetchAllUsage } from "./usage.js";
|
|
24
24
|
import { getPackageVersion } from "./version.js";
|
|
25
25
|
import { isPathWithinAllowedDirectory, resolvePlatformPath, resolvePlatformPathFrom, } from "./path-utils.js";
|
|
26
|
+
import { deriveCodexPermissionsMode, normalizeCodexPermissionsMode, withDerivedCodexPermissionsMode, } from "./codex-permissions.js";
|
|
26
27
|
// ---- Available model lists (delivered to clients via session_list) ----
|
|
27
28
|
const FALLBACK_CLAUDE_MODELS = [
|
|
28
29
|
"claude-opus-4-7",
|
|
@@ -43,6 +44,9 @@ const FALLBACK_CLAUDE_MODEL_EFFORTS = {
|
|
|
43
44
|
"claude-haiku-4-6": [],
|
|
44
45
|
};
|
|
45
46
|
const FALLBACK_CODEX_MODELS = [
|
|
47
|
+
"gpt-5.6-sol",
|
|
48
|
+
"gpt-5.6-terra",
|
|
49
|
+
"gpt-5.6-luna",
|
|
46
50
|
"gpt-5.5",
|
|
47
51
|
"gpt-5.4",
|
|
48
52
|
"gpt-5.4-mini",
|
|
@@ -50,6 +54,22 @@ const FALLBACK_CODEX_MODELS = [
|
|
|
50
54
|
"gpt-5.3-codex-spark",
|
|
51
55
|
];
|
|
52
56
|
const FALLBACK_CODEX_REASONING_EFFORTS = ["low", "medium", "high", "xhigh"];
|
|
57
|
+
const FALLBACK_GPT_5_6_REASONING_EFFORTS = [
|
|
58
|
+
...FALLBACK_CODEX_REASONING_EFFORTS,
|
|
59
|
+
"max",
|
|
60
|
+
];
|
|
61
|
+
const FALLBACK_GPT_5_6_ULTRA_REASONING_EFFORTS = [
|
|
62
|
+
...FALLBACK_GPT_5_6_REASONING_EFFORTS,
|
|
63
|
+
"ultra",
|
|
64
|
+
];
|
|
65
|
+
function fallbackCodexReasoningEfforts(model) {
|
|
66
|
+
if (model === "gpt-5.6-sol" || model === "gpt-5.6-terra") {
|
|
67
|
+
return FALLBACK_GPT_5_6_ULTRA_REASONING_EFFORTS;
|
|
68
|
+
}
|
|
69
|
+
if (model === "gpt-5.6-luna")
|
|
70
|
+
return FALLBACK_GPT_5_6_REASONING_EFFORTS;
|
|
71
|
+
return FALLBACK_CODEX_REASONING_EFFORTS;
|
|
72
|
+
}
|
|
53
73
|
const CODEX_RECENT_THREAD_SOURCE_KINDS = [
|
|
54
74
|
"cli",
|
|
55
75
|
"vscode",
|
|
@@ -207,17 +227,6 @@ function normalizeCodexApprovalPolicy(value) {
|
|
|
207
227
|
return "on-request";
|
|
208
228
|
}
|
|
209
229
|
}
|
|
210
|
-
function normalizeCodexPermissionsMode(value) {
|
|
211
|
-
switch (value) {
|
|
212
|
-
case "default":
|
|
213
|
-
case "autoReview":
|
|
214
|
-
case "fullAccess":
|
|
215
|
-
case "custom":
|
|
216
|
-
return value;
|
|
217
|
-
default:
|
|
218
|
-
return undefined;
|
|
219
|
-
}
|
|
220
|
-
}
|
|
221
230
|
function sanitizeCodexModel(model) {
|
|
222
231
|
if (typeof model !== "string")
|
|
223
232
|
return undefined;
|
|
@@ -253,21 +262,6 @@ function codexSettingsFromPermissionsMode(mode) {
|
|
|
253
262
|
return { codexPermissionsMode: mode };
|
|
254
263
|
}
|
|
255
264
|
}
|
|
256
|
-
function inferCodexPermissionsMode(params) {
|
|
257
|
-
const approvalPolicy = params.approvalPolicy;
|
|
258
|
-
const approvalsReviewer = params.approvalsReviewer ?? "user";
|
|
259
|
-
const sandboxMode = params.sandboxMode;
|
|
260
|
-
if (approvalPolicy === "never" && sandboxMode === "danger-full-access") {
|
|
261
|
-
return "fullAccess";
|
|
262
|
-
}
|
|
263
|
-
if (approvalPolicy === "on-request" && sandboxMode === "workspace-write") {
|
|
264
|
-
return approvalsReviewer === "auto_review" ||
|
|
265
|
-
approvalsReviewer === "guardian_subagent"
|
|
266
|
-
? "autoReview"
|
|
267
|
-
: "default";
|
|
268
|
-
}
|
|
269
|
-
return undefined;
|
|
270
|
-
}
|
|
271
265
|
function errorMessageOf(err) {
|
|
272
266
|
return err instanceof Error ? err.message : String(err);
|
|
273
267
|
}
|
|
@@ -344,6 +338,38 @@ function envFlagEnabled(name) {
|
|
|
344
338
|
const value = process.env[name]?.trim().toLowerCase();
|
|
345
339
|
return value === "1" || value === "true" || value === "yes" || value === "on";
|
|
346
340
|
}
|
|
341
|
+
const MAX_TIMER_DELAY_MS = 2_147_483_647;
|
|
342
|
+
function positiveEnvInt(name, fallback) {
|
|
343
|
+
const raw = process.env[name]?.trim();
|
|
344
|
+
if (!raw)
|
|
345
|
+
return fallback;
|
|
346
|
+
const value = Number(raw);
|
|
347
|
+
return Number.isSafeInteger(value) && value > 0 ? value : fallback;
|
|
348
|
+
}
|
|
349
|
+
function nonNegativeEnvInt(name, fallback) {
|
|
350
|
+
const raw = process.env[name]?.trim();
|
|
351
|
+
if (!raw)
|
|
352
|
+
return fallback;
|
|
353
|
+
const value = Number(raw);
|
|
354
|
+
return Number.isSafeInteger(value) &&
|
|
355
|
+
value >= 0 &&
|
|
356
|
+
value <= MAX_TIMER_DELAY_MS
|
|
357
|
+
? value
|
|
358
|
+
: fallback;
|
|
359
|
+
}
|
|
360
|
+
function normalizePositiveLimit(value, fallback) {
|
|
361
|
+
return value !== undefined && Number.isSafeInteger(value) && value > 0
|
|
362
|
+
? value
|
|
363
|
+
: fallback;
|
|
364
|
+
}
|
|
365
|
+
function normalizeNonNegativeLimit(value, fallback) {
|
|
366
|
+
return value !== undefined &&
|
|
367
|
+
Number.isSafeInteger(value) &&
|
|
368
|
+
value >= 0 &&
|
|
369
|
+
value <= MAX_TIMER_DELAY_MS
|
|
370
|
+
? value
|
|
371
|
+
: fallback;
|
|
372
|
+
}
|
|
347
373
|
function codexThreadToRecentSession(thread, indexed) {
|
|
348
374
|
// thread/list only exposes a single preview blob; prefer the real
|
|
349
375
|
// first/last/summary texts parsed from the rollout file so display-mode
|
|
@@ -391,6 +417,11 @@ function mergeRecentSessionPages(sessions) {
|
|
|
391
417
|
export class BridgeWebSocketServer {
|
|
392
418
|
static MAX_DEBUG_EVENTS = 800;
|
|
393
419
|
static MAX_HISTORY_SUMMARY_ITEMS = 300;
|
|
420
|
+
static CONNECT_METADATA_REFRESH_COOLDOWN_MS = 5 * 60 * 1000;
|
|
421
|
+
static DEFAULT_FILE_LIST_MAX_ENTRIES = 5000;
|
|
422
|
+
static DEFAULT_FILE_LIST_MAX_BYTES = 512 * 1024;
|
|
423
|
+
static DEFAULT_DELTA_BATCH_MS = 100;
|
|
424
|
+
static DEFAULT_DELTA_BATCH_MAX_CHARS = 4096;
|
|
394
425
|
wss;
|
|
395
426
|
sessionManager;
|
|
396
427
|
apiKey;
|
|
@@ -410,7 +441,8 @@ export class BridgeWebSocketServer {
|
|
|
410
441
|
archiveStore;
|
|
411
442
|
codexProfiles = [];
|
|
412
443
|
defaultCodexProfile;
|
|
413
|
-
|
|
444
|
+
codexMetadataRequest = null;
|
|
445
|
+
lastConnectMetadataRefreshAt = null;
|
|
414
446
|
claudeModels = FALLBACK_CLAUDE_MODELS;
|
|
415
447
|
claudeModelEfforts = {
|
|
416
448
|
...FALLBACK_CLAUDE_MODEL_EFFORTS,
|
|
@@ -419,18 +451,23 @@ export class BridgeWebSocketServer {
|
|
|
419
451
|
codexModels = FALLBACK_CODEX_MODELS;
|
|
420
452
|
codexModelReasoningEfforts = Object.fromEntries(FALLBACK_CODEX_MODELS.map((model) => [
|
|
421
453
|
model,
|
|
422
|
-
|
|
454
|
+
fallbackCodexReasoningEfforts(model),
|
|
423
455
|
]));
|
|
424
|
-
codexModelsRequest = null;
|
|
425
456
|
/** FCM token → push notification locale */
|
|
426
457
|
tokenLocales = new Map();
|
|
427
458
|
tokenPrivacyMode = new Map();
|
|
428
459
|
failSetPermissionMode = envFlagEnabled("BRIDGE_FAIL_SET_PERMISSION_MODE");
|
|
429
460
|
failSetSandboxMode = envFlagEnabled("BRIDGE_FAIL_SET_SANDBOX_MODE");
|
|
461
|
+
fileListMaxEntries;
|
|
462
|
+
fileListMaxBytes;
|
|
463
|
+
deltaBatchMs;
|
|
464
|
+
deltaBatchMaxChars;
|
|
465
|
+
deltaBatches = new Map();
|
|
430
466
|
platform;
|
|
431
467
|
clientSupportedServerMessages = new WeakMap();
|
|
468
|
+
pendingClaudeResumeInputs = new WeakMap();
|
|
432
469
|
constructor(options) {
|
|
433
|
-
const { server, apiKey, allowedDirs, imageStore, galleryStore, projectHistory, debugTraceStore, recordingStore, firebaseAuth, promptHistoryBackup, promptHistoryStore, platform, } = options;
|
|
470
|
+
const { server, apiKey, allowedDirs, imageStore, galleryStore, projectHistory, debugTraceStore, recordingStore, firebaseAuth, promptHistoryBackup, promptHistoryStore, platform, fileListMaxEntries, fileListMaxBytes, deltaBatchMs, deltaBatchMaxChars, } = options;
|
|
434
471
|
this.apiKey = apiKey ?? null;
|
|
435
472
|
this.allowedDirs = allowedDirs ?? [];
|
|
436
473
|
this.imageStore = imageStore ?? null;
|
|
@@ -443,6 +480,10 @@ export class BridgeWebSocketServer {
|
|
|
443
480
|
this.promptHistoryBackup = promptHistoryBackup ?? null;
|
|
444
481
|
this.promptHistoryStore = promptHistoryStore ?? null;
|
|
445
482
|
this.platform = platform ?? process.platform;
|
|
483
|
+
this.fileListMaxEntries = normalizePositiveLimit(fileListMaxEntries, positiveEnvInt("BRIDGE_FILE_LIST_MAX_ENTRIES", BridgeWebSocketServer.DEFAULT_FILE_LIST_MAX_ENTRIES));
|
|
484
|
+
this.fileListMaxBytes = normalizePositiveLimit(fileListMaxBytes, positiveEnvInt("BRIDGE_FILE_LIST_MAX_BYTES", BridgeWebSocketServer.DEFAULT_FILE_LIST_MAX_BYTES));
|
|
485
|
+
this.deltaBatchMs = normalizeNonNegativeLimit(deltaBatchMs, nonNegativeEnvInt("BRIDGE_DELTA_BATCH_MS", BridgeWebSocketServer.DEFAULT_DELTA_BATCH_MS));
|
|
486
|
+
this.deltaBatchMaxChars = normalizePositiveLimit(deltaBatchMaxChars, positiveEnvInt("BRIDGE_DELTA_BATCH_MAX_CHARS", BridgeWebSocketServer.DEFAULT_DELTA_BATCH_MAX_CHARS));
|
|
446
487
|
this.archiveStore = new ArchiveStore();
|
|
447
488
|
void this.debugTraceStore.init().catch((err) => {
|
|
448
489
|
console.error("[ws] Failed to initialize debug trace store:", err);
|
|
@@ -529,6 +570,9 @@ export class BridgeWebSocketServer {
|
|
|
529
570
|
}
|
|
530
571
|
buildSessionCreatedMessage(params) {
|
|
531
572
|
const { sessionId, provider, projectPath, session, permissionMode, executionMode, planMode, approvalsReviewer, codexPermissionsMode, sandboxMode, slashCommands, skills, skillMetadata, apps, appMetadata, plugins, pluginMetadata, sourceSessionId, } = params;
|
|
573
|
+
const derivedCodexSettings = provider === "codex"
|
|
574
|
+
? withDerivedCodexPermissionsMode(session?.codexSettings)
|
|
575
|
+
: session?.codexSettings;
|
|
532
576
|
const msg = {
|
|
533
577
|
type: "system",
|
|
534
578
|
subtype: "session_created",
|
|
@@ -540,15 +584,15 @@ export class BridgeWebSocketServer {
|
|
|
540
584
|
permissionMode: permissionMode,
|
|
541
585
|
}
|
|
542
586
|
: {}),
|
|
543
|
-
...((approvalsReviewer ??
|
|
587
|
+
...((approvalsReviewer ?? derivedCodexSettings?.approvalsReviewer)
|
|
544
588
|
? {
|
|
545
|
-
approvalsReviewer: approvalsReviewer ??
|
|
589
|
+
approvalsReviewer: approvalsReviewer ?? derivedCodexSettings?.approvalsReviewer,
|
|
546
590
|
}
|
|
547
591
|
: {}),
|
|
548
|
-
...((codexPermissionsMode ??
|
|
592
|
+
...((codexPermissionsMode ?? derivedCodexSettings?.codexPermissionsMode)
|
|
549
593
|
? {
|
|
550
594
|
codexPermissionsMode: (codexPermissionsMode ??
|
|
551
|
-
|
|
595
|
+
derivedCodexSettings?.codexPermissionsMode),
|
|
552
596
|
}
|
|
553
597
|
: {}),
|
|
554
598
|
...((executionMode ??
|
|
@@ -621,28 +665,28 @@ export class BridgeWebSocketServer {
|
|
|
621
665
|
: {}),
|
|
622
666
|
...(sourceSessionId ? { sourceSessionId } : {}),
|
|
623
667
|
};
|
|
624
|
-
if (provider === "codex" &&
|
|
625
|
-
if (
|
|
626
|
-
msg.model =
|
|
668
|
+
if (provider === "codex" && derivedCodexSettings) {
|
|
669
|
+
if (derivedCodexSettings.model !== undefined) {
|
|
670
|
+
msg.model = derivedCodexSettings.model;
|
|
627
671
|
}
|
|
628
|
-
if (
|
|
629
|
-
msg.approvalPolicy =
|
|
672
|
+
if (derivedCodexSettings.approvalPolicy !== undefined) {
|
|
673
|
+
msg.approvalPolicy = derivedCodexSettings.approvalPolicy;
|
|
630
674
|
}
|
|
631
|
-
if (
|
|
632
|
-
msg.codexPermissionsMode =
|
|
675
|
+
if (derivedCodexSettings.codexPermissionsMode !== undefined) {
|
|
676
|
+
msg.codexPermissionsMode = derivedCodexSettings.codexPermissionsMode;
|
|
633
677
|
}
|
|
634
|
-
if (
|
|
635
|
-
msg.modelReasoningEffort =
|
|
678
|
+
if (derivedCodexSettings.modelReasoningEffort !== undefined) {
|
|
679
|
+
msg.modelReasoningEffort = derivedCodexSettings.modelReasoningEffort;
|
|
636
680
|
}
|
|
637
|
-
if (
|
|
638
|
-
msg.networkAccessEnabled =
|
|
681
|
+
if (derivedCodexSettings.networkAccessEnabled !== undefined) {
|
|
682
|
+
msg.networkAccessEnabled = derivedCodexSettings.networkAccessEnabled;
|
|
639
683
|
}
|
|
640
|
-
if (
|
|
641
|
-
msg.webSearchMode =
|
|
684
|
+
if (derivedCodexSettings.webSearchMode !== undefined) {
|
|
685
|
+
msg.webSearchMode = derivedCodexSettings.webSearchMode;
|
|
642
686
|
}
|
|
643
|
-
if (
|
|
687
|
+
if (derivedCodexSettings.additionalWritableRoots !== undefined) {
|
|
644
688
|
msg.additionalWritableRoots =
|
|
645
|
-
|
|
689
|
+
derivedCodexSettings.additionalWritableRoots;
|
|
646
690
|
}
|
|
647
691
|
}
|
|
648
692
|
return msg;
|
|
@@ -741,7 +785,7 @@ export class BridgeWebSocketServer {
|
|
|
741
785
|
expectedUserTurns: targetOrdinal - 1,
|
|
742
786
|
fallback: buildCodexHistoryPrefix(session, targetOrdinal - 1),
|
|
743
787
|
});
|
|
744
|
-
this.
|
|
788
|
+
this.destroySession(sessionId);
|
|
745
789
|
const newSessionId = this.sessionManager.create(projectPath, undefined, pastMessages, worktreeOpts, "codex", {
|
|
746
790
|
...(codexSettings ?? {}),
|
|
747
791
|
threadId,
|
|
@@ -1345,7 +1389,9 @@ export class BridgeWebSocketServer {
|
|
|
1345
1389
|
}
|
|
1346
1390
|
close() {
|
|
1347
1391
|
console.log("[ws] Shutting down...");
|
|
1392
|
+
this.flushAllDeltaBatches();
|
|
1348
1393
|
this.sessionManager.destroyAll();
|
|
1394
|
+
this.flushAllDeltaBatches();
|
|
1349
1395
|
stopManagedCodexAppServers();
|
|
1350
1396
|
this.debugEvents.clear();
|
|
1351
1397
|
this.wss.close();
|
|
@@ -1360,9 +1406,7 @@ export class BridgeWebSocketServer {
|
|
|
1360
1406
|
}
|
|
1361
1407
|
handleConnection(ws) {
|
|
1362
1408
|
// Send session list and project history on connect
|
|
1363
|
-
|
|
1364
|
-
void this.refreshCodexModels();
|
|
1365
|
-
void this.refreshClaudeModels();
|
|
1409
|
+
this.refreshConnectionMetadata();
|
|
1366
1410
|
this.sendSessionList(ws);
|
|
1367
1411
|
const projects = this.projectHistory?.getProjects() ?? [];
|
|
1368
1412
|
this.send(ws, { type: "project_history", projects });
|
|
@@ -1393,11 +1437,25 @@ export class BridgeWebSocketServer {
|
|
|
1393
1437
|
});
|
|
1394
1438
|
ws.on("close", () => {
|
|
1395
1439
|
console.log("[ws] Client disconnected");
|
|
1440
|
+
this.discardClientDeltaBatches(ws);
|
|
1441
|
+
this.clearPendingClaudeResumeInputs(ws);
|
|
1396
1442
|
});
|
|
1397
1443
|
ws.on("error", (err) => {
|
|
1398
1444
|
console.error("[ws] Client error:", err.message);
|
|
1399
1445
|
});
|
|
1400
1446
|
}
|
|
1447
|
+
refreshConnectionMetadata(now = Date.now()) {
|
|
1448
|
+
const lastRefreshAt = this.lastConnectMetadataRefreshAt;
|
|
1449
|
+
if (lastRefreshAt !== null &&
|
|
1450
|
+
now >= lastRefreshAt &&
|
|
1451
|
+
now - lastRefreshAt <
|
|
1452
|
+
BridgeWebSocketServer.CONNECT_METADATA_REFRESH_COOLDOWN_MS) {
|
|
1453
|
+
return;
|
|
1454
|
+
}
|
|
1455
|
+
this.lastConnectMetadataRefreshAt = now;
|
|
1456
|
+
void this.refreshCodexMetadata();
|
|
1457
|
+
void this.refreshClaudeModels();
|
|
1458
|
+
}
|
|
1401
1459
|
async handleClientMessage(msg, ws) {
|
|
1402
1460
|
if (msg.type === "client_capabilities") {
|
|
1403
1461
|
this.clientSupportedServerMessages.set(ws, new Set(msg.supportedServerMessages ?? []));
|
|
@@ -1522,7 +1580,8 @@ export class BridgeWebSocketServer {
|
|
|
1522
1580
|
? codexPermissionSettings.sandboxMode
|
|
1523
1581
|
: sandboxModeToInternal(msg.sandboxMode),
|
|
1524
1582
|
model: msg.model,
|
|
1525
|
-
modelReasoningEffort: msg.modelReasoningEffort ??
|
|
1583
|
+
modelReasoningEffort: msg.modelReasoningEffort ??
|
|
1584
|
+
undefined,
|
|
1526
1585
|
networkAccessEnabled: msg.networkAccessEnabled,
|
|
1527
1586
|
webSearchMode: msg.webSearchMode ??
|
|
1528
1587
|
undefined,
|
|
@@ -1577,7 +1636,7 @@ export class BridgeWebSocketServer {
|
|
|
1577
1636
|
}));
|
|
1578
1637
|
this.broadcastSessionList();
|
|
1579
1638
|
if (provider === "codex") {
|
|
1580
|
-
void this.
|
|
1639
|
+
void this.refreshCodexMetadata(projectPath);
|
|
1581
1640
|
}
|
|
1582
1641
|
else {
|
|
1583
1642
|
void this.refreshClaudeModels(projectPath);
|
|
@@ -1616,6 +1675,13 @@ export class BridgeWebSocketServer {
|
|
|
1616
1675
|
case "input": {
|
|
1617
1676
|
const session = this.resolveSession(msg.sessionId);
|
|
1618
1677
|
if (!session) {
|
|
1678
|
+
const pendingInputs = msg.sessionId
|
|
1679
|
+
? this.pendingClaudeResumeInputs.get(ws)?.get(msg.sessionId)
|
|
1680
|
+
: undefined;
|
|
1681
|
+
if (pendingInputs) {
|
|
1682
|
+
pendingInputs.push(msg);
|
|
1683
|
+
return;
|
|
1684
|
+
}
|
|
1619
1685
|
this.send(ws, {
|
|
1620
1686
|
type: "error",
|
|
1621
1687
|
message: "No active session. Send 'start' first.",
|
|
@@ -1814,11 +1880,20 @@ export class BridgeWebSocketServer {
|
|
|
1814
1880
|
// Claude Code input path — enqueue first, then interrupt if busy
|
|
1815
1881
|
const claudeProc = session.process;
|
|
1816
1882
|
let wasQueued = false;
|
|
1883
|
+
let shouldInterrupt = false;
|
|
1817
1884
|
if (images.length > 0) {
|
|
1818
1885
|
console.log(`[ws] Sending message with ${images.length} inline Base64 image(s)`);
|
|
1819
|
-
|
|
1820
|
-
|
|
1821
|
-
|
|
1886
|
+
if (typeof claudeProc.dispatchInputWithImages === "function") {
|
|
1887
|
+
const result = claudeProc.dispatchInputWithImages(text, images);
|
|
1888
|
+
wasQueued = result.queued;
|
|
1889
|
+
shouldInterrupt = result.shouldInterrupt;
|
|
1890
|
+
}
|
|
1891
|
+
else {
|
|
1892
|
+
const result = claudeProc.sendInputWithImages(text, images);
|
|
1893
|
+
wasQueued =
|
|
1894
|
+
typeof result === "boolean" ? result : isAgentBusySnapshot;
|
|
1895
|
+
shouldInterrupt = wasQueued;
|
|
1896
|
+
}
|
|
1822
1897
|
}
|
|
1823
1898
|
// Legacy imageId mode (backward compatibility)
|
|
1824
1899
|
else if (msg.imageId && this.galleryStore) {
|
|
@@ -1834,39 +1909,74 @@ export class BridgeWebSocketServer {
|
|
|
1834
1909
|
.then((imageData) => {
|
|
1835
1910
|
let queuedAfterResolve = false;
|
|
1836
1911
|
if (imageData) {
|
|
1837
|
-
|
|
1838
|
-
|
|
1839
|
-
|
|
1840
|
-
|
|
1841
|
-
|
|
1912
|
+
if (typeof claudeProc.dispatchInputWithImages === "function") {
|
|
1913
|
+
const result = claudeProc.dispatchInputWithImages(text, [
|
|
1914
|
+
imageData,
|
|
1915
|
+
]);
|
|
1916
|
+
queuedAfterResolve = result.queued;
|
|
1917
|
+
if (result.shouldInterrupt)
|
|
1918
|
+
claudeProc.interrupt();
|
|
1919
|
+
}
|
|
1920
|
+
else {
|
|
1921
|
+
const result = claudeProc.sendInputWithImages(text, [
|
|
1922
|
+
imageData,
|
|
1923
|
+
]);
|
|
1924
|
+
queuedAfterResolve =
|
|
1925
|
+
typeof result === "boolean"
|
|
1926
|
+
? result
|
|
1927
|
+
: isAgentBusySnapshot;
|
|
1928
|
+
if (queuedAfterResolve)
|
|
1929
|
+
claudeProc.interrupt();
|
|
1930
|
+
}
|
|
1842
1931
|
}
|
|
1843
1932
|
else {
|
|
1844
1933
|
console.warn(`[ws] Image not found: ${msg.imageId}`);
|
|
1845
|
-
|
|
1846
|
-
|
|
1847
|
-
|
|
1848
|
-
|
|
1849
|
-
|
|
1850
|
-
|
|
1851
|
-
|
|
1934
|
+
if (typeof claudeProc.dispatchInput === "function") {
|
|
1935
|
+
const result = claudeProc.dispatchInput(text);
|
|
1936
|
+
queuedAfterResolve = result.queued;
|
|
1937
|
+
if (result.shouldInterrupt)
|
|
1938
|
+
claudeProc.interrupt();
|
|
1939
|
+
}
|
|
1940
|
+
else {
|
|
1941
|
+
const result = session.process.sendInput(text);
|
|
1942
|
+
queuedAfterResolve =
|
|
1943
|
+
typeof result === "boolean"
|
|
1944
|
+
? result
|
|
1945
|
+
: isAgentBusySnapshot;
|
|
1946
|
+
if (queuedAfterResolve)
|
|
1947
|
+
claudeProc.interrupt();
|
|
1948
|
+
}
|
|
1852
1949
|
}
|
|
1853
1950
|
})
|
|
1854
1951
|
.catch((err) => {
|
|
1855
1952
|
console.error(`[ws] Failed to load image: ${err}`);
|
|
1856
|
-
|
|
1857
|
-
|
|
1858
|
-
|
|
1859
|
-
|
|
1860
|
-
|
|
1953
|
+
if (typeof claudeProc.dispatchInput === "function") {
|
|
1954
|
+
const result = claudeProc.dispatchInput(text);
|
|
1955
|
+
if (result.shouldInterrupt)
|
|
1956
|
+
claudeProc.interrupt();
|
|
1957
|
+
}
|
|
1958
|
+
else {
|
|
1959
|
+
const result = session.process.sendInput(text);
|
|
1960
|
+
const queuedAfterResolve = typeof result === "boolean" ? result : isAgentBusySnapshot;
|
|
1961
|
+
if (queuedAfterResolve)
|
|
1962
|
+
claudeProc.interrupt();
|
|
1861
1963
|
}
|
|
1862
1964
|
});
|
|
1863
1965
|
break;
|
|
1864
1966
|
}
|
|
1865
1967
|
// Text-only message
|
|
1866
1968
|
else {
|
|
1867
|
-
|
|
1868
|
-
|
|
1869
|
-
|
|
1969
|
+
if (typeof claudeProc.dispatchInput === "function") {
|
|
1970
|
+
const result = claudeProc.dispatchInput(text);
|
|
1971
|
+
wasQueued = result.queued;
|
|
1972
|
+
shouldInterrupt = result.shouldInterrupt;
|
|
1973
|
+
}
|
|
1974
|
+
else {
|
|
1975
|
+
const result = session.process.sendInput(text);
|
|
1976
|
+
wasQueued =
|
|
1977
|
+
typeof result === "boolean" ? result : isAgentBusySnapshot;
|
|
1978
|
+
shouldInterrupt = wasQueued;
|
|
1979
|
+
}
|
|
1870
1980
|
}
|
|
1871
1981
|
// Acknowledge receipt so the client can mark the message state.
|
|
1872
1982
|
// queued=true means the input was enqueued instead of being consumed
|
|
@@ -1878,7 +1988,7 @@ export class BridgeWebSocketServer {
|
|
|
1878
1988
|
acceptedSeq,
|
|
1879
1989
|
queued: wasQueued,
|
|
1880
1990
|
});
|
|
1881
|
-
if (
|
|
1991
|
+
if (shouldInterrupt) {
|
|
1882
1992
|
console.log(`[ws] Agent is busy — will queue input and interrupt current turn`);
|
|
1883
1993
|
claudeProc.interrupt();
|
|
1884
1994
|
}
|
|
@@ -2059,7 +2169,7 @@ export class BridgeWebSocketServer {
|
|
|
2059
2169
|
: currentSandboxMode;
|
|
2060
2170
|
const newPermissionsMode = codexPermissionSettings?.codexPermissionsMode ??
|
|
2061
2171
|
(collaborationOnlyChange ? currentPermissionsMode : undefined) ??
|
|
2062
|
-
|
|
2172
|
+
deriveCodexPermissionsMode({
|
|
2063
2173
|
approvalPolicy: newApproval,
|
|
2064
2174
|
approvalsReviewer: codexPermissionSettings?.approvalsReviewer ??
|
|
2065
2175
|
msg.approvalsReviewer ??
|
|
@@ -2133,7 +2243,7 @@ export class BridgeWebSocketServer {
|
|
|
2133
2243
|
const worktreePath = session.worktreePath;
|
|
2134
2244
|
const worktreeBranch = session.worktreeBranch;
|
|
2135
2245
|
const sessionName = session.name;
|
|
2136
|
-
this.
|
|
2246
|
+
this.destroySession(oldSessionId);
|
|
2137
2247
|
console.log(`[ws] Permission mode change: destroyed session ${oldSessionId}`);
|
|
2138
2248
|
const hasUserMessages = session.history?.some((m) => m.type === "user_input" || m.type === "assistant") ||
|
|
2139
2249
|
(session.pastMessages && session.pastMessages.length > 0);
|
|
@@ -2364,7 +2474,7 @@ export class BridgeWebSocketServer {
|
|
|
2364
2474
|
const sessionName = session.name;
|
|
2365
2475
|
const permissionMode = session.process.permissionMode;
|
|
2366
2476
|
const model = session.process.model;
|
|
2367
|
-
this.
|
|
2477
|
+
this.destroySession(oldSessionId);
|
|
2368
2478
|
console.log(`[ws] Claude sandbox change: destroyed session ${oldSessionId}`);
|
|
2369
2479
|
const newId = this.sessionManager.create(projectPath, {
|
|
2370
2480
|
sessionId: claudeSessionId,
|
|
@@ -2420,7 +2530,7 @@ export class BridgeWebSocketServer {
|
|
|
2420
2530
|
const executionMode = oldSettings.approvalPolicy === "never" ? "fullAccess" : "default";
|
|
2421
2531
|
const planMode = collaborationMode === "plan";
|
|
2422
2532
|
const legacyPermissionMode = modesToLegacyPermissionMode("codex", executionMode, planMode);
|
|
2423
|
-
this.
|
|
2533
|
+
this.destroySession(oldSessionId);
|
|
2424
2534
|
console.log(`[ws] Sandbox mode change: destroyed session ${oldSessionId}`);
|
|
2425
2535
|
// Check if the user actually exchanged messages in this session.
|
|
2426
2536
|
// session.history always contains system events (init, status, etc.)
|
|
@@ -2557,7 +2667,7 @@ export class BridgeWebSocketServer {
|
|
|
2557
2667
|
const permissionMode = sdkProc.permissionMode;
|
|
2558
2668
|
const worktreePath = session.worktreePath;
|
|
2559
2669
|
const worktreeBranch = session.worktreeBranch;
|
|
2560
|
-
this.
|
|
2670
|
+
this.destroySession(sessionId);
|
|
2561
2671
|
console.log(`[ws] Clear context: destroyed session ${sessionId}`);
|
|
2562
2672
|
const newId = this.sessionManager.create(projectPath, {
|
|
2563
2673
|
...(claudeSessionId
|
|
@@ -2642,7 +2752,7 @@ export class BridgeWebSocketServer {
|
|
|
2642
2752
|
subtype: "stopped",
|
|
2643
2753
|
sessionId: session.claudeSessionId,
|
|
2644
2754
|
});
|
|
2645
|
-
this.
|
|
2755
|
+
this.destroySession(msg.sessionId);
|
|
2646
2756
|
this.recordDebugEvent(msg.sessionId, {
|
|
2647
2757
|
direction: "internal",
|
|
2648
2758
|
channel: "bridge",
|
|
@@ -3020,7 +3130,8 @@ export class BridgeWebSocketServer {
|
|
|
3020
3130
|
? codexPermissionSettings.sandboxMode
|
|
3021
3131
|
: sandboxModeToInternal(msg.sandboxMode),
|
|
3022
3132
|
model: msg.model,
|
|
3023
|
-
modelReasoningEffort: msg.modelReasoningEffort ??
|
|
3133
|
+
modelReasoningEffort: msg.modelReasoningEffort ??
|
|
3134
|
+
undefined,
|
|
3024
3135
|
networkAccessEnabled: msg.networkAccessEnabled,
|
|
3025
3136
|
webSearchMode: msg.webSearchMode ??
|
|
3026
3137
|
undefined,
|
|
@@ -3065,6 +3176,19 @@ export class BridgeWebSocketServer {
|
|
|
3065
3176
|
}
|
|
3066
3177
|
const claudeSessionId = sessionRefId;
|
|
3067
3178
|
const cached = this.sessionManager.getCachedCommands(resumeProjectPath);
|
|
3179
|
+
let pendingResumes = this.pendingClaudeResumeInputs.get(ws);
|
|
3180
|
+
if (!pendingResumes) {
|
|
3181
|
+
pendingResumes = new Map();
|
|
3182
|
+
this.pendingClaudeResumeInputs.set(ws, pendingResumes);
|
|
3183
|
+
}
|
|
3184
|
+
if (pendingResumes.has(claudeSessionId)) {
|
|
3185
|
+
this.send(ws, {
|
|
3186
|
+
type: "error",
|
|
3187
|
+
message: `Session resume already in progress: ${claudeSessionId}`,
|
|
3188
|
+
});
|
|
3189
|
+
break;
|
|
3190
|
+
}
|
|
3191
|
+
pendingResumes.set(claudeSessionId, []);
|
|
3068
3192
|
// Look up worktree mapping for this Claude session
|
|
3069
3193
|
const wtMapping = this.worktreeStore.get(claudeSessionId);
|
|
3070
3194
|
let worktreeOpts;
|
|
@@ -3106,7 +3230,7 @@ export class BridgeWebSocketServer {
|
|
|
3106
3230
|
worktreeOptions: worktreeOpts,
|
|
3107
3231
|
});
|
|
3108
3232
|
const createdSession = this.sessionManager.get(sessionId);
|
|
3109
|
-
|
|
3233
|
+
const finishResume = () => {
|
|
3110
3234
|
this.send(ws, {
|
|
3111
3235
|
...this.buildSessionCreatedMessage({
|
|
3112
3236
|
sessionId,
|
|
@@ -3137,10 +3261,19 @@ export class BridgeWebSocketServer {
|
|
|
3137
3261
|
}),
|
|
3138
3262
|
claudeSessionId,
|
|
3139
3263
|
});
|
|
3264
|
+
const queuedInputs = pendingResumes.get(claudeSessionId) ?? [];
|
|
3265
|
+
pendingResumes.delete(claudeSessionId);
|
|
3266
|
+
for (const input of queuedInputs) {
|
|
3267
|
+
void this.handleClientMessage({ ...input, sessionId }, ws);
|
|
3268
|
+
}
|
|
3140
3269
|
this.broadcastSessionList();
|
|
3141
3270
|
if (autoFallbackUsed) {
|
|
3142
3271
|
this.sendTip(ws, sessionId, "auto_mode_fallback_default", createdSession);
|
|
3143
3272
|
}
|
|
3273
|
+
};
|
|
3274
|
+
void this.loadAndSetSessionName(createdSession, "claude", resumeProjectPath, claudeSessionId).then(finishResume, (err) => {
|
|
3275
|
+
console.error("[ws] Failed to load resumed session name:", err);
|
|
3276
|
+
finishResume();
|
|
3144
3277
|
});
|
|
3145
3278
|
this.debugEvents.set(sessionId, []);
|
|
3146
3279
|
this.recordDebugEvent(sessionId, {
|
|
@@ -3152,6 +3285,18 @@ export class BridgeWebSocketServer {
|
|
|
3152
3285
|
this.projectHistory?.addProject(resumeProjectPath);
|
|
3153
3286
|
})
|
|
3154
3287
|
.catch((err) => {
|
|
3288
|
+
const queuedInputs = pendingResumes.get(claudeSessionId) ?? [];
|
|
3289
|
+
pendingResumes.delete(claudeSessionId);
|
|
3290
|
+
for (const input of queuedInputs) {
|
|
3291
|
+
if (input.clientMessageId) {
|
|
3292
|
+
this.send(ws, {
|
|
3293
|
+
type: "input_rejected",
|
|
3294
|
+
sessionId: claudeSessionId,
|
|
3295
|
+
clientMessageId: input.clientMessageId,
|
|
3296
|
+
reason: "Session resume failed",
|
|
3297
|
+
});
|
|
3298
|
+
}
|
|
3299
|
+
}
|
|
3155
3300
|
this.send(ws, {
|
|
3156
3301
|
type: "error",
|
|
3157
3302
|
message: `Failed to load session history: ${err}`,
|
|
@@ -3389,8 +3534,16 @@ export class BridgeWebSocketServer {
|
|
|
3389
3534
|
}
|
|
3390
3535
|
void (async () => {
|
|
3391
3536
|
try {
|
|
3392
|
-
const
|
|
3393
|
-
|
|
3537
|
+
const result = await listProjectFilesAndDirectoriesForClient(msg.projectPath, {
|
|
3538
|
+
maxEntries: this.fileListMaxEntries,
|
|
3539
|
+
maxBytes: this.fileListMaxBytes,
|
|
3540
|
+
});
|
|
3541
|
+
this.send(ws, {
|
|
3542
|
+
type: "file_list",
|
|
3543
|
+
files: result.files,
|
|
3544
|
+
totalFiles: result.totalFiles,
|
|
3545
|
+
truncated: result.truncated,
|
|
3546
|
+
});
|
|
3394
3547
|
}
|
|
3395
3548
|
catch (err) {
|
|
3396
3549
|
const message = err instanceof Error ? err.message : String(err);
|
|
@@ -4061,6 +4214,7 @@ export class BridgeWebSocketServer {
|
|
|
4061
4214
|
else if (msg.mode === "conversation") {
|
|
4062
4215
|
// Conversation-only rewind: restart session at the target UUID
|
|
4063
4216
|
try {
|
|
4217
|
+
this.flushSessionDeltaBatches(msg.sessionId);
|
|
4064
4218
|
this.sessionManager.rewindConversation(msg.sessionId, msg.targetUuid, (newSessionId) => {
|
|
4065
4219
|
this.send(ws, {
|
|
4066
4220
|
type: "rewind_result",
|
|
@@ -4102,6 +4256,7 @@ export class BridgeWebSocketServer {
|
|
|
4102
4256
|
return;
|
|
4103
4257
|
}
|
|
4104
4258
|
try {
|
|
4259
|
+
this.flushSessionDeltaBatches(msg.sessionId);
|
|
4105
4260
|
this.sessionManager.rewindConversation(msg.sessionId, msg.targetUuid, (newSessionId) => {
|
|
4106
4261
|
this.send(ws, {
|
|
4107
4262
|
type: "rewind_result",
|
|
@@ -4450,6 +4605,10 @@ export class BridgeWebSocketServer {
|
|
|
4450
4605
|
}
|
|
4451
4606
|
}
|
|
4452
4607
|
}
|
|
4608
|
+
clearPendingClaudeResumeInputs(ws) {
|
|
4609
|
+
this.pendingClaudeResumeInputs.get(ws)?.clear();
|
|
4610
|
+
this.pendingClaudeResumeInputs.delete(ws);
|
|
4611
|
+
}
|
|
4453
4612
|
/**
|
|
4454
4613
|
* Load the saved session name from CLI storage and set it on the SessionInfo.
|
|
4455
4614
|
* Called after SessionManager.create() so that session_created carries the name.
|
|
@@ -4592,6 +4751,121 @@ export class BridgeWebSocketServer {
|
|
|
4592
4751
|
});
|
|
4593
4752
|
}
|
|
4594
4753
|
broadcastSessionMessage(sessionId, msg, exclude) {
|
|
4754
|
+
if (this.shouldBatchDelta(msg, exclude)) {
|
|
4755
|
+
this.trackSessionMessage(sessionId, msg);
|
|
4756
|
+
const chunks = this.splitDeltaText(msg.text);
|
|
4757
|
+
for (const client of this.wss.clients) {
|
|
4758
|
+
if (client.readyState !== WebSocket.OPEN)
|
|
4759
|
+
continue;
|
|
4760
|
+
if (!this.shouldSendToClient(client, msg))
|
|
4761
|
+
continue;
|
|
4762
|
+
this.queueDeltaForClient(client, sessionId, msg.type, chunks);
|
|
4763
|
+
}
|
|
4764
|
+
return;
|
|
4765
|
+
}
|
|
4766
|
+
this.flushSessionDeltaBatches(sessionId);
|
|
4767
|
+
this.trackSessionMessage(sessionId, msg);
|
|
4768
|
+
this.broadcastSessionMessageNow(sessionId, msg, exclude);
|
|
4769
|
+
}
|
|
4770
|
+
shouldBatchDelta(msg, exclude) {
|
|
4771
|
+
if (this.deltaBatchMs === 0 || exclude)
|
|
4772
|
+
return false;
|
|
4773
|
+
return msg.type === "stream_delta" || msg.type === "thinking_delta";
|
|
4774
|
+
}
|
|
4775
|
+
queueDeltaForClient(client, sessionId, type, chunks) {
|
|
4776
|
+
for (const chunk of chunks) {
|
|
4777
|
+
let batch = this.deltaBatches.get(client)?.get(sessionId);
|
|
4778
|
+
if (batch &&
|
|
4779
|
+
batch.charCount > 0 &&
|
|
4780
|
+
batch.charCount + chunk.charCount > this.deltaBatchMaxChars) {
|
|
4781
|
+
this.flushClientDeltaBatch(client, sessionId);
|
|
4782
|
+
batch = undefined;
|
|
4783
|
+
}
|
|
4784
|
+
if (!batch) {
|
|
4785
|
+
const clientBatches = this.deltaBatches.get(client) ?? new Map();
|
|
4786
|
+
batch = {
|
|
4787
|
+
messages: [],
|
|
4788
|
+
charCount: 0,
|
|
4789
|
+
timer: setTimeout(() => {
|
|
4790
|
+
this.flushClientDeltaBatch(client, sessionId);
|
|
4791
|
+
}, this.deltaBatchMs),
|
|
4792
|
+
};
|
|
4793
|
+
clientBatches.set(sessionId, batch);
|
|
4794
|
+
this.deltaBatches.set(client, clientBatches);
|
|
4795
|
+
}
|
|
4796
|
+
const last = batch.messages.at(-1);
|
|
4797
|
+
if (last?.type === type) {
|
|
4798
|
+
last.text += chunk.text;
|
|
4799
|
+
}
|
|
4800
|
+
else {
|
|
4801
|
+
batch.messages.push({ type, text: chunk.text });
|
|
4802
|
+
}
|
|
4803
|
+
batch.charCount += chunk.charCount;
|
|
4804
|
+
if (batch.charCount >= this.deltaBatchMaxChars) {
|
|
4805
|
+
this.flushClientDeltaBatch(client, sessionId);
|
|
4806
|
+
}
|
|
4807
|
+
}
|
|
4808
|
+
}
|
|
4809
|
+
splitDeltaText(text) {
|
|
4810
|
+
if (text.length === 0)
|
|
4811
|
+
return [{ text, charCount: 0 }];
|
|
4812
|
+
const chunks = [];
|
|
4813
|
+
let chars = [];
|
|
4814
|
+
let charCount = 0;
|
|
4815
|
+
for (const char of text) {
|
|
4816
|
+
chars.push(char);
|
|
4817
|
+
charCount += 1;
|
|
4818
|
+
if (charCount >= this.deltaBatchMaxChars) {
|
|
4819
|
+
chunks.push({ text: chars.join(""), charCount });
|
|
4820
|
+
chars = [];
|
|
4821
|
+
charCount = 0;
|
|
4822
|
+
}
|
|
4823
|
+
}
|
|
4824
|
+
if (chars.length > 0) {
|
|
4825
|
+
chunks.push({ text: chars.join(""), charCount });
|
|
4826
|
+
}
|
|
4827
|
+
return chunks;
|
|
4828
|
+
}
|
|
4829
|
+
flushSessionDeltaBatches(sessionId) {
|
|
4830
|
+
for (const client of Array.from(this.deltaBatches.keys())) {
|
|
4831
|
+
this.flushClientDeltaBatch(client, sessionId);
|
|
4832
|
+
}
|
|
4833
|
+
}
|
|
4834
|
+
flushAllDeltaBatches() {
|
|
4835
|
+
for (const [client, batches] of Array.from(this.deltaBatches.entries())) {
|
|
4836
|
+
for (const sessionId of Array.from(batches.keys())) {
|
|
4837
|
+
this.flushClientDeltaBatch(client, sessionId);
|
|
4838
|
+
}
|
|
4839
|
+
}
|
|
4840
|
+
}
|
|
4841
|
+
flushClientDeltaBatch(client, sessionId) {
|
|
4842
|
+
const clientBatches = this.deltaBatches.get(client);
|
|
4843
|
+
const batch = clientBatches?.get(sessionId);
|
|
4844
|
+
if (!batch)
|
|
4845
|
+
return;
|
|
4846
|
+
clearTimeout(batch.timer);
|
|
4847
|
+
clientBatches?.delete(sessionId);
|
|
4848
|
+
if (clientBatches?.size === 0)
|
|
4849
|
+
this.deltaBatches.delete(client);
|
|
4850
|
+
if (client.readyState !== WebSocket.OPEN)
|
|
4851
|
+
return;
|
|
4852
|
+
for (const msg of batch.messages) {
|
|
4853
|
+
client.send(JSON.stringify({ ...msg, sessionId }));
|
|
4854
|
+
}
|
|
4855
|
+
}
|
|
4856
|
+
discardClientDeltaBatches(client) {
|
|
4857
|
+
const batches = this.deltaBatches.get(client);
|
|
4858
|
+
if (!batches)
|
|
4859
|
+
return;
|
|
4860
|
+
for (const batch of batches.values())
|
|
4861
|
+
clearTimeout(batch.timer);
|
|
4862
|
+
this.deltaBatches.delete(client);
|
|
4863
|
+
}
|
|
4864
|
+
destroySession(sessionId) {
|
|
4865
|
+
this.flushSessionDeltaBatches(sessionId);
|
|
4866
|
+
this.sessionManager.destroy(sessionId);
|
|
4867
|
+
}
|
|
4868
|
+
trackSessionMessage(sessionId, msg) {
|
|
4595
4869
|
this.maybeSendPushNotification(sessionId, msg);
|
|
4596
4870
|
this.recordDebugEvent(sessionId, {
|
|
4597
4871
|
direction: "outgoing",
|
|
@@ -4614,7 +4888,8 @@ export class BridgeWebSocketServer {
|
|
|
4614
4888
|
});
|
|
4615
4889
|
}
|
|
4616
4890
|
}
|
|
4617
|
-
|
|
4891
|
+
}
|
|
4892
|
+
broadcastSessionMessageNow(sessionId, msg, exclude) {
|
|
4618
4893
|
const data = JSON.stringify({ ...msg, sessionId });
|
|
4619
4894
|
for (const client of this.wss.clients) {
|
|
4620
4895
|
if (client === exclude)
|
|
@@ -4691,28 +4966,58 @@ export class BridgeWebSocketServer {
|
|
|
4691
4966
|
});
|
|
4692
4967
|
}
|
|
4693
4968
|
}
|
|
4694
|
-
async
|
|
4695
|
-
if (this.
|
|
4696
|
-
|
|
4697
|
-
|
|
4698
|
-
.then((models) => {
|
|
4699
|
-
if (models.length > 0) {
|
|
4700
|
-
this.applyCodexModels(models);
|
|
4969
|
+
async refreshCodexMetadata(projectPath) {
|
|
4970
|
+
if (this.codexMetadataRequest) {
|
|
4971
|
+
if (projectPath) {
|
|
4972
|
+
return this.codexMetadataRequest.then(() => this.refreshCodexMetadata(projectPath));
|
|
4701
4973
|
}
|
|
4702
|
-
|
|
4703
|
-
|
|
4704
|
-
|
|
4705
|
-
this.broadcastSessionList();
|
|
4706
|
-
})
|
|
4974
|
+
return this.codexMetadataRequest;
|
|
4975
|
+
}
|
|
4976
|
+
this.codexMetadataRequest = this.loadAndApplyCodexMetadata(projectPath)
|
|
4707
4977
|
.catch((err) => {
|
|
4708
|
-
console.warn(`[ws] Failed to load Codex
|
|
4978
|
+
console.warn(`[ws] Failed to load Codex metadata: ${err}`);
|
|
4979
|
+
this.codexProfiles = [];
|
|
4980
|
+
this.defaultCodexProfile = undefined;
|
|
4709
4981
|
this.applyFallbackCodexModels();
|
|
4710
4982
|
this.broadcastSessionList();
|
|
4711
4983
|
})
|
|
4712
4984
|
.finally(() => {
|
|
4713
|
-
this.
|
|
4985
|
+
this.codexMetadataRequest = null;
|
|
4714
4986
|
});
|
|
4715
|
-
return this.
|
|
4987
|
+
return this.codexMetadataRequest;
|
|
4988
|
+
}
|
|
4989
|
+
async loadAndApplyCodexMetadata(projectPath) {
|
|
4990
|
+
const activeProcess = this.getActiveCodexProcess();
|
|
4991
|
+
const codexProcess = activeProcess ?? (await this.createStandaloneCodexProcess(projectPath));
|
|
4992
|
+
try {
|
|
4993
|
+
const [profileResult, modelResult] = await Promise.allSettled([
|
|
4994
|
+
codexProcess.readProfileConfig(projectPath),
|
|
4995
|
+
this.readCodexModels(codexProcess),
|
|
4996
|
+
]);
|
|
4997
|
+
if (profileResult.status === "fulfilled") {
|
|
4998
|
+
this.codexProfiles = profileResult.value.profiles;
|
|
4999
|
+
this.defaultCodexProfile = profileResult.value.defaultProfile;
|
|
5000
|
+
}
|
|
5001
|
+
else {
|
|
5002
|
+
console.warn(`[ws] Failed to load Codex profiles: ${profileResult.reason}`);
|
|
5003
|
+
this.codexProfiles = [];
|
|
5004
|
+
this.defaultCodexProfile = undefined;
|
|
5005
|
+
}
|
|
5006
|
+
if (modelResult.status === "fulfilled" && modelResult.value.length > 0) {
|
|
5007
|
+
this.applyCodexModels(modelResult.value);
|
|
5008
|
+
}
|
|
5009
|
+
else {
|
|
5010
|
+
if (modelResult.status === "rejected") {
|
|
5011
|
+
console.warn(`[ws] Failed to load Codex models: ${modelResult.reason}`);
|
|
5012
|
+
}
|
|
5013
|
+
this.applyFallbackCodexModels();
|
|
5014
|
+
}
|
|
5015
|
+
this.broadcastSessionList();
|
|
5016
|
+
}
|
|
5017
|
+
finally {
|
|
5018
|
+
if (!activeProcess)
|
|
5019
|
+
codexProcess.stop();
|
|
5020
|
+
}
|
|
4716
5021
|
}
|
|
4717
5022
|
async refreshClaudeModels(projectPath) {
|
|
4718
5023
|
if (this.claudeModelsRequest)
|
|
@@ -4756,28 +5061,18 @@ export class BridgeWebSocketServer {
|
|
|
4756
5061
|
this.claudeModels = FALLBACK_CLAUDE_MODELS;
|
|
4757
5062
|
this.claudeModelEfforts = { ...FALLBACK_CLAUDE_MODEL_EFFORTS };
|
|
4758
5063
|
}
|
|
4759
|
-
async
|
|
4760
|
-
const
|
|
4761
|
-
|
|
4762
|
-
|
|
4763
|
-
try {
|
|
4764
|
-
const modelSource = process;
|
|
4765
|
-
if (typeof modelSource.listAvailableModelMetadata === "function") {
|
|
4766
|
-
return await modelSource.listAvailableModelMetadata();
|
|
4767
|
-
}
|
|
4768
|
-
const models = typeof modelSource.listAvailableModels === "function"
|
|
4769
|
-
? await modelSource.listAvailableModels()
|
|
4770
|
-
: [];
|
|
4771
|
-
return models.map((model) => ({
|
|
4772
|
-
model,
|
|
4773
|
-
supportedReasoningEfforts: FALLBACK_CODEX_REASONING_EFFORTS,
|
|
4774
|
-
}));
|
|
4775
|
-
}
|
|
4776
|
-
finally {
|
|
4777
|
-
if (isStandalone) {
|
|
4778
|
-
process.stop();
|
|
4779
|
-
}
|
|
5064
|
+
async readCodexModels(codexProcess) {
|
|
5065
|
+
const modelSource = codexProcess;
|
|
5066
|
+
if (typeof modelSource.listAvailableModelMetadata === "function") {
|
|
5067
|
+
return modelSource.listAvailableModelMetadata();
|
|
4780
5068
|
}
|
|
5069
|
+
const models = typeof modelSource.listAvailableModels === "function"
|
|
5070
|
+
? await modelSource.listAvailableModels()
|
|
5071
|
+
: [];
|
|
5072
|
+
return models.map((model) => ({
|
|
5073
|
+
model,
|
|
5074
|
+
supportedReasoningEfforts: fallbackCodexReasoningEfforts(model),
|
|
5075
|
+
}));
|
|
4781
5076
|
}
|
|
4782
5077
|
applyCodexModels(models) {
|
|
4783
5078
|
this.codexModels = models.map((model) => model.model);
|
|
@@ -4785,35 +5080,16 @@ export class BridgeWebSocketServer {
|
|
|
4785
5080
|
model.model,
|
|
4786
5081
|
model.supportedReasoningEfforts.length > 0
|
|
4787
5082
|
? model.supportedReasoningEfforts
|
|
4788
|
-
:
|
|
5083
|
+
: fallbackCodexReasoningEfforts(model.model),
|
|
4789
5084
|
]));
|
|
4790
5085
|
}
|
|
4791
5086
|
applyFallbackCodexModels() {
|
|
4792
5087
|
this.codexModels = FALLBACK_CODEX_MODELS;
|
|
4793
5088
|
this.codexModelReasoningEfforts = Object.fromEntries(FALLBACK_CODEX_MODELS.map((model) => [
|
|
4794
5089
|
model,
|
|
4795
|
-
|
|
5090
|
+
fallbackCodexReasoningEfforts(model),
|
|
4796
5091
|
]));
|
|
4797
5092
|
}
|
|
4798
|
-
async refreshCodexProfiles(projectPath) {
|
|
4799
|
-
if (this.codexProfilesRequest)
|
|
4800
|
-
return this.codexProfilesRequest;
|
|
4801
|
-
this.codexProfilesRequest = this.loadCodexProfiles(projectPath)
|
|
4802
|
-
.then(({ profiles, defaultProfile }) => {
|
|
4803
|
-
this.codexProfiles = profiles;
|
|
4804
|
-
this.defaultCodexProfile = defaultProfile;
|
|
4805
|
-
this.broadcastSessionList();
|
|
4806
|
-
})
|
|
4807
|
-
.catch((err) => {
|
|
4808
|
-
console.warn(`[ws] Failed to load Codex profiles: ${err}`);
|
|
4809
|
-
this.codexProfiles = [];
|
|
4810
|
-
this.defaultCodexProfile = undefined;
|
|
4811
|
-
})
|
|
4812
|
-
.finally(() => {
|
|
4813
|
-
this.codexProfilesRequest = null;
|
|
4814
|
-
});
|
|
4815
|
-
return this.codexProfilesRequest;
|
|
4816
|
-
}
|
|
4817
5093
|
async loadCodexProfiles(projectPath) {
|
|
4818
5094
|
const process = this.getActiveCodexProcess() ??
|
|
4819
5095
|
(await this.createStandaloneCodexProcess(projectPath));
|
|
@@ -4925,8 +5201,14 @@ export class BridgeWebSocketServer {
|
|
|
4925
5201
|
}
|
|
4926
5202
|
async createStandaloneCodexProcess(projectPath) {
|
|
4927
5203
|
const proc = new CodexProcess();
|
|
4928
|
-
|
|
4929
|
-
|
|
5204
|
+
try {
|
|
5205
|
+
await proc.initializeOnly(projectPath ?? process.cwd());
|
|
5206
|
+
return proc;
|
|
5207
|
+
}
|
|
5208
|
+
catch (err) {
|
|
5209
|
+
proc.stop();
|
|
5210
|
+
throw err;
|
|
5211
|
+
}
|
|
4930
5212
|
}
|
|
4931
5213
|
/** Extract a short project label from the full projectPath (last directory name). */
|
|
4932
5214
|
projectLabel(sessionId) {
|