@ccpocket/bridge 1.81.4 → 1.81.5

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.
@@ -226,9 +226,8 @@ export declare class CodexProcess extends EventEmitter<CodexProcessEvents> {
226
226
  */
227
227
  archiveThread(threadId: string): Promise<void>;
228
228
  readThread(threadId: string, includeTurns?: boolean): Promise<Record<string, unknown>>;
229
- rollbackThread(numTurns: number): Promise<Record<string, unknown>>;
230
- rollbackThreadById(threadId: string, numTurns: number): Promise<Record<string, unknown>>;
231
- forkThread(): Promise<{
229
+ /** Fork at a complete turn boundary without mutating the source history. */
230
+ forkThreadAtUserTurn(userTurnOrdinal: number): Promise<{
232
231
  threadId: string;
233
232
  thread: Record<string, unknown>;
234
233
  }>;
@@ -364,6 +363,7 @@ export declare class CodexProcess extends EventEmitter<CodexProcessEvents> {
364
363
  private resetReadiness;
365
364
  private resolveReadiness;
366
365
  private rejectReadiness;
366
+ private hasBlockingUserInput;
367
367
  private setStatus;
368
368
  private emitMessage;
369
369
  private extractToolUseId;
@@ -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 { codexThreadToSessionHistory } from "./sessions-index.js";
8
9
  import { codexCliJoinTarget } from "./codex-app-server-config.js";
9
10
  import { normalizeCodexServiceTierForClient, normalizeCodexServiceTierForRpc, } from "./codex-service-tier.js";
10
11
  import { resolvePlatformPath } from "./path-utils.js";
@@ -283,43 +284,108 @@ export class CodexProcess extends EventEmitter {
283
284
  async readThread(threadId, includeTurns = true) {
284
285
  const response = (await this.request("thread/read", {
285
286
  threadId,
286
- includeTurns,
287
+ includeTurns: false,
287
288
  }));
288
289
  const thread = response.thread;
289
290
  if (!thread) {
290
291
  throw new Error("thread/read returned no thread");
291
292
  }
292
- return thread;
293
- }
294
- async rollbackThread(numTurns) {
295
- if (!this._threadId) {
296
- throw new Error("No thread ID available for rollback");
297
- }
298
- return this.rollbackThreadById(this._threadId, numTurns);
299
- }
300
- async rollbackThreadById(threadId, numTurns) {
301
- const response = (await this.request("thread/rollback", {
302
- threadId,
303
- numTurns,
304
- }));
305
- const thread = response.thread;
306
- if (!thread) {
307
- throw new Error("thread/rollback returned no thread");
308
- }
309
- return thread;
293
+ if (!includeTurns)
294
+ return thread;
295
+ const turns = [];
296
+ const seenCursors = new Set();
297
+ let cursor;
298
+ 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);
332
+ if (page.nextCursor != null && typeof page.nextCursor !== "string") {
333
+ throw new Error("thread/turns/list returned an invalid cursor");
334
+ }
335
+ cursor = page.nextCursor ?? undefined;
336
+ if (cursor !== undefined) {
337
+ if (seenCursors.has(cursor)) {
338
+ throw new Error("thread/turns/list returned a repeated cursor");
339
+ }
340
+ seenCursors.add(cursor);
341
+ }
342
+ } while (cursor !== undefined);
343
+ return { ...thread, turns };
310
344
  }
311
- async forkThread() {
312
- if (!this._threadId) {
345
+ /** Fork at a complete turn boundary without mutating the source history. */
346
+ async forkThreadAtUserTurn(userTurnOrdinal) {
347
+ if (!this._threadId)
313
348
  throw new Error("No thread ID available for fork");
349
+ if (!Number.isInteger(userTurnOrdinal) || userTurnOrdinal < 1) {
350
+ throw new Error("Invalid Codex fork target");
351
+ }
352
+ const source = await this.readThread(this._threadId);
353
+ let userCount = 0;
354
+ let lastTurnId;
355
+ for (const raw of Array.isArray(source.turns) ? source.turns : []) {
356
+ const turn = asRecord(raw);
357
+ if (!turn)
358
+ continue;
359
+ userCount += codexThreadToSessionHistory({ turns: [turn] }).filter((message) => message.role === "user").length;
360
+ if (userCount > userTurnOrdinal) {
361
+ throw new Error("Cannot fork within a Codex turn containing multiple user messages");
362
+ }
363
+ if (userCount === userTurnOrdinal) {
364
+ if (turn.status === "inProgress") {
365
+ throw new Error("Cannot fork an in-progress Codex turn");
366
+ }
367
+ lastTurnId = typeof turn.id === "string" ? turn.id : undefined;
368
+ break;
369
+ }
370
+ }
371
+ if (!lastTurnId) {
372
+ throw new Error("Codex fork target was not found in stored history");
314
373
  }
315
374
  const response = (await this.request("thread/fork", {
316
375
  threadId: this._threadId,
317
- persistExtendedHistory: true,
376
+ lastTurnId,
377
+ excludeTurns: true,
318
378
  }));
319
- const thread = response.thread;
320
- const threadId = typeof thread?.id === "string" ? thread.id : undefined;
321
- if (!thread || !threadId) {
379
+ const metadata = asRecord(response.thread);
380
+ const threadId = typeof metadata?.id === "string" ? metadata.id : undefined;
381
+ if (!threadId)
322
382
  throw new Error("thread/fork returned no thread id");
383
+ const thread = await this.readThread(threadId);
384
+ // Older servers may silently ignore lastTurnId. Never resume excess history.
385
+ const turns = Array.isArray(thread.turns) ? thread.turns : [];
386
+ const forkedUserCount = codexThreadToSessionHistory(thread).filter((message) => message.role === "user").length;
387
+ if (asRecord(turns.at(-1))?.id !== lastTurnId || forkedUserCount !== userTurnOrdinal) {
388
+ throw new Error("Codex did not fork at the requested turn; update Codex CLI");
323
389
  }
324
390
  return { threadId, thread };
325
391
  }
@@ -637,7 +703,9 @@ export class CodexProcess extends EventEmitter {
637
703
  this.pendingApprovals.delete(pending.toolUseId);
638
704
  this.respondToServerRequest(pending.requestId, buildApprovalResponse(pending, "accept"));
639
705
  this.emitToolResult(pending.toolUseId, "Approved", "approved");
640
- if (this.pendingApprovals.size === 0) {
706
+ if (this.pendingApprovals.size === 0 &&
707
+ !this.hasBlockingUserInput() &&
708
+ !this.pendingPlanCompletion) {
641
709
  this.setStatus("running");
642
710
  }
643
711
  return true;
@@ -655,7 +723,9 @@ export class CodexProcess extends EventEmitter {
655
723
  this.pendingApprovals.delete(pending.toolUseId);
656
724
  this.respondToServerRequest(pending.requestId, buildApprovalResponse(pending, "acceptForSession"));
657
725
  this.emitToolResult(pending.toolUseId, "Approved (always)", "approved_for_session");
658
- if (this.pendingApprovals.size === 0) {
726
+ if (this.pendingApprovals.size === 0 &&
727
+ !this.hasBlockingUserInput() &&
728
+ !this.pendingPlanCompletion) {
659
729
  this.setStatus("running");
660
730
  }
661
731
  return true;
@@ -678,7 +748,9 @@ export class CodexProcess extends EventEmitter {
678
748
  this.pendingApprovals.delete(pending.toolUseId);
679
749
  this.respondToServerRequest(pending.requestId, buildApprovalResponse(pending, resolveApprovalRejectDecision(pending)));
680
750
  this.emitToolResult(pending.toolUseId, "Rejected", "rejected");
681
- if (this.pendingApprovals.size === 0) {
751
+ if (this.pendingApprovals.size === 0 &&
752
+ !this.hasBlockingUserInput() &&
753
+ !this.pendingPlanCompletion) {
682
754
  this.setStatus("running");
683
755
  }
684
756
  return true;
@@ -692,8 +764,12 @@ export class CodexProcess extends EventEmitter {
692
764
  this.pendingUserInputs.delete(pending.toolUseId);
693
765
  this.respondToServerRequest(pending.requestId, buildUserInputResponse(pending, result));
694
766
  this.emitToolResult(pending.toolUseId, "Answered", "answered");
695
- if (this.pendingApprovals.size === 0 && this.pendingUserInputs.size === 0) {
696
- this.setStatus("running");
767
+ if (this.pendingApprovals.size === 0 &&
768
+ !this.hasBlockingUserInput() &&
769
+ !this.pendingPlanCompletion) {
770
+ if (pending.isBlocking !== false || this.pendingTurnId) {
771
+ this.setStatus("running");
772
+ }
697
773
  }
698
774
  return true;
699
775
  }
@@ -831,8 +907,8 @@ export class CodexProcess extends EventEmitter {
831
907
  resolvePendingUserInput(toolUseId) {
832
908
  if (toolUseId)
833
909
  return this.pendingUserInputs.get(toolUseId);
834
- const first = this.pendingUserInputs.values().next();
835
- return first.done ? undefined : first.value;
910
+ const requests = [...this.pendingUserInputs.values()];
911
+ return requests.find((request) => request.isBlocking !== false) ?? requests[0];
836
912
  }
837
913
  /**
838
914
  * Approve a pending user-input request (McpElicitation fallback).
@@ -858,7 +934,9 @@ export class CodexProcess extends EventEmitter {
858
934
  this.emitToolResult(pending.toolUseId, "Approved", result === "Allow for this session"
859
935
  ? "approved_for_session"
860
936
  : "approved");
861
- if (this.pendingApprovals.size === 0 && this.pendingUserInputs.size === 0) {
937
+ if (this.pendingApprovals.size === 0 &&
938
+ !this.hasBlockingUserInput() &&
939
+ !this.pendingPlanCompletion) {
862
940
  this.setStatus("running");
863
941
  }
864
942
  return true;
@@ -884,7 +962,9 @@ export class CodexProcess extends EventEmitter {
884
962
  toolUseId: pending.toolUseId,
885
963
  });
886
964
  this.emitToolResult(pending.toolUseId, toolResult);
887
- if (this.pendingApprovals.size === 0 && this.pendingUserInputs.size === 0) {
965
+ if (this.pendingApprovals.size === 0 &&
966
+ !this.hasBlockingUserInput() &&
967
+ !this.pendingPlanCompletion) {
888
968
  this.setStatus("running");
889
969
  }
890
970
  }
@@ -898,7 +978,9 @@ export class CodexProcess extends EventEmitter {
898
978
  this.pendingUserInputs.delete(pending.toolUseId);
899
979
  this.respondToServerRequest(pending.requestId, buildUserInputResponse(pending, resolveUserInputRejectResult(pending, result)));
900
980
  this.emitToolResult(pending.toolUseId, "Rejected", "rejected");
901
- if (this.pendingApprovals.size === 0 && this.pendingUserInputs.size === 0) {
981
+ if (this.pendingApprovals.size === 0 &&
982
+ !this.hasBlockingUserInput() &&
983
+ !this.pendingPlanCompletion) {
902
984
  this.setStatus("running");
903
985
  }
904
986
  return true;
@@ -1040,6 +1122,7 @@ export class CodexProcess extends EventEmitter {
1040
1122
  const method = options?.threadId ? "thread/resume" : "thread/start";
1041
1123
  if (options?.threadId) {
1042
1124
  threadParams.threadId = options.threadId;
1125
+ threadParams.excludeTurns = true;
1043
1126
  }
1044
1127
  else {
1045
1128
  threadParams.experimentalRawEvents = false;
@@ -1634,6 +1717,7 @@ export class CodexProcess extends EventEmitter {
1634
1717
  const toolUseId = this.extractToolUseId(params, id);
1635
1718
  const questions = normalizeUserInputQuestions(params.questions);
1636
1719
  const input = {
1720
+ isBlocking: params.isBlocking !== false,
1637
1721
  questions: questions.map((q) => ({
1638
1722
  id: q.id,
1639
1723
  question: q.question,
@@ -1654,6 +1738,7 @@ export class CodexProcess extends EventEmitter {
1654
1738
  })),
1655
1739
  input,
1656
1740
  kind: "questions",
1741
+ isBlocking: params.isBlocking !== false,
1657
1742
  });
1658
1743
  this.emitMessage({
1659
1744
  type: "permission_request",
@@ -1661,7 +1746,8 @@ export class CodexProcess extends EventEmitter {
1661
1746
  toolName: "AskUserQuestion",
1662
1747
  input,
1663
1748
  });
1664
- this.setStatus("waiting_approval");
1749
+ if (params.isBlocking !== false)
1750
+ this.setStatus("waiting_approval");
1665
1751
  break;
1666
1752
  }
1667
1753
  case "item/permissions/requestApproval": {
@@ -2010,7 +2096,7 @@ export class CodexProcess extends EventEmitter {
2010
2096
  else {
2011
2097
  this.lastPlanItemText = null;
2012
2098
  if (this.pendingApprovals.size === 0 &&
2013
- this.pendingUserInputs.size === 0) {
2099
+ !this.hasBlockingUserInput()) {
2014
2100
  this.setStatus("idle");
2015
2101
  }
2016
2102
  }
@@ -2588,6 +2674,9 @@ export class CodexProcess extends EventEmitter {
2588
2674
  waiter.reject(error);
2589
2675
  this.readinessWaiters.clear();
2590
2676
  }
2677
+ hasBlockingUserInput() {
2678
+ return [...this.pendingUserInputs.values()].some((request) => request.isBlocking !== false);
2679
+ }
2591
2680
  setStatus(status) {
2592
2681
  if (this._status !== status) {
2593
2682
  this._status = status;
@@ -2631,7 +2720,7 @@ export class CodexProcess extends EventEmitter {
2631
2720
  }
2632
2721
  if (!this.pendingPlanCompletion &&
2633
2722
  this.pendingApprovals.size === 0 &&
2634
- this.pendingUserInputs.size === 0) {
2723
+ !this.hasBlockingUserInput()) {
2635
2724
  this.setStatus(this.pendingTurnId ? "running" : "idle");
2636
2725
  }
2637
2726
  }
@@ -2910,16 +2999,16 @@ function extractReasoningEfforts(raw) {
2910
2999
  return efforts;
2911
3000
  }
2912
3001
  function extractServiceTiers(raw) {
2913
- // Recent app-server versions expose the user-facing Fast option as
3002
+ // Older app-server versions expose the user-facing Fast option as
2914
3003
  // `additionalSpeedTiers: ["fast"]`, while the lower-level service tier is
2915
- // advertised as `{ id: "priority", name: "Fast" }`. Merge both shapes and
2916
- // normalize them to the value accepted by `service_tier` in config/RPCs.
2917
- const values = [
2918
- ...(Array.isArray(raw.additionalSpeedTiers)
3004
+ // advertised as `{ id: "priority", name: "Fast" }`. Prefer the current
3005
+ // catalog and normalize its values for the client.
3006
+ // The current catalog is authoritative, including an explicitly empty list.
3007
+ const values = Array.isArray(raw.serviceTiers)
3008
+ ? raw.serviceTiers
3009
+ : Array.isArray(raw.additionalSpeedTiers)
2919
3010
  ? raw.additionalSpeedTiers
2920
- : []),
2921
- ...(Array.isArray(raw.serviceTiers) ? raw.serviceTiers : []),
2922
- ];
3011
+ : [];
2923
3012
  const seen = new Set();
2924
3013
  const tiers = [];
2925
3014
  for (const value of values) {