@ccpocket/bridge 1.52.2 → 1.53.1
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.d.ts +2 -0
- package/dist/auto-rename.js +5 -1
- package/dist/auto-rename.js.map +1 -1
- package/dist/codex-process.d.ts +7 -1
- package/dist/codex-process.js +38 -6
- package/dist/codex-process.js.map +1 -1
- package/dist/parser.d.ts +4 -0
- package/dist/parser.js +5 -0
- package/dist/parser.js.map +1 -1
- package/dist/session.d.ts +7 -8
- package/dist/session.js +30 -38
- package/dist/session.js.map +1 -1
- package/dist/sessions-index.d.ts +2 -0
- package/dist/sessions-index.js +250 -2
- package/dist/sessions-index.js.map +1 -1
- package/dist/websocket.d.ts +4 -0
- package/dist/websocket.js +256 -29
- package/dist/websocket.js.map +1 -1
- package/package.json +1 -1
package/dist/websocket.js
CHANGED
|
@@ -9,7 +9,7 @@ import { SessionManager, } from "./session.js";
|
|
|
9
9
|
import { SdkProcess } from "./sdk-process.js";
|
|
10
10
|
import { CodexProcess, } from "./codex-process.js";
|
|
11
11
|
import { parseClientMessage, } from "./parser.js";
|
|
12
|
-
import { getAllRecentSessions, getCodexSessionHistory, getSessionHistory, codexUserTurnUuid, findSessionsByClaudeIds, extractMessageImages, getClaudeSessionName, loadCodexSessionNames, renameClaudeSession, renameCodexSession, saveCodexSessionProfile, } from "./sessions-index.js";
|
|
12
|
+
import { getAllRecentSessions, getCodexSessionHistory, getSessionHistory, codexUserTurnUuid, codexThreadToSessionHistory, findSessionsByClaudeIds, extractMessageImages, getClaudeSessionName, loadCodexSessionNames, renameClaudeSession, renameCodexSession, saveCodexSessionProfile, } from "./sessions-index.js";
|
|
13
13
|
import { ArchiveStore } from "./archive-store.js";
|
|
14
14
|
import { WorktreeStore } from "./worktree-store.js";
|
|
15
15
|
import { listWorktrees, removeWorktree, worktreeExists, getMainBranch, } from "./worktree.js";
|
|
@@ -82,6 +82,95 @@ function countCodexUserTurnsInSession(session) {
|
|
|
82
82
|
function nextCodexUserTurnUuid(session) {
|
|
83
83
|
return codexUserTurnUuid(countCodexUserTurnsInSession(session) + 1);
|
|
84
84
|
}
|
|
85
|
+
function normalizeHistoryContent(content) {
|
|
86
|
+
if (typeof content === "string")
|
|
87
|
+
return content;
|
|
88
|
+
if (!Array.isArray(content))
|
|
89
|
+
return [];
|
|
90
|
+
const normalized = [];
|
|
91
|
+
for (const item of content) {
|
|
92
|
+
if (!item || typeof item !== "object")
|
|
93
|
+
continue;
|
|
94
|
+
const value = item;
|
|
95
|
+
if (typeof value.type !== "string")
|
|
96
|
+
continue;
|
|
97
|
+
if (value.type === "text") {
|
|
98
|
+
normalized.push({
|
|
99
|
+
type: "text",
|
|
100
|
+
text: typeof value.text === "string" ? value.text : "",
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
else if (value.type === "tool_use") {
|
|
104
|
+
normalized.push({
|
|
105
|
+
type: "tool_use",
|
|
106
|
+
id: typeof value.id === "string" ? value.id : undefined,
|
|
107
|
+
name: typeof value.name === "string" ? value.name : undefined,
|
|
108
|
+
input: value.input && typeof value.input === "object" && !Array.isArray(value.input)
|
|
109
|
+
? value.input
|
|
110
|
+
: undefined,
|
|
111
|
+
});
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
return normalized;
|
|
115
|
+
}
|
|
116
|
+
function buildCodexHistoryPrefix(session, targetOrdinal) {
|
|
117
|
+
const messages = [];
|
|
118
|
+
let userOrdinal = 0;
|
|
119
|
+
let reachedEnd = false;
|
|
120
|
+
const appendPastMessage = (message) => {
|
|
121
|
+
if (reachedEnd || !message || typeof message !== "object")
|
|
122
|
+
return;
|
|
123
|
+
const item = message;
|
|
124
|
+
if (item.role === "user" && item.isMeta !== true) {
|
|
125
|
+
userOrdinal += 1;
|
|
126
|
+
if (userOrdinal > targetOrdinal) {
|
|
127
|
+
reachedEnd = true;
|
|
128
|
+
return;
|
|
129
|
+
}
|
|
130
|
+
messages.push({ ...item });
|
|
131
|
+
return;
|
|
132
|
+
}
|
|
133
|
+
if (item.role === "assistant" && userOrdinal > 0 && userOrdinal <= targetOrdinal) {
|
|
134
|
+
messages.push({ ...item });
|
|
135
|
+
}
|
|
136
|
+
};
|
|
137
|
+
for (const message of session.pastMessages ?? []) {
|
|
138
|
+
appendPastMessage(message);
|
|
139
|
+
if (reachedEnd)
|
|
140
|
+
return messages;
|
|
141
|
+
}
|
|
142
|
+
for (const message of session.history) {
|
|
143
|
+
if (message.type === "user_input") {
|
|
144
|
+
if (message.isMeta === true)
|
|
145
|
+
continue;
|
|
146
|
+
const userInput = message;
|
|
147
|
+
userOrdinal += 1;
|
|
148
|
+
if (userOrdinal > targetOrdinal)
|
|
149
|
+
break;
|
|
150
|
+
messages.push({
|
|
151
|
+
role: "user",
|
|
152
|
+
uuid: userInput.userMessageUuid,
|
|
153
|
+
timestamp: userInput.timestamp,
|
|
154
|
+
imageCount: userInput.imageCount,
|
|
155
|
+
content: [{ type: "text", text: userInput.text }],
|
|
156
|
+
});
|
|
157
|
+
}
|
|
158
|
+
else if (message.type === "assistant" &&
|
|
159
|
+
userOrdinal > 0 &&
|
|
160
|
+
userOrdinal <= targetOrdinal) {
|
|
161
|
+
messages.push({
|
|
162
|
+
role: "assistant",
|
|
163
|
+
uuid: message.messageUuid,
|
|
164
|
+
content: normalizeHistoryContent(message.message.content),
|
|
165
|
+
});
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
return messages;
|
|
169
|
+
}
|
|
170
|
+
function countCodexHistoryUserTurns(messages) {
|
|
171
|
+
return messages.filter((message) => message.role === "user" && !message.isMeta)
|
|
172
|
+
.length;
|
|
173
|
+
}
|
|
85
174
|
// ---- Codex mode mapping helpers ----
|
|
86
175
|
/** Map unified PermissionMode to Codex approval_policy.
|
|
87
176
|
* Only "bypassPermissions" maps to "never"; all others use "on-request". */
|
|
@@ -494,13 +583,13 @@ export class BridgeWebSocketServer {
|
|
|
494
583
|
});
|
|
495
584
|
return;
|
|
496
585
|
}
|
|
497
|
-
const numTurns = totalUserTurns - targetOrdinal;
|
|
586
|
+
const numTurns = totalUserTurns - targetOrdinal + 1;
|
|
498
587
|
if (numTurns <= 0) {
|
|
499
588
|
this.send(ws, {
|
|
500
589
|
type: "rewind_result",
|
|
501
590
|
success: false,
|
|
502
591
|
mode,
|
|
503
|
-
error: "
|
|
592
|
+
error: "Invalid Codex rewind target",
|
|
504
593
|
});
|
|
505
594
|
return;
|
|
506
595
|
}
|
|
@@ -522,8 +611,12 @@ export class BridgeWebSocketServer {
|
|
|
522
611
|
worktreeBranch: session.worktreeBranch,
|
|
523
612
|
}
|
|
524
613
|
: undefined;
|
|
525
|
-
await codexProcess.rollbackThread(numTurns);
|
|
526
|
-
const pastMessages =
|
|
614
|
+
const rolledBackThread = await codexProcess.rollbackThread(numTurns);
|
|
615
|
+
const pastMessages = this.codexHistoryFromThreadOrFallback({
|
|
616
|
+
thread: rolledBackThread,
|
|
617
|
+
expectedUserTurns: targetOrdinal - 1,
|
|
618
|
+
fallback: buildCodexHistoryPrefix(session, targetOrdinal - 1),
|
|
619
|
+
});
|
|
527
620
|
this.sessionManager.destroy(sessionId);
|
|
528
621
|
const newSessionId = this.sessionManager.create(projectPath, undefined, pastMessages, worktreeOpts, "codex", {
|
|
529
622
|
...(codexSettings ?? {}),
|
|
@@ -546,6 +639,88 @@ export class BridgeWebSocketServer {
|
|
|
546
639
|
}));
|
|
547
640
|
this.sendSessionList(ws);
|
|
548
641
|
}
|
|
642
|
+
async forkCodexSession(ws, sessionId, targetUuid) {
|
|
643
|
+
const session = this.sessionManager.get(sessionId);
|
|
644
|
+
if (!session) {
|
|
645
|
+
this.send(ws, {
|
|
646
|
+
type: "error",
|
|
647
|
+
message: `Session ${sessionId} not found`,
|
|
648
|
+
errorCode: "fork_failed",
|
|
649
|
+
});
|
|
650
|
+
return;
|
|
651
|
+
}
|
|
652
|
+
const codexProcess = session.process;
|
|
653
|
+
if (session.provider !== "codex" ||
|
|
654
|
+
typeof codexProcess.forkThread !== "function") {
|
|
655
|
+
this.send(ws, {
|
|
656
|
+
type: "error",
|
|
657
|
+
message: "Fork is only supported for Codex sessions",
|
|
658
|
+
errorCode: "fork_failed",
|
|
659
|
+
});
|
|
660
|
+
return;
|
|
661
|
+
}
|
|
662
|
+
if (session.status !== "idle" || (codexProcess.status ?? session.status) !== "idle") {
|
|
663
|
+
this.send(ws, {
|
|
664
|
+
type: "error",
|
|
665
|
+
message: "Cannot fork while Codex is running",
|
|
666
|
+
errorCode: "fork_failed",
|
|
667
|
+
});
|
|
668
|
+
return;
|
|
669
|
+
}
|
|
670
|
+
if (session.codexQueuedInput) {
|
|
671
|
+
this.send(ws, {
|
|
672
|
+
type: "error",
|
|
673
|
+
message: "Cannot fork while Codex has queued input",
|
|
674
|
+
errorCode: "fork_failed",
|
|
675
|
+
});
|
|
676
|
+
return;
|
|
677
|
+
}
|
|
678
|
+
const targetOrdinal = parseCodexUserTurnOrdinal(targetUuid);
|
|
679
|
+
const totalUserTurns = countCodexUserTurnsInSession(session);
|
|
680
|
+
if (targetOrdinal === null || targetOrdinal > totalUserTurns) {
|
|
681
|
+
this.send(ws, {
|
|
682
|
+
type: "error",
|
|
683
|
+
message: "Invalid Codex fork target",
|
|
684
|
+
errorCode: "fork_failed",
|
|
685
|
+
});
|
|
686
|
+
return;
|
|
687
|
+
}
|
|
688
|
+
const projectPath = session.projectPath;
|
|
689
|
+
const codexSettings = session.codexSettings;
|
|
690
|
+
const worktreeOpts = session.worktreePath
|
|
691
|
+
? {
|
|
692
|
+
existingWorktreePath: session.worktreePath,
|
|
693
|
+
worktreeBranch: session.worktreeBranch,
|
|
694
|
+
}
|
|
695
|
+
: undefined;
|
|
696
|
+
const forked = await codexProcess.forkThread();
|
|
697
|
+
const forkedThreadId = forked.threadId;
|
|
698
|
+
const turnsToDrop = totalUserTurns - targetOrdinal;
|
|
699
|
+
let forkedThread = forked.thread;
|
|
700
|
+
if (turnsToDrop > 0) {
|
|
701
|
+
forkedThread = await codexProcess.rollbackThreadById(forkedThreadId, turnsToDrop);
|
|
702
|
+
}
|
|
703
|
+
const pastMessages = this.codexHistoryFromThreadOrFallback({
|
|
704
|
+
thread: forkedThread,
|
|
705
|
+
expectedUserTurns: targetOrdinal,
|
|
706
|
+
fallback: buildCodexHistoryPrefix(session, targetOrdinal),
|
|
707
|
+
});
|
|
708
|
+
const newSessionId = this.sessionManager.create(projectPath, undefined, pastMessages, worktreeOpts, "codex", {
|
|
709
|
+
...(codexSettings ?? {}),
|
|
710
|
+
threadId: forkedThreadId,
|
|
711
|
+
});
|
|
712
|
+
const newSession = this.sessionManager.get(newSessionId);
|
|
713
|
+
this.send(ws, this.buildSessionCreatedMessage({
|
|
714
|
+
sessionId: newSessionId,
|
|
715
|
+
provider: "codex",
|
|
716
|
+
projectPath,
|
|
717
|
+
session: newSession,
|
|
718
|
+
approvalsReviewer: codexSettings?.approvalsReviewer,
|
|
719
|
+
sandboxMode: codexSettings?.sandboxMode,
|
|
720
|
+
sourceSessionId: sessionId,
|
|
721
|
+
}));
|
|
722
|
+
this.sendSessionList(ws);
|
|
723
|
+
}
|
|
549
724
|
sendTip(ws, sessionId, tipCode, session) {
|
|
550
725
|
const tipMsg = {
|
|
551
726
|
type: "system",
|
|
@@ -613,6 +788,39 @@ export class BridgeWebSocketServer {
|
|
|
613
788
|
}
|
|
614
789
|
return { pastMessages, historyMessages };
|
|
615
790
|
}
|
|
791
|
+
async getCodexThreadHistoryFromRpc(threadId, projectPath) {
|
|
792
|
+
const activeProcess = this.getActiveCodexProcess();
|
|
793
|
+
const process = activeProcess ?? (await this.createStandaloneCodexProcess(projectPath));
|
|
794
|
+
const isStandalone = process !== activeProcess;
|
|
795
|
+
try {
|
|
796
|
+
const thread = await process.readThread(threadId, true);
|
|
797
|
+
return codexThreadToSessionHistory(thread);
|
|
798
|
+
}
|
|
799
|
+
finally {
|
|
800
|
+
if (isStandalone) {
|
|
801
|
+
process.stop();
|
|
802
|
+
}
|
|
803
|
+
}
|
|
804
|
+
}
|
|
805
|
+
async getCodexThreadHistory(threadId, projectPath) {
|
|
806
|
+
if (!this.getActiveCodexProcess() && process.env.NODE_ENV === "test") {
|
|
807
|
+
return getCodexSessionHistory(threadId);
|
|
808
|
+
}
|
|
809
|
+
try {
|
|
810
|
+
return await this.getCodexThreadHistoryFromRpc(threadId, projectPath);
|
|
811
|
+
}
|
|
812
|
+
catch (err) {
|
|
813
|
+
console.warn(`[ws] thread/read failed for ${threadId}; falling back to JSONL: ${err instanceof Error ? err.message : String(err)}`);
|
|
814
|
+
return getCodexSessionHistory(threadId);
|
|
815
|
+
}
|
|
816
|
+
}
|
|
817
|
+
codexHistoryFromThreadOrFallback(params) {
|
|
818
|
+
const messages = codexThreadToSessionHistory(params.thread);
|
|
819
|
+
if (countCodexHistoryUserTurns(messages) >= params.expectedUserTurns) {
|
|
820
|
+
return messages;
|
|
821
|
+
}
|
|
822
|
+
return params.fallback;
|
|
823
|
+
}
|
|
616
824
|
createClaudeSessionWithFallback(params) {
|
|
617
825
|
const initialMode = params.options?.permissionMode ?? "default";
|
|
618
826
|
try {
|
|
@@ -921,7 +1129,9 @@ export class BridgeWebSocketServer {
|
|
|
921
1129
|
// The SDK stream does NOT emit user messages, so session.history would
|
|
922
1130
|
// otherwise lack them. This ensures get_history responses include user
|
|
923
1131
|
// messages and replaceEntries on the client side preserves them.
|
|
924
|
-
//
|
|
1132
|
+
// Flutter already shows the user bubble optimistically. For Codex we
|
|
1133
|
+
// echo the accepted user_input back with its synthetic UUID so the live
|
|
1134
|
+
// bubble becomes rewindable/forkable without requiring a stop+resume.
|
|
925
1135
|
//
|
|
926
1136
|
// Register images in the image store so they can be served via HTTP
|
|
927
1137
|
// when the client re-enters the session and loads history.
|
|
@@ -1008,6 +1218,13 @@ export class BridgeWebSocketServer {
|
|
|
1008
1218
|
...(imageRefs ? { images: imageRefs } : {}),
|
|
1009
1219
|
});
|
|
1010
1220
|
const acceptedSeq = userEntry?.seq ?? session.historyRevision;
|
|
1221
|
+
if (session.provider === "codex" && userEntry) {
|
|
1222
|
+
this.send(ws, {
|
|
1223
|
+
...userEntry.message,
|
|
1224
|
+
sessionId: session.id,
|
|
1225
|
+
historySeq: acceptedSeq,
|
|
1226
|
+
});
|
|
1227
|
+
}
|
|
1011
1228
|
// Persist images to Gallery Store asynchronously (fire-and-forget)
|
|
1012
1229
|
if (images.length > 0 && this.galleryStore && session.projectPath) {
|
|
1013
1230
|
for (const img of images) {
|
|
@@ -1422,7 +1639,7 @@ export class BridgeWebSocketServer {
|
|
|
1422
1639
|
worktreeBranch,
|
|
1423
1640
|
};
|
|
1424
1641
|
}
|
|
1425
|
-
|
|
1642
|
+
this.getCodexThreadHistory(threadId, effectiveProjectPath)
|
|
1426
1643
|
.then((pastMessages) => {
|
|
1427
1644
|
const newId = this.sessionManager.create(effectiveProjectPath, undefined, pastMessages, worktreeOpts, "codex", {
|
|
1428
1645
|
threadId,
|
|
@@ -1647,7 +1864,7 @@ export class BridgeWebSocketServer {
|
|
|
1647
1864
|
else if (worktreePath) {
|
|
1648
1865
|
worktreeOpts = { existingWorktreePath: worktreePath, worktreeBranch };
|
|
1649
1866
|
}
|
|
1650
|
-
|
|
1867
|
+
this.getCodexThreadHistory(threadId, effectiveProjectPath)
|
|
1651
1868
|
.then((pastMessages) => {
|
|
1652
1869
|
const newId = this.sessionManager.create(effectiveProjectPath, undefined, pastMessages, worktreeOpts, "codex", {
|
|
1653
1870
|
threadId,
|
|
@@ -2184,8 +2401,8 @@ export class BridgeWebSocketServer {
|
|
|
2184
2401
|
};
|
|
2185
2402
|
}
|
|
2186
2403
|
}
|
|
2187
|
-
|
|
2188
|
-
.
|
|
2404
|
+
try {
|
|
2405
|
+
const pastMessages = await this.getCodexThreadHistory(sessionRefId, effectiveProjectPath);
|
|
2189
2406
|
const sessionId = this.sessionManager.create(effectiveProjectPath, undefined, pastMessages, worktreeOpts, "codex", {
|
|
2190
2407
|
threadId: sessionRefId,
|
|
2191
2408
|
profile: effectiveProfile,
|
|
@@ -2204,22 +2421,21 @@ export class BridgeWebSocketServer {
|
|
|
2204
2421
|
: "default",
|
|
2205
2422
|
});
|
|
2206
2423
|
const createdSession = this.sessionManager.get(sessionId);
|
|
2207
|
-
|
|
2208
|
-
|
|
2209
|
-
|
|
2210
|
-
|
|
2211
|
-
|
|
2212
|
-
|
|
2213
|
-
|
|
2214
|
-
|
|
2215
|
-
|
|
2216
|
-
|
|
2217
|
-
|
|
2218
|
-
|
|
2219
|
-
|
|
2220
|
-
|
|
2221
|
-
|
|
2222
|
-
});
|
|
2424
|
+
await this.loadAndSetSessionName(createdSession, "codex", effectiveProjectPath, sessionRefId);
|
|
2425
|
+
this.send(ws, this.buildSessionCreatedMessage({
|
|
2426
|
+
sessionId,
|
|
2427
|
+
provider: "codex",
|
|
2428
|
+
projectPath: effectiveProjectPath,
|
|
2429
|
+
session: createdSession,
|
|
2430
|
+
sandboxMode: createdSession?.codexSettings?.sandboxMode
|
|
2431
|
+
? sandboxModeToExternal(createdSession.codexSettings.sandboxMode)
|
|
2432
|
+
: undefined,
|
|
2433
|
+
approvalsReviewer: createdSession?.codexSettings?.approvalsReviewer,
|
|
2434
|
+
permissionMode: legacyPermissionMode,
|
|
2435
|
+
executionMode,
|
|
2436
|
+
planMode,
|
|
2437
|
+
}));
|
|
2438
|
+
this.broadcastSessionList();
|
|
2223
2439
|
this.debugEvents.set(sessionId, []);
|
|
2224
2440
|
this.recordDebugEvent(sessionId, {
|
|
2225
2441
|
direction: "internal",
|
|
@@ -2228,13 +2444,13 @@ export class BridgeWebSocketServer {
|
|
|
2228
2444
|
detail: `provider=codex thread=${sessionRefId}`,
|
|
2229
2445
|
});
|
|
2230
2446
|
this.projectHistory?.addProject(effectiveProjectPath);
|
|
2231
|
-
}
|
|
2232
|
-
|
|
2447
|
+
}
|
|
2448
|
+
catch (err) {
|
|
2233
2449
|
this.send(ws, {
|
|
2234
2450
|
type: "error",
|
|
2235
2451
|
message: `Failed to load Codex session history: ${err}`,
|
|
2236
2452
|
});
|
|
2237
|
-
}
|
|
2453
|
+
}
|
|
2238
2454
|
break;
|
|
2239
2455
|
}
|
|
2240
2456
|
const claudeSessionId = sessionRefId;
|
|
@@ -3309,6 +3525,17 @@ export class BridgeWebSocketServer {
|
|
|
3309
3525
|
}
|
|
3310
3526
|
break;
|
|
3311
3527
|
}
|
|
3528
|
+
case "fork": {
|
|
3529
|
+
this.forkCodexSession(ws, msg.sessionId, msg.targetUuid).catch((err) => {
|
|
3530
|
+
const errMsg = err instanceof Error ? err.message : String(err);
|
|
3531
|
+
this.send(ws, {
|
|
3532
|
+
type: "error",
|
|
3533
|
+
message: errMsg,
|
|
3534
|
+
errorCode: "fork_failed",
|
|
3535
|
+
});
|
|
3536
|
+
});
|
|
3537
|
+
break;
|
|
3538
|
+
}
|
|
3312
3539
|
case "list_windows": {
|
|
3313
3540
|
listWindows()
|
|
3314
3541
|
.then((windows) => {
|