@nyssance/codex-acp-v2 0.3.4 → 0.3.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.
Files changed (3) hide show
  1. package/CHANGELOG.md +41 -0
  2. package/dist/index.js +824 -215
  3. package/package.json +5 -2
package/dist/index.js CHANGED
@@ -21721,7 +21721,7 @@ var package_default = {
21721
21721
  publishConfig: {
21722
21722
  access: "public"
21723
21723
  },
21724
- version: "0.3.4",
21724
+ version: "0.3.5",
21725
21725
  description: "Native ACP v2 agent for the OpenAI Codex app-server",
21726
21726
  packageManager: "bun@1.4.0",
21727
21727
  engines: {
@@ -21733,6 +21733,7 @@ var package_default = {
21733
21733
  files: [
21734
21734
  "dist/index.js",
21735
21735
  "README.md",
21736
+ "CHANGELOG.md",
21736
21737
  "LICENSE",
21737
21738
  "package.json"
21738
21739
  ],
@@ -21758,7 +21759,9 @@ var package_default = {
21758
21759
  test: "vitest run --exclude src/__tests__/e2e.test.ts",
21759
21760
  "test:watch": "vitest",
21760
21761
  typecheck: "tsc --noEmit",
21761
- "test:e2e": "RUN_E2E_TESTS=true vitest run src/__tests__/e2e.test.ts"
21762
+ "test:e2e": "RUN_E2E_TESTS=true vitest run src/__tests__/e2e.test.ts",
21763
+ "test:benchmark": "RUN_BENCHMARKS=true vitest run src/__tests__/benchmark.test.ts",
21764
+ "test:soak": "bun --expose-gc src/benchmarks/bridge-soak.ts"
21762
21765
  },
21763
21766
  homepage: "https://github.com/nyssance/codex-acp-v2#readme",
21764
21767
  bugs: {
@@ -21794,6 +21797,46 @@ var package_default = {
21794
21797
  }
21795
21798
  };
21796
21799
 
21800
+ // src/agent/turnErrors.ts
21801
+ function classifyTurnError(info) {
21802
+ const failure = (category, retryable = false, stopReason = "_error") => ({ category, retryable, stopReason });
21803
+ if (info === null)
21804
+ return failure("unknown");
21805
+ if (typeof info === "object") {
21806
+ if ("activeTurnNotSteerable" in info)
21807
+ return failure("turn_not_steerable");
21808
+ const status = "httpConnectionFailed" in info ? info.httpConnectionFailed.httpStatusCode : ("responseStreamConnectionFailed" in info) ? info.responseStreamConnectionFailed.httpStatusCode : ("responseStreamDisconnected" in info) ? info.responseStreamDisconnected.httpStatusCode : info.responseTooManyFailedAttempts.httpStatusCode;
21809
+ return failure("connection", status === null || status === 408 || status === 429 || status >= 500);
21810
+ }
21811
+ switch (info) {
21812
+ case "contextWindowExceeded":
21813
+ return failure("context_window", false, "max_tokens");
21814
+ case "cyberPolicy":
21815
+ case "misalignmentPolicyViolation":
21816
+ return failure("policy", false, "refusal");
21817
+ case "sessionBudgetExceeded":
21818
+ return failure("budget");
21819
+ case "usageLimitExceeded":
21820
+ return failure("usage_limit");
21821
+ case "rateLimitExceeded":
21822
+ return failure("rate_limit", true);
21823
+ case "serverOverloaded":
21824
+ return failure("overloaded", true);
21825
+ case "internalServerError":
21826
+ return failure("internal", true);
21827
+ case "unauthorized":
21828
+ return failure("authentication");
21829
+ case "badRequest":
21830
+ return failure("invalid_request");
21831
+ case "threadRollbackFailed":
21832
+ return failure("rollback");
21833
+ case "sandboxError":
21834
+ return failure("sandbox");
21835
+ case "other":
21836
+ return failure("unknown");
21837
+ }
21838
+ }
21839
+
21797
21840
  // src/agent/CodexAgent.ts
21798
21841
  import path6 from "path";
21799
21842
 
@@ -22756,9 +22799,6 @@ function toAcpStatus(status) {
22756
22799
  return "cancelled";
22757
22800
  }
22758
22801
  }
22759
- function toolCallCancelled(toolCallId) {
22760
- return frame({ toolCallId, status: "cancelled" });
22761
- }
22762
22802
  function frame(fields) {
22763
22803
  return { sessionUpdate: "tool_call_update", ...fields };
22764
22804
  }
@@ -22847,14 +22887,17 @@ function commandStarted(item) {
22847
22887
  });
22848
22888
  }
22849
22889
  function commandCompleted(item) {
22890
+ const output = item.aggregatedOutput ?? "";
22891
+ const truncated = output.length > 16384;
22850
22892
  return frame({
22851
22893
  toolCallId: item.id,
22852
22894
  status: item.status === "completed" ? "completed" : "failed",
22853
22895
  rawOutput: {
22854
- output: item.aggregatedOutput ?? "",
22896
+ output: truncated ? output.slice(0, 16384).replace(/[\uD800-\uDBFF]$/, "") : output,
22855
22897
  exitCode: item.exitCode,
22856
22898
  durationMs: item.durationMs
22857
22899
  },
22900
+ ...truncated ? { _meta: { codex: { outputTruncated: true, outputCharacters: output.length, ...usesTerminal(item) ? { terminalId: item.id } : {} } } } : {},
22858
22901
  ...usesTerminal(item) || !item.aggregatedOutput ? {} : { content: [textContent(item.aggregatedOutput)] }
22859
22902
  });
22860
22903
  }
@@ -23071,8 +23114,8 @@ function fuzzySearchUpdated(event, create) {
23071
23114
  rawInput: { query: event.query }
23072
23115
  });
23073
23116
  }
23074
- function fuzzySearchCompleted(event) {
23075
- return frame({ toolCallId: fuzzySearchToolCallId(event.sessionId), status: "completed" });
23117
+ function fuzzySearchCompleted(event, create = false) {
23118
+ return frame({ toolCallId: fuzzySearchToolCallId(event.sessionId), ...create ? { name: ToolName.FuzzySearch, title: "File search", kind: "search" } : {}, status: "completed" });
23076
23119
  }
23077
23120
  function guardianReviewToolCallId(reviewId) {
23078
23121
  return `guardian-review:${reviewId}`;
@@ -23143,6 +23186,7 @@ function mcpStartupFailed(serverName, message) {
23143
23186
  }
23144
23187
 
23145
23188
  // src/codex/sessionConfig.ts
23189
+ import { pathToFileURL } from "url";
23146
23190
  import path3 from "path";
23147
23191
  var MCP_SERVER_NAME_WHITESPACE = /\p{White_Space}/gu;
23148
23192
  function sanitizeMcpServerName(name) {
@@ -23263,7 +23307,7 @@ function fromUserInput(input) {
23263
23307
  }
23264
23308
  }
23265
23309
  function toFileUri(value) {
23266
- return value.startsWith("file://") ? value : `file://${value}`;
23310
+ return value.startsWith("file://") ? value : pathToFileURL(value).href;
23267
23311
  }
23268
23312
  function isSupportedImageUrl(uri) {
23269
23313
  if (!uri)
@@ -23295,6 +23339,65 @@ function promptTitle(prompt) {
23295
23339
  return null;
23296
23340
  }
23297
23341
 
23342
+ // src/bridge/itemSnapshot.ts
23343
+ function itemSnapshot(item) {
23344
+ switch (item.type) {
23345
+ case "userMessage": {
23346
+ const content = item.content.flatMap(fromUserInput);
23347
+ return content.length > 0 ? [{ sessionUpdate: "user_message", messageId: item.id, content }] : [];
23348
+ }
23349
+ case "agentMessage":
23350
+ return item.text.length > 0 ? [{
23351
+ sessionUpdate: "agent_message",
23352
+ messageId: item.id,
23353
+ content: [{ type: "text", text: item.text }],
23354
+ ...item.phase ? { _meta: { codex: { phase: item.phase } } } : {}
23355
+ }] : [];
23356
+ case "reasoning": {
23357
+ const parts = (item.summary.length > 0 ? item.summary : item.content).filter((part) => part.length > 0);
23358
+ return parts.length > 0 ? [{ sessionUpdate: "agent_thought", messageId: item.id, content: parts.map((text) => ({ type: "text", text })) }] : [];
23359
+ }
23360
+ case "plan":
23361
+ return item.text.length > 0 ? [{ sessionUpdate: "plan_update", plan: { type: "markdown", planId: item.id, content: item.text } }] : [];
23362
+ case "fileChange":
23363
+ return [fileChangeStarted(item)];
23364
+ case "commandExecution": {
23365
+ const updates = [commandStarted(item)];
23366
+ if (usesTerminal(item))
23367
+ updates.unshift(terminalSnapshot(item));
23368
+ if (item.status !== "inProgress")
23369
+ updates.push(commandCompleted(item));
23370
+ return updates;
23371
+ }
23372
+ case "mcpToolCall":
23373
+ return [mcpToolCallStarted(item)];
23374
+ case "dynamicToolCall":
23375
+ return [{ ...dynamicToolCallStarted(item), ...dynamicToolCallCompleted(item) }];
23376
+ case "webSearch":
23377
+ return [webSearchSnapshot(item)];
23378
+ case "imageView":
23379
+ return [imageViewed(item)];
23380
+ case "imageGeneration":
23381
+ return [imageGenerationSnapshot(item)];
23382
+ case "collabAgentToolCall":
23383
+ return [collabToolCall(item, true)];
23384
+ case "subAgentActivity":
23385
+ return [subAgentActivity(item, "completed", true)];
23386
+ case "contextCompaction":
23387
+ return [compactionUpdate(item.id, "completed")];
23388
+ case "enteredReviewMode":
23389
+ return [];
23390
+ case "exitedReviewMode": {
23391
+ const text = item.review.trim();
23392
+ return text.length > 0 ? [{ sessionUpdate: "agent_message", messageId: item.id, content: [{ type: "text", text }] }] : [];
23393
+ }
23394
+ case "hookPrompt":
23395
+ case "functionCallOutput":
23396
+ case "sleep":
23397
+ return [];
23398
+ }
23399
+ }
23400
+
23298
23401
  // src/bridge/EventBridge.ts
23299
23402
  var TURN_PLAN_ID = "codex-turn-plan";
23300
23403
  var PLAN_STREAM_INTERVAL_MS = 150;
@@ -23307,6 +23410,7 @@ class EventBridge {
23307
23410
  completedPlan = null;
23308
23411
  noticeSequence = 0;
23309
23412
  openToolCalls = new Set;
23413
+ announcedTools = new Set;
23310
23414
  messagePhases = new Map;
23311
23415
  reasoningWithDeltas = new Set;
23312
23416
  terminalItems = new Set;
@@ -23327,6 +23431,14 @@ class EventBridge {
23327
23431
  this.lastError = null;
23328
23432
  this.completedPlan = null;
23329
23433
  this.openToolCalls.clear();
23434
+ this.announcedTools.clear();
23435
+ this.messagePhases.clear();
23436
+ this.reasoningWithDeltas.clear();
23437
+ this.terminalItems.clear();
23438
+ this.imageGenerations.clear();
23439
+ this.subAgentActivities.clear();
23440
+ this.fuzzySearches.clear();
23441
+ this.guardianReviews.clear();
23330
23442
  this.clearPlanState();
23331
23443
  }
23332
23444
  takeError() {
@@ -23339,24 +23451,47 @@ class EventBridge {
23339
23451
  this.completedPlan = null;
23340
23452
  return plan;
23341
23453
  }
23454
+ async restore(items) {
23455
+ for (const item of items) {
23456
+ if (item.type === "commandExecution" && item.status === "inProgress" && usesTerminal(item))
23457
+ this.terminalItems.add(item.id);
23458
+ for (const update of itemSnapshot(item)) {
23459
+ this.trackToolCall(update);
23460
+ await this.client.update(update);
23461
+ }
23462
+ }
23463
+ }
23342
23464
  async handle(notification) {
23343
- const updates = await this.translate(notification);
23465
+ let updates = await this.translate(notification);
23466
+ if (notification.method === "item/completed") {
23467
+ const snapshots = itemSnapshot(notification.params.item);
23468
+ const first = snapshots.find((update) => update.sessionUpdate === "tool_call_update");
23469
+ if (first?.sessionUpdate === "tool_call_update" && !this.announcedTools.has(first.toolCallId)) {
23470
+ updates = snapshots;
23471
+ }
23472
+ }
23344
23473
  for (const update of updates) {
23345
23474
  this.trackToolCall(update);
23346
23475
  await this.client.update(update);
23347
23476
  }
23348
23477
  }
23349
- async cancelOpenToolCalls() {
23478
+ async finishOpenToolCalls(status) {
23350
23479
  const ids = [...this.openToolCalls];
23480
+ const terminals = [...this.terminalItems];
23351
23481
  this.openToolCalls.clear();
23482
+ this.terminalItems.clear();
23483
+ for (const terminalId of terminals) {
23484
+ await this.client.update({ sessionUpdate: "terminal_update", terminalId, exitStatus: { exitCode: null, signal: null }, _meta: { codex: { reconciled: true, turnOutcome: status } } });
23485
+ }
23352
23486
  for (const toolCallId of ids) {
23353
- await this.client.update(toolCallCancelled(toolCallId));
23487
+ await this.client.update({ sessionUpdate: "tool_call_update", toolCallId, status, _meta: { codex: { reconciled: true } } });
23354
23488
  }
23355
23489
  }
23356
23490
  trackToolCall(update) {
23357
23491
  if (update.sessionUpdate !== "tool_call_update")
23358
23492
  return;
23359
23493
  const { toolCallId, status } = update;
23494
+ this.announcedTools.add(toolCallId);
23360
23495
  if (status === "pending" || status === "in_progress")
23361
23496
  this.openToolCalls.add(toolCallId);
23362
23497
  else if (status !== undefined && status !== null)
@@ -23366,7 +23501,7 @@ class EventBridge {
23366
23501
  await this.flushPlans();
23367
23502
  }
23368
23503
  dispose() {
23369
- this.clearPlanState();
23504
+ this.beginTurn();
23370
23505
  }
23371
23506
  async translate(notification) {
23372
23507
  switch (notification.method) {
@@ -23398,14 +23533,17 @@ ${stdin}
23398
23533
  `)] : [];
23399
23534
  }
23400
23535
  case "item/fileChange/patchUpdated":
23401
- return [fileChangePatched({
23536
+ return [(this.announcedTools.has(notification.params.itemId) ? fileChangePatched : fileChangeStarted)({
23402
23537
  type: "fileChange",
23403
23538
  id: notification.params.itemId,
23404
23539
  changes: notification.params.changes,
23405
23540
  status: "inProgress"
23406
23541
  })];
23407
23542
  case "item/mcpToolCall/progress":
23408
- return [mcpToolCallProgress(notification.params.itemId, notification.params.message)];
23543
+ return [{
23544
+ ...!this.announcedTools.has(notification.params.itemId) ? { name: ToolName.Mcp, title: "MCP tool", kind: "execute", status: "in_progress" } : {},
23545
+ ...mcpToolCallProgress(notification.params.itemId, notification.params.message)
23546
+ }];
23409
23547
  case "item/plan/delta":
23410
23548
  this.planDelta(notification.params.itemId, notification.params.delta);
23411
23549
  return [];
@@ -23427,9 +23565,11 @@ ${stdin}
23427
23565
  this.fuzzySearches.add(id);
23428
23566
  return [fuzzySearchUpdated(notification.params, create)];
23429
23567
  }
23430
- case "fuzzyFileSearch/sessionCompleted":
23568
+ case "fuzzyFileSearch/sessionCompleted": {
23569
+ const create = !this.fuzzySearches.has(notification.params.sessionId);
23431
23570
  this.fuzzySearches.delete(notification.params.sessionId);
23432
- return [fuzzySearchCompleted(notification.params)];
23571
+ return [fuzzySearchCompleted(notification.params, create)];
23572
+ }
23433
23573
  case "thread/tokenUsage/updated": {
23434
23574
  const usage = notification.params.tokenUsage;
23435
23575
  this.session.lastUsage = toTokenCount(usage.last);
@@ -23538,7 +23678,7 @@ ${details}` : summary)];
23538
23678
  case "commandExecution":
23539
23679
  if (usesTerminal(item)) {
23540
23680
  this.terminalItems.add(item.id);
23541
- return [commandStarted(item), terminalStarted(item)];
23681
+ return [terminalStarted(item), commandStarted(item)];
23542
23682
  }
23543
23683
  return [commandStarted(item)];
23544
23684
  case "mcpToolCall":
@@ -23577,7 +23717,7 @@ ${details}` : summary)];
23577
23717
  switch (item.type) {
23578
23718
  case "agentMessage":
23579
23719
  this.messagePhases.delete(item.id);
23580
- return [];
23720
+ return itemSnapshot(item);
23581
23721
  case "reasoning": {
23582
23722
  if (this.reasoningWithDeltas.delete(item.id))
23583
23723
  return [];
@@ -25551,10 +25691,6 @@ async function logout(codex) {
25551
25691
  await updated;
25552
25692
  });
25553
25693
  }
25554
- async function authRequired(codex) {
25555
- const account = await codex.accountRead({ refreshToken: false });
25556
- return account.requiresOpenaiAuth && account.account === null;
25557
- }
25558
25694
 
25559
25695
  // src/agent/clientSession.ts
25560
25696
  class ClientSession {
@@ -25565,6 +25701,11 @@ class ClientSession {
25565
25701
  turnActive = false;
25566
25702
  turnGeneration = 0;
25567
25703
  disposed = false;
25704
+ output = new AbortController;
25705
+ terminalAttempted = false;
25706
+ get signal() {
25707
+ return this.output.signal;
25708
+ }
25568
25709
  constructor(sessionId, link, capabilities) {
25569
25710
  this.sessionId = sessionId;
25570
25711
  this.link = link;
@@ -25573,38 +25714,46 @@ class ClientSession {
25573
25714
  async update(update) {
25574
25715
  if (this.disposed)
25575
25716
  return;
25576
- await this.link.notify(methods.client.session.update, { sessionId: this.sessionId, update });
25717
+ await abortable(this.link.notify(methods.client.session.update, { sessionId: this.sessionId, update }), this.output.signal);
25577
25718
  }
25578
25719
  async updateAll(updates) {
25579
25720
  for (const update of updates)
25580
25721
  await this.update(update);
25581
25722
  }
25582
25723
  async requestPermission(request, signal) {
25583
- signal?.throwIfAborted();
25724
+ signal = signal ? AbortSignal.any([signal, this.output.signal]) : this.output.signal;
25725
+ signal.throwIfAborted();
25584
25726
  return await this.waitingOnClient(() => {
25727
+ signal?.throwIfAborted();
25585
25728
  const pending = this.link.request(methods.client.session.requestPermission, { sessionId: this.sessionId, ...request }, signal ? { cancellationSignal: signal } : undefined);
25586
25729
  return signal ? abortable(pending, signal) : pending;
25587
- });
25730
+ }, signal);
25588
25731
  }
25589
25732
  async createElicitation(request, signal) {
25590
- signal?.throwIfAborted();
25733
+ signal = signal ? AbortSignal.any([signal, this.output.signal]) : this.output.signal;
25734
+ signal.throwIfAborted();
25591
25735
  return await this.waitingOnClient(() => {
25736
+ signal?.throwIfAborted();
25592
25737
  const pending = this.link.request(methods.client.elicitation.create, request, signal ? { cancellationSignal: signal } : undefined);
25593
25738
  return signal ? abortable(pending, signal) : pending;
25594
- });
25739
+ }, signal);
25595
25740
  }
25596
25741
  async completeElicitation(elicitationId) {
25597
25742
  if (this.disposed)
25598
25743
  return;
25599
- await this.link.notify(methods.client.elicitation.complete, { elicitationId });
25744
+ await abortable(this.link.notify(methods.client.elicitation.complete, { elicitationId }), this.output.signal);
25600
25745
  }
25601
25746
  reportRunning() {
25747
+ this.terminalAttempted = false;
25602
25748
  this.turnGeneration += 1;
25603
25749
  this.turnActive = true;
25604
25750
  this.waiting = 0;
25605
25751
  this.state({ sessionUpdate: "state_update", state: "running" });
25606
25752
  }
25607
25753
  async reportIdle(stopReason, extra) {
25754
+ if (this.terminalAttempted)
25755
+ return;
25756
+ this.terminalAttempted = true;
25608
25757
  this.turnActive = false;
25609
25758
  this.waiting = 0;
25610
25759
  await this.update({
@@ -25615,18 +25764,20 @@ class ClientSession {
25615
25764
  ...extra?._meta === undefined ? {} : { _meta: extra._meta }
25616
25765
  });
25617
25766
  }
25618
- async waitingOnClient(operation) {
25767
+ async waitingOnClient(operation, signal) {
25619
25768
  if (this.disposed)
25620
25769
  throw new Error("Session is closed");
25621
25770
  const generation = this.turnActive ? this.turnGeneration : null;
25622
- if (generation !== null && this.waiting++ === 0) {
25623
- await this.state({ sessionUpdate: "state_update", state: "requires_action" });
25624
- }
25625
25771
  try {
25772
+ if (generation !== null && this.waiting++ === 0) {
25773
+ const pending = this.state({ sessionUpdate: "state_update", state: "requires_action" });
25774
+ await (signal ? abortable(pending, signal) : pending);
25775
+ }
25626
25776
  return await operation();
25627
25777
  } finally {
25628
25778
  if (this.turnActive && generation === this.turnGeneration && --this.waiting === 0) {
25629
- await this.state({ sessionUpdate: "state_update", state: "running" });
25779
+ const pending = this.state({ sessionUpdate: "state_update", state: "running" });
25780
+ await (signal ? abortable(pending, signal).catch(() => {}) : pending);
25630
25781
  }
25631
25782
  }
25632
25783
  }
@@ -25639,6 +25790,7 @@ class ClientSession {
25639
25790
  }
25640
25791
  dispose() {
25641
25792
  this.disposed = true;
25793
+ this.output.abort();
25642
25794
  this.turnActive = false;
25643
25795
  this.waiting = 0;
25644
25796
  }
@@ -25878,7 +26030,7 @@ function historyUpdates(turns) {
25878
26030
  const updates = [];
25879
26031
  for (const turn of turns) {
25880
26032
  for (const item of turn.items) {
25881
- updates.push(...itemHistory(item));
26033
+ updates.push(...itemSnapshot(item));
25882
26034
  }
25883
26035
  }
25884
26036
  return updates;
@@ -25903,63 +26055,6 @@ function firstLine(text2) {
25903
26055
  const line = text2.split(/\r?\n/).map((part) => part.trim()).find((part) => part.length > 0) ?? text2;
25904
26056
  return line.length > 120 ? `${line.slice(0, 117)}...` : line;
25905
26057
  }
25906
- function itemHistory(item) {
25907
- switch (item.type) {
25908
- case "userMessage": {
25909
- const content = item.content.flatMap(fromUserInput);
25910
- return content.length > 0 ? [{ sessionUpdate: "user_message", messageId: item.id, content }] : [];
25911
- }
25912
- case "agentMessage":
25913
- return item.text.length > 0 ? [{
25914
- sessionUpdate: "agent_message",
25915
- messageId: item.id,
25916
- content: [{ type: "text", text: item.text }],
25917
- ...item.phase ? { _meta: { codex: { phase: item.phase } } } : {}
25918
- }] : [];
25919
- case "reasoning": {
25920
- const parts = (item.summary.length > 0 ? item.summary : item.content).filter((part) => part.length > 0);
25921
- return parts.length > 0 ? [{ sessionUpdate: "agent_thought", messageId: item.id, content: parts.map((text2) => ({ type: "text", text: text2 })) }] : [];
25922
- }
25923
- case "plan":
25924
- return item.text.length > 0 ? [{ sessionUpdate: "plan_update", plan: { type: "markdown", planId: item.id, content: item.text } }] : [];
25925
- case "fileChange":
25926
- return [fileChangeStarted(item)];
25927
- case "commandExecution": {
25928
- const updates = [commandStarted(item)];
25929
- if (usesTerminal(item))
25930
- updates.push(terminalSnapshot(item));
25931
- if (item.status !== "inProgress")
25932
- updates.push(commandCompleted(item));
25933
- return updates;
25934
- }
25935
- case "mcpToolCall":
25936
- return [mcpToolCallStarted(item)];
25937
- case "dynamicToolCall":
25938
- return [{ ...dynamicToolCallStarted(item), ...dynamicToolCallCompleted(item) }];
25939
- case "webSearch":
25940
- return [webSearchSnapshot(item)];
25941
- case "imageView":
25942
- return [imageViewed(item)];
25943
- case "imageGeneration":
25944
- return [imageGenerationSnapshot(item)];
25945
- case "collabAgentToolCall":
25946
- return [collabToolCall(item, true)];
25947
- case "subAgentActivity":
25948
- return [subAgentActivity(item, "completed", true)];
25949
- case "contextCompaction":
25950
- return [compactionUpdate(item.id, "completed")];
25951
- case "enteredReviewMode":
25952
- return [];
25953
- case "exitedReviewMode": {
25954
- const text2 = item.review.trim();
25955
- return text2.length > 0 ? [{ sessionUpdate: "agent_message", messageId: item.id, content: [{ type: "text", text: text2 }] }] : [];
25956
- }
25957
- case "hookPrompt":
25958
- case "functionCallOutput":
25959
- case "sleep":
25960
- return [];
25961
- }
25962
- }
25963
26058
 
25964
26059
  // src/agent/providers.ts
25965
26060
  var OPENAI_PROVIDER_ID = "openai";
@@ -25974,6 +26069,11 @@ class ProviderRouting {
25974
26069
  this.baseConfig = baseConfig;
25975
26070
  this.configuredProvider = configuredProvider;
25976
26071
  }
26072
+ copy() {
26073
+ const copy = new ProviderRouting(this.baseConfig, this.configuredProvider);
26074
+ copy.gateway = this.gateway;
26075
+ return copy;
26076
+ }
25977
26077
  get active() {
25978
26078
  return this.gateway;
25979
26079
  }
@@ -26137,7 +26237,6 @@ function createActiveTurn(threadId) {
26137
26237
  // src/agent/CodexAgent.ts
26138
26238
  var CLOSE_TURN_GRACE_MS = 5000;
26139
26239
  var HISTORY_PAGE_SIZE = 50;
26140
- var ERROR_STOP_REASON = "_error";
26141
26240
  var IMPLEMENT_PLAN_OPTION = "implement_plan";
26142
26241
  var REVISE_PLAN_OPTION = "revise_plan";
26143
26242
 
@@ -26146,6 +26245,11 @@ class CodexAgent {
26146
26245
  codex;
26147
26246
  process;
26148
26247
  providers;
26248
+ switching = false;
26249
+ admissions = 0;
26250
+ pendingUnsubscribes = new Map;
26251
+ sessionMutations = new Set;
26252
+ providerQueue = Promise.resolve();
26149
26253
  info;
26150
26254
  env;
26151
26255
  closeGraceMs;
@@ -26153,7 +26257,15 @@ class CodexAgent {
26153
26257
  capabilities = null;
26154
26258
  codexInitialized = false;
26155
26259
  initializing = null;
26260
+ terminalTurns = new WeakSet;
26261
+ completedTurns = new WeakMap;
26156
26262
  skillRoots = [];
26263
+ skillsGeneration = 0;
26264
+ changingSkillRoots = false;
26265
+ publishedSkills = new WeakMap;
26266
+ skillSnapshots = new Map;
26267
+ skillsQueue = Promise.resolve();
26268
+ accountGeneration = 0;
26157
26269
  constructor(link, options) {
26158
26270
  this.link = link;
26159
26271
  this.codex = options.codex;
@@ -26164,6 +26276,18 @@ class CodexAgent {
26164
26276
  this.closeGraceMs = options.closeGraceMs ?? CLOSE_TURN_GRACE_MS;
26165
26277
  this.process?.exited.then(() => this.handleCodexExit());
26166
26278
  this.codex.connection.onClose(() => this.handleCodexExit());
26279
+ const stopObserving = this.codex.observeNotifications((notification) => {
26280
+ if (notification.method === "skills/changed") {
26281
+ this.skillsGeneration += 1;
26282
+ this.skillSnapshots.clear();
26283
+ if (!this.changingSkillRoots)
26284
+ for (const runtime of this.sessions.values())
26285
+ this.refreshAvailableCommands(runtime);
26286
+ }
26287
+ if (notification.method === "account/updated")
26288
+ this.refreshAccounts().catch((error40) => logger.error("refreshing account failed", error40));
26289
+ });
26290
+ this.codex.disconnectSignal.addEventListener("abort", stopObserving, { once: true });
26167
26291
  }
26168
26292
  async initialize(params) {
26169
26293
  if (params.protocolVersion !== PROTOCOL_VERSION) {
@@ -26222,7 +26346,11 @@ class CodexAgent {
26222
26346
  return {};
26223
26347
  }
26224
26348
  async refreshAccounts() {
26349
+ this.codex.invalidateModels();
26350
+ const generation = ++this.accountGeneration;
26225
26351
  const account = (await this.codex.accountRead({ refreshToken: false })).account;
26352
+ if (generation !== this.accountGeneration)
26353
+ return;
26226
26354
  for (const runtime of this.sessions.values())
26227
26355
  runtime.session.account = account;
26228
26356
  }
@@ -26232,16 +26360,14 @@ class CodexAgent {
26232
26360
  }
26233
26361
  async setProvider(params) {
26234
26362
  this.requireInitialized("providers/set");
26235
- this.assertNoActiveTurns("providers/set");
26236
- this.providers.set(params);
26237
- await this.rebindSessions();
26363
+ await this.changeProvider((candidate) => candidate.set(params));
26238
26364
  return {};
26239
26365
  }
26240
26366
  async disableProvider(params) {
26241
26367
  this.requireInitialized("providers/disable");
26242
- this.assertNoActiveTurns("providers/disable");
26243
- this.providers.disable(params);
26244
- await this.rebindSessions();
26368
+ if (params.providerId !== OPENAI_PROVIDER_ID)
26369
+ return {};
26370
+ await this.changeProvider((candidate) => candidate.disable(params));
26245
26371
  return {};
26246
26372
  }
26247
26373
  assertNoActiveTurns(method) {
@@ -26250,39 +26376,147 @@ class CodexAgent {
26250
26376
  throw RequestError.invalidRequest({ sessions: busy }, `${method} cannot change routing while a turn is running; cancel it or wait for idle`);
26251
26377
  }
26252
26378
  }
26253
- async rebindSessions() {
26254
- const gateway = this.providers.active;
26255
- for (const runtime of this.sessions.values()) {
26256
- const { session } = runtime;
26257
- const config2 = buildThreadConfig(this.providers.threadConfig(), session.cwd, session.additionalDirectories, [], new Set);
26258
- const thread = await this.withCodex(() => this.codex.threadResume({
26259
- threadId: session.id,
26260
- cwd: session.cwd,
26261
- config: config2,
26262
- modelProvider: this.providers.modelProvider(),
26263
- excludeTurns: true
26264
- }));
26265
- const codexCatalog = await this.withCodex(() => this.codex.allModels());
26266
- session.catalog = this.providers.catalog(codexCatalog);
26267
- session.model = resolveModelSelection(session.catalog, gateway?.model ?? thread.model, thread.reasoningEffort);
26268
- await runtime.client.update({ sessionUpdate: "config_option_update", configOptions: sessionConfigOptions(session) });
26269
- }
26379
+ assertRoutingAvailable() {
26380
+ if (this.switching)
26381
+ throw RequestError.invalidRequest(undefined, "Provider routing is changing; retry after providers/set or providers/disable finishes");
26382
+ }
26383
+ async admission(operation, sessionId) {
26384
+ this.assertRoutingAvailable();
26385
+ if (sessionId && this.sessionMutations.has(sessionId))
26386
+ throw RequestError.invalidRequest({ sessionId }, "Session lifecycle or configuration work is in progress; retry when it finishes");
26387
+ if (sessionId)
26388
+ this.sessionMutations.add(sessionId);
26389
+ this.admissions += 1;
26390
+ try {
26391
+ return await operation();
26392
+ } finally {
26393
+ this.admissions -= 1;
26394
+ if (sessionId)
26395
+ this.sessionMutations.delete(sessionId);
26396
+ }
26397
+ }
26398
+ async changeProvider(change) {
26399
+ const pending = this.providerQueue.then(async () => {
26400
+ this.assertNoActiveTurns("providers/set or providers/disable");
26401
+ if (this.admissions > 0)
26402
+ throw RequestError.invalidRequest(undefined, "Session configuration or lifecycle work is in progress; retry the provider change when it finishes");
26403
+ this.switching = true;
26404
+ const candidate = this.providers.copy();
26405
+ const attempted = [];
26406
+ const staged = new Map;
26407
+ try {
26408
+ change(candidate);
26409
+ const catalog = await this.withCodex(() => this.codex.allModels(true));
26410
+ const modelProvider = await this.resolveModelProvider(candidate);
26411
+ for (const runtime of this.sessions.values()) {
26412
+ try {
26413
+ await this.codex.threadTurnsList({ threadId: runtime.session.id, limit: 1, itemsView: "notLoaded" });
26414
+ } catch (error40) {
26415
+ throw RequestError.invalidRequest({ sessionId: runtime.session.id, details: errorMessage(error40) }, "Cannot reload this session's history for a provider change; close empty sessions and configure the provider before creating them, or retry after history is available");
26416
+ }
26417
+ }
26418
+ for (const runtime of this.sessions.values()) {
26419
+ const config2 = { ...runtime.config };
26420
+ const providers = candidate.threadConfig()["model_providers"];
26421
+ if (providers === undefined)
26422
+ delete config2["model_providers"];
26423
+ else
26424
+ config2["model_providers"] = providers;
26425
+ const nextCatalog = candidate.catalog(catalog);
26426
+ const requestedModel = candidate.active?.model ?? (findModel(nextCatalog, runtime.session.model.model) ? runtime.session.model.model : null);
26427
+ const selection = resolveModelSelection(nextCatalog, requestedModel, requestedModel === runtime.session.model.model ? runtime.session.model.effort : null);
26428
+ attempted.push(runtime);
26429
+ await this.codex.threadUnsubscribe({ threadId: runtime.session.id });
26430
+ const thread = await this.withCodex(() => this.codex.threadResume({
26431
+ threadId: runtime.session.id,
26432
+ cwd: runtime.session.cwd,
26433
+ config: config2,
26434
+ modelProvider,
26435
+ model: selection.model,
26436
+ excludeTurns: true
26437
+ }));
26438
+ staged.set(runtime, { config: config2, catalog: nextCatalog, model: resolveModelSelection(nextCatalog, candidate.active?.model ?? thread.model, thread.reasoningEffort) });
26439
+ }
26440
+ this.providers = candidate;
26441
+ for (const [runtime, next] of staged) {
26442
+ runtime.config = next.config;
26443
+ runtime.modelProvider = modelProvider;
26444
+ runtime.session.catalog = next.catalog;
26445
+ runtime.session.model = next.model;
26446
+ runtime.stale = false;
26447
+ runtime.client.update({ sessionUpdate: "config_option_update", configOptions: sessionConfigOptions(runtime.session), _meta: { codex: { routing: { stale: false } } } }).catch((error40) => logger.error("Provider config notification failed", error40));
26448
+ }
26449
+ } catch (error40) {
26450
+ if (attempted.length === 0 && error40 instanceof RequestError)
26451
+ throw error40;
26452
+ const staleSessions = [];
26453
+ for (const runtime of attempted) {
26454
+ try {
26455
+ await this.codex.threadUnsubscribe({ threadId: runtime.session.id });
26456
+ await this.codex.threadResume({
26457
+ threadId: runtime.session.id,
26458
+ cwd: runtime.session.cwd,
26459
+ config: runtime.config,
26460
+ modelProvider: runtime.modelProvider,
26461
+ model: runtime.session.model.model,
26462
+ excludeTurns: true
26463
+ });
26464
+ } catch (rollbackError) {
26465
+ runtime.stale = true;
26466
+ staleSessions.push(runtime.session.id);
26467
+ logger.error("Provider rollback failed", rollbackError, { sessionId: runtime.session.id });
26468
+ runtime.client.update({ sessionUpdate: "config_option_update", configOptions: sessionConfigOptions(runtime.session), _meta: { codex: { routing: { stale: true } } } }).catch(() => {});
26469
+ }
26470
+ }
26471
+ throw RequestError.internalError({ staleSessions, attemptedSessions: attempted.map((runtime) => runtime.session.id) }, `Provider change failed: ${errorMessage(error40)}${staleSessions.length ? "; resume affected sessions or retry the provider change before prompting" : "; previous routing restored"}`);
26472
+ } finally {
26473
+ this.switching = false;
26474
+ }
26475
+ });
26476
+ this.providerQueue = pending.catch(() => {});
26477
+ await pending;
26270
26478
  }
26271
- async newSession(params) {
26272
- const runtime = await this.openSession({ kind: "new", request: params });
26479
+ async newSession(params, signal) {
26480
+ const runtime = await this.openSession({ kind: "new", request: params }, signal);
26273
26481
  return { sessionId: runtime.session.id, configOptions: sessionConfigOptions(runtime.session) };
26274
26482
  }
26275
- async resumeSession(params) {
26276
- const runtime = await this.openSession({ kind: "resume", request: params });
26483
+ async resumeSession(params, signal) {
26484
+ const runtime = await this.openSession({ kind: "resume", request: params }, signal);
26277
26485
  return { configOptions: sessionConfigOptions(runtime.session) };
26278
26486
  }
26279
- async forkSession(params) {
26280
- const runtime = await this.openSession({ kind: "fork", request: params });
26487
+ async forkSession(params, signal) {
26488
+ const runtime = await this.openSession({ kind: "fork", request: params }, signal);
26281
26489
  return { sessionId: runtime.session.id, configOptions: sessionConfigOptions(runtime.session) };
26282
26490
  }
26283
- async openSession(open2) {
26491
+ async openSession(open2, signal) {
26492
+ if (signal?.aborted)
26493
+ throw RequestError.requestCancelled(undefined, "Session opening was cancelled");
26494
+ const pending = this.admission(async () => {
26495
+ const runtime = await this.openSessionInternal(open2, signal);
26496
+ if (signal?.aborted) {
26497
+ await this.closeRuntime(runtime.session.id);
26498
+ throw RequestError.requestCancelled(undefined, "Session opening was cancelled");
26499
+ }
26500
+ return runtime;
26501
+ }, open2.kind === "new" ? undefined : open2.request.sessionId);
26502
+ try {
26503
+ return signal ? await abortable(pending, signal) : await pending;
26504
+ } catch (error40) {
26505
+ if (signal?.aborted)
26506
+ throw RequestError.requestCancelled(undefined, "Session opening was cancelled; any thread returned later will be unsubscribed");
26507
+ throw error40;
26508
+ }
26509
+ }
26510
+ assertUnsubscribeSettled(sessionId) {
26511
+ if (this.pendingUnsubscribes.has(sessionId)) {
26512
+ throw RequestError.invalidRequest({ sessionId }, "A previous unsubscribe is still pending; retry after Codex responds or reconnect the agent");
26513
+ }
26514
+ }
26515
+ async openSessionInternal(open2, signal) {
26284
26516
  const method = { new: "session/new", resume: "session/resume", fork: "session/fork" }[open2.kind];
26285
26517
  const capabilities = this.requireInitialized(method);
26518
+ if (open2.kind !== "new")
26519
+ this.assertUnsubscribeSettled(open2.request.sessionId);
26286
26520
  const { request } = open2;
26287
26521
  if (typeof request.cwd !== "string" || !path6.isAbsolute(request.cwd)) {
26288
26522
  throw RequestError.invalidParams({ cwd: request.cwd }, "cwd must be an absolute path");
@@ -26294,17 +26528,21 @@ class CodexAgent {
26294
26528
  const additionalDirectories = readAdditionalDirectories(request.cwd, request.additionalDirectories);
26295
26529
  if (open2.kind === "resume" && this.sessions.has(open2.request.sessionId)) {
26296
26530
  await this.closeRuntime(open2.request.sessionId);
26531
+ this.assertUnsubscribeSettled(open2.request.sessionId);
26297
26532
  }
26298
26533
  const mcpServers = request.mcpServers ?? [];
26299
- if (this.providers.active === null && await this.withCodex(() => authRequired(this.codex))) {
26534
+ let accountVersion = this.accountGeneration;
26535
+ const account = await this.withCodex(() => this.codex.accountRead({ refreshToken: false }));
26536
+ if (this.providers.active === null && account.requiresOpenaiAuth && account.account === null) {
26300
26537
  throw RequestError.authRequired(undefined, "Log in to Codex first (auth/login)");
26301
26538
  }
26302
- await this.withCodex(() => this.refreshSkills(request.cwd, additionalDirectories));
26303
26539
  const existingMcp = mcpServers.length > 0 ? await this.withCodex(() => this.configuredMcpServerNames(request.cwd)) : new Set;
26304
26540
  const config2 = buildThreadConfig(this.providers.threadConfig(), request.cwd, additionalDirectories, mcpServers, existingMcp);
26305
26541
  const mcpStartupGeneration = this.codex.mcpStartupGeneration;
26306
26542
  const modelProvider = await this.withCodex(() => this.resolveModelProvider());
26307
- const thread = await this.withCodex(async () => {
26543
+ const { thread, skills } = await this.withSkillsContext(request.cwd, additionalDirectories, async (skills2) => ({ skills: skills2, thread: await this.withCodex(async () => {
26544
+ if (signal?.aborted)
26545
+ throw RequestError.requestCancelled(undefined, "Session opening was cancelled");
26308
26546
  switch (open2.kind) {
26309
26547
  case "new":
26310
26548
  return await this.codex.threadStart({ config: config2, cwd: request.cwd, modelProvider });
@@ -26313,13 +26551,32 @@ class CodexAgent {
26313
26551
  case "fork":
26314
26552
  return await this.codex.threadFork({ threadId: open2.request.sessionId, config: config2, cwd: request.cwd, modelProvider, excludeTurns: true });
26315
26553
  }
26316
- });
26554
+ }) }));
26317
26555
  const sessionId = thread.thread.id;
26556
+ let removeCancellation = () => {};
26557
+ const openingCompletions = new Map;
26558
+ const openingStarts = new Set;
26559
+ const stopObservingOpen = this.codex.observeNotifications((notification) => {
26560
+ if (notification.method === "turn/completed" && notification.params.threadId === sessionId) {
26561
+ if (openingCompletions.size >= 32)
26562
+ openingCompletions.delete(openingCompletions.keys().next().value);
26563
+ openingCompletions.set(notification.params.turn.id, notification.params);
26564
+ }
26565
+ if (notification.method === "turn/started" && notification.params.threadId === sessionId && this.sessions.has(sessionId)) {
26566
+ if (openingStarts.size >= 32)
26567
+ openingStarts.delete(openingStarts.values().next().value);
26568
+ openingStarts.add(notification.params.turn.id);
26569
+ }
26570
+ });
26318
26571
  try {
26319
- const [codexCatalog, account] = await this.withCodex(() => Promise.all([
26320
- this.codex.allModels(),
26321
- this.codex.accountRead({ refreshToken: false })
26322
- ]));
26572
+ if (signal?.aborted)
26573
+ throw RequestError.requestCancelled(undefined, "Session opening was cancelled");
26574
+ const codexCatalog = await this.withCodex(() => this.codex.allModels());
26575
+ let openedAccount = account.account;
26576
+ while (accountVersion !== this.accountGeneration) {
26577
+ accountVersion = this.accountGeneration;
26578
+ openedAccount = (await this.withCodex(() => this.codex.accountRead({ refreshToken: false }))).account;
26579
+ }
26323
26580
  const gateway = this.providers.active;
26324
26581
  const catalog = this.providers.catalog(codexCatalog);
26325
26582
  const model = resolveModelSelection(catalog, gateway?.model ?? thread.model, thread.reasoningEffort);
@@ -26333,7 +26590,7 @@ class CodexAgent {
26333
26590
  mode: initialAgentMode(this.env),
26334
26591
  collaborationMode: DEFAULT_COLLABORATION_MODE,
26335
26592
  fastMode: thread.serviceTier === FAST_SERVICE_TIER,
26336
- account: account.account,
26593
+ account: openedAccount,
26337
26594
  title: null,
26338
26595
  titleIsExplicit: false,
26339
26596
  activeTurn: null,
@@ -26341,7 +26598,16 @@ class CodexAgent {
26341
26598
  contextWindow: null,
26342
26599
  closed: false
26343
26600
  };
26344
- const runtime = this.installRuntime(session, capabilities);
26601
+ const runtime = this.installRuntime(session, capabilities, config2, thread.modelProvider);
26602
+ const cancelOpening = () => {
26603
+ runtime.session.closed = true;
26604
+ runtime.lifetime.abort();
26605
+ runtime.client.dispose();
26606
+ };
26607
+ signal?.addEventListener("abort", cancelOpening, { once: true });
26608
+ removeCancellation = () => signal?.removeEventListener("abort", cancelOpening);
26609
+ if (signal?.aborted)
26610
+ cancelOpening();
26345
26611
  const replay = open2.kind === "fork" || open2.kind === "resume" && open2.request.replayFrom?.type === "start";
26346
26612
  if (open2.kind !== "new") {
26347
26613
  await this.replayHistory(runtime, replay, thread.thread);
@@ -26350,9 +26616,17 @@ class CodexAgent {
26350
26616
  await this.withCodex(() => this.codex.threadInjectItems({ threadId: sessionId, items: seed.map(seedItem) }));
26351
26617
  }
26352
26618
  }
26619
+ if (thread.thread.status.type === "active" && runtime.session.activeTurn === null) {
26620
+ const latest = await this.withCodex(() => this.codex.threadTurnsList({ threadId: session.id, limit: 1, sortDirection: "desc", itemsView: "full" }));
26621
+ const active = latest.data.find((turn) => turn.status === "inProgress");
26622
+ if (active && !(openingStarts.has(active.id) && openingCompletions.has(active.id))) {
26623
+ const completed = openingCompletions.get(active.id);
26624
+ this.observeForeignTurn(runtime, active.id, completed ? Promise.resolve(completed) : undefined, active.items);
26625
+ }
26626
+ }
26353
26627
  if (mcpServers.length > 0)
26354
26628
  this.reportMcpStartup(runtime, mcpStartupGeneration);
26355
- this.publishAvailableCommands(runtime);
26629
+ this.publishAvailableCommands(runtime, skills);
26356
26630
  return runtime;
26357
26631
  } catch (error40) {
26358
26632
  const runtime = this.sessions.get(sessionId);
@@ -26364,23 +26638,26 @@ class CodexAgent {
26364
26638
  }
26365
26639
  this.sessions.delete(sessionId);
26366
26640
  this.codex.detachThread(sessionId);
26367
- await this.codex.threadUnsubscribe({ threadId: sessionId }).catch(() => {});
26641
+ await within(this.unsubscribeThread(sessionId), this.closeGraceMs).catch(() => {});
26368
26642
  throw error40;
26643
+ } finally {
26644
+ removeCancellation();
26645
+ stopObservingOpen();
26369
26646
  }
26370
26647
  }
26371
- installRuntime(session, capabilities) {
26648
+ installRuntime(session, capabilities, config2, modelProvider) {
26372
26649
  const client = new ClientSession(session.id, this.link, capabilities);
26373
26650
  const bridge = new EventBridge(client, session);
26374
26651
  const turnContext = new TurnContext(session.id);
26375
26652
  const signal = () => {
26376
26653
  const turn = session.activeTurn;
26377
- return turn ? AbortSignal.any([turn.abort.signal, turn.stop.signal]) : undefined;
26654
+ return AbortSignal.any([this.codex.disconnectSignal, ...turn ? [turn.abort.signal, turn.stop.signal] : []]);
26378
26655
  };
26379
26656
  const approval = new CodexApprovalHandler(client, turnContext, signal);
26380
26657
  const elicitation = new CodexElicitationHandler(client, turnContext, signal);
26381
- const runtime = { session, client, bridge, turnContext, elicitation, lifetime: new AbortController, queue: Promise.resolve() };
26658
+ const runtime = { config: config2, modelProvider, stale: false, session, client, bridge, turnContext, elicitation, lifetime: new AbortController, queue: Promise.resolve() };
26382
26659
  const drained = (operation) => async (params) => {
26383
- await this.drain(runtime);
26660
+ await abortable(this.drain(runtime), this.codex.disconnectSignal);
26384
26661
  return await operation(params);
26385
26662
  };
26386
26663
  this.codex.attachThread(session.id, {
@@ -26399,7 +26676,23 @@ class CodexAgent {
26399
26676
  return runtime;
26400
26677
  }
26401
26678
  enqueue(runtime, notification) {
26679
+ if (runtime.lifetime.signal.aborted)
26680
+ return;
26681
+ if (notification.method === "turn/started" && runtime.session.activeTurn === null) {
26682
+ this.observeForeignTurn(runtime, notification.params.turn.id);
26683
+ }
26684
+ const turn = runtime.session.activeTurn;
26685
+ if (notification.method.startsWith("item/") && "turnId" in notification.params && typeof notification.params.turnId === "string") {
26686
+ if (!turn || turn.turnId !== null && notification.params.turnId !== turn.turnId)
26687
+ return;
26688
+ }
26689
+ if (turn && notification.method === "turn/completed")
26690
+ this.completedTurns.set(turn, notification.params.turn.id);
26691
+ if (runtime.lifetime.signal.aborted)
26692
+ return;
26402
26693
  const run = async () => {
26694
+ if (runtime.lifetime.signal.aborted)
26695
+ return;
26403
26696
  try {
26404
26697
  runtime.turnContext.observe(notification);
26405
26698
  await runtime.elicitation.observe(notification);
@@ -26410,11 +26703,21 @@ class CodexAgent {
26410
26703
  };
26411
26704
  runtime.queue = runtime.queue.then(run, run);
26412
26705
  }
26706
+ observeForeignTurn(runtime, turnId, completion, items = []) {
26707
+ if (runtime.session.activeTurn || runtime.session.closed)
26708
+ return;
26709
+ const turn = createActiveTurn(runtime.session.id);
26710
+ runtime.session.activeTurn = turn;
26711
+ this.turnStarted(runtime, turn, turnId, runtime.session.id);
26712
+ runtime.client.reportRunning();
26713
+ const completed = completion ?? this.codex.awaitTurnCompleted(runtime.session.id, turnId, turn.stop.signal);
26714
+ this.runPrompt(runtime, turn, { sessionId: runtime.session.id, prompt: [] }, completed, items);
26715
+ }
26413
26716
  async drain(runtime) {
26414
26717
  let current;
26415
26718
  do {
26416
26719
  current = runtime.queue;
26417
- await current;
26720
+ await abortable(current, runtime.client.signal);
26418
26721
  } while (runtime.queue !== current);
26419
26722
  }
26420
26723
  async replayHistory(runtime, replay, thread) {
@@ -26437,12 +26740,18 @@ class CodexAgent {
26437
26740
  await publishTitle(firstPage.data);
26438
26741
  return;
26439
26742
  }
26743
+ const cursors = new Set;
26440
26744
  let cursor = null;
26441
26745
  do {
26442
26746
  const page = await this.turnPage(session.id, cursor);
26443
26747
  await publishTitle(page.data);
26444
26748
  await client.updateAll(historyUpdates(page.data));
26445
26749
  cursor = page.nextCursor;
26750
+ if (cursor !== null) {
26751
+ if (cursors.has(cursor))
26752
+ throw RequestError.internalError({ sessionId: session.id }, "Codex history pagination repeated a cursor; retry after restarting Codex");
26753
+ cursors.add(cursor);
26754
+ }
26446
26755
  } while (cursor !== null && !session.closed);
26447
26756
  await publishTitle([]);
26448
26757
  }
@@ -26467,12 +26776,13 @@ class CodexAgent {
26467
26776
  logger.error("MCP startup reporting failed", error40, { sessionId: runtime.session.id });
26468
26777
  }
26469
26778
  }
26470
- async publishAvailableCommands(runtime) {
26779
+ async publishAvailableCommands(runtime, snapshot) {
26471
26780
  try {
26472
- const skills = await this.codex.skillsList({ cwds: [runtime.session.cwd, ...runtime.session.additionalDirectories] });
26781
+ const skills = snapshot ?? await this.codex.skillsList({ cwds: [runtime.session.cwd, ...runtime.session.additionalDirectories] });
26473
26782
  if (runtime.session.closed)
26474
26783
  return;
26475
26784
  await runtime.client.update({ sessionUpdate: "available_commands_update", availableCommands: availableCommands(skills.data) });
26785
+ this.publishedSkills.set(runtime, skills);
26476
26786
  } catch (error40) {
26477
26787
  logger.error("publishing available commands failed", error40, { sessionId: runtime.session.id });
26478
26788
  }
@@ -26499,58 +26809,92 @@ class CodexAgent {
26499
26809
  };
26500
26810
  }
26501
26811
  async closeSession(params) {
26812
+ return await this.admission(() => this.closeSessionInternal(params), params.sessionId);
26813
+ }
26814
+ async closeSessionInternal(params) {
26502
26815
  this.requireInitialized("session/close");
26503
- await this.closeRuntime(params.sessionId);
26504
- return {};
26816
+ return await this.closeRuntime(params.sessionId);
26505
26817
  }
26506
26818
  async deleteSession(params) {
26819
+ return await this.admission(() => this.deleteSessionInternal(params), params.sessionId);
26820
+ }
26821
+ async deleteSessionInternal(params) {
26507
26822
  this.requireInitialized("session/delete");
26508
26823
  await this.closeRuntime(params.sessionId);
26509
26824
  await this.withCodex(() => this.codex.threadDelete({ threadId: params.sessionId }));
26510
26825
  return {};
26511
26826
  }
26512
26827
  async archiveSession(params) {
26828
+ return await this.admission(() => this.archiveSessionInternal(params), params.sessionId);
26829
+ }
26830
+ async archiveSessionInternal(params) {
26513
26831
  this.requireInitialized("_codex/session_archive");
26514
26832
  await this.closeRuntime(params.sessionId);
26515
26833
  await this.withCodex(() => this.codex.threadArchive({ threadId: params.sessionId }));
26516
26834
  return {};
26517
26835
  }
26518
26836
  async unarchiveSession(params) {
26837
+ return await this.admission(() => this.unarchiveSessionInternal(params), params.sessionId);
26838
+ }
26839
+ async unarchiveSessionInternal(params) {
26519
26840
  this.requireInitialized("_codex/session_unarchive");
26520
26841
  await this.withCodex(() => this.codex.threadUnarchive({ threadId: params.sessionId }));
26521
26842
  return {};
26522
26843
  }
26523
26844
  async closeRuntime(sessionId) {
26845
+ const deadline = performance.now() + this.closeGraceMs;
26524
26846
  const runtime = this.sessions.get(sessionId);
26525
26847
  if (!runtime)
26526
- return;
26848
+ return {};
26527
26849
  runtime.session.closed = true;
26528
26850
  runtime.lifetime.abort();
26529
26851
  const turn = runtime.session.activeTurn;
26530
26852
  if (turn) {
26531
26853
  this.interruptTurn(runtime, turn);
26532
- let timer;
26533
- const timeout = new Promise((resolve) => {
26534
- timer = setTimeout(() => resolve("timeout"), this.closeGraceMs);
26535
- });
26536
- try {
26537
- if (await Promise.race([turn.finished.then(() => "finished"), timeout]) === "timeout") {
26538
- turn.stop.abort();
26539
- await turn.finished;
26540
- }
26541
- } finally {
26542
- clearTimeout(timer);
26854
+ await within(turn.finished, Math.max(0, this.closeGraceMs / 2));
26855
+ if (runtime.session.activeTurn === turn) {
26856
+ await within(this.reportIdle(runtime, turn, "cancelled", { usage: usageOf(runtime.session) }), Math.max(0, (deadline - performance.now()) / 2)).catch(() => {});
26543
26857
  }
26858
+ turn.stop.abort();
26544
26859
  }
26545
- this.sessions.delete(sessionId);
26860
+ if (this.sessions.get(sessionId) === runtime)
26861
+ this.sessions.delete(sessionId);
26546
26862
  this.codex.detachThread(sessionId);
26547
- runtime.bridge.dispose();
26548
26863
  runtime.client.dispose();
26549
- await this.codex.threadUnsubscribe({ threadId: sessionId }).catch((error40) => {
26864
+ runtime.bridge.dispose();
26865
+ if (turn) {
26866
+ turn.resolveStarted(null);
26867
+ runtime.session.activeTurn = null;
26868
+ turn.resolveFinished();
26869
+ }
26870
+ const unsubscribe = this.unsubscribeThread(sessionId);
26871
+ let remoteUnsubscribe = "confirmed";
26872
+ try {
26873
+ if (!await within(unsubscribe, Math.max(0, deadline - performance.now()))) {
26874
+ remoteUnsubscribe = "timed_out";
26875
+ }
26876
+ } catch (error40) {
26877
+ remoteUnsubscribe = "failed";
26550
26878
  logger.error("thread/unsubscribe failed", error40, { sessionId });
26551
- });
26879
+ }
26880
+ if (remoteUnsubscribe !== "confirmed")
26881
+ logger.log("session close detached locally", { sessionId, remoteUnsubscribe });
26882
+ return { _meta: { codex: { close: { attempted: true, localDetached: true, remoteUnsubscribe } } } };
26883
+ }
26884
+ unsubscribeThread(sessionId) {
26885
+ const unsubscribe = this.codex.threadUnsubscribe({ threadId: sessionId });
26886
+ this.pendingUnsubscribes.set(sessionId, unsubscribe);
26887
+ const release = () => {
26888
+ if (this.pendingUnsubscribes.get(sessionId) === unsubscribe)
26889
+ this.pendingUnsubscribes.delete(sessionId);
26890
+ };
26891
+ unsubscribe.then(release, release);
26892
+ return unsubscribe;
26552
26893
  }
26553
26894
  async setSessionConfigOption(params) {
26895
+ return await this.admission(() => this.setSessionConfigOptionInternal(params), params.sessionId);
26896
+ }
26897
+ async setSessionConfigOptionInternal(params) {
26554
26898
  const runtime = this.runtime(params.sessionId, "session/set_config_option");
26555
26899
  await this.withCodex(() => applyConfigOption(runtime.session, this.codex, params));
26556
26900
  const configOptions = sessionConfigOptions(runtime.session);
@@ -26565,7 +26909,12 @@ class CodexAgent {
26565
26909
  return runtime;
26566
26910
  }
26567
26911
  async prompt(params) {
26912
+ this.assertRoutingAvailable();
26913
+ if (this.sessionMutations.has(params.sessionId))
26914
+ throw RequestError.invalidRequest({ sessionId: params.sessionId }, "Session lifecycle or configuration work is in progress; retry when it finishes");
26568
26915
  const runtime = this.runtime(params.sessionId, "session/prompt");
26916
+ if (runtime.stale)
26917
+ throw RequestError.invalidRequest({ sessionId: params.sessionId, _meta: { codex: { routing: { stale: true } } } }, "Session routing is stale; resume the session or successfully change providers before prompting");
26569
26918
  const { session } = runtime;
26570
26919
  if (!Array.isArray(params.prompt) || params.prompt.length === 0) {
26571
26920
  throw RequestError.invalidParams(undefined, "prompt must contain at least one content block");
@@ -26592,23 +26941,28 @@ class CodexAgent {
26592
26941
  await this.codex.turnSteer({ threadId: runtime.session.id, expectedTurnId: turnId, input: toUserInput(params.prompt) });
26593
26942
  return { _meta: { codex: { steered: turnId } } };
26594
26943
  } catch (error40) {
26944
+ if (this.completedTurns.get(turn) === turnId || runtime.session.activeTurn !== turn) {
26945
+ await turn.finished;
26946
+ return await this.prompt(params);
26947
+ }
26595
26948
  throw RequestError.invalidRequest({ sessionId: params.sessionId, turnId }, `Could not steer the running turn: ${errorMessage(error40)}`);
26596
26949
  }
26597
26950
  }
26598
- async runPrompt(runtime, turn, params) {
26951
+ async runPrompt(runtime, turn, params, observed, items = []) {
26599
26952
  const { session, bridge } = runtime;
26600
26953
  bridge.beginTurn();
26954
+ if (items.length)
26955
+ runtime.queue = runtime.queue.then(() => bridge.restore(items));
26601
26956
  try {
26602
26957
  const command = parseCommand(params.prompt);
26603
26958
  const outcome = command ? resolveCommand(command, session) : { kind: "prompt" };
26604
- let completed = await abortable(this.executePrompt(runtime, turn, command, outcome, params), turn.stop.signal);
26959
+ let completed = await abortable(observed ?? this.executePrompt(runtime, turn, command, outcome, params), turn.stop.signal);
26605
26960
  await this.drain(runtime);
26606
26961
  await bridge.flush();
26607
- if (completed?.turn.status === "completed" && !turn.abort.signal.aborted) {
26962
+ if (completed?.turn.status === "completed" && !turn.abort.signal.aborted && !this.exitHandled) {
26608
26963
  completed = await abortable(this.maybeImplementPlan(runtime, turn, completed), turn.stop.signal);
26609
26964
  }
26610
26965
  if (completed?.turn.status === "interrupted" || turn.abort.signal.aborted) {
26611
- await bridge.cancelOpenToolCalls();
26612
26966
  await this.reportIdle(runtime, turn, "cancelled", { usage: usageOf(session) });
26613
26967
  return;
26614
26968
  }
@@ -26624,8 +26978,13 @@ class CodexAgent {
26624
26978
  await this.publishFallbackTitle(runtime, promptTitle(params.prompt));
26625
26979
  await this.reportIdle(runtime, turn, "end_turn", { usage: usageOf(session) });
26626
26980
  } catch (error40) {
26981
+ if (this.terminalTurns.has(turn)) {
26982
+ logger.error("Terminal update failed", error40, { sessionId: session.id });
26983
+ return;
26984
+ }
26985
+ if (turn.stop.signal.aborted && session.closed)
26986
+ return;
26627
26987
  if (turn.abort.signal.aborted || session.closed) {
26628
- await bridge.cancelOpenToolCalls().catch(() => {});
26629
26988
  await this.reportIdle(runtime, turn, "cancelled", { usage: usageOf(session) }).catch(() => {});
26630
26989
  return;
26631
26990
  }
@@ -26671,23 +27030,42 @@ class CodexAgent {
26671
27030
  const { session } = runtime;
26672
27031
  const model = findModel(session.catalog, session.model.model);
26673
27032
  const disableSummary = session.account?.type === "apiKey" || modelLacksReasoning(model);
26674
- await abortable(this.withCodex(() => this.refreshSkills(session.cwd, session.additionalDirectories)), turn.abort.signal);
26675
- if (turn.abort.signal.aborted) {
26676
- return interruptedTurn(session.id);
26677
- }
26678
- return await this.withCodex(() => this.codex.runTurn({
26679
- threadId: session.id,
26680
- input: toUserInput(prompt),
26681
- approvalPolicy: session.mode.approvalPolicy,
26682
- approvalsReviewer: session.mode.approvalsReviewer,
26683
- sandboxPolicy: withWritableRoots(session.mode.sandboxPolicy, session.additionalDirectories),
26684
- model: session.model.model,
26685
- effort: session.model.effort,
26686
- summary: disableSummary ? "none" : "auto",
26687
- serviceTier: session.fastMode ? FAST_SERVICE_TIER : null
26688
- }, (turnId) => {
26689
- this.turnStarted(runtime, turn, turnId, session.id);
26690
- }, turn.stop.signal));
27033
+ let startSent = false;
27034
+ const beforeStart = new AbortController;
27035
+ const cancelBeforeStart = () => {
27036
+ if (!startSent)
27037
+ beforeStart.abort(turn.abort.signal.reason);
27038
+ };
27039
+ turn.abort.signal.addEventListener("abort", cancelBeforeStart, { once: true });
27040
+ if (turn.abort.signal.aborted)
27041
+ cancelBeforeStart();
27042
+ try {
27043
+ const result = await abortable(this.withSkillsContext(session.cwd, session.additionalDirectories, async (skills) => {
27044
+ if (this.publishedSkills.get(runtime) !== skills)
27045
+ this.publishAvailableCommands(runtime, skills);
27046
+ if (turn.abort.signal.aborted || turn.stop.signal.aborted)
27047
+ return { completed: Promise.resolve(interruptedTurn(session.id)) };
27048
+ startSent = true;
27049
+ const completed = this.withCodex(() => this.codex.runTurn({
27050
+ threadId: session.id,
27051
+ input: toUserInput(prompt),
27052
+ approvalPolicy: session.mode.approvalPolicy,
27053
+ approvalsReviewer: session.mode.approvalsReviewer,
27054
+ sandboxPolicy: withWritableRoots(session.mode.sandboxPolicy, session.additionalDirectories),
27055
+ model: session.model.model,
27056
+ effort: session.model.effort,
27057
+ summary: disableSummary ? "none" : "auto",
27058
+ serviceTier: session.fastMode ? FAST_SERVICE_TIER : null
27059
+ }, (turnId) => {
27060
+ this.turnStarted(runtime, turn, turnId, session.id);
27061
+ }, turn.stop.signal));
27062
+ await Promise.race([turn.started, completed]);
27063
+ return { completed };
27064
+ }), beforeStart.signal);
27065
+ return await result.completed;
27066
+ } finally {
27067
+ turn.abort.signal.removeEventListener("abort", cancelBeforeStart);
27068
+ }
26691
27069
  }
26692
27070
  async runCompaction(runtime, turn) {
26693
27071
  const threadId = runtime.session.id;
@@ -26711,6 +27089,7 @@ class CodexAgent {
26711
27089
  const plan = runtime.bridge.takeCompletedPlan();
26712
27090
  if (!plan || runtime.session.collaborationMode !== PLAN_COLLABORATION_MODE)
26713
27091
  return completed;
27092
+ this.completedTurns.delete(turn);
26714
27093
  const signal = AbortSignal.any([turn.abort.signal, turn.stop.signal]);
26715
27094
  const approved = await this.requestPlanApproval(runtime, plan, signal);
26716
27095
  if (!approved || signal.aborted)
@@ -26766,11 +27145,16 @@ class CodexAgent {
26766
27145
  }
26767
27146
  }
26768
27147
  async reportIdle(runtime, turn, reason, extra) {
27148
+ if (this.terminalTurns.has(turn))
27149
+ return;
27150
+ this.terminalTurns.add(turn);
27151
+ await runtime.bridge.finishOpenToolCalls(reason === "cancelled" ? "cancelled" : reason === "end_turn" ? "completed" : "failed");
26769
27152
  if (runtime.session.activeTurn === turn)
26770
27153
  runtime.session.activeTurn = null;
26771
27154
  await runtime.client.reportIdle(reason, extra);
26772
27155
  }
26773
27156
  async reportTurnFailure(runtime, turn, error40) {
27157
+ const { stopReason, ...classification } = classifyTurnError(error40.codexErrorInfo);
26774
27158
  const message = error40.additionalDetails ? `${error40.message}
26775
27159
 
26776
27160
  ${error40.additionalDetails}` : error40.message;
@@ -26778,11 +27162,11 @@ ${error40.additionalDetails}` : error40.message;
26778
27162
  sessionUpdate: "agent_message_chunk",
26779
27163
  messageId: `codex-error:${runtime.session.id}:${Date.now()}`,
26780
27164
  content: { type: "text", text: message },
26781
- _meta: { codex: { error: { message: error40.message, codexErrorInfo: error40.codexErrorInfo, additionalDetails: error40.additionalDetails } } }
27165
+ _meta: { codex: { error: { ...classification, message: error40.message, codexErrorInfo: error40.codexErrorInfo, additionalDetails: error40.additionalDetails } } }
26782
27166
  });
26783
- await this.reportIdle(runtime, turn, ERROR_STOP_REASON, {
27167
+ await this.reportIdle(runtime, turn, stopReason, {
26784
27168
  usage: usageOf(runtime.session),
26785
- _meta: { codex: { error: { message: error40.message, codexErrorInfo: error40.codexErrorInfo } } }
27169
+ _meta: { codex: { error: { ...classification, message: error40.message, codexErrorInfo: error40.codexErrorInfo } } }
26786
27170
  });
26787
27171
  }
26788
27172
  async publishFallbackTitle(runtime, title) {
@@ -26811,6 +27195,7 @@ ${error40.additionalDetails}` : error40.message;
26811
27195
  let pending = turn.interrupts.get(key);
26812
27196
  if (!pending) {
26813
27197
  pending = this.codex.turnInterrupt({ threadId: turn.threadId, turnId }).then(() => {}, (error40) => {
27198
+ turn.interrupts.delete(key);
26814
27199
  logger.error("turn/interrupt failed", error40, { sessionId: runtime.session.id, turnId });
26815
27200
  });
26816
27201
  turn.interrupts.set(key, pending);
@@ -26824,13 +27209,56 @@ ${error40.additionalDetails}` : error40.message;
26824
27209
  if (turn.abort.signal.aborted)
26825
27210
  this.sendInterrupt(runtime, turn, turnId);
26826
27211
  }
26827
- async refreshSkills(cwd, additionalDirectories) {
26828
- const roots = additionalDirectories.map((root) => path6.join(root, ".agents", "skills"));
26829
- if (roots.length !== this.skillRoots.length || roots.some((root, index) => root !== this.skillRoots[index])) {
26830
- await this.codex.skillsExtraRootsSet({ extraRoots: roots });
26831
- this.skillRoots = roots;
27212
+ withSkillsContext(cwd, additionalDirectories, operation) {
27213
+ const run = async () => {
27214
+ const roots = additionalDirectories.map((root) => path6.join(root, ".agents", "skills"));
27215
+ if (roots.length !== this.skillRoots.length || roots.some((root, index) => root !== this.skillRoots[index])) {
27216
+ this.changingSkillRoots = true;
27217
+ try {
27218
+ await this.codex.skillsExtraRootsSet({ extraRoots: roots });
27219
+ } finally {
27220
+ this.changingSkillRoots = false;
27221
+ }
27222
+ this.skillRoots = roots;
27223
+ this.skillSnapshots.clear();
27224
+ }
27225
+ const skills = await this.loadSkills(cwd, additionalDirectories);
27226
+ return await operation(skills);
27227
+ };
27228
+ const result = this.skillsQueue.then(run, run);
27229
+ this.skillsQueue = result.then(() => {}, () => {});
27230
+ return result;
27231
+ }
27232
+ async loadSkills(cwd, additionalDirectories) {
27233
+ const key = JSON.stringify([cwd, ...additionalDirectories]);
27234
+ let skills = this.skillSnapshots.get(key);
27235
+ if (!skills) {
27236
+ const generation = this.skillsGeneration;
27237
+ skills = await this.withCodex(() => this.codex.skillsList({ cwds: [cwd, ...additionalDirectories], forceReload: true }));
27238
+ if (generation === this.skillsGeneration) {
27239
+ if (this.skillSnapshots.size >= 64)
27240
+ this.skillSnapshots.delete(this.skillSnapshots.keys().next().value);
27241
+ this.skillSnapshots.set(key, skills);
27242
+ }
27243
+ }
27244
+ return skills;
27245
+ }
27246
+ async refreshAvailableCommands(runtime) {
27247
+ const run = async () => {
27248
+ const roots = runtime.session.additionalDirectories.map((root) => path6.join(root, ".agents", "skills"));
27249
+ if (runtime.session.closed || roots.length !== this.skillRoots.length || roots.some((root, i) => root !== this.skillRoots[i]))
27250
+ return;
27251
+ return await this.loadSkills(runtime.session.cwd, runtime.session.additionalDirectories);
27252
+ };
27253
+ const pending = this.skillsQueue.then(run, run);
27254
+ this.skillsQueue = pending.then(() => {}, () => {});
27255
+ try {
27256
+ const skills = await pending;
27257
+ if (skills && !runtime.session.closed)
27258
+ await this.publishAvailableCommands(runtime, skills);
27259
+ } catch (error40) {
27260
+ logger.error("refreshing available commands failed", error40, { sessionId: runtime.session.id });
26832
27261
  }
26833
- await this.codex.skillsList({ cwds: [cwd, ...additionalDirectories], forceReload: true });
26834
27262
  }
26835
27263
  async configuredMcpServerNames(cwd) {
26836
27264
  const response = await this.codex.configRead({ includeLayers: true, cwd });
@@ -26843,13 +27271,13 @@ ${error40.additionalDetails}` : error40.message;
26843
27271
  }
26844
27272
  return names;
26845
27273
  }
26846
- async resolveModelProvider() {
26847
- const routed = this.providers.modelProvider();
27274
+ async resolveModelProvider(routing = this.providers) {
27275
+ const routed = routing.modelProvider();
26848
27276
  if (routed)
26849
27277
  return routed;
26850
27278
  const config2 = await this.codex.configRead({ includeLayers: false });
26851
27279
  const provider = config2.config["model_provider"];
26852
- return typeof provider === "string" && provider.length > 0 ? provider : null;
27280
+ return typeof provider === "string" && provider.length > 0 ? provider : OPENAI_PROVIDER_ID;
26853
27281
  }
26854
27282
  async withCodex(operation) {
26855
27283
  try {
@@ -26889,7 +27317,7 @@ ${stderr}` : ""}`);
26889
27317
  this.exitHandled = true;
26890
27318
  for (const runtime of this.sessions.values()) {
26891
27319
  const turn = runtime.session.activeTurn;
26892
- if (!turn)
27320
+ if (!turn || this.completedTurns.get(turn) === turn.turnId)
26893
27321
  continue;
26894
27322
  const stderr = this.process?.recentStderr() || null;
26895
27323
  turn.stop.abort(Object.assign(new Error("Connection to Codex was lost"), { additionalDetails: stderr }));
@@ -26953,6 +27381,19 @@ function seedItem(message) {
26953
27381
  content: [{ type: message.role === "user" ? "input_text" : "output_text", text: message.text }]
26954
27382
  };
26955
27383
  }
27384
+ async function within(operation, ms) {
27385
+ let timer;
27386
+ try {
27387
+ return await Promise.race([
27388
+ operation.then(() => true),
27389
+ new Promise((resolve) => {
27390
+ timer = setTimeout(() => resolve(false), ms);
27391
+ })
27392
+ ]);
27393
+ } finally {
27394
+ clearTimeout(timer);
27395
+ }
27396
+ }
26956
27397
 
26957
27398
  // src/agent/createAgent.ts
26958
27399
  function createAgentApp(options) {
@@ -26964,7 +27405,7 @@ function createAgentApp(options) {
26964
27405
  };
26965
27406
  return agent({ name: options.info.name }).onConnect((connection) => {
26966
27407
  agent2 = new CodexAgent(connection.client, options);
26967
- }).onRequest(methods.agent.initialize, (ctx) => current().initialize(ctx.params)).onRequest(methods.agent.auth.login, (ctx) => current().login(ctx.params, ctx.requestId)).onRequest(methods.agent.auth.logout, (ctx) => current().logout(ctx.params)).onRequest(methods.agent.providers.list, (ctx) => current().listProviders(ctx.params)).onRequest(methods.agent.providers.set, (ctx) => current().setProvider(ctx.params)).onRequest(methods.agent.providers.disable, (ctx) => current().disableProvider(ctx.params)).onRequest(methods.agent.session.new, (ctx) => current().newSession(ctx.params)).onRequest(methods.agent.session.resume, (ctx) => current().resumeSession(ctx.params)).onRequest(methods.agent.session.fork, (ctx) => current().forkSession(ctx.params)).onRequest(methods.agent.session.list, (ctx) => current().listSessions(ctx.params)).onRequest(methods.agent.session.close, (ctx) => current().closeSession(ctx.params)).onRequest(methods.agent.session.delete, (ctx) => current().deleteSession(ctx.params)).onRequest("_codex/session_archive", { parse: parseSessionIdParams }, (ctx) => current().archiveSession(ctx.params)).onRequest("_codex/session_unarchive", { parse: parseSessionIdParams }, (ctx) => current().unarchiveSession(ctx.params)).onRequest(methods.agent.session.setConfigOption, (ctx) => current().setSessionConfigOption(ctx.params)).onRequest(methods.agent.session.prompt, (ctx) => current().prompt(ctx.params)).onNotification(methods.agent.session.cancel, (ctx) => current().cancel(ctx.params));
27408
+ }).onRequest(methods.agent.initialize, (ctx) => current().initialize(ctx.params)).onRequest(methods.agent.auth.login, (ctx) => current().login(ctx.params, ctx.requestId)).onRequest(methods.agent.auth.logout, (ctx) => current().logout(ctx.params)).onRequest(methods.agent.providers.list, (ctx) => current().listProviders(ctx.params)).onRequest(methods.agent.providers.set, (ctx) => current().setProvider(ctx.params)).onRequest(methods.agent.providers.disable, (ctx) => current().disableProvider(ctx.params)).onRequest(methods.agent.session.new, (ctx) => current().newSession(ctx.params, ctx.signal)).onRequest(methods.agent.session.resume, (ctx) => current().resumeSession(ctx.params, ctx.signal)).onRequest(methods.agent.session.fork, (ctx) => current().forkSession(ctx.params, ctx.signal)).onRequest(methods.agent.session.list, (ctx) => current().listSessions(ctx.params)).onRequest(methods.agent.session.close, (ctx) => current().closeSession(ctx.params)).onRequest(methods.agent.session.delete, (ctx) => current().deleteSession(ctx.params)).onRequest("_codex/session_archive", { parse: parseSessionIdParams }, (ctx) => current().archiveSession(ctx.params)).onRequest("_codex/session_unarchive", { parse: parseSessionIdParams }, (ctx) => current().unarchiveSession(ctx.params)).onRequest(methods.agent.session.setConfigOption, (ctx) => current().setSessionConfigOption(ctx.params)).onRequest(methods.agent.session.prompt, (ctx) => current().prompt(ctx.params)).onNotification(methods.agent.session.cancel, (ctx) => current().cancel(ctx.params));
26968
27409
  }
26969
27410
 
26970
27411
  // src/codex/AppServerClient.ts
@@ -26984,6 +27425,7 @@ class AppServerClient {
26984
27425
  earlyTurnCompletions = new Map;
26985
27426
  startingThreads = new Map;
26986
27427
  startingReviews = 0;
27428
+ modelCatalog = null;
26987
27429
  mcpStartupStates = new Map;
26988
27430
  mcpStartupWaiters = [];
26989
27431
  notificationWaiters = [];
@@ -27017,6 +27459,9 @@ class AppServerClient {
27017
27459
  return handler ? await handler.handleUserInput(params) : { answers: {} };
27018
27460
  });
27019
27461
  }
27462
+ get disconnectSignal() {
27463
+ return this.disconnected.signal;
27464
+ }
27020
27465
  attachThread(threadId, handlers) {
27021
27466
  this.threadHandlers.set(threadId, handlers.notification);
27022
27467
  this.approvalHandlers.set(threadId, handlers.approval);
@@ -27057,7 +27502,18 @@ class AppServerClient {
27057
27502
  scope.abort();
27058
27503
  }
27059
27504
  }
27505
+ observers = new Set;
27506
+ observeNotifications(handler) {
27507
+ this.observers.add(handler);
27508
+ return () => {
27509
+ this.observers.delete(handler);
27510
+ };
27511
+ }
27060
27512
  dispatchNotification(notification) {
27513
+ if (notification.method === "account/updated")
27514
+ this.modelCatalog = null;
27515
+ for (const observer of this.observers)
27516
+ observer(notification);
27061
27517
  if (this.notificationWaiters.length > 0) {
27062
27518
  const remaining = [];
27063
27519
  for (const waiter of this.notificationWaiters) {
@@ -27126,10 +27582,26 @@ class AppServerClient {
27126
27582
  this.earlyTurnCompletions.delete(key);
27127
27583
  return Promise.resolve(early);
27128
27584
  }
27129
- const completed = new Promise((resolve) => {
27130
- this.turnCompletionWaiters.set(key, resolve);
27585
+ const lifetime = signal ? AbortSignal.any([signal, this.disconnected.signal]) : this.disconnected.signal;
27586
+ return new Promise((resolve, reject) => {
27587
+ const cleanup = () => {
27588
+ lifetime.removeEventListener("abort", aborted2);
27589
+ this.turnCompletionWaiters.delete(key);
27590
+ };
27591
+ const aborted2 = () => {
27592
+ cleanup();
27593
+ reject(lifetime.reason);
27594
+ };
27595
+ if (lifetime.aborted) {
27596
+ reject(lifetime.reason);
27597
+ return;
27598
+ }
27599
+ lifetime.addEventListener("abort", aborted2, { once: true });
27600
+ this.turnCompletionWaiters.set(key, (event) => {
27601
+ cleanup();
27602
+ resolve(event);
27603
+ });
27131
27604
  });
27132
- return abortable(completed, signal ? AbortSignal.any([signal, this.disconnected.signal]) : this.disconnected.signal).finally(() => this.turnCompletionWaiters.delete(key));
27133
27605
  }
27134
27606
  resolveTurnInterrupted(threadId, turnId) {
27135
27607
  this.recordTurnCompleted({
@@ -27279,13 +27751,34 @@ class AppServerClient {
27279
27751
  modelList(params) {
27280
27752
  return this.send({ method: "model/list", params });
27281
27753
  }
27282
- async allModels() {
27754
+ invalidateModels() {
27755
+ this.modelCatalog = null;
27756
+ }
27757
+ allModels(refresh = false) {
27758
+ if (!refresh && this.modelCatalog && this.modelCatalog.expires > performance.now())
27759
+ return this.modelCatalog.pending;
27760
+ const pending = this.loadModels();
27761
+ const entry = { expires: performance.now() + 30000, pending };
27762
+ this.modelCatalog = entry;
27763
+ pending.catch(() => {
27764
+ if (this.modelCatalog === entry)
27765
+ this.modelCatalog = null;
27766
+ });
27767
+ return pending;
27768
+ }
27769
+ async loadModels() {
27283
27770
  const models = [];
27771
+ const cursors = new Set;
27284
27772
  let cursor = null;
27285
27773
  do {
27286
27774
  const page = await this.modelList({ cursor, limit: null });
27287
27775
  models.push(...page.data);
27288
27776
  cursor = page.nextCursor;
27777
+ if (cursor) {
27778
+ if (cursors.has(cursor))
27779
+ throw new Error("Codex model/list returned a repeated pagination cursor; retry after restarting Codex");
27780
+ cursors.add(cursor);
27781
+ }
27289
27782
  } while (cursor);
27290
27783
  return models;
27291
27784
  }
@@ -27332,9 +27825,10 @@ function threadIdOf(notification) {
27332
27825
  }
27333
27826
 
27334
27827
  // src/codex/process.ts
27828
+ import { existsSync } from "fs";
27829
+ import path7 from "path";
27335
27830
  import { spawn, spawnSync } from "child_process";
27336
27831
  import { createRequire } from "module";
27337
- var rpc = __toESM(require_main(), 1);
27338
27832
 
27339
27833
  // src/codex/transport.ts
27340
27834
  var import_node2 = __toESM(require_main(), 1);
@@ -27342,7 +27836,7 @@ import { StringDecoder } from "string_decoder";
27342
27836
  function wireLog(value) {
27343
27837
  return JSON.stringify(value, (key, nested) => {
27344
27838
  const normalized = key.replace(/[_-]/g, "").toLowerCase();
27345
- if (/^(apikey|accesskey|accesstoken|refreshtoken|idtoken|secretaccesskey|sessiontoken|authorization|proxyauthorization|password|secret|token|cookie|setcookie|headers|httpheaders)$/.test(normalized))
27839
+ if (/^(apikey|accesskey|accesstoken|refreshtoken|idtoken|secretaccesskey|sessiontoken|authorization|proxyauthorization|password|secret|token|cookie|setcookie|headers|httpheaders|env|queryparams)$/.test(normalized))
27346
27840
  return "***";
27347
27841
  return nested;
27348
27842
  });
@@ -27415,7 +27909,7 @@ function createReader(readable) {
27415
27909
  const closed = new import_node2.Emitter;
27416
27910
  const partial2 = new import_node2.Emitter;
27417
27911
  const decoder = new StringDecoder("utf8");
27418
- let buffer = "";
27912
+ let fragments = [];
27419
27913
  let ended = false;
27420
27914
  let callback = null;
27421
27915
  const deliver = (line) => {
@@ -27441,16 +27935,21 @@ function createReader(readable) {
27441
27935
  callback?.(message);
27442
27936
  };
27443
27937
  const onData = (chunk) => {
27444
- buffer += typeof chunk === "string" ? chunk : decoder.write(chunk);
27938
+ const text2 = typeof chunk === "string" ? chunk : decoder.write(chunk);
27939
+ let offset = 0;
27445
27940
  for (;; ) {
27446
- const newline2 = buffer.indexOf(`
27447
- `);
27941
+ const newline2 = text2.indexOf(`
27942
+ `, offset);
27448
27943
  if (newline2 < 0)
27449
27944
  break;
27450
- const line = buffer.slice(0, newline2);
27451
- buffer = buffer.slice(newline2 + 1);
27945
+ const part = text2.slice(offset, newline2);
27946
+ const line = fragments.length ? fragments.join("") + part : part;
27947
+ fragments = [];
27452
27948
  deliver(line);
27949
+ offset = newline2 + 1;
27453
27950
  }
27951
+ if (offset < text2.length)
27952
+ fragments.push(text2.slice(offset));
27454
27953
  };
27455
27954
  const onClose = () => {
27456
27955
  if (ended)
@@ -27459,9 +27958,9 @@ function createReader(readable) {
27459
27958
  closed.fire();
27460
27959
  };
27461
27960
  const onEnd = () => {
27462
- buffer += decoder.end();
27463
- deliver(buffer);
27464
- buffer = "";
27961
+ fragments.push(decoder.end());
27962
+ deliver(fragments.join(""));
27963
+ fragments = [];
27465
27964
  onClose();
27466
27965
  };
27467
27966
  const onError = (error40) => errors3.fire(error40);
@@ -27471,7 +27970,7 @@ function createReader(readable) {
27471
27970
  readable.off("close", onClose);
27472
27971
  readable.off("error", onError);
27473
27972
  callback = null;
27474
- buffer = "";
27973
+ fragments = [];
27475
27974
  };
27476
27975
  return {
27477
27976
  listen(receive) {
@@ -27493,6 +27992,92 @@ function createReader(readable) {
27493
27992
  }
27494
27993
  };
27495
27994
  }
27995
+ function createCodexConnection(readable, writable) {
27996
+ const reader = createReader(readable);
27997
+ const writer = createWriter(writable);
27998
+ const closed = new import_node2.Emitter;
27999
+ let queued = 0;
28000
+ let eof = false;
28001
+ let finished = false;
28002
+ let timer;
28003
+ let resolveDrained;
28004
+ const drained = new Promise((resolve) => {
28005
+ resolveDrained = resolve;
28006
+ });
28007
+ const finish = () => {
28008
+ if (finished)
28009
+ return;
28010
+ finished = true;
28011
+ clearTimeout(timer);
28012
+ closed.fire();
28013
+ resolveDrained();
28014
+ };
28015
+ const check2 = () => {
28016
+ if (eof && queued === 0)
28017
+ setImmediate(() => {
28018
+ if (queued === 0)
28019
+ finish();
28020
+ });
28021
+ };
28022
+ reader.onClose(() => {
28023
+ eof = true;
28024
+ timer ??= setTimeout(() => {
28025
+ logger.error("Codex dispatch drain deadline exceeded", new Error(`Undispatched frames: ${queued}`));
28026
+ finish();
28027
+ }, 5000);
28028
+ check2();
28029
+ });
28030
+ const inputFailed = () => {
28031
+ if (finished || timer)
28032
+ return;
28033
+ timer = setTimeout(() => {
28034
+ logger.error("Codex stdout did not close after stdin", new Error("Transport shutdown deadline exceeded"));
28035
+ finish();
28036
+ }, 5000);
28037
+ };
28038
+ writer.onClose(inputFailed);
28039
+ const counted = (message) => !(("method" in message) && message.method === "$/cancelRequest");
28040
+ const connection = import_node2.createMessageConnection({
28041
+ ...reader,
28042
+ onClose: closed.event,
28043
+ listen(receive) {
28044
+ return reader.listen((message) => {
28045
+ if (counted(message))
28046
+ queued += 1;
28047
+ receive(message);
28048
+ });
28049
+ }
28050
+ }, {
28051
+ ...writer,
28052
+ onClose: () => ({ dispose() {} }),
28053
+ async write(message) {
28054
+ try {
28055
+ await writer.write(message);
28056
+ } catch (error40) {
28057
+ logger.error("Codex write failed; draining stdout before disconnect", error40);
28058
+ inputFailed();
28059
+ await drained;
28060
+ }
28061
+ }
28062
+ }, undefined, {
28063
+ messageStrategy: { handleMessage(message, next) {
28064
+ try {
28065
+ return next(message);
28066
+ } finally {
28067
+ if (counted(message))
28068
+ queued -= 1;
28069
+ check2();
28070
+ }
28071
+ } }
28072
+ });
28073
+ connection.onDispose(() => {
28074
+ finished = true;
28075
+ clearTimeout(timer);
28076
+ resolveDrained();
28077
+ closed.dispose();
28078
+ });
28079
+ return { connection, drained };
28080
+ }
27496
28081
 
27497
28082
  // src/codex/process.ts
27498
28083
  var STDERR_RETAIN_BYTES = 8 * 1024;
@@ -27519,7 +28104,29 @@ function resolveCodexLauncher(codexPath) {
27519
28104
  return process.platform === "win32" ? { command: `"${codexPath}"`, prefixArgs: [], shell: true } : { command: codexPath, prefixArgs: [], shell: false };
27520
28105
  }
27521
28106
  try {
27522
- return { command: process.execPath, prefixArgs: [createRequire(import.meta.url).resolve("@openai/codex/bin/codex.js")], shell: false };
28107
+ const require2 = createRequire(import.meta.url);
28108
+ const launcher = require2.resolve("@openai/codex/bin/codex.js");
28109
+ const platformRequire = createRequire(launcher);
28110
+ const targets = {
28111
+ "linux-x64": "x86_64-unknown-linux-musl",
28112
+ "linux-arm64": "aarch64-unknown-linux-musl",
28113
+ "darwin-x64": "x86_64-apple-darwin",
28114
+ "darwin-arm64": "aarch64-apple-darwin",
28115
+ "win32-x64": "x86_64-pc-windows-msvc",
28116
+ "win32-arm64": "aarch64-pc-windows-msvc"
28117
+ };
28118
+ const platform2 = `${process.platform}-${process.arch}`;
28119
+ const target = targets[platform2];
28120
+ if (!target)
28121
+ throw new Error(`Unsupported Codex platform: ${platform2}`);
28122
+ let vendor = path7.resolve(path7.dirname(launcher), "..", "vendor");
28123
+ try {
28124
+ vendor = path7.join(path7.dirname(platformRequire.resolve(`@openai/codex-${platform2}/package.json`)), "vendor");
28125
+ } catch {}
28126
+ const command = path7.join(vendor, target, "bin", process.platform === "win32" ? "codex.exe" : "codex");
28127
+ if (!existsSync(command))
28128
+ throw new Error(`Missing Codex platform binary: ${command}`);
28129
+ return { command, prefixArgs: [], shell: false };
27523
28130
  } catch (error40) {
27524
28131
  throw new Error("No Codex CLI: set CODEX_PATH to an installed Codex executable, or install the optional " + `@openai/codex dependency (${String(error40)})`);
27525
28132
  }
@@ -27536,11 +28143,13 @@ function startCodexProcess(codexPath, env = process.env) {
27536
28143
  stderr = (stderr + data.toString()).slice(-STDERR_RETAIN_BYTES);
27537
28144
  logger.log("[codex stderr]", { data: data.toString() });
27538
28145
  });
27539
- const connection = rpc.createMessageConnection(createReader(child.stdout), createWriter(child.stdin));
28146
+ const { connection, drained } = createCodexConnection(child.stdout, child.stdin);
27540
28147
  connection.listen();
28148
+ drained.then(() => connection.dispose());
27541
28149
  const exited = new Promise((resolve) => {
27542
- child.on("exit", (code, signal) => {
28150
+ child.on("close", async (code, signal) => {
27543
28151
  logger.log("codex exited", { code, signal });
28152
+ await drained;
27544
28153
  connection.dispose();
27545
28154
  resolve(code);
27546
28155
  });