@ccpocket/bridge 1.86.1 → 1.87.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/codex-process.d.ts +11 -0
- package/dist/codex-process.js +121 -6
- package/dist/codex-process.js.map +1 -1
- package/dist/codex-recovery.d.ts +35 -0
- package/dist/codex-recovery.js +121 -0
- package/dist/codex-recovery.js.map +1 -0
- package/dist/parser.d.ts +12 -0
- package/dist/parser.js +10 -0
- package/dist/parser.js.map +1 -1
- package/dist/session.js +3 -1
- package/dist/session.js.map +1 -1
- package/dist/sessions-index.js +138 -30
- package/dist/sessions-index.js.map +1 -1
- package/dist/websocket.js +54 -16
- package/dist/websocket.js.map +1 -1
- package/package.json +1 -1
package/dist/codex-process.d.ts
CHANGED
|
@@ -211,6 +211,17 @@ export declare class CodexProcess extends EventEmitter<CodexProcessEvents> {
|
|
|
211
211
|
* Sends thread/name/set which persists to ~/.codex/session_index.jsonl.
|
|
212
212
|
*/
|
|
213
213
|
renameThread(name: string): Promise<void>;
|
|
214
|
+
private manualInputVersion;
|
|
215
|
+
private recoveryFailure;
|
|
216
|
+
private upstreamWillRetry;
|
|
217
|
+
private inputTurnStarted;
|
|
218
|
+
private recoveryNotice;
|
|
219
|
+
private readonly recovery;
|
|
220
|
+
getRecoveryState(): import("./codex-recovery.js").CodexRecoveryState;
|
|
221
|
+
setRecoveryEnabled(enabled: boolean): void;
|
|
222
|
+
cancelRecovery(): void;
|
|
223
|
+
noteManualInput(): void;
|
|
224
|
+
private canDispatchRecovery;
|
|
214
225
|
private goalLookup;
|
|
215
226
|
/** Read the persisted goal attached to this Codex thread. */
|
|
216
227
|
getGoal(timeoutMs?: number): Promise<CodexGoal | null>;
|
package/dist/codex-process.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { CodexRecovery } from "./codex-recovery.js";
|
|
1
2
|
import { EventEmitter } from "node:events";
|
|
2
3
|
import { randomUUID } from "node:crypto";
|
|
3
4
|
import { realpathSync, statSync } from "node:fs";
|
|
@@ -240,6 +241,57 @@ export class CodexProcess extends EventEmitter {
|
|
|
240
241
|
name,
|
|
241
242
|
});
|
|
242
243
|
}
|
|
244
|
+
manualInputVersion = 0;
|
|
245
|
+
recoveryFailure = null;
|
|
246
|
+
upstreamWillRetry = false;
|
|
247
|
+
inputTurnStarted = false;
|
|
248
|
+
recoveryNotice = "";
|
|
249
|
+
recovery = new CodexRecovery({
|
|
250
|
+
changed: (recovery) => {
|
|
251
|
+
this.emitMessage({ type: "codex_recovery_state", recovery });
|
|
252
|
+
const notice = `${recovery.phase}:${recovery.reason}`;
|
|
253
|
+
if (notice !== this.recoveryNotice && ["waiting", "blocked", "exhausted"].includes(recovery.phase)) {
|
|
254
|
+
this.emitMessage({ type: "error", errorCode: "codex_recovery_notice", message: recovery.phase === "waiting"
|
|
255
|
+
? `Automatic recovery is waiting for the usage limit (${recovery.attempts}/${recovery.maxAttempts} attempts used). You can cancel or send a message. ${recovery.reason ?? ""}`
|
|
256
|
+
: `Automatic recovery ${recovery.phase}. ${recovery.reason ?? ""}` });
|
|
257
|
+
}
|
|
258
|
+
this.recoveryNotice = notice;
|
|
259
|
+
},
|
|
260
|
+
canResume: async () => {
|
|
261
|
+
if (!this.canDispatchRecovery())
|
|
262
|
+
return false;
|
|
263
|
+
const goal = await this.getGoal();
|
|
264
|
+
return this.canDispatchRecovery() && (goal === null ||
|
|
265
|
+
((goal.status === "active" || goal.status === "usageLimited") &&
|
|
266
|
+
!(typeof goal.tokenBudget === "number" && goal.tokensUsed >= goal.tokenBudget)));
|
|
267
|
+
},
|
|
268
|
+
dispatch: (input) => {
|
|
269
|
+
if (!this.canDispatchRecovery())
|
|
270
|
+
return false;
|
|
271
|
+
const resolve = this.inputResolve;
|
|
272
|
+
this.inputResolve = null;
|
|
273
|
+
resolve({ ...input, recoveryGeneration: this.recovery.version });
|
|
274
|
+
return true;
|
|
275
|
+
},
|
|
276
|
+
resetAt: async () => {
|
|
277
|
+
const response = await this.readRateLimits(3_000);
|
|
278
|
+
const limits = response.rateLimitsByLimitId?.codex ?? response.rateLimits;
|
|
279
|
+
const resets = [limits?.primary, limits?.secondary]
|
|
280
|
+
.filter((window) => window && window.usedPercent >= 100 && typeof window.resetsAt === "number" && Number.isFinite(window.resetsAt))
|
|
281
|
+
.map((window) => window.resetsAt * 1000);
|
|
282
|
+
return resets.length ? Math.max(...resets) : undefined;
|
|
283
|
+
},
|
|
284
|
+
});
|
|
285
|
+
getRecoveryState() { return this.recovery.state; }
|
|
286
|
+
setRecoveryEnabled(enabled) { this.recovery.setEnabled(enabled); }
|
|
287
|
+
cancelRecovery() { this.manualInputVersion++; this.recovery.cancel(); }
|
|
288
|
+
noteManualInput() { this.manualInputVersion++; this.recovery.manualInput(); }
|
|
289
|
+
canDispatchRecovery() {
|
|
290
|
+
return !this.stopped && this._threadId !== null && this.inputResolve !== null &&
|
|
291
|
+
this.pendingTurnId === null && this.pendingTurnCompletion === null &&
|
|
292
|
+
this.pendingApprovals.size === 0 && !this.hasBlockingUserInput() &&
|
|
293
|
+
this.pendingPlanCompletion === null && this._pendingPlanInput === null;
|
|
294
|
+
}
|
|
243
295
|
goalLookup;
|
|
244
296
|
/** Read the persisted goal attached to this Codex thread. */
|
|
245
297
|
async getGoal(timeoutMs = 3_000) {
|
|
@@ -258,6 +310,7 @@ export class CodexProcess extends EventEmitter {
|
|
|
258
310
|
}
|
|
259
311
|
/** Create or update the persisted goal attached to this Codex thread. */
|
|
260
312
|
async setGoal(update) {
|
|
313
|
+
this.cancelRecovery();
|
|
261
314
|
if (!this._threadId) {
|
|
262
315
|
throw new Error("No thread ID available for goal update");
|
|
263
316
|
}
|
|
@@ -272,6 +325,7 @@ export class CodexProcess extends EventEmitter {
|
|
|
272
325
|
}
|
|
273
326
|
/** Remove the persisted goal attached to this Codex thread. */
|
|
274
327
|
async clearGoal() {
|
|
328
|
+
this.cancelRecovery();
|
|
275
329
|
if (!this._threadId) {
|
|
276
330
|
throw new Error("No thread ID available for goal clear");
|
|
277
331
|
}
|
|
@@ -601,6 +655,7 @@ export class CodexProcess extends EventEmitter {
|
|
|
601
655
|
}
|
|
602
656
|
stop() {
|
|
603
657
|
this.stopped = true;
|
|
658
|
+
this.recovery.setEnabled(false);
|
|
604
659
|
this.rejectReadiness(new Error("codex app-server stopped"));
|
|
605
660
|
if (this.inputResolve) {
|
|
606
661
|
this.inputResolve({ text: "" });
|
|
@@ -621,6 +676,7 @@ export class CodexProcess extends EventEmitter {
|
|
|
621
676
|
}
|
|
622
677
|
prepareLaunch(projectPath, options) {
|
|
623
678
|
this.stopped = false;
|
|
679
|
+
this.recovery.setEnabled(false);
|
|
624
680
|
this._threadId = null;
|
|
625
681
|
this._agentNickname = null;
|
|
626
682
|
this._agentRole = null;
|
|
@@ -702,6 +758,7 @@ export class CodexProcess extends EventEmitter {
|
|
|
702
758
|
transport.start(projectPath);
|
|
703
759
|
}
|
|
704
760
|
interrupt() {
|
|
761
|
+
this.cancelRecovery();
|
|
705
762
|
if (!this._threadId || !this.pendingTurnId)
|
|
706
763
|
return;
|
|
707
764
|
void this.request("turn/interrupt", {
|
|
@@ -714,6 +771,7 @@ export class CodexProcess extends EventEmitter {
|
|
|
714
771
|
});
|
|
715
772
|
}
|
|
716
773
|
sendInput(text) {
|
|
774
|
+
this.noteManualInput();
|
|
717
775
|
if (!this.inputResolve) {
|
|
718
776
|
console.error("[codex-process] No pending input resolver for sendInput");
|
|
719
777
|
return;
|
|
@@ -723,6 +781,7 @@ export class CodexProcess extends EventEmitter {
|
|
|
723
781
|
resolve({ text });
|
|
724
782
|
}
|
|
725
783
|
sendInputWithImages(text, images) {
|
|
784
|
+
this.noteManualInput();
|
|
726
785
|
if (!this.inputResolve) {
|
|
727
786
|
console.error("[codex-process] No pending input resolver for sendInputWithImages");
|
|
728
787
|
return;
|
|
@@ -735,6 +794,7 @@ export class CodexProcess extends EventEmitter {
|
|
|
735
794
|
this.sendInputStructured(text, { skills: [skill] });
|
|
736
795
|
}
|
|
737
796
|
sendInputStructured(text, options) {
|
|
797
|
+
this.noteManualInput();
|
|
738
798
|
if (!this.inputResolve) {
|
|
739
799
|
console.error("[codex-process] No pending input resolver for sendInputStructured");
|
|
740
800
|
return;
|
|
@@ -749,6 +809,7 @@ export class CodexProcess extends EventEmitter {
|
|
|
749
809
|
});
|
|
750
810
|
}
|
|
751
811
|
async steerInputStructured(text, options) {
|
|
812
|
+
this.noteManualInput();
|
|
752
813
|
if (!this._threadId || !this.pendingTurnId) {
|
|
753
814
|
throw new Error("No active Codex turn to steer");
|
|
754
815
|
}
|
|
@@ -1134,7 +1195,7 @@ export class CodexProcess extends EventEmitter {
|
|
|
1134
1195
|
}
|
|
1135
1196
|
async bootstrap(projectPath, options) {
|
|
1136
1197
|
try {
|
|
1137
|
-
await this.initializeRpcConnection();
|
|
1198
|
+
const supportsPermissionProfiles = await this.initializeRpcConnection();
|
|
1138
1199
|
const autoReviewDisabled = options?.autoReviewDisabledByPolicy === null
|
|
1139
1200
|
? (await this.readConfigRequirements()).autoReviewDisabled
|
|
1140
1201
|
: options?.autoReviewDisabledByPolicy === true;
|
|
@@ -1186,7 +1247,19 @@ export class CodexProcess extends EventEmitter {
|
|
|
1186
1247
|
threadParams.approvalsReviewer = requestedApprovalsReviewer;
|
|
1187
1248
|
}
|
|
1188
1249
|
if (requestedSandboxMode) {
|
|
1189
|
-
|
|
1250
|
+
// A legacy sandbox override does not persist the selected built-in
|
|
1251
|
+
// profile. Desktop can restore its configured sandbox on resume.
|
|
1252
|
+
// Keep legacy requests for older/unknown servers, which may silently
|
|
1253
|
+
// ignore the new field. Never send permissions together with sandbox.
|
|
1254
|
+
if (supportsPermissionProfiles &&
|
|
1255
|
+
effectiveCodexPermissionsMode === "fullAccess" &&
|
|
1256
|
+
!options?.profile &&
|
|
1257
|
+
requestedSandboxMode === "danger-full-access") {
|
|
1258
|
+
threadParams.permissions = ":danger-full-access";
|
|
1259
|
+
}
|
|
1260
|
+
else {
|
|
1261
|
+
threadParams.sandbox = requestedSandboxMode;
|
|
1262
|
+
}
|
|
1190
1263
|
}
|
|
1191
1264
|
const threadConfig = {};
|
|
1192
1265
|
const requestedModel = sanitizeCodexModel(options?.model);
|
|
@@ -1201,7 +1274,8 @@ export class CodexProcess extends EventEmitter {
|
|
|
1201
1274
|
// not the top-level thread/start payload.
|
|
1202
1275
|
threadConfig.model_reasoning_effort = requestedReasoningEffort;
|
|
1203
1276
|
}
|
|
1204
|
-
if (options?.networkAccessEnabled !== undefined
|
|
1277
|
+
if (options?.networkAccessEnabled !== undefined &&
|
|
1278
|
+
threadParams.permissions === undefined) {
|
|
1205
1279
|
threadParams.sandboxPolicy = {
|
|
1206
1280
|
type: normalizeSandboxMode(options?.sandboxMode ?? "workspace-write"),
|
|
1207
1281
|
networkAccess: options.networkAccessEnabled,
|
|
@@ -1353,7 +1427,7 @@ export class CodexProcess extends EventEmitter {
|
|
|
1353
1427
|
return extractPermissionsFromConfigRead(response, projectLookupPaths, this.platform, profile);
|
|
1354
1428
|
}
|
|
1355
1429
|
async initializeRpcConnection(timeoutMs) {
|
|
1356
|
-
await this.request("initialize", {
|
|
1430
|
+
const response = (await this.request("initialize", {
|
|
1357
1431
|
clientInfo: {
|
|
1358
1432
|
name: "ccpocket_bridge",
|
|
1359
1433
|
version: "1.0.0",
|
|
@@ -1362,8 +1436,12 @@ export class CodexProcess extends EventEmitter {
|
|
|
1362
1436
|
capabilities: {
|
|
1363
1437
|
experimentalApi: true,
|
|
1364
1438
|
},
|
|
1365
|
-
}, timeoutMs);
|
|
1439
|
+
}, timeoutMs));
|
|
1366
1440
|
this.notify("initialized", {});
|
|
1441
|
+
// 0.157.0 is the oldest version verified for built-in permission profiles.
|
|
1442
|
+
const version = response.userAgent?.match(/^\S+\/(\d+)\.(\d+)\.(\d+)(?=\s|$)/);
|
|
1443
|
+
return (version != null &&
|
|
1444
|
+
(Number(version[1]) > 0 || Number(version[2]) >= 157));
|
|
1367
1445
|
}
|
|
1368
1446
|
async readProfileConfig(cwd) {
|
|
1369
1447
|
const response = (await this.request("config/read", {
|
|
@@ -1583,10 +1661,21 @@ export class CodexProcess extends EventEmitter {
|
|
|
1583
1661
|
});
|
|
1584
1662
|
continue;
|
|
1585
1663
|
}
|
|
1664
|
+
const manualVersion = this.manualInputVersion;
|
|
1665
|
+
this.recoveryFailure = null;
|
|
1666
|
+
this.upstreamWillRetry = false;
|
|
1667
|
+
this.inputTurnStarted = false;
|
|
1668
|
+
let startFailure = null;
|
|
1586
1669
|
const { input, tempPaths } = await this.toRpcInput(pendingInput);
|
|
1587
|
-
if (!input
|
|
1670
|
+
if (!input || (pendingInput.recoveryGeneration !== undefined &&
|
|
1671
|
+
(pendingInput.recoveryGeneration !== this.recovery.version || !this.recovery.state.enabled))) {
|
|
1672
|
+
await Promise.all(tempPaths.map((path) => rm(path, { force: true }).catch(() => { })));
|
|
1588
1673
|
continue;
|
|
1589
1674
|
}
|
|
1675
|
+
if (pendingInput.recoveryContinuation && !pendingInput.recoveryEchoed) {
|
|
1676
|
+
pendingInput.recoveryEchoed = true;
|
|
1677
|
+
this.emitMessage({ type: "user_input", text: pendingInput.text, timestamp: new Date().toISOString() });
|
|
1678
|
+
}
|
|
1590
1679
|
this.setStatus("running");
|
|
1591
1680
|
this.lastTokenUsage = null;
|
|
1592
1681
|
const completion = await new Promise((resolve, reject) => {
|
|
@@ -1640,6 +1729,9 @@ export class CodexProcess extends EventEmitter {
|
|
|
1640
1729
|
reject(err instanceof Error ? err : new Error(String(err)));
|
|
1641
1730
|
});
|
|
1642
1731
|
}).catch((err) => {
|
|
1732
|
+
// Only a confirmed turn/start rejection can replay original attachments.
|
|
1733
|
+
if (err instanceof CodexRpcError && !this.inputTurnStarted && !this.pendingTurnId)
|
|
1734
|
+
startFailure = err;
|
|
1643
1735
|
if (!this.stopped) {
|
|
1644
1736
|
const message = err instanceof Error ? err.message : String(err);
|
|
1645
1737
|
this.emitMessage({ type: "error", message });
|
|
@@ -1654,6 +1746,14 @@ export class CodexProcess extends EventEmitter {
|
|
|
1654
1746
|
});
|
|
1655
1747
|
await Promise.all(tempPaths.map((path) => rm(path, { force: true }).catch(() => { })));
|
|
1656
1748
|
void completion;
|
|
1749
|
+
if (!this.stopped && manualVersion === this.manualInputVersion && !this.upstreamWillRetry) {
|
|
1750
|
+
const failure = startFailure ?? this.recoveryFailure;
|
|
1751
|
+
if (failure)
|
|
1752
|
+
this.recovery.schedule(failure, startFailure ? pendingInput : {
|
|
1753
|
+
text: "Automatic recovery: Continue exactly where you left off. Do not repeat completed work. If the task is already complete, stop.",
|
|
1754
|
+
recoveryContinuation: true,
|
|
1755
|
+
});
|
|
1756
|
+
}
|
|
1657
1757
|
}
|
|
1658
1758
|
}
|
|
1659
1759
|
handleStdoutChunk(chunk) {
|
|
@@ -1938,6 +2038,8 @@ export class CodexProcess extends EventEmitter {
|
|
|
1938
2038
|
break;
|
|
1939
2039
|
}
|
|
1940
2040
|
case "turn/started": {
|
|
2041
|
+
this.inputTurnStarted = true;
|
|
2042
|
+
this.recovery.cancel();
|
|
1941
2043
|
const turn = params.turn;
|
|
1942
2044
|
if (typeof turn?.id === "string") {
|
|
1943
2045
|
this.pendingTurnId = turn.id;
|
|
@@ -1959,6 +2061,10 @@ export class CodexProcess extends EventEmitter {
|
|
|
1959
2061
|
}
|
|
1960
2062
|
case "thread/goal/updated": {
|
|
1961
2063
|
try {
|
|
2064
|
+
const goal = parseCodexGoal(params.goal);
|
|
2065
|
+
if (!["active", "usageLimited"].includes(goal.status) ||
|
|
2066
|
+
(typeof goal.tokenBudget === "number" && goal.tokensUsed >= goal.tokenBudget))
|
|
2067
|
+
this.cancelRecovery();
|
|
1962
2068
|
this.emitMessage({
|
|
1963
2069
|
type: "goal_state",
|
|
1964
2070
|
goal: parseCodexGoal(params.goal),
|
|
@@ -1970,6 +2076,7 @@ export class CodexProcess extends EventEmitter {
|
|
|
1970
2076
|
break;
|
|
1971
2077
|
}
|
|
1972
2078
|
case "thread/goal/cleared": {
|
|
2079
|
+
this.cancelRecovery();
|
|
1973
2080
|
this.emitMessage({ type: "goal_state", goal: null });
|
|
1974
2081
|
break;
|
|
1975
2082
|
}
|
|
@@ -2118,6 +2225,7 @@ export class CodexProcess extends EventEmitter {
|
|
|
2118
2225
|
const error = asRecord(params.error);
|
|
2119
2226
|
const message = stringValue(error?.message) ?? "Codex runtime error";
|
|
2120
2227
|
if (params.willRetry === true) {
|
|
2228
|
+
this.upstreamWillRetry = true;
|
|
2121
2229
|
console.warn(`[codex-process] Codex will retry: ${message}`);
|
|
2122
2230
|
break;
|
|
2123
2231
|
}
|
|
@@ -2157,6 +2265,13 @@ export class CodexProcess extends EventEmitter {
|
|
|
2157
2265
|
this.lastTokenUsage = null;
|
|
2158
2266
|
if (status === "failed") {
|
|
2159
2267
|
const errorObj = turn?.error;
|
|
2268
|
+
// A terminal failure ends upstream retry ownership. Warnings alone never
|
|
2269
|
+
// schedule work, and an explicitly retrying terminal payload is excluded.
|
|
2270
|
+
if (this.pendingTurnCompletion && errorObj?.willRetry !== true &&
|
|
2271
|
+
(!this.pendingTurnId || !turn?.id || turn.id === this.pendingTurnId)) {
|
|
2272
|
+
this.upstreamWillRetry = false;
|
|
2273
|
+
this.recoveryFailure = errorObj;
|
|
2274
|
+
}
|
|
2160
2275
|
const message = typeof errorObj?.message === "string"
|
|
2161
2276
|
? errorObj.message
|
|
2162
2277
|
: "Turn failed";
|