@ccpocket/bridge 1.80.1 → 1.81.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/dist/auto-rename.js +1 -0
- package/dist/auto-rename.js.map +1 -1
- package/dist/codex-process.d.ts +7 -0
- package/dist/codex-process.js +50 -5
- package/dist/codex-process.js.map +1 -1
- package/dist/index.js +12 -0
- package/dist/index.js.map +1 -1
- package/dist/parser.d.ts +36 -0
- package/dist/parser.js +45 -1
- package/dist/parser.js.map +1 -1
- package/dist/sdk-process.d.ts +2 -0
- package/dist/sdk-process.js +3 -0
- package/dist/sdk-process.js.map +1 -1
- package/dist/session.d.ts +6 -1
- package/dist/session.js +59 -8
- package/dist/session.js.map +1 -1
- package/dist/sessions-index.d.ts +6 -0
- package/dist/sessions-index.js +3 -0
- package/dist/sessions-index.js.map +1 -1
- package/dist/websocket.d.ts +19 -0
- package/dist/websocket.js +496 -23
- package/dist/websocket.js.map +1 -1
- package/dist/workspace-store.d.ts +47 -0
- package/dist/workspace-store.js +209 -0
- package/dist/workspace-store.js.map +1 -0
- package/package.json +1 -1
package/dist/websocket.js
CHANGED
|
@@ -7,7 +7,7 @@ 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 { codexErrorMessage, CodexRpcError, CodexProcess, } from "./codex-process.js";
|
|
10
|
+
import { codexErrorMessage, isCodexThreadWriterConflict, CodexRpcError, CodexProcess, } from "./codex-process.js";
|
|
11
11
|
import { stopManagedCodexAppServers } from "./codex-transport.js";
|
|
12
12
|
import { parseClientMessage, } from "./parser.js";
|
|
13
13
|
import { BRIDGE_PROTOCOL_MAX_VERSION, BRIDGE_PROTOCOL_MIN_VERSION, clientProtocolRange, negotiateProtocolVersion, } from "./protocol-version.js";
|
|
@@ -104,6 +104,7 @@ const OPT_IN_SERVER_MESSAGES = new Set([
|
|
|
104
104
|
"goal_state",
|
|
105
105
|
"guardian_approval",
|
|
106
106
|
"prompt_history_status",
|
|
107
|
+
"projects",
|
|
107
108
|
"push_registration_result",
|
|
108
109
|
]);
|
|
109
110
|
function isRecord(value) {
|
|
@@ -517,6 +518,8 @@ export class BridgeWebSocketServer {
|
|
|
517
518
|
uploadStore;
|
|
518
519
|
galleryStore;
|
|
519
520
|
projectHistory;
|
|
521
|
+
workspaceStore;
|
|
522
|
+
pendingSessionWorkspaces = new Map();
|
|
520
523
|
debugTraceStore;
|
|
521
524
|
recordingStore;
|
|
522
525
|
worktreeStore;
|
|
@@ -570,7 +573,7 @@ export class BridgeWebSocketServer {
|
|
|
570
573
|
pendingClaudeResumeInputs = new WeakMap();
|
|
571
574
|
resumeOperations = new Map();
|
|
572
575
|
constructor(options) {
|
|
573
|
-
const { server, apiKey, allowedDirs, imageStore, mediaStore, uploadStore, galleryStore, projectHistory, debugTraceStore, recordingStore, firebaseAuth, promptHistoryBackup, promptHistoryStore, platform, fileListMaxEntries, fileListMaxBytes, fileDownloadMaxBytes, fileUploadMaxBytes, deltaBatchMs, deltaBatchMaxChars, } = options;
|
|
576
|
+
const { server, apiKey, allowedDirs, imageStore, mediaStore, uploadStore, galleryStore, projectHistory, workspaceStore, debugTraceStore, recordingStore, firebaseAuth, promptHistoryBackup, promptHistoryStore, platform, fileListMaxEntries, fileListMaxBytes, fileDownloadMaxBytes, fileUploadMaxBytes, deltaBatchMs, deltaBatchMaxChars, } = options;
|
|
574
577
|
this.apiKey = apiKey ?? null;
|
|
575
578
|
this.allowedDirs = allowedDirs ?? [];
|
|
576
579
|
this.imageStore = imageStore ?? null;
|
|
@@ -578,6 +581,7 @@ export class BridgeWebSocketServer {
|
|
|
578
581
|
this.uploadStore = uploadStore ?? null;
|
|
579
582
|
this.galleryStore = galleryStore ?? null;
|
|
580
583
|
this.projectHistory = projectHistory ?? null;
|
|
584
|
+
this.workspaceStore = workspaceStore ?? null;
|
|
581
585
|
this.debugTraceStore = debugTraceStore ?? new DebugTraceStore();
|
|
582
586
|
this.recordingStore = recordingStore ?? null;
|
|
583
587
|
this.worktreeStore = new WorktreeStore();
|
|
@@ -619,7 +623,10 @@ export class BridgeWebSocketServer {
|
|
|
619
623
|
const info = this.galleryStore.metaToInfo(meta);
|
|
620
624
|
this.broadcast({ type: "gallery_new_image", image: info });
|
|
621
625
|
}
|
|
622
|
-
}, this.worktreeStore, () =>
|
|
626
|
+
}, this.worktreeStore, (sessionId) => {
|
|
627
|
+
void this.persistPendingSessionWorkspace(sessionId);
|
|
628
|
+
this.broadcastSessionList();
|
|
629
|
+
});
|
|
623
630
|
this.wss.on("connection", (ws, req) => {
|
|
624
631
|
// API key authentication
|
|
625
632
|
if (this.apiKey) {
|
|
@@ -1069,6 +1076,7 @@ export class BridgeWebSocketServer {
|
|
|
1069
1076
|
}
|
|
1070
1077
|
const projectPath = session.projectPath;
|
|
1071
1078
|
const codexSettings = session.codexSettings;
|
|
1079
|
+
const workspace = this.workspaceForRuntimeSession(session);
|
|
1072
1080
|
const worktreeOpts = session.worktreePath
|
|
1073
1081
|
? {
|
|
1074
1082
|
existingWorktreePath: session.worktreePath,
|
|
@@ -1086,6 +1094,7 @@ export class BridgeWebSocketServer {
|
|
|
1086
1094
|
...(codexSettings ?? {}),
|
|
1087
1095
|
threadId,
|
|
1088
1096
|
}));
|
|
1097
|
+
this.attachWorkspaceToRuntimeSession(newSessionId, workspace);
|
|
1089
1098
|
const newSession = this.sessionManager.get(newSessionId);
|
|
1090
1099
|
this.send(ws, {
|
|
1091
1100
|
type: "rewind_result",
|
|
@@ -1152,6 +1161,7 @@ export class BridgeWebSocketServer {
|
|
|
1152
1161
|
}
|
|
1153
1162
|
const projectPath = session.projectPath;
|
|
1154
1163
|
const codexSettings = session.codexSettings;
|
|
1164
|
+
const workspace = this.workspaceForRuntimeSession(session);
|
|
1155
1165
|
const worktreeOpts = session.worktreePath
|
|
1156
1166
|
? {
|
|
1157
1167
|
existingWorktreePath: session.worktreePath,
|
|
@@ -1174,6 +1184,7 @@ export class BridgeWebSocketServer {
|
|
|
1174
1184
|
...(codexSettings ?? {}),
|
|
1175
1185
|
threadId: forkedThreadId,
|
|
1176
1186
|
}));
|
|
1187
|
+
this.attachWorkspaceToRuntimeSession(newSessionId, workspace);
|
|
1177
1188
|
const newSession = this.sessionManager.get(newSessionId);
|
|
1178
1189
|
this.send(ws, this.buildSessionCreatedMessage({
|
|
1179
1190
|
sessionId: newSessionId,
|
|
@@ -1852,7 +1863,7 @@ export class BridgeWebSocketServer {
|
|
|
1852
1863
|
// Send session list and project history on connect
|
|
1853
1864
|
this.refreshConnectionMetadata();
|
|
1854
1865
|
this.sendSessionList(ws);
|
|
1855
|
-
const projects = this.
|
|
1866
|
+
const projects = this.legacyProjectHistoryProjects();
|
|
1856
1867
|
this.send(ws, { type: "project_history", projects });
|
|
1857
1868
|
ws.on("message", (data) => {
|
|
1858
1869
|
if (this.rejectedProtocolClients.has(ws))
|
|
@@ -1954,7 +1965,32 @@ export class BridgeWebSocketServer {
|
|
|
1954
1965
|
}
|
|
1955
1966
|
switch (msg.type) {
|
|
1956
1967
|
case "start": {
|
|
1957
|
-
|
|
1968
|
+
let projectPath = resolvePlatformPath(msg.projectPath, this.platform);
|
|
1969
|
+
let resolvedWorkspace;
|
|
1970
|
+
if (msg.projectId) {
|
|
1971
|
+
const project = this.workspaceStore?.getProject(msg.projectId);
|
|
1972
|
+
if (!project) {
|
|
1973
|
+
this.send(ws, {
|
|
1974
|
+
type: "error",
|
|
1975
|
+
requestId: msg.requestId,
|
|
1976
|
+
errorCode: "project_not_found",
|
|
1977
|
+
message: `Project not found: ${msg.projectId}`,
|
|
1978
|
+
});
|
|
1979
|
+
break;
|
|
1980
|
+
}
|
|
1981
|
+
const normalized = this.normalizeWorkspaceRoots(project.rootPaths);
|
|
1982
|
+
if (normalized.deniedRoot || !normalized.roots) {
|
|
1983
|
+
this.send(ws, this.buildPathNotAllowedError(normalized.deniedRoot ?? project.rootPaths[0]));
|
|
1984
|
+
break;
|
|
1985
|
+
}
|
|
1986
|
+
projectPath = normalized.roots[0];
|
|
1987
|
+
resolvedWorkspace = {
|
|
1988
|
+
kind: "project",
|
|
1989
|
+
projectId: project.id,
|
|
1990
|
+
projectName: project.name,
|
|
1991
|
+
rootPaths: normalized.roots,
|
|
1992
|
+
};
|
|
1993
|
+
}
|
|
1958
1994
|
if (!this.isPathAllowed(projectPath)) {
|
|
1959
1995
|
this.send(ws, {
|
|
1960
1996
|
...this.buildPathNotAllowedError(msg.projectPath),
|
|
@@ -2012,9 +2048,9 @@ export class BridgeWebSocketServer {
|
|
|
2012
2048
|
break;
|
|
2013
2049
|
}
|
|
2014
2050
|
}
|
|
2015
|
-
const
|
|
2016
|
-
|
|
2017
|
-
|
|
2051
|
+
const requestedAdditionalRoots = resolvedWorkspace?.rootPaths.slice(1) ??
|
|
2052
|
+
msg.additionalWritableRoots;
|
|
2053
|
+
const additionalWritableRoots = this.normalizeAdditionalWritableRoots(requestedAdditionalRoots, projectPath);
|
|
2018
2054
|
if (additionalWritableRoots.deniedRoot) {
|
|
2019
2055
|
this.send(ws, {
|
|
2020
2056
|
...this.buildPathNotAllowedError(additionalWritableRoots.deniedRoot),
|
|
@@ -2041,6 +2077,7 @@ export class BridgeWebSocketServer {
|
|
|
2041
2077
|
...(msg.sandboxMode
|
|
2042
2078
|
? { sandboxEnabled: msg.sandboxMode === "on" }
|
|
2043
2079
|
: {}),
|
|
2080
|
+
additionalDirectories: additionalWritableRoots.roots,
|
|
2044
2081
|
},
|
|
2045
2082
|
worktreeOptions: {
|
|
2046
2083
|
useWorktree: msg.useWorktree,
|
|
@@ -2086,6 +2123,10 @@ export class BridgeWebSocketServer {
|
|
|
2086
2123
|
usedFallback: false,
|
|
2087
2124
|
};
|
|
2088
2125
|
const createdSession = this.sessionManager.get(sessionId);
|
|
2126
|
+
if (resolvedWorkspace) {
|
|
2127
|
+
this.pendingSessionWorkspaces.set(sessionId, resolvedWorkspace);
|
|
2128
|
+
void this.persistPendingSessionWorkspace(sessionId);
|
|
2129
|
+
}
|
|
2089
2130
|
const cached = this.sessionManager.getCachedCommands(provider, createdSession?.worktreePath ?? projectPath);
|
|
2090
2131
|
// Load saved session name from CLI storage (for resumed sessions)
|
|
2091
2132
|
void this.loadAndSetSessionName(createdSession, provider, projectPath, msg.sessionId).then(() => {
|
|
@@ -2152,7 +2193,9 @@ export class BridgeWebSocketServer {
|
|
|
2152
2193
|
projectPath,
|
|
2153
2194
|
createdAt: new Date().toISOString(),
|
|
2154
2195
|
});
|
|
2155
|
-
|
|
2196
|
+
if (!resolvedWorkspace) {
|
|
2197
|
+
this.projectHistory?.addProject(projectPath);
|
|
2198
|
+
}
|
|
2156
2199
|
}
|
|
2157
2200
|
catch (err) {
|
|
2158
2201
|
console.error(`[ws] Failed to start session:`, err);
|
|
@@ -2805,6 +2848,7 @@ export class BridgeWebSocketServer {
|
|
|
2805
2848
|
const worktreePath = session.worktreePath;
|
|
2806
2849
|
const worktreeBranch = session.worktreeBranch;
|
|
2807
2850
|
const sessionName = session.name;
|
|
2851
|
+
const workspace = this.workspaceForRuntimeSession(session);
|
|
2808
2852
|
this.destroySession(oldSessionId);
|
|
2809
2853
|
console.log(`[ws] Permission mode change: destroyed session ${oldSessionId}`);
|
|
2810
2854
|
const hasUserMessages = session.history?.some((m) => m.type === "user_input" || m.type === "assistant") ||
|
|
@@ -2822,8 +2866,10 @@ export class BridgeWebSocketServer {
|
|
|
2822
2866
|
serviceTier: oldSettings.serviceTier,
|
|
2823
2867
|
networkAccessEnabled: oldSettings.networkAccessEnabled,
|
|
2824
2868
|
webSearchMode: oldSettings.webSearchMode,
|
|
2869
|
+
additionalWritableRoots: oldSettings.additionalWritableRoots,
|
|
2825
2870
|
collaborationMode: newCollaboration,
|
|
2826
2871
|
}));
|
|
2872
|
+
this.attachWorkspaceToRuntimeSession(newId, workspace);
|
|
2827
2873
|
const newSession = this.sessionManager.get(newId);
|
|
2828
2874
|
if (newSession && sessionName)
|
|
2829
2875
|
newSession.name = sessionName;
|
|
@@ -2883,8 +2929,10 @@ export class BridgeWebSocketServer {
|
|
|
2883
2929
|
serviceTier: oldSettings.serviceTier,
|
|
2884
2930
|
networkAccessEnabled: oldSettings.networkAccessEnabled,
|
|
2885
2931
|
webSearchMode: oldSettings.webSearchMode,
|
|
2932
|
+
additionalWritableRoots: oldSettings.additionalWritableRoots,
|
|
2886
2933
|
collaborationMode: newCollaboration,
|
|
2887
2934
|
}));
|
|
2935
|
+
this.attachWorkspaceToRuntimeSession(newId, workspace);
|
|
2888
2936
|
const newSession = this.sessionManager.get(newId);
|
|
2889
2937
|
if (newSession && sessionName) {
|
|
2890
2938
|
newSession.name = sessionName;
|
|
@@ -3174,6 +3222,7 @@ export class BridgeWebSocketServer {
|
|
|
3174
3222
|
const sessionName = session.name;
|
|
3175
3223
|
const permissionMode = session.process.permissionMode;
|
|
3176
3224
|
const model = session.process.model;
|
|
3225
|
+
const workspace = this.workspaceForRuntimeSession(session);
|
|
3177
3226
|
this.destroySession(oldSessionId);
|
|
3178
3227
|
console.log(`[ws] Claude sandbox change: destroyed session ${oldSessionId}`);
|
|
3179
3228
|
const newId = this.sessionManager.create(projectPath, {
|
|
@@ -3181,9 +3230,11 @@ export class BridgeWebSocketServer {
|
|
|
3181
3230
|
permissionMode,
|
|
3182
3231
|
model,
|
|
3183
3232
|
sandboxEnabled: newEnabled,
|
|
3233
|
+
additionalDirectories: workspace?.rootPaths.slice(1),
|
|
3184
3234
|
}, undefined, worktreePath
|
|
3185
3235
|
? { existingWorktreePath: worktreePath, worktreeBranch }
|
|
3186
3236
|
: undefined, "claude");
|
|
3237
|
+
this.attachWorkspaceToRuntimeSession(newId, workspace);
|
|
3187
3238
|
const newSession = this.sessionManager.get(newId);
|
|
3188
3239
|
if (newSession && sessionName)
|
|
3189
3240
|
newSession.name = sessionName;
|
|
@@ -3227,6 +3278,7 @@ export class BridgeWebSocketServer {
|
|
|
3227
3278
|
const sessionName = session.name;
|
|
3228
3279
|
const collaborationMode = session.process
|
|
3229
3280
|
.collaborationMode;
|
|
3281
|
+
const workspace = this.workspaceForRuntimeSession(session);
|
|
3230
3282
|
const executionMode = oldSettings.approvalPolicy === "never" ? "fullAccess" : "default";
|
|
3231
3283
|
const planMode = collaborationMode === "plan";
|
|
3232
3284
|
const legacyPermissionMode = modesToLegacyPermissionMode("codex", executionMode, planMode);
|
|
@@ -3255,8 +3307,10 @@ export class BridgeWebSocketServer {
|
|
|
3255
3307
|
serviceTier: oldSettings.serviceTier,
|
|
3256
3308
|
networkAccessEnabled: oldSettings.networkAccessEnabled,
|
|
3257
3309
|
webSearchMode: oldSettings.webSearchMode,
|
|
3310
|
+
additionalWritableRoots: oldSettings.additionalWritableRoots,
|
|
3258
3311
|
collaborationMode,
|
|
3259
3312
|
}));
|
|
3313
|
+
this.attachWorkspaceToRuntimeSession(newId, workspace);
|
|
3260
3314
|
const newSession = this.sessionManager.get(newId);
|
|
3261
3315
|
if (newSession && sessionName)
|
|
3262
3316
|
newSession.name = sessionName;
|
|
@@ -3309,8 +3363,10 @@ export class BridgeWebSocketServer {
|
|
|
3309
3363
|
serviceTier: oldSettings.serviceTier,
|
|
3310
3364
|
networkAccessEnabled: oldSettings.networkAccessEnabled,
|
|
3311
3365
|
webSearchMode: oldSettings.webSearchMode,
|
|
3366
|
+
additionalWritableRoots: oldSettings.additionalWritableRoots,
|
|
3312
3367
|
collaborationMode,
|
|
3313
3368
|
}));
|
|
3369
|
+
this.attachWorkspaceToRuntimeSession(newId, workspace);
|
|
3314
3370
|
// Restore session name
|
|
3315
3371
|
const newSession = this.sessionManager.get(newId);
|
|
3316
3372
|
if (newSession && sessionName) {
|
|
@@ -3377,6 +3433,7 @@ export class BridgeWebSocketServer {
|
|
|
3377
3433
|
const permissionMode = sdkProc.permissionMode;
|
|
3378
3434
|
const worktreePath = session.worktreePath;
|
|
3379
3435
|
const worktreeBranch = session.worktreeBranch;
|
|
3436
|
+
const workspace = this.workspaceForRuntimeSession(session);
|
|
3380
3437
|
this.destroySession(sessionId);
|
|
3381
3438
|
console.log(`[ws] Clear context: destroyed session ${sessionId}`);
|
|
3382
3439
|
const newId = this.sessionManager.create(projectPath, {
|
|
@@ -3388,9 +3445,11 @@ export class BridgeWebSocketServer {
|
|
|
3388
3445
|
: {}),
|
|
3389
3446
|
permissionMode,
|
|
3390
3447
|
initialInput: planText || undefined,
|
|
3448
|
+
additionalDirectories: workspace?.rootPaths.slice(1),
|
|
3391
3449
|
}, undefined, worktreePath
|
|
3392
3450
|
? { existingWorktreePath: worktreePath, worktreeBranch }
|
|
3393
3451
|
: undefined);
|
|
3452
|
+
this.attachWorkspaceToRuntimeSession(newId, workspace);
|
|
3394
3453
|
console.log(`[ws] Clear context: created new session ${newId} (CLI session: ${claudeSessionId ?? "new"})`);
|
|
3395
3454
|
// Notify all clients. Broadcast is used so reconnecting clients also receive it.
|
|
3396
3455
|
const newSession = this.sessionManager.get(newId);
|
|
@@ -3576,7 +3635,7 @@ export class BridgeWebSocketServer {
|
|
|
3576
3635
|
this.send(ws, {
|
|
3577
3636
|
type: "session_context",
|
|
3578
3637
|
sessionId: msg.sessionId,
|
|
3579
|
-
context: { ...context },
|
|
3638
|
+
context: { ...this.runtimeSessionWithWorkspace(context) },
|
|
3580
3639
|
});
|
|
3581
3640
|
}
|
|
3582
3641
|
else {
|
|
@@ -3830,6 +3889,8 @@ export class BridgeWebSocketServer {
|
|
|
3830
3889
|
limit: msg.limit,
|
|
3831
3890
|
offset: msg.offset,
|
|
3832
3891
|
projectPath: msg.projectPath,
|
|
3892
|
+
projectId: msg.projectId,
|
|
3893
|
+
workspaceKind: msg.workspaceKind,
|
|
3833
3894
|
requestScope: msg.requestScope,
|
|
3834
3895
|
requestId: msg.requestId,
|
|
3835
3896
|
});
|
|
@@ -3844,6 +3905,8 @@ export class BridgeWebSocketServer {
|
|
|
3844
3905
|
message: `Failed to list recent sessions: ${err}`,
|
|
3845
3906
|
errorCode: "recent_sessions_failed",
|
|
3846
3907
|
path: msg.projectPath,
|
|
3908
|
+
projectId: msg.projectId,
|
|
3909
|
+
workspaceKind: msg.workspaceKind,
|
|
3847
3910
|
requestId: msg.requestId,
|
|
3848
3911
|
requestScope: msg.requestScope,
|
|
3849
3912
|
offset: msg.offset,
|
|
@@ -3904,9 +3967,41 @@ export class BridgeWebSocketServer {
|
|
|
3904
3967
|
}
|
|
3905
3968
|
case "resume_session": {
|
|
3906
3969
|
const resumeStartedAt = Date.now();
|
|
3907
|
-
console.log(`[ws] resume_session: sessionId=${msg.sessionId} projectPath=${msg.projectPath} provider=${msg.provider ?? "claude"}`);
|
|
3908
|
-
const resumeProjectPath = resolvePlatformPath(msg.projectPath, this.platform);
|
|
3909
3970
|
const provider = msg.provider ?? "claude";
|
|
3971
|
+
const storedAssignment = this.workspaceStore?.getAssignment(provider, msg.sessionId);
|
|
3972
|
+
const requestedProject = !storedAssignment && msg.projectId
|
|
3973
|
+
? this.workspaceStore?.getProject(msg.projectId)
|
|
3974
|
+
: undefined;
|
|
3975
|
+
const assignedProject = storedAssignment?.projectId
|
|
3976
|
+
? this.workspaceStore?.getProject(storedAssignment.projectId)
|
|
3977
|
+
: undefined;
|
|
3978
|
+
const resumeRoots = storedAssignment?.rootPaths ?? requestedProject?.rootPaths;
|
|
3979
|
+
console.log(`[ws] resume_session: sessionId=${msg.sessionId} projectPath=${msg.projectPath} provider=${msg.provider ?? "claude"}`);
|
|
3980
|
+
// A stored assignment supplies the root snapshot and secondary roots,
|
|
3981
|
+
// while the recent-session path may be a worktree cwd that must win.
|
|
3982
|
+
const resumeProjectPath = resolvePlatformPath(storedAssignment
|
|
3983
|
+
? msg.projectPath
|
|
3984
|
+
: (resumeRoots?.[0] ?? msg.projectPath), this.platform);
|
|
3985
|
+
const resumeAdditionalRoots = resumeRoots?.slice(1) ?? msg.additionalWritableRoots;
|
|
3986
|
+
const resolvedResumeWorkspace = storedAssignment
|
|
3987
|
+
? {
|
|
3988
|
+
kind: storedAssignment.kind,
|
|
3989
|
+
...(storedAssignment.projectId
|
|
3990
|
+
? { projectId: storedAssignment.projectId }
|
|
3991
|
+
: {}),
|
|
3992
|
+
...(assignedProject
|
|
3993
|
+
? { projectName: assignedProject.name }
|
|
3994
|
+
: {}),
|
|
3995
|
+
rootPaths: storedAssignment.rootPaths,
|
|
3996
|
+
}
|
|
3997
|
+
: requestedProject
|
|
3998
|
+
? {
|
|
3999
|
+
kind: "project",
|
|
4000
|
+
projectId: requestedProject.id,
|
|
4001
|
+
projectName: requestedProject.name,
|
|
4002
|
+
rootPaths: requestedProject.rootPaths,
|
|
4003
|
+
}
|
|
4004
|
+
: undefined;
|
|
3910
4005
|
if (!this.isPathAllowed(resumeProjectPath)) {
|
|
3911
4006
|
this.sendResumeFailed(ws, {
|
|
3912
4007
|
provider,
|
|
@@ -3917,6 +4012,13 @@ export class BridgeWebSocketServer {
|
|
|
3917
4012
|
this.send(ws, this.buildPathNotAllowedError(msg.projectPath));
|
|
3918
4013
|
break;
|
|
3919
4014
|
}
|
|
4015
|
+
if (resolvedResumeWorkspace &&
|
|
4016
|
+
!storedAssignment &&
|
|
4017
|
+
this.workspaceStore) {
|
|
4018
|
+
void this.workspaceStore.assignSession(provider, msg.sessionId, resolvedResumeWorkspace).catch((error) => {
|
|
4019
|
+
console.error("[workspace] Failed to persist resume assignment:", error);
|
|
4020
|
+
});
|
|
4021
|
+
}
|
|
3920
4022
|
const normalizedCodexPermissionsMode = provider === "codex"
|
|
3921
4023
|
? normalizeCodexPermissionsMode(msg.codexPermissionsMode)
|
|
3922
4024
|
: undefined;
|
|
@@ -3960,7 +4062,7 @@ export class BridgeWebSocketServer {
|
|
|
3960
4062
|
const effectiveProfile = msg.profile
|
|
3961
4063
|
? await this.resolveCodexResumeProfile(msg.profile, sessionRefId, effectiveProjectPath)
|
|
3962
4064
|
: undefined;
|
|
3963
|
-
const additionalWritableRoots = this.normalizeAdditionalWritableRoots(
|
|
4065
|
+
const additionalWritableRoots = this.normalizeAdditionalWritableRoots(resumeAdditionalRoots, effectiveProjectPath);
|
|
3964
4066
|
if (additionalWritableRoots.deniedRoot) {
|
|
3965
4067
|
this.sendResumeFailed(ws, {
|
|
3966
4068
|
provider,
|
|
@@ -4031,9 +4133,18 @@ export class BridgeWebSocketServer {
|
|
|
4031
4133
|
collaborationMode: planMode
|
|
4032
4134
|
? "plan"
|
|
4033
4135
|
: "default",
|
|
4034
|
-
}));
|
|
4136
|
+
}), { deferProcessMessages: true });
|
|
4137
|
+
if (!this.attachProvisionalResumeSession(resumeOperation.key, resumeOperation.operationId, sessionId)) {
|
|
4138
|
+
this.sessionManager.destroy(sessionId);
|
|
4139
|
+
break;
|
|
4140
|
+
}
|
|
4035
4141
|
sessionCreateMs = Date.now() - createStartedAt;
|
|
4036
4142
|
const createdSession = this.sessionManager.get(sessionId);
|
|
4143
|
+
const waitUntilReady = createdSession?.process?.waitUntilReady;
|
|
4144
|
+
if (!waitUntilReady) {
|
|
4145
|
+
throw new Error("Codex session readiness is unavailable");
|
|
4146
|
+
}
|
|
4147
|
+
await waitUntilReady.call(createdSession?.process);
|
|
4037
4148
|
if (createdSession) {
|
|
4038
4149
|
// get_history immediately follows session_created on the app.
|
|
4039
4150
|
// Reuse the canonical history loaded above instead of issuing a
|
|
@@ -4080,6 +4191,7 @@ export class BridgeWebSocketServer {
|
|
|
4080
4191
|
this.sessionManager.destroy(sessionId);
|
|
4081
4192
|
break;
|
|
4082
4193
|
}
|
|
4194
|
+
this.sessionManager.releaseDeferredProcessMessages(sessionId);
|
|
4083
4195
|
this.broadcastSessionList();
|
|
4084
4196
|
this.debugEvents.set(sessionId, []);
|
|
4085
4197
|
this.recordDebugEvent(sessionId, {
|
|
@@ -4088,7 +4200,9 @@ export class BridgeWebSocketServer {
|
|
|
4088
4200
|
type: "session_resumed",
|
|
4089
4201
|
detail: `provider=codex thread=${sessionRefId}`,
|
|
4090
4202
|
});
|
|
4091
|
-
|
|
4203
|
+
if (!resolvedResumeWorkspace) {
|
|
4204
|
+
this.projectHistory?.addProject(effectiveProjectPath);
|
|
4205
|
+
}
|
|
4092
4206
|
console.info(formatResumePerformanceLog({
|
|
4093
4207
|
provider: "codex",
|
|
4094
4208
|
sourceSessionId: sessionRefId,
|
|
@@ -4114,11 +4228,29 @@ export class BridgeWebSocketServer {
|
|
|
4114
4228
|
nameLoadMs,
|
|
4115
4229
|
totalMs: Date.now() - resumeStartedAt,
|
|
4116
4230
|
}));
|
|
4117
|
-
|
|
4231
|
+
const activeWriterConflict = isCodexThreadWriterConflict(err);
|
|
4232
|
+
this.failResumeOperation(resumeOperation.key, resumeOperation.operationId, activeWriterConflict
|
|
4233
|
+
? codexErrorMessage(err)
|
|
4234
|
+
: historyLoaded
|
|
4235
|
+
? `Failed to restore Codex session: ${err}`
|
|
4236
|
+
: `Failed to load Codex session history: ${err}`, activeWriterConflict
|
|
4237
|
+
? "codex_thread_writer_conflict"
|
|
4238
|
+
: undefined);
|
|
4118
4239
|
}
|
|
4119
4240
|
break;
|
|
4120
4241
|
}
|
|
4121
4242
|
const claudeSessionId = sessionRefId;
|
|
4243
|
+
const additionalDirectories = this.normalizeAdditionalWritableRoots(resumeAdditionalRoots, resumeProjectPath);
|
|
4244
|
+
if (additionalDirectories.deniedRoot) {
|
|
4245
|
+
this.sendResumeFailed(ws, {
|
|
4246
|
+
provider,
|
|
4247
|
+
sourceSessionId: sessionRefId,
|
|
4248
|
+
projectPath: resumeProjectPath,
|
|
4249
|
+
resumeRequestId: msg.resumeRequestId,
|
|
4250
|
+
});
|
|
4251
|
+
this.send(ws, this.buildPathNotAllowedError(additionalDirectories.deniedRoot));
|
|
4252
|
+
break;
|
|
4253
|
+
}
|
|
4122
4254
|
// Look up worktree mapping for this Claude session
|
|
4123
4255
|
const wtMapping = this.worktreeStore.get(claudeSessionId);
|
|
4124
4256
|
let worktreeOpts;
|
|
@@ -4173,6 +4305,7 @@ export class BridgeWebSocketServer {
|
|
|
4173
4305
|
...(msg.sandboxMode
|
|
4174
4306
|
? { sandboxEnabled: msg.sandboxMode === "on" }
|
|
4175
4307
|
: {}),
|
|
4308
|
+
additionalDirectories: additionalDirectories.roots,
|
|
4176
4309
|
},
|
|
4177
4310
|
pastMessages,
|
|
4178
4311
|
worktreeOptions: worktreeOpts,
|
|
@@ -4243,7 +4376,9 @@ export class BridgeWebSocketServer {
|
|
|
4243
4376
|
type: "session_resumed",
|
|
4244
4377
|
detail: `provider=claude session=${claudeSessionId}`,
|
|
4245
4378
|
});
|
|
4246
|
-
|
|
4379
|
+
if (!resolvedResumeWorkspace) {
|
|
4380
|
+
this.projectHistory?.addProject(resumeProjectPath);
|
|
4381
|
+
}
|
|
4247
4382
|
})
|
|
4248
4383
|
.catch((err) => {
|
|
4249
4384
|
if (!historyLoaded) {
|
|
@@ -4330,16 +4465,104 @@ export class BridgeWebSocketServer {
|
|
|
4330
4465
|
break;
|
|
4331
4466
|
}
|
|
4332
4467
|
case "list_project_history": {
|
|
4333
|
-
const projects = this.
|
|
4468
|
+
const projects = this.legacyProjectHistoryProjects();
|
|
4334
4469
|
this.send(ws, { type: "project_history", projects });
|
|
4335
4470
|
break;
|
|
4336
4471
|
}
|
|
4337
4472
|
case "remove_project_history": {
|
|
4338
4473
|
this.projectHistory?.removeProject(msg.projectPath);
|
|
4339
|
-
const projects = this.
|
|
4474
|
+
const projects = this.legacyProjectHistoryProjects();
|
|
4340
4475
|
this.send(ws, { type: "project_history", projects });
|
|
4341
4476
|
break;
|
|
4342
4477
|
}
|
|
4478
|
+
case "list_projects": {
|
|
4479
|
+
this.send(ws, this.projectsMessage(msg.requestId));
|
|
4480
|
+
break;
|
|
4481
|
+
}
|
|
4482
|
+
case "create_project": {
|
|
4483
|
+
if (!this.workspaceStore) {
|
|
4484
|
+
this.send(ws, {
|
|
4485
|
+
type: "error",
|
|
4486
|
+
requestId: msg.requestId,
|
|
4487
|
+
errorCode: "projects_unavailable",
|
|
4488
|
+
message: "Project storage is unavailable",
|
|
4489
|
+
});
|
|
4490
|
+
break;
|
|
4491
|
+
}
|
|
4492
|
+
const normalized = this.normalizeWorkspaceRoots(msg.rootPaths);
|
|
4493
|
+
if (normalized.deniedRoot || !normalized.roots) {
|
|
4494
|
+
this.send(ws, this.buildPathNotAllowedError(normalized.deniedRoot ?? msg.rootPaths[0]));
|
|
4495
|
+
break;
|
|
4496
|
+
}
|
|
4497
|
+
try {
|
|
4498
|
+
await this.workspaceStore.createProject(msg.name, normalized.roots);
|
|
4499
|
+
this.broadcast(this.projectsMessage(msg.requestId));
|
|
4500
|
+
}
|
|
4501
|
+
catch (error) {
|
|
4502
|
+
this.sendWorkspaceMutationError(ws, msg.requestId, "create Project", error);
|
|
4503
|
+
}
|
|
4504
|
+
break;
|
|
4505
|
+
}
|
|
4506
|
+
case "update_project": {
|
|
4507
|
+
if (!this.workspaceStore) {
|
|
4508
|
+
this.send(ws, {
|
|
4509
|
+
type: "error",
|
|
4510
|
+
requestId: msg.requestId,
|
|
4511
|
+
errorCode: "projects_unavailable",
|
|
4512
|
+
message: "Project storage is unavailable",
|
|
4513
|
+
});
|
|
4514
|
+
break;
|
|
4515
|
+
}
|
|
4516
|
+
const normalized = this.normalizeWorkspaceRoots(msg.rootPaths);
|
|
4517
|
+
if (normalized.deniedRoot || !normalized.roots) {
|
|
4518
|
+
this.send(ws, this.buildPathNotAllowedError(normalized.deniedRoot ?? msg.rootPaths[0]));
|
|
4519
|
+
break;
|
|
4520
|
+
}
|
|
4521
|
+
let updated;
|
|
4522
|
+
try {
|
|
4523
|
+
updated = await this.workspaceStore.updateProject(msg.projectId, msg.name, normalized.roots);
|
|
4524
|
+
}
|
|
4525
|
+
catch (error) {
|
|
4526
|
+
this.sendWorkspaceMutationError(ws, msg.requestId, "update Project", error);
|
|
4527
|
+
break;
|
|
4528
|
+
}
|
|
4529
|
+
if (!updated) {
|
|
4530
|
+
this.send(ws, {
|
|
4531
|
+
type: "error",
|
|
4532
|
+
requestId: msg.requestId,
|
|
4533
|
+
errorCode: "project_not_found",
|
|
4534
|
+
message: `Project not found: ${msg.projectId}`,
|
|
4535
|
+
});
|
|
4536
|
+
break;
|
|
4537
|
+
}
|
|
4538
|
+
this.broadcast(this.projectsMessage(msg.requestId));
|
|
4539
|
+
break;
|
|
4540
|
+
}
|
|
4541
|
+
case "remove_project": {
|
|
4542
|
+
if (!this.workspaceStore) {
|
|
4543
|
+
this.sendWorkspaceMutationError(ws, msg.requestId, "remove Project", new Error("Project storage is unavailable"));
|
|
4544
|
+
break;
|
|
4545
|
+
}
|
|
4546
|
+
let removed;
|
|
4547
|
+
try {
|
|
4548
|
+
removed = await this.workspaceStore.removeProject(msg.projectId);
|
|
4549
|
+
}
|
|
4550
|
+
catch (error) {
|
|
4551
|
+
this.sendWorkspaceMutationError(ws, msg.requestId, "remove Project", error);
|
|
4552
|
+
break;
|
|
4553
|
+
}
|
|
4554
|
+
if (!removed) {
|
|
4555
|
+
this.send(ws, {
|
|
4556
|
+
type: "error",
|
|
4557
|
+
requestId: msg.requestId,
|
|
4558
|
+
errorCode: "project_not_found",
|
|
4559
|
+
message: `Project not found: ${msg.projectId}`,
|
|
4560
|
+
});
|
|
4561
|
+
break;
|
|
4562
|
+
}
|
|
4563
|
+
this.broadcast(this.projectsMessage(msg.requestId));
|
|
4564
|
+
break;
|
|
4565
|
+
}
|
|
4343
4566
|
case "list_directory": {
|
|
4344
4567
|
try {
|
|
4345
4568
|
const listing = await listAllowedDirectories(msg.path, this.allowedDirs, this.platform, msg.includeHidden ?? false);
|
|
@@ -6007,6 +6230,7 @@ export class BridgeWebSocketServer {
|
|
|
6007
6230
|
return false;
|
|
6008
6231
|
if (operation.timeout)
|
|
6009
6232
|
clearTimeout(operation.timeout);
|
|
6233
|
+
operation.provisionalSessionId = undefined;
|
|
6010
6234
|
operation.completed = {
|
|
6011
6235
|
sessionId,
|
|
6012
6236
|
message,
|
|
@@ -6025,16 +6249,36 @@ export class BridgeWebSocketServer {
|
|
|
6025
6249
|
this.pruneCompletedResumeOperations();
|
|
6026
6250
|
return true;
|
|
6027
6251
|
}
|
|
6028
|
-
failResumeOperation(key, operationId, message) {
|
|
6252
|
+
failResumeOperation(key, operationId, message, errorCode) {
|
|
6029
6253
|
const operation = this.resumeOperations.get(key);
|
|
6030
6254
|
if (!operation || operation.id !== operationId)
|
|
6031
6255
|
return;
|
|
6256
|
+
if (operation.provisionalSessionId) {
|
|
6257
|
+
this.sessionManager.destroy(operation.provisionalSessionId);
|
|
6258
|
+
operation.provisionalSessionId = undefined;
|
|
6259
|
+
}
|
|
6032
6260
|
this.clearResumeOperation(key, operation);
|
|
6033
6261
|
for (const waiter of operation.waiters) {
|
|
6034
6262
|
this.rejectPendingClaudeResumeInputs(waiter, operation.sourceSessionId);
|
|
6035
6263
|
this.sendResumeFailed(waiter, operation);
|
|
6036
|
-
this.send(waiter, {
|
|
6264
|
+
this.send(waiter, {
|
|
6265
|
+
type: "error",
|
|
6266
|
+
message,
|
|
6267
|
+
sessionId: operation.sourceSessionId,
|
|
6268
|
+
...(operation.resumeRequestId
|
|
6269
|
+
? { requestId: operation.resumeRequestId }
|
|
6270
|
+
: {}),
|
|
6271
|
+
...(errorCode ? { errorCode } : {}),
|
|
6272
|
+
});
|
|
6273
|
+
}
|
|
6274
|
+
}
|
|
6275
|
+
attachProvisionalResumeSession(key, operationId, sessionId) {
|
|
6276
|
+
const operation = this.resumeOperations.get(key);
|
|
6277
|
+
if (!operation || operation.id !== operationId || operation.completed) {
|
|
6278
|
+
return false;
|
|
6037
6279
|
}
|
|
6280
|
+
operation.provisionalSessionId = sessionId;
|
|
6281
|
+
return true;
|
|
6038
6282
|
}
|
|
6039
6283
|
sendResumeFailed(ws, resume) {
|
|
6040
6284
|
this.send(ws, {
|
|
@@ -6166,7 +6410,7 @@ export class BridgeWebSocketServer {
|
|
|
6166
6410
|
}
|
|
6167
6411
|
sendSessionList(ws) {
|
|
6168
6412
|
this.pruneDebugEvents();
|
|
6169
|
-
const sessions = this.
|
|
6413
|
+
const sessions = this.runtimeSessionsWithWorkspaces();
|
|
6170
6414
|
this.send(ws, {
|
|
6171
6415
|
type: "session_list",
|
|
6172
6416
|
sessions,
|
|
@@ -6204,7 +6448,7 @@ export class BridgeWebSocketServer {
|
|
|
6204
6448
|
/** Broadcast session list to all connected clients. */
|
|
6205
6449
|
broadcastSessionList() {
|
|
6206
6450
|
this.pruneDebugEvents();
|
|
6207
|
-
const sessions = this.
|
|
6451
|
+
const sessions = this.runtimeSessionsWithWorkspaces();
|
|
6208
6452
|
this.broadcast({
|
|
6209
6453
|
type: "session_list",
|
|
6210
6454
|
sessions,
|
|
@@ -6226,6 +6470,18 @@ export class BridgeWebSocketServer {
|
|
|
6226
6470
|
],
|
|
6227
6471
|
});
|
|
6228
6472
|
}
|
|
6473
|
+
runtimeSessionsWithWorkspaces() {
|
|
6474
|
+
return this.sessionManager
|
|
6475
|
+
.list()
|
|
6476
|
+
.map((summary) => this.runtimeSessionWithWorkspace(summary));
|
|
6477
|
+
}
|
|
6478
|
+
runtimeSessionWithWorkspace(summary) {
|
|
6479
|
+
const session = this.sessionManager.get(summary.id);
|
|
6480
|
+
const workspace = session
|
|
6481
|
+
? this.workspaceForRuntimeSession(session)
|
|
6482
|
+
: undefined;
|
|
6483
|
+
return workspace ? { ...summary, workspace } : summary;
|
|
6484
|
+
}
|
|
6229
6485
|
broadcastPromptHistoryStatus() {
|
|
6230
6486
|
if (!this.promptHistoryStore)
|
|
6231
6487
|
return;
|
|
@@ -6352,6 +6608,7 @@ export class BridgeWebSocketServer {
|
|
|
6352
6608
|
}
|
|
6353
6609
|
destroySession(sessionId) {
|
|
6354
6610
|
this.flushSessionDeltaBatches(sessionId);
|
|
6611
|
+
this.pendingSessionWorkspaces.delete(sessionId);
|
|
6355
6612
|
this.sessionManager.destroy(sessionId);
|
|
6356
6613
|
}
|
|
6357
6614
|
trackSessionMessage(sessionId, msg) {
|
|
@@ -6395,6 +6652,128 @@ export class BridgeWebSocketServer {
|
|
|
6395
6652
|
}
|
|
6396
6653
|
}
|
|
6397
6654
|
async listRecentSessions(msg) {
|
|
6655
|
+
const primaryProjectIds = new Set(msg.projectPath === undefined
|
|
6656
|
+
? []
|
|
6657
|
+
: (this.workspaceStore?.listProjects() ?? [])
|
|
6658
|
+
.filter((project) => project.rootPaths[0] === msg.projectPath)
|
|
6659
|
+
.map((project) => project.id));
|
|
6660
|
+
const workspaceFilterRequested = msg.projectId !== undefined ||
|
|
6661
|
+
msg.workspaceKind !== undefined ||
|
|
6662
|
+
primaryProjectIds.size > 0;
|
|
6663
|
+
if (!workspaceFilterRequested) {
|
|
6664
|
+
const rawResult = await this.listRecentSessionsRaw(msg);
|
|
6665
|
+
const enriched = rawResult.sessions.map((session) => this.enrichRecentSessionWorkspace(session));
|
|
6666
|
+
return { sessions: enriched, hasMore: rawResult.hasMore };
|
|
6667
|
+
}
|
|
6668
|
+
const matchesWorkspace = (session) => {
|
|
6669
|
+
const value = session;
|
|
6670
|
+
const workspace = value.workspace;
|
|
6671
|
+
if (msg.projectId && workspace?.projectId !== msg.projectId)
|
|
6672
|
+
return false;
|
|
6673
|
+
if (msg.workspaceKind && workspace?.kind !== msg.workspaceKind)
|
|
6674
|
+
return false;
|
|
6675
|
+
if (msg.projectPath !== undefined) {
|
|
6676
|
+
const matchesPrimaryProject = typeof workspace?.projectId === "string" &&
|
|
6677
|
+
primaryProjectIds.has(workspace.projectId);
|
|
6678
|
+
if (value.projectPath !== msg.projectPath &&
|
|
6679
|
+
!matchesPrimaryProject) {
|
|
6680
|
+
return false;
|
|
6681
|
+
}
|
|
6682
|
+
}
|
|
6683
|
+
return true;
|
|
6684
|
+
};
|
|
6685
|
+
const offset = msg.offset ?? 0;
|
|
6686
|
+
const limit = msg.limit ?? 20;
|
|
6687
|
+
const requiredMatches = offset + limit + 1;
|
|
6688
|
+
let filtered;
|
|
6689
|
+
if (msg.provider === "codex") {
|
|
6690
|
+
filtered = await this.listWorkspaceFilteredCodexSessions(msg, matchesWorkspace, requiredMatches);
|
|
6691
|
+
}
|
|
6692
|
+
else if (msg.provider === "claude") {
|
|
6693
|
+
filtered = await this.listWorkspaceFilteredIndexedSessions(msg, matchesWorkspace, requiredMatches);
|
|
6694
|
+
}
|
|
6695
|
+
else {
|
|
6696
|
+
// The filesystem index includes both providers and is also the fallback
|
|
6697
|
+
// for Codex rollouts not returned by app-server. Merge it with one
|
|
6698
|
+
// cursor-preserving app-server scan for the freshest Codex metadata.
|
|
6699
|
+
const [indexed, codex] = await Promise.all([
|
|
6700
|
+
this.listWorkspaceFilteredIndexedSessions(msg, matchesWorkspace, requiredMatches),
|
|
6701
|
+
this.listWorkspaceFilteredCodexSessions({ ...msg, provider: "codex" }, matchesWorkspace, requiredMatches),
|
|
6702
|
+
]);
|
|
6703
|
+
filtered = mergeRecentSessionPages([...codex, ...indexed]);
|
|
6704
|
+
}
|
|
6705
|
+
return {
|
|
6706
|
+
sessions: filtered.slice(offset, offset + limit),
|
|
6707
|
+
hasMore: filtered.length > offset + limit,
|
|
6708
|
+
};
|
|
6709
|
+
}
|
|
6710
|
+
async listWorkspaceFilteredIndexedSessions(msg, matchesWorkspace, limit) {
|
|
6711
|
+
const result = await getAllRecentSessions({
|
|
6712
|
+
limit,
|
|
6713
|
+
offset: 0,
|
|
6714
|
+
// Project identity is authoritative. Do not pre-filter by the current
|
|
6715
|
+
// primary root: assignments may retain an older snapshot or worktree cwd.
|
|
6716
|
+
provider: msg.provider,
|
|
6717
|
+
namedOnly: msg.namedOnly,
|
|
6718
|
+
searchQuery: msg.searchQuery,
|
|
6719
|
+
archivedSessionIds: this.archiveStore.archivedIds(),
|
|
6720
|
+
sessionFilter: (session) => matchesWorkspace(this.enrichRecentSessionWorkspace(session)),
|
|
6721
|
+
});
|
|
6722
|
+
return result.sessions.map((session) => this.enrichRecentSessionWorkspace(session));
|
|
6723
|
+
}
|
|
6724
|
+
async listWorkspaceFilteredCodexSessions(msg, matchesWorkspace, limit) {
|
|
6725
|
+
try {
|
|
6726
|
+
return await this.listWorkspaceFilteredCodexThreads(msg, matchesWorkspace, limit);
|
|
6727
|
+
}
|
|
6728
|
+
catch (err) {
|
|
6729
|
+
console.warn(`[ws] Codex thread/list failed, falling back to rollout scan: ${err}`);
|
|
6730
|
+
return this.listWorkspaceFilteredIndexedSessions({ ...msg, provider: "codex" }, matchesWorkspace, limit);
|
|
6731
|
+
}
|
|
6732
|
+
}
|
|
6733
|
+
async listWorkspaceFilteredCodexThreads(msg, matchesWorkspace, limit) {
|
|
6734
|
+
const activeProcess = this.getActiveCodexProcess();
|
|
6735
|
+
const process = activeProcess ?? (await this.createStandaloneCodexProcess(undefined));
|
|
6736
|
+
const isStandalone = activeProcess === null;
|
|
6737
|
+
try {
|
|
6738
|
+
const archivedIds = this.archiveStore.archivedIds();
|
|
6739
|
+
const matchingThreads = [];
|
|
6740
|
+
let cursor;
|
|
6741
|
+
do {
|
|
6742
|
+
const request = {
|
|
6743
|
+
limit: 500,
|
|
6744
|
+
searchTerm: msg.searchQuery,
|
|
6745
|
+
sourceKinds: CODEX_RECENT_THREAD_SOURCE_KINDS,
|
|
6746
|
+
};
|
|
6747
|
+
if (cursor != null)
|
|
6748
|
+
request.cursor = cursor;
|
|
6749
|
+
const result = await process.listThreads(request);
|
|
6750
|
+
for (const thread of result.data) {
|
|
6751
|
+
if (archivedIds.has(thread.id))
|
|
6752
|
+
continue;
|
|
6753
|
+
if (msg.namedOnly && !thread.name)
|
|
6754
|
+
continue;
|
|
6755
|
+
const workspaceProbe = this.enrichRecentSessionWorkspace({
|
|
6756
|
+
provider: "codex",
|
|
6757
|
+
sessionId: thread.id,
|
|
6758
|
+
projectPath: thread.cwd,
|
|
6759
|
+
});
|
|
6760
|
+
if (!matchesWorkspace(workspaceProbe))
|
|
6761
|
+
continue;
|
|
6762
|
+
matchingThreads.push(thread);
|
|
6763
|
+
if (matchingThreads.length >= limit)
|
|
6764
|
+
break;
|
|
6765
|
+
}
|
|
6766
|
+
cursor = result.nextCursor;
|
|
6767
|
+
} while (matchingThreads.length < limit && cursor != null);
|
|
6768
|
+
const indexedById = await getCodexSessionIndexMetadata(matchingThreads.map((thread) => thread.id));
|
|
6769
|
+
return matchingThreads.map((thread) => this.enrichRecentSessionWorkspace(codexThreadToRecentSession(thread, indexedById.get(thread.id))));
|
|
6770
|
+
}
|
|
6771
|
+
finally {
|
|
6772
|
+
if (isStandalone)
|
|
6773
|
+
process.stop();
|
|
6774
|
+
}
|
|
6775
|
+
}
|
|
6776
|
+
async listRecentSessionsRaw(msg) {
|
|
6398
6777
|
if (msg.provider === "codex") {
|
|
6399
6778
|
return this.listRecentCodexSessions(msg);
|
|
6400
6779
|
}
|
|
@@ -6411,11 +6790,105 @@ export class BridgeWebSocketServer {
|
|
|
6411
6790
|
archivedSessionIds: this.archiveStore.archivedIds(),
|
|
6412
6791
|
});
|
|
6413
6792
|
}
|
|
6793
|
+
enrichRecentSessionWorkspace(session) {
|
|
6794
|
+
if (!session || typeof session !== "object")
|
|
6795
|
+
return session;
|
|
6796
|
+
const value = session;
|
|
6797
|
+
const provider = value.provider;
|
|
6798
|
+
const providerSessionId = value.sessionId;
|
|
6799
|
+
const projectPath = value.projectPath;
|
|
6800
|
+
if ((provider !== "claude" && provider !== "codex") ||
|
|
6801
|
+
typeof providerSessionId !== "string" ||
|
|
6802
|
+
typeof projectPath !== "string") {
|
|
6803
|
+
return session;
|
|
6804
|
+
}
|
|
6805
|
+
const workspace = this.workspaceStore?.resolveRecentWorkspace(provider, providerSessionId);
|
|
6806
|
+
return {
|
|
6807
|
+
...value,
|
|
6808
|
+
workspace: workspace ?? {
|
|
6809
|
+
kind: "unassigned",
|
|
6810
|
+
rootPaths: projectPath ? [projectPath] : [],
|
|
6811
|
+
},
|
|
6812
|
+
};
|
|
6813
|
+
}
|
|
6814
|
+
normalizeWorkspaceRoots(rootPaths) {
|
|
6815
|
+
const roots = [];
|
|
6816
|
+
const seen = new Set();
|
|
6817
|
+
for (const rawPath of rootPaths) {
|
|
6818
|
+
const path = resolvePlatformPath(rawPath, this.platform);
|
|
6819
|
+
if (!this.isPathAllowed(path))
|
|
6820
|
+
return { deniedRoot: rawPath };
|
|
6821
|
+
if (!seen.has(path)) {
|
|
6822
|
+
seen.add(path);
|
|
6823
|
+
roots.push(path);
|
|
6824
|
+
}
|
|
6825
|
+
}
|
|
6826
|
+
return roots.length > 0 ? { roots } : {};
|
|
6827
|
+
}
|
|
6828
|
+
async persistPendingSessionWorkspace(sessionId) {
|
|
6829
|
+
const workspace = this.pendingSessionWorkspaces.get(sessionId);
|
|
6830
|
+
const session = this.sessionManager.get(sessionId);
|
|
6831
|
+
if (!workspace || !session?.claudeSessionId || !this.workspaceStore)
|
|
6832
|
+
return;
|
|
6833
|
+
try {
|
|
6834
|
+
await this.workspaceStore.assignSession(session.provider, session.claudeSessionId, workspace);
|
|
6835
|
+
this.pendingSessionWorkspaces.delete(sessionId);
|
|
6836
|
+
}
|
|
6837
|
+
catch (error) {
|
|
6838
|
+
console.error("[workspace] Failed to persist session assignment:", error);
|
|
6839
|
+
}
|
|
6840
|
+
}
|
|
6841
|
+
workspaceForRuntimeSession(session) {
|
|
6842
|
+
const pending = this.pendingSessionWorkspaces.get(session.id);
|
|
6843
|
+
if (pending) {
|
|
6844
|
+
return { ...pending, rootPaths: [...pending.rootPaths] };
|
|
6845
|
+
}
|
|
6846
|
+
if (!session.claudeSessionId || !this.workspaceStore)
|
|
6847
|
+
return undefined;
|
|
6848
|
+
const assignment = this.workspaceStore.getAssignment(session.provider, session.claudeSessionId);
|
|
6849
|
+
if (!assignment)
|
|
6850
|
+
return undefined;
|
|
6851
|
+
const project = assignment.projectId
|
|
6852
|
+
? this.workspaceStore.getProject(assignment.projectId)
|
|
6853
|
+
: undefined;
|
|
6854
|
+
return {
|
|
6855
|
+
kind: assignment.kind,
|
|
6856
|
+
...(assignment.projectId ? { projectId: assignment.projectId } : {}),
|
|
6857
|
+
...(project ? { projectName: project.name } : {}),
|
|
6858
|
+
rootPaths: [...assignment.rootPaths],
|
|
6859
|
+
};
|
|
6860
|
+
}
|
|
6861
|
+
attachWorkspaceToRuntimeSession(sessionId, workspace) {
|
|
6862
|
+
if (!workspace)
|
|
6863
|
+
return;
|
|
6864
|
+
this.pendingSessionWorkspaces.set(sessionId, workspace);
|
|
6865
|
+
void this.persistPendingSessionWorkspace(sessionId);
|
|
6866
|
+
}
|
|
6867
|
+
projectsMessage(requestId) {
|
|
6868
|
+
return {
|
|
6869
|
+
type: "projects",
|
|
6870
|
+
projects: this.workspaceStore?.listProjects() ?? [],
|
|
6871
|
+
...(requestId ? { requestId } : {}),
|
|
6872
|
+
};
|
|
6873
|
+
}
|
|
6874
|
+
sendWorkspaceMutationError(ws, requestId, action, error) {
|
|
6875
|
+
this.send(ws, {
|
|
6876
|
+
type: "error",
|
|
6877
|
+
requestId,
|
|
6878
|
+
errorCode: "workspace_write_failed",
|
|
6879
|
+
message: `Failed to ${action}: ${error instanceof Error ? error.message : String(error)}`,
|
|
6880
|
+
});
|
|
6881
|
+
}
|
|
6882
|
+
legacyProjectHistoryProjects() {
|
|
6883
|
+
return this.projectHistory?.getProjects() ?? [];
|
|
6884
|
+
}
|
|
6414
6885
|
listRecentSessionsCoalesced(msg) {
|
|
6415
6886
|
const key = JSON.stringify({
|
|
6416
6887
|
limit: msg.limit ?? null,
|
|
6417
6888
|
offset: msg.offset ?? null,
|
|
6418
6889
|
projectPath: msg.projectPath ?? null,
|
|
6890
|
+
projectId: msg.projectId ?? null,
|
|
6891
|
+
workspaceKind: msg.workspaceKind ?? null,
|
|
6419
6892
|
provider: msg.provider ?? null,
|
|
6420
6893
|
namedOnly: msg.namedOnly ?? null,
|
|
6421
6894
|
searchQuery: msg.searchQuery ?? null,
|