@ccpocket/bridge 1.85.0 → 1.85.2

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.
@@ -0,0 +1,5 @@
1
+ /** Bound unusually large tool payloads before retaining all history pages. */
2
+ export declare function compactCodexHistoryItem(item: Record<string, unknown>): {
3
+ item: Record<string, unknown>;
4
+ chars: number;
5
+ };
@@ -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":"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"}
@@ -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;
@@ -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 completeStdoutLine;
348
+ private failStdout;
345
349
  private handleRpcEnvelope;
346
350
  private handleRpcResponse;
347
351
  private handleServerRequest;
@@ -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;
@@ -292,55 +296,133 @@ export class CodexProcess extends EventEmitter {
292
296
  }
293
297
  if (!includeTurns)
294
298
  return thread;
299
+ try {
300
+ // A single autonomous turn can contain thousands of screenshots/tool
301
+ // results. Page turn metadata separately, then hydrate by item count.
302
+ const turns = await this.readTurnPages(threadId, "notLoaded");
303
+ const byId = new Map();
304
+ for (const turn of turns) {
305
+ if (typeof turn.id !== "string")
306
+ throw new Error("Codex turn has no id");
307
+ turn.items = [];
308
+ byId.set(turn.id, turn);
309
+ }
310
+ let retainedChars = JSON.stringify(turns).length;
311
+ await this.readHistoryPages("thread/items/list", {
312
+ threadId, limit: 10, sortDirection: "asc",
313
+ }, (entries) => {
314
+ for (const rawEntry of entries) {
315
+ const entry = asRecord(rawEntry);
316
+ const item = asRecord(entry?.item);
317
+ const turn = typeof entry?.turnId === "string"
318
+ ? byId.get(entry.turnId) : undefined;
319
+ if (!item || !turn) {
320
+ throw new Error("Codex history changed while reading; retry opening the session");
321
+ }
322
+ const compacted = compactCodexHistoryItem(item);
323
+ retainedChars += compacted.chars;
324
+ this.checkHistorySize(retainedChars);
325
+ turn.items.push(compacted.item);
326
+ }
327
+ });
328
+ for (const turn of turns)
329
+ turn.itemsView = "full";
330
+ return { ...thread, turns };
331
+ }
332
+ catch (err) {
333
+ if (err instanceof CodexRpcError && err.code === -32600 &&
334
+ err.message.includes("is not materialized yet")) {
335
+ return { ...thread, turns: [] };
336
+ }
337
+ // Unsupported methods/views permit the compatibility path. Never retry
338
+ // a size/transport failure with a larger full-history response.
339
+ const unsupportedMetadataView = err instanceof CodexRpcError &&
340
+ err.code === -32602 && /unknown variant.*notLoaded/i.test(err.message);
341
+ if (!unsupportedMetadataView &&
342
+ (!(err instanceof CodexRpcError) || err.code !== -32601))
343
+ throw err;
344
+ }
345
+ try {
346
+ return { ...thread, turns: await this.readTurnPages(threadId, "full") };
347
+ }
348
+ catch (err) {
349
+ if (!(err instanceof CodexRpcError) || err.code !== -32601)
350
+ throw err;
351
+ }
352
+ const legacy = (await this.request("thread/read", {
353
+ threadId, includeTurns: true,
354
+ }));
355
+ const legacyThread = asRecord(legacy.thread);
356
+ if (!legacyThread)
357
+ throw new Error("thread/read returned no thread");
358
+ if (Array.isArray(legacyThread.turns)) {
359
+ let chars = 0;
360
+ for (const rawTurn of legacyThread.turns) {
361
+ const turn = asRecord(rawTurn);
362
+ if (!turn || !Array.isArray(turn.items))
363
+ continue;
364
+ turn.items = turn.items.map((rawItem) => {
365
+ const item = asRecord(rawItem);
366
+ if (!item)
367
+ return rawItem;
368
+ const compacted = compactCodexHistoryItem(item);
369
+ chars += compacted.chars;
370
+ this.checkHistorySize(chars);
371
+ return compacted.item;
372
+ });
373
+ }
374
+ }
375
+ return legacyThread;
376
+ }
377
+ checkHistorySize(chars) {
378
+ if (chars > MAX_RETAINED_HISTORY_CHARS) {
379
+ throw new Error("Codex display history exceeds the Bridge 64 Mi-character limit");
380
+ }
381
+ }
382
+ async readTurnPages(threadId, itemsView) {
295
383
  const turns = [];
384
+ let retainedChars = 0;
385
+ await this.readHistoryPages("thread/turns/list", {
386
+ threadId, limit: itemsView === "full" ? 1 : 50,
387
+ sortDirection: "asc", itemsView,
388
+ }, (data) => {
389
+ for (const rawTurn of data) {
390
+ const turn = asRecord(rawTurn);
391
+ if (!turn)
392
+ throw new Error("thread/turns/list returned an invalid turn");
393
+ if (itemsView === "full" && Array.isArray(turn.items)) {
394
+ turn.items = turn.items.map((rawItem) => {
395
+ const item = asRecord(rawItem);
396
+ return item ? compactCodexHistoryItem(item).item : rawItem;
397
+ });
398
+ }
399
+ retainedChars += JSON.stringify(turn).length;
400
+ this.checkHistorySize(retainedChars);
401
+ turns.push(turn);
402
+ }
403
+ });
404
+ return turns;
405
+ }
406
+ async readHistoryPages(method, params, consume) {
296
407
  const seenCursors = new Set();
297
408
  let cursor;
298
409
  do {
299
- let page;
300
- try {
301
- page = (await this.request("thread/turns/list", {
302
- threadId,
303
- ...(cursor !== undefined ? { cursor } : {}),
304
- limit: 50,
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);
410
+ const page = (await this.request(method, {
411
+ ...params, ...(cursor !== undefined ? { cursor } : {}),
412
+ }));
413
+ if (!Array.isArray(page.data))
414
+ throw new Error(`${method} returned invalid data`);
415
+ consume(page.data);
332
416
  if (page.nextCursor != null && typeof page.nextCursor !== "string") {
333
- throw new Error("thread/turns/list returned an invalid cursor");
417
+ throw new Error(`${method} returned an invalid cursor`);
334
418
  }
335
419
  cursor = page.nextCursor ?? undefined;
336
420
  if (cursor !== undefined) {
337
- if (seenCursors.has(cursor)) {
338
- throw new Error("thread/turns/list returned a repeated cursor");
339
- }
421
+ if (seenCursors.has(cursor))
422
+ throw new Error(`${method} returned a repeated cursor`);
340
423
  seenCursors.add(cursor);
341
424
  }
342
425
  } while (cursor !== undefined);
343
- return { ...thread, turns };
344
426
  }
345
427
  /** Fork at a complete turn boundary without mutating the source history. */
346
428
  async forkThreadAtUserTurn(userTurnOrdinal) {
@@ -520,6 +602,8 @@ export class CodexProcess extends EventEmitter {
520
602
  }
521
603
  this.pendingApprovals.clear();
522
604
  this.pendingUserInputs.clear();
605
+ this.stdoutLineChunks = [];
606
+ this.stdoutLineChars = 0;
523
607
  this.cleanupSteerTempPaths();
524
608
  this.rejectAllPending(new Error("stopped"));
525
609
  if (this.transport) {
@@ -570,6 +654,7 @@ export class CodexProcess extends EventEmitter {
570
654
  this._pendingPlanInput = null;
571
655
  this._projectPath = projectPath;
572
656
  this.stdoutLineChunks = [];
657
+ this.stdoutLineChars = 0;
573
658
  }
574
659
  launchAppServer(projectPath, options) {
575
660
  const sandboxLog = options?.sandboxMode ?? "config";
@@ -1566,17 +1651,37 @@ export class CodexProcess extends EventEmitter {
1566
1651
  }
1567
1652
  }
1568
1653
  handleStdoutChunk(chunk) {
1654
+ if (this.stopped)
1655
+ return;
1569
1656
  let lineStart = 0;
1570
1657
  while (lineStart < chunk.length) {
1571
1658
  const newlineIndex = chunk.indexOf("\n", lineStart);
1659
+ const end = newlineIndex < 0 ? chunk.length : newlineIndex;
1660
+ const fragment = chunk.slice(lineStart, end);
1661
+ this.stdoutLineChars += fragment.length;
1662
+ if (this.stdoutLineChars > MAX_STDOUT_LINE_CHARS) {
1663
+ this.failStdout(new Error("Codex app-server response exceeds the Bridge 64 Mi-character limit " +
1664
+ `(pending: ${[...this.pendingRpc.values()].map((rpc) => rpc.method).join(", ") || "notification"}). ` +
1665
+ "Only this Codex connection was closed; Bridge is still running."));
1666
+ return;
1667
+ }
1572
1668
  if (newlineIndex < 0) {
1573
- this.stdoutLineChunks.push(chunk.slice(lineStart));
1669
+ this.stdoutLineChunks.push(fragment);
1670
+ return;
1671
+ }
1672
+ let line;
1673
+ try {
1674
+ // Guard both fragmented and single-chunk records before joining, and
1675
+ // keep allocation errors inside the connection's failure boundary.
1676
+ this.stdoutLineChunks.push(fragment);
1677
+ line = this.stdoutLineChunks.join("").trim();
1678
+ }
1679
+ catch (err) {
1680
+ this.failStdout(err instanceof Error ? err : new Error(String(err)));
1574
1681
  return;
1575
1682
  }
1576
- const lineFragment = chunk.slice(lineStart, newlineIndex);
1577
- const line = this.stdoutLineChunks.length === 0
1578
- ? lineFragment.trim()
1579
- : this.completeStdoutLine(lineFragment);
1683
+ this.stdoutLineChunks = [];
1684
+ this.stdoutLineChars = 0;
1580
1685
  lineStart = newlineIndex + 1;
1581
1686
  if (!line)
1582
1687
  continue;
@@ -1585,7 +1690,7 @@ export class CodexProcess extends EventEmitter {
1585
1690
  this.handleRpcEnvelope(envelope);
1586
1691
  }
1587
1692
  catch (err) {
1588
- console.warn(`[codex-process] failed to parse app-server JSON line: ${line.slice(0, 200)}`);
1693
+ console.warn("[codex-process] failed to parse app-server JSON line");
1589
1694
  if (!this.stopped) {
1590
1695
  this.emitMessage({
1591
1696
  type: "error",
@@ -1595,11 +1700,12 @@ export class CodexProcess extends EventEmitter {
1595
1700
  }
1596
1701
  }
1597
1702
  }
1598
- completeStdoutLine(finalChunk) {
1599
- this.stdoutLineChunks.push(finalChunk);
1600
- const line = this.stdoutLineChunks.join("").trim();
1601
- this.stdoutLineChunks = [];
1602
- return line;
1703
+ failStdout(error) {
1704
+ console.error(`[codex-process] ${error.message}`);
1705
+ this.rejectReadiness(error);
1706
+ this.rejectAllPending(error);
1707
+ this.emitMessage({ type: "error", message: error.message });
1708
+ this.stop();
1603
1709
  }
1604
1710
  handleRpcEnvelope(envelope) {
1605
1711
  if (envelope.id != null &&