@ccpocket/bridge 1.85.1 → 1.86.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 +1 -0
- package/dist/codex-history.d.ts +5 -0
- package/dist/codex-history.js +53 -0
- package/dist/codex-history.js.map +1 -0
- package/dist/codex-process.d.ts +6 -2
- package/dist/codex-process.js +157 -53
- package/dist/codex-process.js.map +1 -1
- package/dist/goal-notifications.d.ts +13 -0
- package/dist/goal-notifications.js +44 -0
- package/dist/goal-notifications.js.map +1 -0
- package/dist/parser.d.ts +3 -0
- package/dist/parser.js.map +1 -1
- package/dist/push-i18n.js +20 -0
- package/dist/push-i18n.js.map +1 -1
- package/dist/repository-root.d.ts +27 -0
- package/dist/repository-root.js +116 -0
- package/dist/repository-root.js.map +1 -0
- package/dist/session.js +39 -1
- package/dist/session.js.map +1 -1
- package/dist/sessions-index.d.ts +3 -1
- package/dist/sessions-index.js +104 -45
- package/dist/sessions-index.js.map +1 -1
- package/dist/websocket.js +22 -0
- package/dist/websocket.js.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -39,6 +39,7 @@ ccpocket-bridge --version
|
|
|
39
39
|
| Environment Variable | Default | Description |
|
|
40
40
|
|---------------------|---------|-------------|
|
|
41
41
|
| `BRIDGE_PORT` | `8765` | WebSocket port |
|
|
42
|
+
| `BRIDGE_NOTIFY_GOAL_TURN_COMPLETED` | `false` | Set exactly to `true` to notify intermediate Codex turns while a Goal is active. Goal completion, blockers, limits, approvals and errors remain enabled. Read at session creation; restart Bridge to apply to all sessions. |
|
|
42
43
|
| `BRIDGE_HOST` | `0.0.0.0` | Bind address |
|
|
43
44
|
| `BRIDGE_API_KEY` | (none) | API key authentication (enabled when set) |
|
|
44
45
|
| `BRIDGE_ALLOWED_DIRS` | `$HOME` | Comma-separated list of project directories the Bridge may access; set exactly to `*` to allow any directory |
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
// These limits apply only to the Bridge's display copy, never to Codex's
|
|
2
|
+
// persisted history or the model's context.
|
|
3
|
+
const MAX_TOOL_ITEM_CHARS = 256 * 1024;
|
|
4
|
+
const MAX_TOOL_STRING_CHARS = 16 * 1024;
|
|
5
|
+
const OMITTED = "[Large tool detail omitted from Bridge history]";
|
|
6
|
+
const TRUNCATED = "\n[Truncated in Bridge history]";
|
|
7
|
+
const TOOL_TYPES = new Set([
|
|
8
|
+
"commandExecution", "fileChange", "mcpToolCall", "dynamicToolCall",
|
|
9
|
+
"functionCallOutput", "imageGeneration",
|
|
10
|
+
]);
|
|
11
|
+
/** Bound unusually large tool payloads before retaining all history pages. */
|
|
12
|
+
export function compactCodexHistoryItem(item) {
|
|
13
|
+
const chars = JSON.stringify(item).length;
|
|
14
|
+
if (!TOOL_TYPES.has(String(item.type)) || chars <= MAX_TOOL_ITEM_CHARS) {
|
|
15
|
+
return { item, chars };
|
|
16
|
+
}
|
|
17
|
+
const compacted = compactValue(item, 0);
|
|
18
|
+
// Generated images use a bare base64 string. Never pass truncated base64 to
|
|
19
|
+
// the image decoder; show an explicit text result instead.
|
|
20
|
+
if (item.type === "imageGeneration") {
|
|
21
|
+
delete compacted.result;
|
|
22
|
+
compacted.revisedPrompt = `${compacted.revisedPrompt ?? ""}\n${OMITTED}`;
|
|
23
|
+
}
|
|
24
|
+
return { item: compacted, chars: JSON.stringify(compacted).length };
|
|
25
|
+
}
|
|
26
|
+
function compactValue(value, depth) {
|
|
27
|
+
if (typeof value === "string") {
|
|
28
|
+
if (value.length <= MAX_TOOL_STRING_CHARS)
|
|
29
|
+
return value;
|
|
30
|
+
if (value.startsWith("data:image/"))
|
|
31
|
+
return OMITTED;
|
|
32
|
+
return value.slice(0, MAX_TOOL_STRING_CHARS) + TRUNCATED;
|
|
33
|
+
}
|
|
34
|
+
if (value == null || typeof value !== "object")
|
|
35
|
+
return value;
|
|
36
|
+
if (depth >= 20)
|
|
37
|
+
return OMITTED;
|
|
38
|
+
if (Array.isArray(value)) {
|
|
39
|
+
return value.map((entry) => compactValue(entry, depth + 1));
|
|
40
|
+
}
|
|
41
|
+
const record = value;
|
|
42
|
+
if (record.type === "image" && typeof record.data === "string"
|
|
43
|
+
&& record.data.length > MAX_TOOL_STRING_CHARS) {
|
|
44
|
+
return { type: "text", text: OMITTED };
|
|
45
|
+
}
|
|
46
|
+
if (record.type === "inputImage" && typeof record.imageUrl === "string"
|
|
47
|
+
&& record.imageUrl.length > MAX_TOOL_STRING_CHARS) {
|
|
48
|
+
return { type: "inputText", text: OMITTED };
|
|
49
|
+
}
|
|
50
|
+
return Object.fromEntries(Object.entries(record)
|
|
51
|
+
.map(([key, entry]) => [key, compactValue(entry, depth + 1)]));
|
|
52
|
+
}
|
|
53
|
+
//# sourceMappingURL=codex-history.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"codex-history.js","sourceRoot":"","sources":["../src/codex-history.ts"],"names":[],"mappings":"AAAA,yEAAyE;AACzE,4CAA4C;AAC5C,MAAM,mBAAmB,GAAG,GAAG,GAAG,IAAI,CAAC;AACvC,MAAM,qBAAqB,GAAG,EAAE,GAAG,IAAI,CAAC;AACxC,MAAM,OAAO,GAAG,iDAAiD,CAAC;AAClE,MAAM,SAAS,GAAG,iCAAiC,CAAC;AAEpD,MAAM,UAAU,GAAG,IAAI,GAAG,CAAC;IACzB,kBAAkB,EAAE,YAAY,EAAE,aAAa,EAAE,iBAAiB;IAClE,oBAAoB,EAAE,iBAAiB;CACxC,CAAC,CAAC;AAEH,8EAA8E;AAC9E,MAAM,UAAU,uBAAuB,CAAC,IAA6B;IAInE,MAAM,KAAK,GAAG,IAAI,CAAC,SAAS,CAAC,IAAI,CAAC,CAAC,MAAM,CAAC;IAC1C,IAAI,CAAC,UAAU,CAAC,GAAG,CAAC,MAAM,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,IAAI,KAAK,IAAI,mBAAmB,EAAE,CAAC;QACvE,OAAO,EAAE,IAAI,EAAE,KAAK,EAAE,CAAC;IACzB,CAAC;IACD,MAAM,SAAS,GAAG,YAAY,CAAC,IAAI,EAAE,CAAC,CAA4B,CAAC;IACnE,4EAA4E;IAC5E,2DAA2D;IAC3D,IAAI,IAAI,CAAC,IAAI,KAAK,iBAAiB,EAAE,CAAC;QACpC,OAAO,SAAS,CAAC,MAAM,CAAC;QACxB,SAAS,CAAC,aAAa,GAAG,GAAG,SAAS,CAAC,aAAa,IAAI,EAAE,KAAK,OAAO,EAAE,CAAC;IAC3E,CAAC;IACD,OAAO,EAAE,IAAI,EAAE,SAAS,EAAE,KAAK,EAAE,IAAI,CAAC,SAAS,CAAC,SAAS,CAAC,CAAC,MAAM,EAAE,CAAC;AACtE,CAAC;AAED,SAAS,YAAY,CAAC,KAAc,EAAE,KAAa;IACjD,IAAI,OAAO,KAAK,KAAK,QAAQ,EAAE,CAAC;QAC9B,IAAI,KAAK,CAAC,MAAM,IAAI,qBAAqB;YAAE,OAAO,KAAK,CAAC;QACxD,IAAI,KAAK,CAAC,UAAU,CAAC,aAAa,CAAC;YAAE,OAAO,OAAO,CAAC;QACpD,OAAO,KAAK,CAAC,KAAK,CAAC,CAAC,EAAE,qBAAqB,CAAC,GAAG,SAAS,CAAC;IAC3D,CAAC;IACD,IAAI,KAAK,IAAI,IAAI,IAAI,OAAO,KAAK,KAAK,QAAQ;QAAE,OAAO,KAAK,CAAC;IAC7D,IAAI,KAAK,IAAI,EAAE;QAAE,OAAO,OAAO,CAAC;IAChC,IAAI,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC,EAAE,CAAC;QACzB,OAAO,KAAK,CAAC,GAAG,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,YAAY,CAAC,KAAK,EAAE,KAAK,GAAG,CAAC,CAAC,CAAC,CAAC;IAC9D,CAAC;IACD,MAAM,MAAM,GAAG,KAAgC,CAAC;IAChD,IAAI,MAAM,CAAC,IAAI,KAAK,OAAO,IAAI,OAAO,MAAM,CAAC,IAAI,KAAK,QAAQ;WACvD,MAAM,CAAC,IAAI,CAAC,MAAM,GAAG,qBAAqB,EAAE,CAAC;QAClD,OAAO,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,OAAO,EAAE,CAAC;IACzC,CAAC;IACD,IAAI,MAAM,CAAC,IAAI,KAAK,YAAY,IAAI,OAAO,MAAM,CAAC,QAAQ,KAAK,QAAQ;WAChE,MAAM,CAAC,QAAQ,CAAC,MAAM,GAAG,qBAAqB,EAAE,CAAC;QACtD,OAAO,EAAE,IAAI,EAAE,WAAW,EAAE,IAAI,EAAE,OAAO,EAAE,CAAC;IAC9C,CAAC;IACD,OAAO,MAAM,CAAC,WAAW,CAAC,MAAM,CAAC,OAAO,CAAC,MAAM,CAAC;SAC7C,GAAG,CAAC,CAAC,CAAC,GAAG,EAAE,KAAK,CAAC,EAAE,EAAE,CAAC,CAAC,GAAG,EAAE,YAAY,CAAC,KAAK,EAAE,KAAK,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;AACnE,CAAC"}
|
package/dist/codex-process.d.ts
CHANGED
|
@@ -146,6 +146,7 @@ export declare class CodexProcess extends EventEmitter<CodexProcessEvents> {
|
|
|
146
146
|
* copied at most once instead of repeatedly rebuilding one growing string.
|
|
147
147
|
*/
|
|
148
148
|
private stdoutLineChunks;
|
|
149
|
+
private stdoutLineChars;
|
|
149
150
|
private _approvalPolicy;
|
|
150
151
|
private _approvalsReviewer;
|
|
151
152
|
private _codexPermissionsMode;
|
|
@@ -211,7 +212,7 @@ export declare class CodexProcess extends EventEmitter<CodexProcessEvents> {
|
|
|
211
212
|
*/
|
|
212
213
|
renameThread(name: string): Promise<void>;
|
|
213
214
|
/** Read the persisted goal attached to this Codex thread. */
|
|
214
|
-
getGoal(): Promise<CodexGoal | null>;
|
|
215
|
+
getGoal(timeoutMs?: number): Promise<CodexGoal | null>;
|
|
215
216
|
/** Create or update the persisted goal attached to this Codex thread. */
|
|
216
217
|
setGoal(update: {
|
|
217
218
|
objective?: string;
|
|
@@ -226,6 +227,9 @@ export declare class CodexProcess extends EventEmitter<CodexProcessEvents> {
|
|
|
226
227
|
*/
|
|
227
228
|
archiveThread(threadId: string): Promise<void>;
|
|
228
229
|
readThread(threadId: string, includeTurns?: boolean): Promise<Record<string, unknown>>;
|
|
230
|
+
private checkHistorySize;
|
|
231
|
+
private readTurnPages;
|
|
232
|
+
private readHistoryPages;
|
|
229
233
|
/** Fork at a complete turn boundary without mutating the source history. */
|
|
230
234
|
forkThreadAtUserTurn(userTurnOrdinal: number): Promise<{
|
|
231
235
|
threadId: string;
|
|
@@ -341,7 +345,7 @@ export declare class CodexProcess extends EventEmitter<CodexProcessEvents> {
|
|
|
341
345
|
private emitCompletionEntitiesSnapshot;
|
|
342
346
|
private runInputLoop;
|
|
343
347
|
private handleStdoutChunk;
|
|
344
|
-
private
|
|
348
|
+
private failStdout;
|
|
345
349
|
private handleRpcEnvelope;
|
|
346
350
|
private handleRpcResponse;
|
|
347
351
|
private handleServerRequest;
|
package/dist/codex-process.js
CHANGED
|
@@ -5,6 +5,7 @@ import { tmpdir } from "node:os";
|
|
|
5
5
|
import { dirname, join } from "node:path";
|
|
6
6
|
import { rm, writeFile } from "node:fs/promises";
|
|
7
7
|
import { createCodexTransport, buildCodexSpawnSpec, } from "./codex-transport.js";
|
|
8
|
+
import { compactCodexHistoryItem } from "./codex-history.js";
|
|
8
9
|
import { codexThreadToSessionHistory } from "./sessions-index.js";
|
|
9
10
|
import { codexCliJoinTarget } from "./codex-app-server-config.js";
|
|
10
11
|
import { normalizeCodexServiceTierForClient, normalizeCodexServiceTierForRpc, } from "./codex-service-tier.js";
|
|
@@ -13,6 +14,8 @@ export { buildCodexSpawnSpec };
|
|
|
13
14
|
const DEFAULT_CODEX_MODEL = "gpt-5.5";
|
|
14
15
|
const COMPLETION_FETCH_COOLDOWN_MS = 1000;
|
|
15
16
|
const ARCHIVE_RPC_TIMEOUT_MS = 15_000;
|
|
17
|
+
const MAX_STDOUT_LINE_CHARS = 64 * 1024 * 1024;
|
|
18
|
+
const MAX_RETAINED_HISTORY_CHARS = 64 * 1024 * 1024;
|
|
16
19
|
const UNKNOWN_AGENT_ITEM_ID = "__unknown_agent_message__";
|
|
17
20
|
const CODEX_CLI_NOT_FOUND_MESSAGE = "Codex CLI is not installed or not available on PATH on the Bridge machine. Install it with `curl -fsSL https://chatgpt.com/codex/install.sh | sh`, then restart Bridge.";
|
|
18
21
|
export class CodexRpcError extends Error {
|
|
@@ -102,6 +105,7 @@ export class CodexProcess extends EventEmitter {
|
|
|
102
105
|
* copied at most once instead of repeatedly rebuilding one growing string.
|
|
103
106
|
*/
|
|
104
107
|
stdoutLineChunks = [];
|
|
108
|
+
stdoutLineChars = 0;
|
|
105
109
|
// Collaboration mode & plan completion state
|
|
106
110
|
_approvalPolicy = undefined;
|
|
107
111
|
_approvalsReviewer = undefined;
|
|
@@ -237,13 +241,11 @@ export class CodexProcess extends EventEmitter {
|
|
|
237
241
|
});
|
|
238
242
|
}
|
|
239
243
|
/** Read the persisted goal attached to this Codex thread. */
|
|
240
|
-
async getGoal() {
|
|
244
|
+
async getGoal(timeoutMs = 3_000) {
|
|
241
245
|
if (!this._threadId) {
|
|
242
246
|
throw new Error("No thread ID available for goal lookup");
|
|
243
247
|
}
|
|
244
|
-
const response = (await this.request("thread/goal/get", {
|
|
245
|
-
threadId: this._threadId,
|
|
246
|
-
}));
|
|
248
|
+
const response = (await this.request("thread/goal/get", { threadId: this._threadId }, timeoutMs));
|
|
247
249
|
return response.goal == null ? null : parseCodexGoal(response.goal);
|
|
248
250
|
}
|
|
249
251
|
/** Create or update the persisted goal attached to this Codex thread. */
|
|
@@ -292,55 +294,133 @@ export class CodexProcess extends EventEmitter {
|
|
|
292
294
|
}
|
|
293
295
|
if (!includeTurns)
|
|
294
296
|
return thread;
|
|
297
|
+
try {
|
|
298
|
+
// A single autonomous turn can contain thousands of screenshots/tool
|
|
299
|
+
// results. Page turn metadata separately, then hydrate by item count.
|
|
300
|
+
const turns = await this.readTurnPages(threadId, "notLoaded");
|
|
301
|
+
const byId = new Map();
|
|
302
|
+
for (const turn of turns) {
|
|
303
|
+
if (typeof turn.id !== "string")
|
|
304
|
+
throw new Error("Codex turn has no id");
|
|
305
|
+
turn.items = [];
|
|
306
|
+
byId.set(turn.id, turn);
|
|
307
|
+
}
|
|
308
|
+
let retainedChars = JSON.stringify(turns).length;
|
|
309
|
+
await this.readHistoryPages("thread/items/list", {
|
|
310
|
+
threadId, limit: 10, sortDirection: "asc",
|
|
311
|
+
}, (entries) => {
|
|
312
|
+
for (const rawEntry of entries) {
|
|
313
|
+
const entry = asRecord(rawEntry);
|
|
314
|
+
const item = asRecord(entry?.item);
|
|
315
|
+
const turn = typeof entry?.turnId === "string"
|
|
316
|
+
? byId.get(entry.turnId) : undefined;
|
|
317
|
+
if (!item || !turn) {
|
|
318
|
+
throw new Error("Codex history changed while reading; retry opening the session");
|
|
319
|
+
}
|
|
320
|
+
const compacted = compactCodexHistoryItem(item);
|
|
321
|
+
retainedChars += compacted.chars;
|
|
322
|
+
this.checkHistorySize(retainedChars);
|
|
323
|
+
turn.items.push(compacted.item);
|
|
324
|
+
}
|
|
325
|
+
});
|
|
326
|
+
for (const turn of turns)
|
|
327
|
+
turn.itemsView = "full";
|
|
328
|
+
return { ...thread, turns };
|
|
329
|
+
}
|
|
330
|
+
catch (err) {
|
|
331
|
+
if (err instanceof CodexRpcError && err.code === -32600 &&
|
|
332
|
+
err.message.includes("is not materialized yet")) {
|
|
333
|
+
return { ...thread, turns: [] };
|
|
334
|
+
}
|
|
335
|
+
// Unsupported methods/views permit the compatibility path. Never retry
|
|
336
|
+
// a size/transport failure with a larger full-history response.
|
|
337
|
+
const unsupportedMetadataView = err instanceof CodexRpcError &&
|
|
338
|
+
err.code === -32602 && /unknown variant.*notLoaded/i.test(err.message);
|
|
339
|
+
if (!unsupportedMetadataView &&
|
|
340
|
+
(!(err instanceof CodexRpcError) || err.code !== -32601))
|
|
341
|
+
throw err;
|
|
342
|
+
}
|
|
343
|
+
try {
|
|
344
|
+
return { ...thread, turns: await this.readTurnPages(threadId, "full") };
|
|
345
|
+
}
|
|
346
|
+
catch (err) {
|
|
347
|
+
if (!(err instanceof CodexRpcError) || err.code !== -32601)
|
|
348
|
+
throw err;
|
|
349
|
+
}
|
|
350
|
+
const legacy = (await this.request("thread/read", {
|
|
351
|
+
threadId, includeTurns: true,
|
|
352
|
+
}));
|
|
353
|
+
const legacyThread = asRecord(legacy.thread);
|
|
354
|
+
if (!legacyThread)
|
|
355
|
+
throw new Error("thread/read returned no thread");
|
|
356
|
+
if (Array.isArray(legacyThread.turns)) {
|
|
357
|
+
let chars = 0;
|
|
358
|
+
for (const rawTurn of legacyThread.turns) {
|
|
359
|
+
const turn = asRecord(rawTurn);
|
|
360
|
+
if (!turn || !Array.isArray(turn.items))
|
|
361
|
+
continue;
|
|
362
|
+
turn.items = turn.items.map((rawItem) => {
|
|
363
|
+
const item = asRecord(rawItem);
|
|
364
|
+
if (!item)
|
|
365
|
+
return rawItem;
|
|
366
|
+
const compacted = compactCodexHistoryItem(item);
|
|
367
|
+
chars += compacted.chars;
|
|
368
|
+
this.checkHistorySize(chars);
|
|
369
|
+
return compacted.item;
|
|
370
|
+
});
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
return legacyThread;
|
|
374
|
+
}
|
|
375
|
+
checkHistorySize(chars) {
|
|
376
|
+
if (chars > MAX_RETAINED_HISTORY_CHARS) {
|
|
377
|
+
throw new Error("Codex display history exceeds the Bridge 64 Mi-character limit");
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
async readTurnPages(threadId, itemsView) {
|
|
295
381
|
const turns = [];
|
|
382
|
+
let retainedChars = 0;
|
|
383
|
+
await this.readHistoryPages("thread/turns/list", {
|
|
384
|
+
threadId, limit: itemsView === "full" ? 1 : 50,
|
|
385
|
+
sortDirection: "asc", itemsView,
|
|
386
|
+
}, (data) => {
|
|
387
|
+
for (const rawTurn of data) {
|
|
388
|
+
const turn = asRecord(rawTurn);
|
|
389
|
+
if (!turn)
|
|
390
|
+
throw new Error("thread/turns/list returned an invalid turn");
|
|
391
|
+
if (itemsView === "full" && Array.isArray(turn.items)) {
|
|
392
|
+
turn.items = turn.items.map((rawItem) => {
|
|
393
|
+
const item = asRecord(rawItem);
|
|
394
|
+
return item ? compactCodexHistoryItem(item).item : rawItem;
|
|
395
|
+
});
|
|
396
|
+
}
|
|
397
|
+
retainedChars += JSON.stringify(turn).length;
|
|
398
|
+
this.checkHistorySize(retainedChars);
|
|
399
|
+
turns.push(turn);
|
|
400
|
+
}
|
|
401
|
+
});
|
|
402
|
+
return turns;
|
|
403
|
+
}
|
|
404
|
+
async readHistoryPages(method, params, consume) {
|
|
296
405
|
const seenCursors = new Set();
|
|
297
406
|
let cursor;
|
|
298
407
|
do {
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
sortDirection: "asc",
|
|
306
|
-
itemsView: "full",
|
|
307
|
-
}));
|
|
308
|
-
}
|
|
309
|
-
catch (err) {
|
|
310
|
-
// New threads have metadata but no persisted turn history yet.
|
|
311
|
-
if (cursor === undefined &&
|
|
312
|
-
err instanceof CodexRpcError &&
|
|
313
|
-
err.code === -32600 &&
|
|
314
|
-
err.message.includes("is not materialized yet; thread/turns/list is unavailable before first user message")) {
|
|
315
|
-
return { ...thread, turns: [] };
|
|
316
|
-
}
|
|
317
|
-
// Older app-servers do not expose turn pagination.
|
|
318
|
-
if (!(err instanceof CodexRpcError) || err.code !== -32601)
|
|
319
|
-
throw err;
|
|
320
|
-
const legacy = (await this.request("thread/read", {
|
|
321
|
-
threadId,
|
|
322
|
-
includeTurns: true,
|
|
323
|
-
}));
|
|
324
|
-
if (!legacy.thread)
|
|
325
|
-
throw new Error("thread/read returned no thread");
|
|
326
|
-
return legacy.thread;
|
|
327
|
-
}
|
|
328
|
-
if (!Array.isArray(page.data)) {
|
|
329
|
-
throw new Error("thread/turns/list returned invalid data");
|
|
330
|
-
}
|
|
331
|
-
turns.push(...page.data);
|
|
408
|
+
const page = (await this.request(method, {
|
|
409
|
+
...params, ...(cursor !== undefined ? { cursor } : {}),
|
|
410
|
+
}));
|
|
411
|
+
if (!Array.isArray(page.data))
|
|
412
|
+
throw new Error(`${method} returned invalid data`);
|
|
413
|
+
consume(page.data);
|
|
332
414
|
if (page.nextCursor != null && typeof page.nextCursor !== "string") {
|
|
333
|
-
throw new Error(
|
|
415
|
+
throw new Error(`${method} returned an invalid cursor`);
|
|
334
416
|
}
|
|
335
417
|
cursor = page.nextCursor ?? undefined;
|
|
336
418
|
if (cursor !== undefined) {
|
|
337
|
-
if (seenCursors.has(cursor))
|
|
338
|
-
throw new Error(
|
|
339
|
-
}
|
|
419
|
+
if (seenCursors.has(cursor))
|
|
420
|
+
throw new Error(`${method} returned a repeated cursor`);
|
|
340
421
|
seenCursors.add(cursor);
|
|
341
422
|
}
|
|
342
423
|
} while (cursor !== undefined);
|
|
343
|
-
return { ...thread, turns };
|
|
344
424
|
}
|
|
345
425
|
/** Fork at a complete turn boundary without mutating the source history. */
|
|
346
426
|
async forkThreadAtUserTurn(userTurnOrdinal) {
|
|
@@ -520,6 +600,8 @@ export class CodexProcess extends EventEmitter {
|
|
|
520
600
|
}
|
|
521
601
|
this.pendingApprovals.clear();
|
|
522
602
|
this.pendingUserInputs.clear();
|
|
603
|
+
this.stdoutLineChunks = [];
|
|
604
|
+
this.stdoutLineChars = 0;
|
|
523
605
|
this.cleanupSteerTempPaths();
|
|
524
606
|
this.rejectAllPending(new Error("stopped"));
|
|
525
607
|
if (this.transport) {
|
|
@@ -570,6 +652,7 @@ export class CodexProcess extends EventEmitter {
|
|
|
570
652
|
this._pendingPlanInput = null;
|
|
571
653
|
this._projectPath = projectPath;
|
|
572
654
|
this.stdoutLineChunks = [];
|
|
655
|
+
this.stdoutLineChars = 0;
|
|
573
656
|
}
|
|
574
657
|
launchAppServer(projectPath, options) {
|
|
575
658
|
const sandboxLog = options?.sandboxMode ?? "config";
|
|
@@ -1566,17 +1649,37 @@ export class CodexProcess extends EventEmitter {
|
|
|
1566
1649
|
}
|
|
1567
1650
|
}
|
|
1568
1651
|
handleStdoutChunk(chunk) {
|
|
1652
|
+
if (this.stopped)
|
|
1653
|
+
return;
|
|
1569
1654
|
let lineStart = 0;
|
|
1570
1655
|
while (lineStart < chunk.length) {
|
|
1571
1656
|
const newlineIndex = chunk.indexOf("\n", lineStart);
|
|
1657
|
+
const end = newlineIndex < 0 ? chunk.length : newlineIndex;
|
|
1658
|
+
const fragment = chunk.slice(lineStart, end);
|
|
1659
|
+
this.stdoutLineChars += fragment.length;
|
|
1660
|
+
if (this.stdoutLineChars > MAX_STDOUT_LINE_CHARS) {
|
|
1661
|
+
this.failStdout(new Error("Codex app-server response exceeds the Bridge 64 Mi-character limit " +
|
|
1662
|
+
`(pending: ${[...this.pendingRpc.values()].map((rpc) => rpc.method).join(", ") || "notification"}). ` +
|
|
1663
|
+
"Only this Codex connection was closed; Bridge is still running."));
|
|
1664
|
+
return;
|
|
1665
|
+
}
|
|
1572
1666
|
if (newlineIndex < 0) {
|
|
1573
|
-
this.stdoutLineChunks.push(
|
|
1667
|
+
this.stdoutLineChunks.push(fragment);
|
|
1574
1668
|
return;
|
|
1575
1669
|
}
|
|
1576
|
-
|
|
1577
|
-
|
|
1578
|
-
|
|
1579
|
-
|
|
1670
|
+
let line;
|
|
1671
|
+
try {
|
|
1672
|
+
// Guard both fragmented and single-chunk records before joining, and
|
|
1673
|
+
// keep allocation errors inside the connection's failure boundary.
|
|
1674
|
+
this.stdoutLineChunks.push(fragment);
|
|
1675
|
+
line = this.stdoutLineChunks.join("").trim();
|
|
1676
|
+
}
|
|
1677
|
+
catch (err) {
|
|
1678
|
+
this.failStdout(err instanceof Error ? err : new Error(String(err)));
|
|
1679
|
+
return;
|
|
1680
|
+
}
|
|
1681
|
+
this.stdoutLineChunks = [];
|
|
1682
|
+
this.stdoutLineChars = 0;
|
|
1580
1683
|
lineStart = newlineIndex + 1;
|
|
1581
1684
|
if (!line)
|
|
1582
1685
|
continue;
|
|
@@ -1585,7 +1688,7 @@ export class CodexProcess extends EventEmitter {
|
|
|
1585
1688
|
this.handleRpcEnvelope(envelope);
|
|
1586
1689
|
}
|
|
1587
1690
|
catch (err) {
|
|
1588
|
-
console.warn(
|
|
1691
|
+
console.warn("[codex-process] failed to parse app-server JSON line");
|
|
1589
1692
|
if (!this.stopped) {
|
|
1590
1693
|
this.emitMessage({
|
|
1591
1694
|
type: "error",
|
|
@@ -1595,11 +1698,12 @@ export class CodexProcess extends EventEmitter {
|
|
|
1595
1698
|
}
|
|
1596
1699
|
}
|
|
1597
1700
|
}
|
|
1598
|
-
|
|
1599
|
-
|
|
1600
|
-
|
|
1601
|
-
this.
|
|
1602
|
-
|
|
1701
|
+
failStdout(error) {
|
|
1702
|
+
console.error(`[codex-process] ${error.message}`);
|
|
1703
|
+
this.rejectReadiness(error);
|
|
1704
|
+
this.rejectAllPending(error);
|
|
1705
|
+
this.emitMessage({ type: "error", message: error.message });
|
|
1706
|
+
this.stop();
|
|
1603
1707
|
}
|
|
1604
1708
|
handleRpcEnvelope(envelope) {
|
|
1605
1709
|
if (envelope.id != null &&
|