@nyssance/codex-acp-v2 0.3.4 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/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.4.0",
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,19 @@ 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
+ this.notifySkillsChanged();
26287
+ }
26288
+ if (notification.method === "account/updated")
26289
+ this.refreshAccounts().catch((error40) => logger.error("refreshing account failed", error40));
26290
+ });
26291
+ this.codex.disconnectSignal.addEventListener("abort", stopObserving, { once: true });
26167
26292
  }
26168
26293
  async initialize(params) {
26169
26294
  if (params.protocolVersion !== PROTOCOL_VERSION) {
@@ -26198,7 +26323,7 @@ class CodexAgent {
26198
26323
  additionalDirectories: {}
26199
26324
  },
26200
26325
  providers: {},
26201
- _meta: { codex: { archive: true, seedHistory: true } }
26326
+ _meta: { codex: { archive: true, seedHistory: true, skills: true, plugins: true } }
26202
26327
  },
26203
26328
  authMethods: authMethods(this.capabilities, this.env)
26204
26329
  };
@@ -26222,7 +26347,11 @@ class CodexAgent {
26222
26347
  return {};
26223
26348
  }
26224
26349
  async refreshAccounts() {
26350
+ this.codex.invalidateModels();
26351
+ const generation = ++this.accountGeneration;
26225
26352
  const account = (await this.codex.accountRead({ refreshToken: false })).account;
26353
+ if (generation !== this.accountGeneration)
26354
+ return;
26226
26355
  for (const runtime of this.sessions.values())
26227
26356
  runtime.session.account = account;
26228
26357
  }
@@ -26232,16 +26361,14 @@ class CodexAgent {
26232
26361
  }
26233
26362
  async setProvider(params) {
26234
26363
  this.requireInitialized("providers/set");
26235
- this.assertNoActiveTurns("providers/set");
26236
- this.providers.set(params);
26237
- await this.rebindSessions();
26364
+ await this.changeProvider((candidate) => candidate.set(params));
26238
26365
  return {};
26239
26366
  }
26240
26367
  async disableProvider(params) {
26241
26368
  this.requireInitialized("providers/disable");
26242
- this.assertNoActiveTurns("providers/disable");
26243
- this.providers.disable(params);
26244
- await this.rebindSessions();
26369
+ if (params.providerId !== OPENAI_PROVIDER_ID)
26370
+ return {};
26371
+ await this.changeProvider((candidate) => candidate.disable(params));
26245
26372
  return {};
26246
26373
  }
26247
26374
  assertNoActiveTurns(method) {
@@ -26250,39 +26377,147 @@ class CodexAgent {
26250
26377
  throw RequestError.invalidRequest({ sessions: busy }, `${method} cannot change routing while a turn is running; cancel it or wait for idle`);
26251
26378
  }
26252
26379
  }
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
- }
26380
+ assertRoutingAvailable() {
26381
+ if (this.switching)
26382
+ throw RequestError.invalidRequest(undefined, "Provider routing is changing; retry after providers/set or providers/disable finishes");
26383
+ }
26384
+ async admission(operation, sessionId) {
26385
+ this.assertRoutingAvailable();
26386
+ if (sessionId && this.sessionMutations.has(sessionId))
26387
+ throw RequestError.invalidRequest({ sessionId }, "Session lifecycle or configuration work is in progress; retry when it finishes");
26388
+ if (sessionId)
26389
+ this.sessionMutations.add(sessionId);
26390
+ this.admissions += 1;
26391
+ try {
26392
+ return await operation();
26393
+ } finally {
26394
+ this.admissions -= 1;
26395
+ if (sessionId)
26396
+ this.sessionMutations.delete(sessionId);
26397
+ }
26398
+ }
26399
+ async changeProvider(change) {
26400
+ const pending = this.providerQueue.then(async () => {
26401
+ this.assertNoActiveTurns("providers/set or providers/disable");
26402
+ if (this.admissions > 0)
26403
+ throw RequestError.invalidRequest(undefined, "Session configuration or lifecycle work is in progress; retry the provider change when it finishes");
26404
+ this.switching = true;
26405
+ const candidate = this.providers.copy();
26406
+ const attempted = [];
26407
+ const staged = new Map;
26408
+ try {
26409
+ change(candidate);
26410
+ const catalog = await this.withCodex(() => this.codex.allModels(true));
26411
+ const modelProvider = await this.resolveModelProvider(candidate);
26412
+ for (const runtime of this.sessions.values()) {
26413
+ try {
26414
+ await this.codex.threadTurnsList({ threadId: runtime.session.id, limit: 1, itemsView: "notLoaded" });
26415
+ } catch (error40) {
26416
+ 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");
26417
+ }
26418
+ }
26419
+ for (const runtime of this.sessions.values()) {
26420
+ const config2 = { ...runtime.config };
26421
+ const providers = candidate.threadConfig()["model_providers"];
26422
+ if (providers === undefined)
26423
+ delete config2["model_providers"];
26424
+ else
26425
+ config2["model_providers"] = providers;
26426
+ const nextCatalog = candidate.catalog(catalog);
26427
+ const requestedModel = candidate.active?.model ?? (findModel(nextCatalog, runtime.session.model.model) ? runtime.session.model.model : null);
26428
+ const selection = resolveModelSelection(nextCatalog, requestedModel, requestedModel === runtime.session.model.model ? runtime.session.model.effort : null);
26429
+ attempted.push(runtime);
26430
+ await this.codex.threadUnsubscribe({ threadId: runtime.session.id });
26431
+ const thread = await this.withCodex(() => this.codex.threadResume({
26432
+ threadId: runtime.session.id,
26433
+ cwd: runtime.session.cwd,
26434
+ config: config2,
26435
+ modelProvider,
26436
+ model: selection.model,
26437
+ excludeTurns: true
26438
+ }));
26439
+ staged.set(runtime, { config: config2, catalog: nextCatalog, model: resolveModelSelection(nextCatalog, candidate.active?.model ?? thread.model, thread.reasoningEffort) });
26440
+ }
26441
+ this.providers = candidate;
26442
+ for (const [runtime, next] of staged) {
26443
+ runtime.config = next.config;
26444
+ runtime.modelProvider = modelProvider;
26445
+ runtime.session.catalog = next.catalog;
26446
+ runtime.session.model = next.model;
26447
+ runtime.stale = false;
26448
+ 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));
26449
+ }
26450
+ } catch (error40) {
26451
+ if (attempted.length === 0 && error40 instanceof RequestError)
26452
+ throw error40;
26453
+ const staleSessions = [];
26454
+ for (const runtime of attempted) {
26455
+ try {
26456
+ await this.codex.threadUnsubscribe({ threadId: runtime.session.id });
26457
+ await this.codex.threadResume({
26458
+ threadId: runtime.session.id,
26459
+ cwd: runtime.session.cwd,
26460
+ config: runtime.config,
26461
+ modelProvider: runtime.modelProvider,
26462
+ model: runtime.session.model.model,
26463
+ excludeTurns: true
26464
+ });
26465
+ } catch (rollbackError) {
26466
+ runtime.stale = true;
26467
+ staleSessions.push(runtime.session.id);
26468
+ logger.error("Provider rollback failed", rollbackError, { sessionId: runtime.session.id });
26469
+ runtime.client.update({ sessionUpdate: "config_option_update", configOptions: sessionConfigOptions(runtime.session), _meta: { codex: { routing: { stale: true } } } }).catch(() => {});
26470
+ }
26471
+ }
26472
+ 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"}`);
26473
+ } finally {
26474
+ this.switching = false;
26475
+ }
26476
+ });
26477
+ this.providerQueue = pending.catch(() => {});
26478
+ await pending;
26270
26479
  }
26271
- async newSession(params) {
26272
- const runtime = await this.openSession({ kind: "new", request: params });
26480
+ async newSession(params, signal) {
26481
+ const runtime = await this.openSession({ kind: "new", request: params }, signal);
26273
26482
  return { sessionId: runtime.session.id, configOptions: sessionConfigOptions(runtime.session) };
26274
26483
  }
26275
- async resumeSession(params) {
26276
- const runtime = await this.openSession({ kind: "resume", request: params });
26484
+ async resumeSession(params, signal) {
26485
+ const runtime = await this.openSession({ kind: "resume", request: params }, signal);
26277
26486
  return { configOptions: sessionConfigOptions(runtime.session) };
26278
26487
  }
26279
- async forkSession(params) {
26280
- const runtime = await this.openSession({ kind: "fork", request: params });
26488
+ async forkSession(params, signal) {
26489
+ const runtime = await this.openSession({ kind: "fork", request: params }, signal);
26281
26490
  return { sessionId: runtime.session.id, configOptions: sessionConfigOptions(runtime.session) };
26282
26491
  }
26283
- async openSession(open2) {
26492
+ async openSession(open2, signal) {
26493
+ if (signal?.aborted)
26494
+ throw RequestError.requestCancelled(undefined, "Session opening was cancelled");
26495
+ const pending = this.admission(async () => {
26496
+ const runtime = await this.openSessionInternal(open2, signal);
26497
+ if (signal?.aborted) {
26498
+ await this.closeRuntime(runtime.session.id);
26499
+ throw RequestError.requestCancelled(undefined, "Session opening was cancelled");
26500
+ }
26501
+ return runtime;
26502
+ }, open2.kind === "new" ? undefined : open2.request.sessionId);
26503
+ try {
26504
+ return signal ? await abortable(pending, signal) : await pending;
26505
+ } catch (error40) {
26506
+ if (signal?.aborted)
26507
+ throw RequestError.requestCancelled(undefined, "Session opening was cancelled; any thread returned later will be unsubscribed");
26508
+ throw error40;
26509
+ }
26510
+ }
26511
+ assertUnsubscribeSettled(sessionId) {
26512
+ if (this.pendingUnsubscribes.has(sessionId)) {
26513
+ throw RequestError.invalidRequest({ sessionId }, "A previous unsubscribe is still pending; retry after Codex responds or reconnect the agent");
26514
+ }
26515
+ }
26516
+ async openSessionInternal(open2, signal) {
26284
26517
  const method = { new: "session/new", resume: "session/resume", fork: "session/fork" }[open2.kind];
26285
26518
  const capabilities = this.requireInitialized(method);
26519
+ if (open2.kind !== "new")
26520
+ this.assertUnsubscribeSettled(open2.request.sessionId);
26286
26521
  const { request } = open2;
26287
26522
  if (typeof request.cwd !== "string" || !path6.isAbsolute(request.cwd)) {
26288
26523
  throw RequestError.invalidParams({ cwd: request.cwd }, "cwd must be an absolute path");
@@ -26294,17 +26529,21 @@ class CodexAgent {
26294
26529
  const additionalDirectories = readAdditionalDirectories(request.cwd, request.additionalDirectories);
26295
26530
  if (open2.kind === "resume" && this.sessions.has(open2.request.sessionId)) {
26296
26531
  await this.closeRuntime(open2.request.sessionId);
26532
+ this.assertUnsubscribeSettled(open2.request.sessionId);
26297
26533
  }
26298
26534
  const mcpServers = request.mcpServers ?? [];
26299
- if (this.providers.active === null && await this.withCodex(() => authRequired(this.codex))) {
26535
+ let accountVersion = this.accountGeneration;
26536
+ const account = await this.withCodex(() => this.codex.accountRead({ refreshToken: false }));
26537
+ if (this.providers.active === null && account.requiresOpenaiAuth && account.account === null) {
26300
26538
  throw RequestError.authRequired(undefined, "Log in to Codex first (auth/login)");
26301
26539
  }
26302
- await this.withCodex(() => this.refreshSkills(request.cwd, additionalDirectories));
26303
26540
  const existingMcp = mcpServers.length > 0 ? await this.withCodex(() => this.configuredMcpServerNames(request.cwd)) : new Set;
26304
26541
  const config2 = buildThreadConfig(this.providers.threadConfig(), request.cwd, additionalDirectories, mcpServers, existingMcp);
26305
26542
  const mcpStartupGeneration = this.codex.mcpStartupGeneration;
26306
26543
  const modelProvider = await this.withCodex(() => this.resolveModelProvider());
26307
- const thread = await this.withCodex(async () => {
26544
+ const { thread, skills } = await this.withSkillsContext(request.cwd, additionalDirectories, async (skills2) => ({ skills: skills2, thread: await this.withCodex(async () => {
26545
+ if (signal?.aborted)
26546
+ throw RequestError.requestCancelled(undefined, "Session opening was cancelled");
26308
26547
  switch (open2.kind) {
26309
26548
  case "new":
26310
26549
  return await this.codex.threadStart({ config: config2, cwd: request.cwd, modelProvider });
@@ -26313,13 +26552,32 @@ class CodexAgent {
26313
26552
  case "fork":
26314
26553
  return await this.codex.threadFork({ threadId: open2.request.sessionId, config: config2, cwd: request.cwd, modelProvider, excludeTurns: true });
26315
26554
  }
26316
- });
26555
+ }) }));
26317
26556
  const sessionId = thread.thread.id;
26557
+ let removeCancellation = () => {};
26558
+ const openingCompletions = new Map;
26559
+ const openingStarts = new Set;
26560
+ const stopObservingOpen = this.codex.observeNotifications((notification) => {
26561
+ if (notification.method === "turn/completed" && notification.params.threadId === sessionId) {
26562
+ if (openingCompletions.size >= 32)
26563
+ openingCompletions.delete(openingCompletions.keys().next().value);
26564
+ openingCompletions.set(notification.params.turn.id, notification.params);
26565
+ }
26566
+ if (notification.method === "turn/started" && notification.params.threadId === sessionId && this.sessions.has(sessionId)) {
26567
+ if (openingStarts.size >= 32)
26568
+ openingStarts.delete(openingStarts.values().next().value);
26569
+ openingStarts.add(notification.params.turn.id);
26570
+ }
26571
+ });
26318
26572
  try {
26319
- const [codexCatalog, account] = await this.withCodex(() => Promise.all([
26320
- this.codex.allModels(),
26321
- this.codex.accountRead({ refreshToken: false })
26322
- ]));
26573
+ if (signal?.aborted)
26574
+ throw RequestError.requestCancelled(undefined, "Session opening was cancelled");
26575
+ const codexCatalog = await this.withCodex(() => this.codex.allModels());
26576
+ let openedAccount = account.account;
26577
+ while (accountVersion !== this.accountGeneration) {
26578
+ accountVersion = this.accountGeneration;
26579
+ openedAccount = (await this.withCodex(() => this.codex.accountRead({ refreshToken: false }))).account;
26580
+ }
26323
26581
  const gateway = this.providers.active;
26324
26582
  const catalog = this.providers.catalog(codexCatalog);
26325
26583
  const model = resolveModelSelection(catalog, gateway?.model ?? thread.model, thread.reasoningEffort);
@@ -26333,7 +26591,7 @@ class CodexAgent {
26333
26591
  mode: initialAgentMode(this.env),
26334
26592
  collaborationMode: DEFAULT_COLLABORATION_MODE,
26335
26593
  fastMode: thread.serviceTier === FAST_SERVICE_TIER,
26336
- account: account.account,
26594
+ account: openedAccount,
26337
26595
  title: null,
26338
26596
  titleIsExplicit: false,
26339
26597
  activeTurn: null,
@@ -26341,7 +26599,16 @@ class CodexAgent {
26341
26599
  contextWindow: null,
26342
26600
  closed: false
26343
26601
  };
26344
- const runtime = this.installRuntime(session, capabilities);
26602
+ const runtime = this.installRuntime(session, capabilities, config2, thread.modelProvider);
26603
+ const cancelOpening = () => {
26604
+ runtime.session.closed = true;
26605
+ runtime.lifetime.abort();
26606
+ runtime.client.dispose();
26607
+ };
26608
+ signal?.addEventListener("abort", cancelOpening, { once: true });
26609
+ removeCancellation = () => signal?.removeEventListener("abort", cancelOpening);
26610
+ if (signal?.aborted)
26611
+ cancelOpening();
26345
26612
  const replay = open2.kind === "fork" || open2.kind === "resume" && open2.request.replayFrom?.type === "start";
26346
26613
  if (open2.kind !== "new") {
26347
26614
  await this.replayHistory(runtime, replay, thread.thread);
@@ -26350,9 +26617,17 @@ class CodexAgent {
26350
26617
  await this.withCodex(() => this.codex.threadInjectItems({ threadId: sessionId, items: seed.map(seedItem) }));
26351
26618
  }
26352
26619
  }
26620
+ if (thread.thread.status.type === "active" && runtime.session.activeTurn === null) {
26621
+ const latest = await this.withCodex(() => this.codex.threadTurnsList({ threadId: session.id, limit: 1, sortDirection: "desc", itemsView: "full" }));
26622
+ const active = latest.data.find((turn) => turn.status === "inProgress");
26623
+ if (active && !(openingStarts.has(active.id) && openingCompletions.has(active.id))) {
26624
+ const completed = openingCompletions.get(active.id);
26625
+ this.observeForeignTurn(runtime, active.id, completed ? Promise.resolve(completed) : undefined, active.items);
26626
+ }
26627
+ }
26353
26628
  if (mcpServers.length > 0)
26354
26629
  this.reportMcpStartup(runtime, mcpStartupGeneration);
26355
- this.publishAvailableCommands(runtime);
26630
+ this.publishAvailableCommands(runtime, skills);
26356
26631
  return runtime;
26357
26632
  } catch (error40) {
26358
26633
  const runtime = this.sessions.get(sessionId);
@@ -26364,23 +26639,26 @@ class CodexAgent {
26364
26639
  }
26365
26640
  this.sessions.delete(sessionId);
26366
26641
  this.codex.detachThread(sessionId);
26367
- await this.codex.threadUnsubscribe({ threadId: sessionId }).catch(() => {});
26642
+ await within(this.unsubscribeThread(sessionId), this.closeGraceMs).catch(() => {});
26368
26643
  throw error40;
26644
+ } finally {
26645
+ removeCancellation();
26646
+ stopObservingOpen();
26369
26647
  }
26370
26648
  }
26371
- installRuntime(session, capabilities) {
26649
+ installRuntime(session, capabilities, config2, modelProvider) {
26372
26650
  const client = new ClientSession(session.id, this.link, capabilities);
26373
26651
  const bridge = new EventBridge(client, session);
26374
26652
  const turnContext = new TurnContext(session.id);
26375
26653
  const signal = () => {
26376
26654
  const turn = session.activeTurn;
26377
- return turn ? AbortSignal.any([turn.abort.signal, turn.stop.signal]) : undefined;
26655
+ return AbortSignal.any([this.codex.disconnectSignal, ...turn ? [turn.abort.signal, turn.stop.signal] : []]);
26378
26656
  };
26379
26657
  const approval = new CodexApprovalHandler(client, turnContext, signal);
26380
26658
  const elicitation = new CodexElicitationHandler(client, turnContext, signal);
26381
- const runtime = { session, client, bridge, turnContext, elicitation, lifetime: new AbortController, queue: Promise.resolve() };
26659
+ const runtime = { config: config2, modelProvider, stale: false, session, client, bridge, turnContext, elicitation, lifetime: new AbortController, queue: Promise.resolve() };
26382
26660
  const drained = (operation) => async (params) => {
26383
- await this.drain(runtime);
26661
+ await abortable(this.drain(runtime), this.codex.disconnectSignal);
26384
26662
  return await operation(params);
26385
26663
  };
26386
26664
  this.codex.attachThread(session.id, {
@@ -26399,7 +26677,23 @@ class CodexAgent {
26399
26677
  return runtime;
26400
26678
  }
26401
26679
  enqueue(runtime, notification) {
26680
+ if (runtime.lifetime.signal.aborted)
26681
+ return;
26682
+ if (notification.method === "turn/started" && runtime.session.activeTurn === null) {
26683
+ this.observeForeignTurn(runtime, notification.params.turn.id);
26684
+ }
26685
+ const turn = runtime.session.activeTurn;
26686
+ if (notification.method.startsWith("item/") && "turnId" in notification.params && typeof notification.params.turnId === "string") {
26687
+ if (!turn || turn.turnId !== null && notification.params.turnId !== turn.turnId)
26688
+ return;
26689
+ }
26690
+ if (turn && notification.method === "turn/completed")
26691
+ this.completedTurns.set(turn, notification.params.turn.id);
26692
+ if (runtime.lifetime.signal.aborted)
26693
+ return;
26402
26694
  const run = async () => {
26695
+ if (runtime.lifetime.signal.aborted)
26696
+ return;
26403
26697
  try {
26404
26698
  runtime.turnContext.observe(notification);
26405
26699
  await runtime.elicitation.observe(notification);
@@ -26410,11 +26704,21 @@ class CodexAgent {
26410
26704
  };
26411
26705
  runtime.queue = runtime.queue.then(run, run);
26412
26706
  }
26707
+ observeForeignTurn(runtime, turnId, completion, items = []) {
26708
+ if (runtime.session.activeTurn || runtime.session.closed)
26709
+ return;
26710
+ const turn = createActiveTurn(runtime.session.id);
26711
+ runtime.session.activeTurn = turn;
26712
+ this.turnStarted(runtime, turn, turnId, runtime.session.id);
26713
+ runtime.client.reportRunning();
26714
+ const completed = completion ?? this.codex.awaitTurnCompleted(runtime.session.id, turnId, turn.stop.signal);
26715
+ this.runPrompt(runtime, turn, { sessionId: runtime.session.id, prompt: [] }, completed, items);
26716
+ }
26413
26717
  async drain(runtime) {
26414
26718
  let current;
26415
26719
  do {
26416
26720
  current = runtime.queue;
26417
- await current;
26721
+ await abortable(current, runtime.client.signal);
26418
26722
  } while (runtime.queue !== current);
26419
26723
  }
26420
26724
  async replayHistory(runtime, replay, thread) {
@@ -26437,12 +26741,18 @@ class CodexAgent {
26437
26741
  await publishTitle(firstPage.data);
26438
26742
  return;
26439
26743
  }
26744
+ const cursors = new Set;
26440
26745
  let cursor = null;
26441
26746
  do {
26442
26747
  const page = await this.turnPage(session.id, cursor);
26443
26748
  await publishTitle(page.data);
26444
26749
  await client.updateAll(historyUpdates(page.data));
26445
26750
  cursor = page.nextCursor;
26751
+ if (cursor !== null) {
26752
+ if (cursors.has(cursor))
26753
+ throw RequestError.internalError({ sessionId: session.id }, "Codex history pagination repeated a cursor; retry after restarting Codex");
26754
+ cursors.add(cursor);
26755
+ }
26446
26756
  } while (cursor !== null && !session.closed);
26447
26757
  await publishTitle([]);
26448
26758
  }
@@ -26467,12 +26777,13 @@ class CodexAgent {
26467
26777
  logger.error("MCP startup reporting failed", error40, { sessionId: runtime.session.id });
26468
26778
  }
26469
26779
  }
26470
- async publishAvailableCommands(runtime) {
26780
+ async publishAvailableCommands(runtime, snapshot) {
26471
26781
  try {
26472
- const skills = await this.codex.skillsList({ cwds: [runtime.session.cwd, ...runtime.session.additionalDirectories] });
26782
+ const skills = snapshot ?? await this.codex.skillsList({ cwds: [runtime.session.cwd, ...runtime.session.additionalDirectories] });
26473
26783
  if (runtime.session.closed)
26474
26784
  return;
26475
26785
  await runtime.client.update({ sessionUpdate: "available_commands_update", availableCommands: availableCommands(skills.data) });
26786
+ this.publishedSkills.set(runtime, skills);
26476
26787
  } catch (error40) {
26477
26788
  logger.error("publishing available commands failed", error40, { sessionId: runtime.session.id });
26478
26789
  }
@@ -26499,58 +26810,147 @@ class CodexAgent {
26499
26810
  };
26500
26811
  }
26501
26812
  async closeSession(params) {
26813
+ return await this.admission(() => this.closeSessionInternal(params), params.sessionId);
26814
+ }
26815
+ async closeSessionInternal(params) {
26502
26816
  this.requireInitialized("session/close");
26503
- await this.closeRuntime(params.sessionId);
26504
- return {};
26817
+ return await this.closeRuntime(params.sessionId);
26505
26818
  }
26506
26819
  async deleteSession(params) {
26820
+ return await this.admission(() => this.deleteSessionInternal(params), params.sessionId);
26821
+ }
26822
+ async deleteSessionInternal(params) {
26507
26823
  this.requireInitialized("session/delete");
26508
26824
  await this.closeRuntime(params.sessionId);
26509
26825
  await this.withCodex(() => this.codex.threadDelete({ threadId: params.sessionId }));
26510
26826
  return {};
26511
26827
  }
26512
26828
  async archiveSession(params) {
26829
+ return await this.admission(() => this.archiveSessionInternal(params), params.sessionId);
26830
+ }
26831
+ async archiveSessionInternal(params) {
26513
26832
  this.requireInitialized("_codex/session_archive");
26514
26833
  await this.closeRuntime(params.sessionId);
26515
26834
  await this.withCodex(() => this.codex.threadArchive({ threadId: params.sessionId }));
26516
26835
  return {};
26517
26836
  }
26518
26837
  async unarchiveSession(params) {
26838
+ return await this.admission(() => this.unarchiveSessionInternal(params), params.sessionId);
26839
+ }
26840
+ async unarchiveSessionInternal(params) {
26519
26841
  this.requireInitialized("_codex/session_unarchive");
26520
26842
  await this.withCodex(() => this.codex.threadUnarchive({ threadId: params.sessionId }));
26521
26843
  return {};
26522
26844
  }
26845
+ async skillsList(params) {
26846
+ this.requireInitialized("_codex/skills_list");
26847
+ return await this.withCodex(() => this.codex.skillsList(params));
26848
+ }
26849
+ async skillsConfigWrite(params) {
26850
+ this.requireInitialized("_codex/skills_config_write");
26851
+ const response = await this.withCodex(() => this.codex.skillsConfigWrite(params));
26852
+ this.notifySkillsChanged();
26853
+ return response;
26854
+ }
26855
+ async pluginList(params) {
26856
+ this.requireInitialized("_codex/plugin_list");
26857
+ return await this.withCodex(() => this.codex.pluginList(params));
26858
+ }
26859
+ async pluginInstalled(params) {
26860
+ this.requireInitialized("_codex/plugin_installed");
26861
+ return await this.withCodex(() => this.codex.pluginInstalled(params));
26862
+ }
26863
+ async pluginInstall(params) {
26864
+ this.requireInitialized("_codex/plugin_install");
26865
+ const response = await this.withCodex(() => this.codex.pluginInstall(params));
26866
+ this.notifySkillsChanged();
26867
+ return response;
26868
+ }
26869
+ async pluginUninstall(params) {
26870
+ this.requireInitialized("_codex/plugin_uninstall");
26871
+ await this.withCodex(() => this.codex.pluginUninstall(params));
26872
+ this.notifySkillsChanged();
26873
+ return {};
26874
+ }
26875
+ async pluginRead(params) {
26876
+ this.requireInitialized("_codex/plugin_read");
26877
+ return await this.withCodex(() => this.codex.pluginRead(params));
26878
+ }
26879
+ async marketplaceAdd(params) {
26880
+ this.requireInitialized("_codex/marketplace_add");
26881
+ const response = await this.withCodex(() => this.codex.marketplaceAdd(params));
26882
+ this.notifySkillsChanged();
26883
+ return response;
26884
+ }
26885
+ async marketplaceRemove(params) {
26886
+ this.requireInitialized("_codex/marketplace_remove");
26887
+ await this.withCodex(() => this.codex.marketplaceRemove(params));
26888
+ this.notifySkillsChanged();
26889
+ return {};
26890
+ }
26891
+ async marketplaceUpgrade(params) {
26892
+ this.requireInitialized("_codex/marketplace_upgrade");
26893
+ const response = await this.withCodex(() => this.codex.marketplaceUpgrade(params));
26894
+ this.notifySkillsChanged();
26895
+ return response;
26896
+ }
26897
+ notifySkillsChanged() {
26898
+ this.link.notify("_codex/skills_changed", {}).catch((error40) => logger.error("skills change notification failed", error40));
26899
+ }
26523
26900
  async closeRuntime(sessionId) {
26901
+ const deadline = performance.now() + this.closeGraceMs;
26524
26902
  const runtime = this.sessions.get(sessionId);
26525
26903
  if (!runtime)
26526
- return;
26904
+ return {};
26527
26905
  runtime.session.closed = true;
26528
26906
  runtime.lifetime.abort();
26529
26907
  const turn = runtime.session.activeTurn;
26530
26908
  if (turn) {
26531
26909
  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);
26910
+ await within(turn.finished, Math.max(0, this.closeGraceMs / 2));
26911
+ if (runtime.session.activeTurn === turn) {
26912
+ await within(this.reportIdle(runtime, turn, "cancelled", { usage: usageOf(runtime.session) }), Math.max(0, (deadline - performance.now()) / 2)).catch(() => {});
26543
26913
  }
26914
+ turn.stop.abort();
26544
26915
  }
26545
- this.sessions.delete(sessionId);
26916
+ if (this.sessions.get(sessionId) === runtime)
26917
+ this.sessions.delete(sessionId);
26546
26918
  this.codex.detachThread(sessionId);
26547
- runtime.bridge.dispose();
26548
26919
  runtime.client.dispose();
26549
- await this.codex.threadUnsubscribe({ threadId: sessionId }).catch((error40) => {
26920
+ runtime.bridge.dispose();
26921
+ if (turn) {
26922
+ turn.resolveStarted(null);
26923
+ runtime.session.activeTurn = null;
26924
+ turn.resolveFinished();
26925
+ }
26926
+ const unsubscribe = this.unsubscribeThread(sessionId);
26927
+ let remoteUnsubscribe = "confirmed";
26928
+ try {
26929
+ if (!await within(unsubscribe, Math.max(0, deadline - performance.now()))) {
26930
+ remoteUnsubscribe = "timed_out";
26931
+ }
26932
+ } catch (error40) {
26933
+ remoteUnsubscribe = "failed";
26550
26934
  logger.error("thread/unsubscribe failed", error40, { sessionId });
26551
- });
26935
+ }
26936
+ if (remoteUnsubscribe !== "confirmed")
26937
+ logger.log("session close detached locally", { sessionId, remoteUnsubscribe });
26938
+ return { _meta: { codex: { close: { attempted: true, localDetached: true, remoteUnsubscribe } } } };
26939
+ }
26940
+ unsubscribeThread(sessionId) {
26941
+ const unsubscribe = this.codex.threadUnsubscribe({ threadId: sessionId });
26942
+ this.pendingUnsubscribes.set(sessionId, unsubscribe);
26943
+ const release = () => {
26944
+ if (this.pendingUnsubscribes.get(sessionId) === unsubscribe)
26945
+ this.pendingUnsubscribes.delete(sessionId);
26946
+ };
26947
+ unsubscribe.then(release, release);
26948
+ return unsubscribe;
26552
26949
  }
26553
26950
  async setSessionConfigOption(params) {
26951
+ return await this.admission(() => this.setSessionConfigOptionInternal(params), params.sessionId);
26952
+ }
26953
+ async setSessionConfigOptionInternal(params) {
26554
26954
  const runtime = this.runtime(params.sessionId, "session/set_config_option");
26555
26955
  await this.withCodex(() => applyConfigOption(runtime.session, this.codex, params));
26556
26956
  const configOptions = sessionConfigOptions(runtime.session);
@@ -26565,11 +26965,19 @@ class CodexAgent {
26565
26965
  return runtime;
26566
26966
  }
26567
26967
  async prompt(params) {
26968
+ return await this.promptInternal(params);
26969
+ }
26970
+ async promptInternal(params, allowReentry = false) {
26971
+ this.assertRoutingAvailable();
26972
+ if (!allowReentry && this.sessionMutations.has(params.sessionId))
26973
+ throw RequestError.invalidRequest({ sessionId: params.sessionId }, "Session lifecycle or configuration work is in progress; retry when it finishes");
26568
26974
  const runtime = this.runtime(params.sessionId, "session/prompt");
26569
- const { session } = runtime;
26975
+ if (runtime.stale)
26976
+ throw RequestError.invalidRequest({ sessionId: params.sessionId, _meta: { codex: { routing: { stale: true } } } }, "Session routing is stale; resume the session or successfully change providers before prompting");
26570
26977
  if (!Array.isArray(params.prompt) || params.prompt.length === 0) {
26571
26978
  throw RequestError.invalidParams(undefined, "prompt must contain at least one content block");
26572
26979
  }
26980
+ const { session } = runtime;
26573
26981
  const model = findModel(session.catalog, session.model.model);
26574
26982
  if (!modelSupportsImages(model) && params.prompt.some((block) => block.type === "image")) {
26575
26983
  throw RequestError.invalidParams({ model: session.model.model }, "The current model does not support image input");
@@ -26592,23 +27000,28 @@ class CodexAgent {
26592
27000
  await this.codex.turnSteer({ threadId: runtime.session.id, expectedTurnId: turnId, input: toUserInput(params.prompt) });
26593
27001
  return { _meta: { codex: { steered: turnId } } };
26594
27002
  } catch (error40) {
27003
+ if (this.completedTurns.get(turn) === turnId || runtime.session.activeTurn !== turn) {
27004
+ await turn.finished;
27005
+ return await this.promptInternal(params, true);
27006
+ }
26595
27007
  throw RequestError.invalidRequest({ sessionId: params.sessionId, turnId }, `Could not steer the running turn: ${errorMessage(error40)}`);
26596
27008
  }
26597
27009
  }
26598
- async runPrompt(runtime, turn, params) {
27010
+ async runPrompt(runtime, turn, params, observed, items = []) {
26599
27011
  const { session, bridge } = runtime;
26600
27012
  bridge.beginTurn();
27013
+ if (items.length)
27014
+ runtime.queue = runtime.queue.then(() => bridge.restore(items));
26601
27015
  try {
26602
27016
  const command = parseCommand(params.prompt);
26603
27017
  const outcome = command ? resolveCommand(command, session) : { kind: "prompt" };
26604
- let completed = await abortable(this.executePrompt(runtime, turn, command, outcome, params), turn.stop.signal);
27018
+ let completed = await abortable(observed ?? this.executePrompt(runtime, turn, command, outcome, params), turn.stop.signal);
26605
27019
  await this.drain(runtime);
26606
27020
  await bridge.flush();
26607
- if (completed?.turn.status === "completed" && !turn.abort.signal.aborted) {
27021
+ if (completed?.turn.status === "completed" && !turn.abort.signal.aborted && !this.exitHandled) {
26608
27022
  completed = await abortable(this.maybeImplementPlan(runtime, turn, completed), turn.stop.signal);
26609
27023
  }
26610
27024
  if (completed?.turn.status === "interrupted" || turn.abort.signal.aborted) {
26611
- await bridge.cancelOpenToolCalls();
26612
27025
  await this.reportIdle(runtime, turn, "cancelled", { usage: usageOf(session) });
26613
27026
  return;
26614
27027
  }
@@ -26624,8 +27037,13 @@ class CodexAgent {
26624
27037
  await this.publishFallbackTitle(runtime, promptTitle(params.prompt));
26625
27038
  await this.reportIdle(runtime, turn, "end_turn", { usage: usageOf(session) });
26626
27039
  } catch (error40) {
27040
+ if (this.terminalTurns.has(turn)) {
27041
+ logger.error("Terminal update failed", error40, { sessionId: session.id });
27042
+ return;
27043
+ }
27044
+ if (turn.stop.signal.aborted && session.closed)
27045
+ return;
26627
27046
  if (turn.abort.signal.aborted || session.closed) {
26628
- await bridge.cancelOpenToolCalls().catch(() => {});
26629
27047
  await this.reportIdle(runtime, turn, "cancelled", { usage: usageOf(session) }).catch(() => {});
26630
27048
  return;
26631
27049
  }
@@ -26671,23 +27089,42 @@ class CodexAgent {
26671
27089
  const { session } = runtime;
26672
27090
  const model = findModel(session.catalog, session.model.model);
26673
27091
  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));
27092
+ let startSent = false;
27093
+ const beforeStart = new AbortController;
27094
+ const cancelBeforeStart = () => {
27095
+ if (!startSent)
27096
+ beforeStart.abort(turn.abort.signal.reason);
27097
+ };
27098
+ turn.abort.signal.addEventListener("abort", cancelBeforeStart, { once: true });
27099
+ if (turn.abort.signal.aborted)
27100
+ cancelBeforeStart();
27101
+ try {
27102
+ const result = await abortable(this.withSkillsContext(session.cwd, session.additionalDirectories, async (skills) => {
27103
+ if (this.publishedSkills.get(runtime) !== skills)
27104
+ this.publishAvailableCommands(runtime, skills);
27105
+ if (turn.abort.signal.aborted || turn.stop.signal.aborted)
27106
+ return { completed: Promise.resolve(interruptedTurn(session.id)) };
27107
+ startSent = true;
27108
+ const completed = this.withCodex(() => this.codex.runTurn({
27109
+ threadId: session.id,
27110
+ input: toUserInput(prompt),
27111
+ approvalPolicy: session.mode.approvalPolicy,
27112
+ approvalsReviewer: session.mode.approvalsReviewer,
27113
+ sandboxPolicy: withWritableRoots(session.mode.sandboxPolicy, session.additionalDirectories),
27114
+ model: session.model.model,
27115
+ effort: session.model.effort,
27116
+ summary: disableSummary ? "none" : "auto",
27117
+ serviceTier: session.fastMode ? FAST_SERVICE_TIER : null
27118
+ }, (turnId) => {
27119
+ this.turnStarted(runtime, turn, turnId, session.id);
27120
+ }, turn.stop.signal));
27121
+ await Promise.race([turn.started, completed]);
27122
+ return { completed };
27123
+ }), beforeStart.signal);
27124
+ return await result.completed;
27125
+ } finally {
27126
+ turn.abort.signal.removeEventListener("abort", cancelBeforeStart);
27127
+ }
26691
27128
  }
26692
27129
  async runCompaction(runtime, turn) {
26693
27130
  const threadId = runtime.session.id;
@@ -26711,6 +27148,7 @@ class CodexAgent {
26711
27148
  const plan = runtime.bridge.takeCompletedPlan();
26712
27149
  if (!plan || runtime.session.collaborationMode !== PLAN_COLLABORATION_MODE)
26713
27150
  return completed;
27151
+ this.completedTurns.delete(turn);
26714
27152
  const signal = AbortSignal.any([turn.abort.signal, turn.stop.signal]);
26715
27153
  const approved = await this.requestPlanApproval(runtime, plan, signal);
26716
27154
  if (!approved || signal.aborted)
@@ -26766,11 +27204,16 @@ class CodexAgent {
26766
27204
  }
26767
27205
  }
26768
27206
  async reportIdle(runtime, turn, reason, extra) {
27207
+ if (this.terminalTurns.has(turn))
27208
+ return;
27209
+ this.terminalTurns.add(turn);
27210
+ await runtime.bridge.finishOpenToolCalls(reason === "cancelled" ? "cancelled" : reason === "end_turn" ? "completed" : "failed");
26769
27211
  if (runtime.session.activeTurn === turn)
26770
27212
  runtime.session.activeTurn = null;
26771
27213
  await runtime.client.reportIdle(reason, extra);
26772
27214
  }
26773
27215
  async reportTurnFailure(runtime, turn, error40) {
27216
+ const { stopReason, ...classification } = classifyTurnError(error40.codexErrorInfo);
26774
27217
  const message = error40.additionalDetails ? `${error40.message}
26775
27218
 
26776
27219
  ${error40.additionalDetails}` : error40.message;
@@ -26778,11 +27221,11 @@ ${error40.additionalDetails}` : error40.message;
26778
27221
  sessionUpdate: "agent_message_chunk",
26779
27222
  messageId: `codex-error:${runtime.session.id}:${Date.now()}`,
26780
27223
  content: { type: "text", text: message },
26781
- _meta: { codex: { error: { message: error40.message, codexErrorInfo: error40.codexErrorInfo, additionalDetails: error40.additionalDetails } } }
27224
+ _meta: { codex: { error: { ...classification, message: error40.message, codexErrorInfo: error40.codexErrorInfo, additionalDetails: error40.additionalDetails } } }
26782
27225
  });
26783
- await this.reportIdle(runtime, turn, ERROR_STOP_REASON, {
27226
+ await this.reportIdle(runtime, turn, stopReason, {
26784
27227
  usage: usageOf(runtime.session),
26785
- _meta: { codex: { error: { message: error40.message, codexErrorInfo: error40.codexErrorInfo } } }
27228
+ _meta: { codex: { error: { ...classification, message: error40.message, codexErrorInfo: error40.codexErrorInfo } } }
26786
27229
  });
26787
27230
  }
26788
27231
  async publishFallbackTitle(runtime, title) {
@@ -26811,6 +27254,7 @@ ${error40.additionalDetails}` : error40.message;
26811
27254
  let pending = turn.interrupts.get(key);
26812
27255
  if (!pending) {
26813
27256
  pending = this.codex.turnInterrupt({ threadId: turn.threadId, turnId }).then(() => {}, (error40) => {
27257
+ turn.interrupts.delete(key);
26814
27258
  logger.error("turn/interrupt failed", error40, { sessionId: runtime.session.id, turnId });
26815
27259
  });
26816
27260
  turn.interrupts.set(key, pending);
@@ -26824,13 +27268,56 @@ ${error40.additionalDetails}` : error40.message;
26824
27268
  if (turn.abort.signal.aborted)
26825
27269
  this.sendInterrupt(runtime, turn, turnId);
26826
27270
  }
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;
27271
+ withSkillsContext(cwd, additionalDirectories, operation) {
27272
+ const run = async () => {
27273
+ const roots = additionalDirectories.map((root) => path6.join(root, ".agents", "skills"));
27274
+ if (roots.length !== this.skillRoots.length || roots.some((root, index) => root !== this.skillRoots[index])) {
27275
+ this.changingSkillRoots = true;
27276
+ try {
27277
+ await this.codex.skillsExtraRootsSet({ extraRoots: roots });
27278
+ } finally {
27279
+ this.changingSkillRoots = false;
27280
+ }
27281
+ this.skillRoots = roots;
27282
+ this.skillSnapshots.clear();
27283
+ }
27284
+ const skills = await this.loadSkills(cwd, additionalDirectories);
27285
+ return await operation(skills);
27286
+ };
27287
+ const result = this.skillsQueue.then(run, run);
27288
+ this.skillsQueue = result.then(() => {}, () => {});
27289
+ return result;
27290
+ }
27291
+ async loadSkills(cwd, additionalDirectories) {
27292
+ const key = JSON.stringify([cwd, ...additionalDirectories]);
27293
+ let skills = this.skillSnapshots.get(key);
27294
+ if (!skills) {
27295
+ const generation = this.skillsGeneration;
27296
+ skills = await this.withCodex(() => this.codex.skillsList({ cwds: [cwd, ...additionalDirectories], forceReload: true }));
27297
+ if (generation === this.skillsGeneration) {
27298
+ if (this.skillSnapshots.size >= 64)
27299
+ this.skillSnapshots.delete(this.skillSnapshots.keys().next().value);
27300
+ this.skillSnapshots.set(key, skills);
27301
+ }
27302
+ }
27303
+ return skills;
27304
+ }
27305
+ async refreshAvailableCommands(runtime) {
27306
+ const run = async () => {
27307
+ const roots = runtime.session.additionalDirectories.map((root) => path6.join(root, ".agents", "skills"));
27308
+ if (runtime.session.closed || roots.length !== this.skillRoots.length || roots.some((root, i) => root !== this.skillRoots[i]))
27309
+ return;
27310
+ return await this.loadSkills(runtime.session.cwd, runtime.session.additionalDirectories);
27311
+ };
27312
+ const pending = this.skillsQueue.then(run, run);
27313
+ this.skillsQueue = pending.then(() => {}, () => {});
27314
+ try {
27315
+ const skills = await pending;
27316
+ if (skills && !runtime.session.closed)
27317
+ await this.publishAvailableCommands(runtime, skills);
27318
+ } catch (error40) {
27319
+ logger.error("refreshing available commands failed", error40, { sessionId: runtime.session.id });
26832
27320
  }
26833
- await this.codex.skillsList({ cwds: [cwd, ...additionalDirectories], forceReload: true });
26834
27321
  }
26835
27322
  async configuredMcpServerNames(cwd) {
26836
27323
  const response = await this.codex.configRead({ includeLayers: true, cwd });
@@ -26843,13 +27330,13 @@ ${error40.additionalDetails}` : error40.message;
26843
27330
  }
26844
27331
  return names;
26845
27332
  }
26846
- async resolveModelProvider() {
26847
- const routed = this.providers.modelProvider();
27333
+ async resolveModelProvider(routing = this.providers) {
27334
+ const routed = routing.modelProvider();
26848
27335
  if (routed)
26849
27336
  return routed;
26850
27337
  const config2 = await this.codex.configRead({ includeLayers: false });
26851
27338
  const provider = config2.config["model_provider"];
26852
- return typeof provider === "string" && provider.length > 0 ? provider : null;
27339
+ return typeof provider === "string" && provider.length > 0 ? provider : OPENAI_PROVIDER_ID;
26853
27340
  }
26854
27341
  async withCodex(operation) {
26855
27342
  try {
@@ -26889,7 +27376,7 @@ ${stderr}` : ""}`);
26889
27376
  this.exitHandled = true;
26890
27377
  for (const runtime of this.sessions.values()) {
26891
27378
  const turn = runtime.session.activeTurn;
26892
- if (!turn)
27379
+ if (!turn || this.completedTurns.get(turn) === turn.turnId)
26893
27380
  continue;
26894
27381
  const stderr = this.process?.recentStderr() || null;
26895
27382
  turn.stop.abort(Object.assign(new Error("Connection to Codex was lost"), { additionalDetails: stderr }));
@@ -26920,6 +27407,16 @@ function interruptedTurn(threadId) {
26920
27407
  turn: { id: "", items: [], itemsView: "notLoaded", status: "interrupted", error: null, startedAt: null, completedAt: null, durationMs: null }
26921
27408
  };
26922
27409
  }
27410
+ function objectParams() {
27411
+ return (raw) => {
27412
+ if (raw === undefined || raw === null)
27413
+ return {};
27414
+ if (typeof raw !== "object" || Array.isArray(raw)) {
27415
+ throw RequestError.invalidParams({ params: raw }, "expected a params object");
27416
+ }
27417
+ return raw;
27418
+ };
27419
+ }
26923
27420
  function parseSessionIdParams(raw) {
26924
27421
  const sessionId = raw?.sessionId;
26925
27422
  if (typeof sessionId !== "string" || sessionId.length === 0) {
@@ -26953,6 +27450,19 @@ function seedItem(message) {
26953
27450
  content: [{ type: message.role === "user" ? "input_text" : "output_text", text: message.text }]
26954
27451
  };
26955
27452
  }
27453
+ async function within(operation, ms) {
27454
+ let timer;
27455
+ try {
27456
+ return await Promise.race([
27457
+ operation.then(() => true),
27458
+ new Promise((resolve) => {
27459
+ timer = setTimeout(() => resolve(false), ms);
27460
+ })
27461
+ ]);
27462
+ } finally {
27463
+ clearTimeout(timer);
27464
+ }
27465
+ }
26956
27466
 
26957
27467
  // src/agent/createAgent.ts
26958
27468
  function createAgentApp(options) {
@@ -26964,7 +27474,7 @@ function createAgentApp(options) {
26964
27474
  };
26965
27475
  return agent({ name: options.info.name }).onConnect((connection) => {
26966
27476
  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));
27477
+ }).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("_codex/skills_list", { parse: objectParams() }, (ctx) => current().skillsList(ctx.params)).onRequest("_codex/skills_config_write", { parse: objectParams() }, (ctx) => current().skillsConfigWrite(ctx.params)).onRequest("_codex/plugin_list", { parse: objectParams() }, (ctx) => current().pluginList(ctx.params)).onRequest("_codex/plugin_installed", { parse: objectParams() }, (ctx) => current().pluginInstalled(ctx.params)).onRequest("_codex/plugin_install", { parse: objectParams() }, (ctx) => current().pluginInstall(ctx.params)).onRequest("_codex/plugin_uninstall", { parse: objectParams() }, (ctx) => current().pluginUninstall(ctx.params)).onRequest("_codex/plugin_read", { parse: objectParams() }, (ctx) => current().pluginRead(ctx.params)).onRequest("_codex/marketplace_add", { parse: objectParams() }, (ctx) => current().marketplaceAdd(ctx.params)).onRequest("_codex/marketplace_remove", { parse: objectParams() }, (ctx) => current().marketplaceRemove(ctx.params)).onRequest("_codex/marketplace_upgrade", { parse: objectParams() }, (ctx) => current().marketplaceUpgrade(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
27478
  }
26969
27479
 
26970
27480
  // src/codex/AppServerClient.ts
@@ -26984,6 +27494,7 @@ class AppServerClient {
26984
27494
  earlyTurnCompletions = new Map;
26985
27495
  startingThreads = new Map;
26986
27496
  startingReviews = 0;
27497
+ modelCatalog = null;
26987
27498
  mcpStartupStates = new Map;
26988
27499
  mcpStartupWaiters = [];
26989
27500
  notificationWaiters = [];
@@ -27017,6 +27528,9 @@ class AppServerClient {
27017
27528
  return handler ? await handler.handleUserInput(params) : { answers: {} };
27018
27529
  });
27019
27530
  }
27531
+ get disconnectSignal() {
27532
+ return this.disconnected.signal;
27533
+ }
27020
27534
  attachThread(threadId, handlers) {
27021
27535
  this.threadHandlers.set(threadId, handlers.notification);
27022
27536
  this.approvalHandlers.set(threadId, handlers.approval);
@@ -27057,7 +27571,18 @@ class AppServerClient {
27057
27571
  scope.abort();
27058
27572
  }
27059
27573
  }
27574
+ observers = new Set;
27575
+ observeNotifications(handler) {
27576
+ this.observers.add(handler);
27577
+ return () => {
27578
+ this.observers.delete(handler);
27579
+ };
27580
+ }
27060
27581
  dispatchNotification(notification) {
27582
+ if (notification.method === "account/updated")
27583
+ this.modelCatalog = null;
27584
+ for (const observer of this.observers)
27585
+ observer(notification);
27061
27586
  if (this.notificationWaiters.length > 0) {
27062
27587
  const remaining = [];
27063
27588
  for (const waiter of this.notificationWaiters) {
@@ -27126,10 +27651,26 @@ class AppServerClient {
27126
27651
  this.earlyTurnCompletions.delete(key);
27127
27652
  return Promise.resolve(early);
27128
27653
  }
27129
- const completed = new Promise((resolve) => {
27130
- this.turnCompletionWaiters.set(key, resolve);
27654
+ const lifetime = signal ? AbortSignal.any([signal, this.disconnected.signal]) : this.disconnected.signal;
27655
+ return new Promise((resolve, reject) => {
27656
+ const cleanup = () => {
27657
+ lifetime.removeEventListener("abort", aborted2);
27658
+ this.turnCompletionWaiters.delete(key);
27659
+ };
27660
+ const aborted2 = () => {
27661
+ cleanup();
27662
+ reject(lifetime.reason);
27663
+ };
27664
+ if (lifetime.aborted) {
27665
+ reject(lifetime.reason);
27666
+ return;
27667
+ }
27668
+ lifetime.addEventListener("abort", aborted2, { once: true });
27669
+ this.turnCompletionWaiters.set(key, (event) => {
27670
+ cleanup();
27671
+ resolve(event);
27672
+ });
27131
27673
  });
27132
- return abortable(completed, signal ? AbortSignal.any([signal, this.disconnected.signal]) : this.disconnected.signal).finally(() => this.turnCompletionWaiters.delete(key));
27133
27674
  }
27134
27675
  resolveTurnInterrupted(threadId, turnId) {
27135
27676
  this.recordTurnCompleted({
@@ -27279,13 +27820,34 @@ class AppServerClient {
27279
27820
  modelList(params) {
27280
27821
  return this.send({ method: "model/list", params });
27281
27822
  }
27282
- async allModels() {
27823
+ invalidateModels() {
27824
+ this.modelCatalog = null;
27825
+ }
27826
+ allModels(refresh = false) {
27827
+ if (!refresh && this.modelCatalog && this.modelCatalog.expires > performance.now())
27828
+ return this.modelCatalog.pending;
27829
+ const pending = this.loadModels();
27830
+ const entry = { expires: performance.now() + 30000, pending };
27831
+ this.modelCatalog = entry;
27832
+ pending.catch(() => {
27833
+ if (this.modelCatalog === entry)
27834
+ this.modelCatalog = null;
27835
+ });
27836
+ return pending;
27837
+ }
27838
+ async loadModels() {
27283
27839
  const models = [];
27840
+ const cursors = new Set;
27284
27841
  let cursor = null;
27285
27842
  do {
27286
27843
  const page = await this.modelList({ cursor, limit: null });
27287
27844
  models.push(...page.data);
27288
27845
  cursor = page.nextCursor;
27846
+ if (cursor) {
27847
+ if (cursors.has(cursor))
27848
+ throw new Error("Codex model/list returned a repeated pagination cursor; retry after restarting Codex");
27849
+ cursors.add(cursor);
27850
+ }
27289
27851
  } while (cursor);
27290
27852
  return models;
27291
27853
  }
@@ -27295,6 +27857,33 @@ class AppServerClient {
27295
27857
  skillsExtraRootsSet(params) {
27296
27858
  return this.send({ method: "skills/extraRoots/set", params });
27297
27859
  }
27860
+ skillsConfigWrite(params) {
27861
+ return this.send({ method: "skills/config/write", params });
27862
+ }
27863
+ pluginList(params) {
27864
+ return this.send({ method: "plugin/list", params });
27865
+ }
27866
+ pluginInstalled(params) {
27867
+ return this.send({ method: "plugin/installed", params });
27868
+ }
27869
+ pluginInstall(params) {
27870
+ return this.send({ method: "plugin/install", params });
27871
+ }
27872
+ pluginUninstall(params) {
27873
+ return this.send({ method: "plugin/uninstall", params });
27874
+ }
27875
+ pluginRead(params) {
27876
+ return this.send({ method: "plugin/read", params });
27877
+ }
27878
+ marketplaceAdd(params) {
27879
+ return this.send({ method: "marketplace/add", params });
27880
+ }
27881
+ marketplaceRemove(params) {
27882
+ return this.send({ method: "marketplace/remove", params });
27883
+ }
27884
+ marketplaceUpgrade(params) {
27885
+ return this.send({ method: "marketplace/upgrade", params });
27886
+ }
27298
27887
  accountRead(params) {
27299
27888
  return this.send({ method: "account/read", params });
27300
27889
  }
@@ -27332,9 +27921,10 @@ function threadIdOf(notification) {
27332
27921
  }
27333
27922
 
27334
27923
  // src/codex/process.ts
27924
+ import { existsSync } from "fs";
27925
+ import path7 from "path";
27335
27926
  import { spawn, spawnSync } from "child_process";
27336
27927
  import { createRequire } from "module";
27337
- var rpc = __toESM(require_main(), 1);
27338
27928
 
27339
27929
  // src/codex/transport.ts
27340
27930
  var import_node2 = __toESM(require_main(), 1);
@@ -27342,7 +27932,7 @@ import { StringDecoder } from "string_decoder";
27342
27932
  function wireLog(value) {
27343
27933
  return JSON.stringify(value, (key, nested) => {
27344
27934
  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))
27935
+ if (/^(apikey|accesskey|accesstoken|refreshtoken|idtoken|secretaccesskey|sessiontoken|authorization|proxyauthorization|password|secret|token|cookie|setcookie|headers|httpheaders|env|queryparams)$/.test(normalized))
27346
27936
  return "***";
27347
27937
  return nested;
27348
27938
  });
@@ -27415,7 +28005,7 @@ function createReader(readable) {
27415
28005
  const closed = new import_node2.Emitter;
27416
28006
  const partial2 = new import_node2.Emitter;
27417
28007
  const decoder = new StringDecoder("utf8");
27418
- let buffer = "";
28008
+ let fragments = [];
27419
28009
  let ended = false;
27420
28010
  let callback = null;
27421
28011
  const deliver = (line) => {
@@ -27441,16 +28031,21 @@ function createReader(readable) {
27441
28031
  callback?.(message);
27442
28032
  };
27443
28033
  const onData = (chunk) => {
27444
- buffer += typeof chunk === "string" ? chunk : decoder.write(chunk);
28034
+ const text2 = typeof chunk === "string" ? chunk : decoder.write(chunk);
28035
+ let offset = 0;
27445
28036
  for (;; ) {
27446
- const newline2 = buffer.indexOf(`
27447
- `);
28037
+ const newline2 = text2.indexOf(`
28038
+ `, offset);
27448
28039
  if (newline2 < 0)
27449
28040
  break;
27450
- const line = buffer.slice(0, newline2);
27451
- buffer = buffer.slice(newline2 + 1);
28041
+ const part = text2.slice(offset, newline2);
28042
+ const line = fragments.length ? fragments.join("") + part : part;
28043
+ fragments = [];
27452
28044
  deliver(line);
28045
+ offset = newline2 + 1;
27453
28046
  }
28047
+ if (offset < text2.length)
28048
+ fragments.push(text2.slice(offset));
27454
28049
  };
27455
28050
  const onClose = () => {
27456
28051
  if (ended)
@@ -27459,9 +28054,9 @@ function createReader(readable) {
27459
28054
  closed.fire();
27460
28055
  };
27461
28056
  const onEnd = () => {
27462
- buffer += decoder.end();
27463
- deliver(buffer);
27464
- buffer = "";
28057
+ fragments.push(decoder.end());
28058
+ deliver(fragments.join(""));
28059
+ fragments = [];
27465
28060
  onClose();
27466
28061
  };
27467
28062
  const onError = (error40) => errors3.fire(error40);
@@ -27471,7 +28066,7 @@ function createReader(readable) {
27471
28066
  readable.off("close", onClose);
27472
28067
  readable.off("error", onError);
27473
28068
  callback = null;
27474
- buffer = "";
28069
+ fragments = [];
27475
28070
  };
27476
28071
  return {
27477
28072
  listen(receive) {
@@ -27493,6 +28088,92 @@ function createReader(readable) {
27493
28088
  }
27494
28089
  };
27495
28090
  }
28091
+ function createCodexConnection(readable, writable) {
28092
+ const reader = createReader(readable);
28093
+ const writer = createWriter(writable);
28094
+ const closed = new import_node2.Emitter;
28095
+ let queued = 0;
28096
+ let eof = false;
28097
+ let finished = false;
28098
+ let timer;
28099
+ let resolveDrained;
28100
+ const drained = new Promise((resolve) => {
28101
+ resolveDrained = resolve;
28102
+ });
28103
+ const finish = () => {
28104
+ if (finished)
28105
+ return;
28106
+ finished = true;
28107
+ clearTimeout(timer);
28108
+ closed.fire();
28109
+ resolveDrained();
28110
+ };
28111
+ const check2 = () => {
28112
+ if (eof && queued === 0)
28113
+ setImmediate(() => {
28114
+ if (queued === 0)
28115
+ finish();
28116
+ });
28117
+ };
28118
+ reader.onClose(() => {
28119
+ eof = true;
28120
+ timer ??= setTimeout(() => {
28121
+ logger.error("Codex dispatch drain deadline exceeded", new Error(`Undispatched frames: ${queued}`));
28122
+ finish();
28123
+ }, 5000);
28124
+ check2();
28125
+ });
28126
+ const inputFailed = () => {
28127
+ if (finished || timer)
28128
+ return;
28129
+ timer = setTimeout(() => {
28130
+ logger.error("Codex stdout did not close after stdin", new Error("Transport shutdown deadline exceeded"));
28131
+ finish();
28132
+ }, 5000);
28133
+ };
28134
+ writer.onClose(inputFailed);
28135
+ const counted = (message) => !(("method" in message) && message.method === "$/cancelRequest");
28136
+ const connection = import_node2.createMessageConnection({
28137
+ ...reader,
28138
+ onClose: closed.event,
28139
+ listen(receive) {
28140
+ return reader.listen((message) => {
28141
+ if (counted(message))
28142
+ queued += 1;
28143
+ receive(message);
28144
+ });
28145
+ }
28146
+ }, {
28147
+ ...writer,
28148
+ onClose: () => ({ dispose() {} }),
28149
+ async write(message) {
28150
+ try {
28151
+ await writer.write(message);
28152
+ } catch (error40) {
28153
+ logger.error("Codex write failed; draining stdout before disconnect", error40);
28154
+ inputFailed();
28155
+ await drained;
28156
+ }
28157
+ }
28158
+ }, undefined, {
28159
+ messageStrategy: { handleMessage(message, next) {
28160
+ try {
28161
+ return next(message);
28162
+ } finally {
28163
+ if (counted(message))
28164
+ queued -= 1;
28165
+ check2();
28166
+ }
28167
+ } }
28168
+ });
28169
+ connection.onDispose(() => {
28170
+ finished = true;
28171
+ clearTimeout(timer);
28172
+ resolveDrained();
28173
+ closed.dispose();
28174
+ });
28175
+ return { connection, drained };
28176
+ }
27496
28177
 
27497
28178
  // src/codex/process.ts
27498
28179
  var STDERR_RETAIN_BYTES = 8 * 1024;
@@ -27519,7 +28200,29 @@ function resolveCodexLauncher(codexPath) {
27519
28200
  return process.platform === "win32" ? { command: `"${codexPath}"`, prefixArgs: [], shell: true } : { command: codexPath, prefixArgs: [], shell: false };
27520
28201
  }
27521
28202
  try {
27522
- return { command: process.execPath, prefixArgs: [createRequire(import.meta.url).resolve("@openai/codex/bin/codex.js")], shell: false };
28203
+ const require2 = createRequire(import.meta.url);
28204
+ const launcher = require2.resolve("@openai/codex/bin/codex.js");
28205
+ const platformRequire = createRequire(launcher);
28206
+ const targets = {
28207
+ "linux-x64": "x86_64-unknown-linux-musl",
28208
+ "linux-arm64": "aarch64-unknown-linux-musl",
28209
+ "darwin-x64": "x86_64-apple-darwin",
28210
+ "darwin-arm64": "aarch64-apple-darwin",
28211
+ "win32-x64": "x86_64-pc-windows-msvc",
28212
+ "win32-arm64": "aarch64-pc-windows-msvc"
28213
+ };
28214
+ const platform2 = `${process.platform}-${process.arch}`;
28215
+ const target = targets[platform2];
28216
+ if (!target)
28217
+ throw new Error(`Unsupported Codex platform: ${platform2}`);
28218
+ let vendor = path7.resolve(path7.dirname(launcher), "..", "vendor");
28219
+ try {
28220
+ vendor = path7.join(path7.dirname(platformRequire.resolve(`@openai/codex-${platform2}/package.json`)), "vendor");
28221
+ } catch {}
28222
+ const command = path7.join(vendor, target, "bin", process.platform === "win32" ? "codex.exe" : "codex");
28223
+ if (!existsSync(command))
28224
+ throw new Error(`Missing Codex platform binary: ${command}`);
28225
+ return { command, prefixArgs: [], shell: false };
27523
28226
  } catch (error40) {
27524
28227
  throw new Error("No Codex CLI: set CODEX_PATH to an installed Codex executable, or install the optional " + `@openai/codex dependency (${String(error40)})`);
27525
28228
  }
@@ -27536,11 +28239,13 @@ function startCodexProcess(codexPath, env = process.env) {
27536
28239
  stderr = (stderr + data.toString()).slice(-STDERR_RETAIN_BYTES);
27537
28240
  logger.log("[codex stderr]", { data: data.toString() });
27538
28241
  });
27539
- const connection = rpc.createMessageConnection(createReader(child.stdout), createWriter(child.stdin));
28242
+ const { connection, drained } = createCodexConnection(child.stdout, child.stdin);
27540
28243
  connection.listen();
28244
+ drained.then(() => connection.dispose());
27541
28245
  const exited = new Promise((resolve) => {
27542
- child.on("exit", (code, signal) => {
28246
+ child.on("close", async (code, signal) => {
27543
28247
  logger.log("codex exited", { code, signal });
28248
+ await drained;
27544
28249
  connection.dispose();
27545
28250
  resolve(code);
27546
28251
  });