@nyssance/codex-acp-v2 0.3.3 → 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 +1302 -395
  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.3",
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 [];
@@ -24749,6 +24889,29 @@ class TurnContext {
24749
24889
  }
24750
24890
  }
24751
24891
 
24892
+ // src/util/abort.ts
24893
+ function abortable(operation, signal) {
24894
+ if (signal.aborted) {
24895
+ operation.catch(() => {});
24896
+ return Promise.reject(signal.reason);
24897
+ }
24898
+ return new Promise((resolve, reject) => {
24899
+ const abort = () => {
24900
+ cleanup();
24901
+ reject(signal.reason);
24902
+ };
24903
+ const cleanup = () => signal.removeEventListener("abort", abort);
24904
+ signal.addEventListener("abort", abort, { once: true });
24905
+ operation.then((value) => {
24906
+ cleanup();
24907
+ resolve(value);
24908
+ }, (error40) => {
24909
+ cleanup();
24910
+ reject(error40);
24911
+ });
24912
+ });
24913
+ }
24914
+
24752
24915
  // node_modules/open/index.js
24753
24916
  import process10 from "process";
24754
24917
  import path5 from "path";
@@ -25470,63 +25633,63 @@ async function loginWithApiKey(codex, request, env) {
25470
25633
  if (!apiKey) {
25471
25634
  throw RequestError.invalidParams({ envVars: [CODEX_API_KEY_ENV, OPENAI_API_KEY_ENV] }, `No API key: set ${CODEX_API_KEY_ENV} or ${OPENAI_API_KEY_ENV}, or pass _meta["api-key"].apiKey`);
25472
25635
  }
25473
- const completed = codex.awaitNotification("account/login/completed");
25474
- await codex.accountLogin({ type: "apiKey", apiKey });
25475
- const result = await completed;
25476
- if (!result.success)
25477
- throw RequestError.authRequired({ error: result.error }, result.error ?? "API key login failed");
25636
+ await codex.withNotification("account/login/completed", async (completed) => {
25637
+ await codex.accountLogin({ type: "apiKey", apiKey });
25638
+ const result = await completed;
25639
+ if (!result.success)
25640
+ throw RequestError.authRequired({ error: result.error }, result.error ?? "API key login failed");
25641
+ });
25478
25642
  }
25479
25643
  async function loginWithChatGpt(codex) {
25480
25644
  const account = await codex.accountRead({ refreshToken: true });
25481
25645
  if (account.account?.type === "chatgpt")
25482
25646
  return;
25483
- const completed = codex.awaitNotification("account/login/completed");
25484
- const started = await codex.accountLogin({ type: "chatgpt" });
25485
- if (started.type !== "chatgpt")
25486
- throw RequestError.internalError({ type: started.type }, "Unexpected login response");
25487
- logger.log("opening browser for ChatGPT login", { loginId: started.loginId });
25488
- await open_default(started.authUrl);
25489
- const result = await completed;
25490
- if (!result.success)
25491
- throw RequestError.authRequired({ error: result.error }, result.error ?? "ChatGPT login failed");
25647
+ await codex.withNotification("account/login/completed", async (completed) => {
25648
+ const started = await codex.accountLogin({ type: "chatgpt" });
25649
+ if (started.type !== "chatgpt")
25650
+ throw RequestError.internalError({ type: started.type }, "Unexpected login response");
25651
+ logger.log("opening browser for ChatGPT login", { loginId: started.loginId });
25652
+ await open_default(started.authUrl);
25653
+ const result = await completed;
25654
+ if (!result.success)
25655
+ throw RequestError.authRequired({ error: result.error }, result.error ?? "ChatGPT login failed");
25656
+ });
25492
25657
  }
25493
25658
  async function loginWithDeviceCode(codex, link, requestId) {
25494
25659
  const account = await codex.accountRead({ refreshToken: true });
25495
25660
  if (account.account?.type === "chatgpt")
25496
25661
  return;
25497
- const completed = codex.awaitNotification("account/login/completed");
25498
- const started = await codex.accountLogin({ type: "chatgptDeviceCode" });
25499
- if (started.type !== "chatgptDeviceCode")
25500
- throw RequestError.internalError({ type: started.type }, "Unexpected login response");
25501
- const elicitation = link.request(methods.client.elicitation.create, {
25502
- mode: "url",
25503
- requestId,
25504
- elicitationId: started.loginId,
25505
- url: started.verificationUrl,
25506
- message: `Sign in to ChatGPT and enter this code: ${started.userCode}`
25507
- });
25508
- elicitation.catch(() => {});
25509
- const first = await Promise.race([
25510
- completed.then((result2) => ({ kind: "completed", result: result2 })),
25511
- elicitation.then((response) => ({ kind: "elicitation", response }))
25512
- ]);
25513
- if (first.kind === "elicitation" && !CreateElicitationResponse.isAccept(first.response)) {
25514
- await codex.accountLoginCancel({ loginId: started.loginId });
25515
- throw RequestError.authRequired(undefined, "ChatGPT device code login was cancelled");
25516
- }
25517
- const result = first.kind === "completed" ? first.result : await completed;
25518
- await link.notify(methods.client.elicitation.complete, { elicitationId: started.loginId });
25519
- if (!result.success)
25520
- throw RequestError.authRequired({ error: result.error }, result.error ?? "ChatGPT login failed");
25662
+ await codex.withNotification("account/login/completed", async (completed) => {
25663
+ const started = await codex.accountLogin({ type: "chatgptDeviceCode" });
25664
+ if (started.type !== "chatgptDeviceCode")
25665
+ throw RequestError.internalError({ type: started.type }, "Unexpected login response");
25666
+ const elicitation = link.request(methods.client.elicitation.create, {
25667
+ mode: "url",
25668
+ requestId,
25669
+ elicitationId: started.loginId,
25670
+ url: started.verificationUrl,
25671
+ message: `Sign in to ChatGPT and enter this code: ${started.userCode}`
25672
+ });
25673
+ elicitation.catch(() => {});
25674
+ const first = await Promise.race([
25675
+ completed.then((result2) => ({ kind: "completed", result: result2 })),
25676
+ elicitation.then((response) => ({ kind: "elicitation", response }))
25677
+ ]);
25678
+ if (first.kind === "elicitation" && !CreateElicitationResponse.isAccept(first.response)) {
25679
+ await codex.accountLoginCancel({ loginId: started.loginId });
25680
+ throw RequestError.authRequired(undefined, "ChatGPT device code login was cancelled");
25681
+ }
25682
+ const result = first.kind === "completed" ? first.result : await completed;
25683
+ await link.notify(methods.client.elicitation.complete, { elicitationId: started.loginId });
25684
+ if (!result.success)
25685
+ throw RequestError.authRequired({ error: result.error }, result.error ?? "ChatGPT login failed");
25686
+ });
25521
25687
  }
25522
25688
  async function logout(codex) {
25523
- const updated = codex.awaitNotification("account/updated");
25524
- await codex.accountLogout();
25525
- await updated;
25526
- }
25527
- async function authRequired(codex) {
25528
- const account = await codex.accountRead({ refreshToken: false });
25529
- return account.requiresOpenaiAuth && account.account === null;
25689
+ await codex.withNotification("account/updated", async (updated) => {
25690
+ await codex.accountLogout();
25691
+ await updated;
25692
+ });
25530
25693
  }
25531
25694
 
25532
25695
  // src/agent/clientSession.ts
@@ -25536,33 +25699,61 @@ class ClientSession {
25536
25699
  capabilities;
25537
25700
  waiting = 0;
25538
25701
  turnActive = false;
25702
+ turnGeneration = 0;
25703
+ disposed = false;
25704
+ output = new AbortController;
25705
+ terminalAttempted = false;
25706
+ get signal() {
25707
+ return this.output.signal;
25708
+ }
25539
25709
  constructor(sessionId, link, capabilities) {
25540
25710
  this.sessionId = sessionId;
25541
25711
  this.link = link;
25542
25712
  this.capabilities = capabilities;
25543
25713
  }
25544
25714
  async update(update) {
25545
- await this.link.notify(methods.client.session.update, { sessionId: this.sessionId, update });
25715
+ if (this.disposed)
25716
+ return;
25717
+ await abortable(this.link.notify(methods.client.session.update, { sessionId: this.sessionId, update }), this.output.signal);
25546
25718
  }
25547
25719
  async updateAll(updates) {
25548
25720
  for (const update of updates)
25549
25721
  await this.update(update);
25550
25722
  }
25551
25723
  async requestPermission(request, signal) {
25552
- return await this.waitingOnClient(() => this.link.request(methods.client.session.requestPermission, { sessionId: this.sessionId, ...request }, signal ? { cancellationSignal: signal } : undefined));
25724
+ signal = signal ? AbortSignal.any([signal, this.output.signal]) : this.output.signal;
25725
+ signal.throwIfAborted();
25726
+ return await this.waitingOnClient(() => {
25727
+ signal?.throwIfAborted();
25728
+ const pending = this.link.request(methods.client.session.requestPermission, { sessionId: this.sessionId, ...request }, signal ? { cancellationSignal: signal } : undefined);
25729
+ return signal ? abortable(pending, signal) : pending;
25730
+ }, signal);
25553
25731
  }
25554
25732
  async createElicitation(request, signal) {
25555
- return await this.waitingOnClient(() => this.link.request(methods.client.elicitation.create, request, signal ? { cancellationSignal: signal } : undefined));
25733
+ signal = signal ? AbortSignal.any([signal, this.output.signal]) : this.output.signal;
25734
+ signal.throwIfAborted();
25735
+ return await this.waitingOnClient(() => {
25736
+ signal?.throwIfAborted();
25737
+ const pending = this.link.request(methods.client.elicitation.create, request, signal ? { cancellationSignal: signal } : undefined);
25738
+ return signal ? abortable(pending, signal) : pending;
25739
+ }, signal);
25556
25740
  }
25557
25741
  async completeElicitation(elicitationId) {
25558
- await this.link.notify(methods.client.elicitation.complete, { elicitationId });
25742
+ if (this.disposed)
25743
+ return;
25744
+ await abortable(this.link.notify(methods.client.elicitation.complete, { elicitationId }), this.output.signal);
25559
25745
  }
25560
25746
  reportRunning() {
25747
+ this.terminalAttempted = false;
25748
+ this.turnGeneration += 1;
25561
25749
  this.turnActive = true;
25562
25750
  this.waiting = 0;
25563
25751
  this.state({ sessionUpdate: "state_update", state: "running" });
25564
25752
  }
25565
25753
  async reportIdle(stopReason, extra) {
25754
+ if (this.terminalAttempted)
25755
+ return;
25756
+ this.terminalAttempted = true;
25566
25757
  this.turnActive = false;
25567
25758
  this.waiting = 0;
25568
25759
  await this.update({
@@ -25573,18 +25764,21 @@ class ClientSession {
25573
25764
  ...extra?._meta === undefined ? {} : { _meta: extra._meta }
25574
25765
  });
25575
25766
  }
25576
- async waitingOnClient(operation) {
25577
- if (this.turnActive && this.waiting++ === 0) {
25578
- await this.state({ sessionUpdate: "state_update", state: "requires_action" });
25579
- }
25767
+ async waitingOnClient(operation, signal) {
25768
+ if (this.disposed)
25769
+ throw new Error("Session is closed");
25770
+ const generation = this.turnActive ? this.turnGeneration : null;
25580
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
+ }
25581
25776
  return await operation();
25582
25777
  } finally {
25583
- if (this.turnActive && --this.waiting === 0) {
25584
- await this.state({ sessionUpdate: "state_update", state: "running" });
25778
+ if (this.turnActive && generation === this.turnGeneration && --this.waiting === 0) {
25779
+ const pending = this.state({ sessionUpdate: "state_update", state: "running" });
25780
+ await (signal ? abortable(pending, signal).catch(() => {}) : pending);
25585
25781
  }
25586
- if (this.waiting < 0)
25587
- this.waiting = 0;
25588
25782
  }
25589
25783
  }
25590
25784
  async state(update) {
@@ -25594,6 +25788,12 @@ class ClientSession {
25594
25788
  logger.error("Failed to publish session state", error40, { sessionId: this.sessionId });
25595
25789
  }
25596
25790
  }
25791
+ dispose() {
25792
+ this.disposed = true;
25793
+ this.output.abort();
25794
+ this.turnActive = false;
25795
+ this.waiting = 0;
25796
+ }
25597
25797
  }
25598
25798
 
25599
25799
  // src/agent/commands.ts
@@ -25830,7 +26030,7 @@ function historyUpdates(turns) {
25830
26030
  const updates = [];
25831
26031
  for (const turn of turns) {
25832
26032
  for (const item of turn.items) {
25833
- updates.push(...itemHistory(item));
26033
+ updates.push(...itemSnapshot(item));
25834
26034
  }
25835
26035
  }
25836
26036
  return updates;
@@ -25855,63 +26055,6 @@ function firstLine(text2) {
25855
26055
  const line = text2.split(/\r?\n/).map((part) => part.trim()).find((part) => part.length > 0) ?? text2;
25856
26056
  return line.length > 120 ? `${line.slice(0, 117)}...` : line;
25857
26057
  }
25858
- function itemHistory(item) {
25859
- switch (item.type) {
25860
- case "userMessage": {
25861
- const content = item.content.flatMap(fromUserInput);
25862
- return content.length > 0 ? [{ sessionUpdate: "user_message", messageId: item.id, content }] : [];
25863
- }
25864
- case "agentMessage":
25865
- return item.text.length > 0 ? [{
25866
- sessionUpdate: "agent_message",
25867
- messageId: item.id,
25868
- content: [{ type: "text", text: item.text }],
25869
- ...item.phase ? { _meta: { codex: { phase: item.phase } } } : {}
25870
- }] : [];
25871
- case "reasoning": {
25872
- const parts = (item.summary.length > 0 ? item.summary : item.content).filter((part) => part.length > 0);
25873
- return parts.length > 0 ? [{ sessionUpdate: "agent_thought", messageId: item.id, content: parts.map((text2) => ({ type: "text", text: text2 })) }] : [];
25874
- }
25875
- case "plan":
25876
- return item.text.length > 0 ? [{ sessionUpdate: "plan_update", plan: { type: "markdown", planId: item.id, content: item.text } }] : [];
25877
- case "fileChange":
25878
- return [fileChangeStarted(item)];
25879
- case "commandExecution": {
25880
- const updates = [commandStarted(item)];
25881
- if (usesTerminal(item))
25882
- updates.push(terminalSnapshot(item));
25883
- if (item.status !== "inProgress")
25884
- updates.push(commandCompleted(item));
25885
- return updates;
25886
- }
25887
- case "mcpToolCall":
25888
- return [mcpToolCallStarted(item)];
25889
- case "dynamicToolCall":
25890
- return [{ ...dynamicToolCallStarted(item), ...dynamicToolCallCompleted(item) }];
25891
- case "webSearch":
25892
- return [webSearchSnapshot(item)];
25893
- case "imageView":
25894
- return [imageViewed(item)];
25895
- case "imageGeneration":
25896
- return [imageGenerationSnapshot(item)];
25897
- case "collabAgentToolCall":
25898
- return [collabToolCall(item, true)];
25899
- case "subAgentActivity":
25900
- return [subAgentActivity(item, "completed", true)];
25901
- case "contextCompaction":
25902
- return [compactionUpdate(item.id, "completed")];
25903
- case "enteredReviewMode":
25904
- return [];
25905
- case "exitedReviewMode": {
25906
- const text2 = item.review.trim();
25907
- return text2.length > 0 ? [{ sessionUpdate: "agent_message", messageId: item.id, content: [{ type: "text", text: text2 }] }] : [];
25908
- }
25909
- case "hookPrompt":
25910
- case "functionCallOutput":
25911
- case "sleep":
25912
- return [];
25913
- }
25914
- }
25915
26058
 
25916
26059
  // src/agent/providers.ts
25917
26060
  var OPENAI_PROVIDER_ID = "openai";
@@ -25926,6 +26069,11 @@ class ProviderRouting {
25926
26069
  this.baseConfig = baseConfig;
25927
26070
  this.configuredProvider = configuredProvider;
25928
26071
  }
26072
+ copy() {
26073
+ const copy = new ProviderRouting(this.baseConfig, this.configuredProvider);
26074
+ copy.gateway = this.gateway;
26075
+ return copy;
26076
+ }
25929
26077
  get active() {
25930
26078
  return this.gateway;
25931
26079
  }
@@ -26046,7 +26194,7 @@ function gatewayModel(model, isDefault, template) {
26046
26194
  function createActiveTurn(threadId) {
26047
26195
  let resolveStarted = () => {};
26048
26196
  let resolveFinished = () => {};
26049
- const started = new Promise((resolve) => {
26197
+ let started = new Promise((resolve) => {
26050
26198
  resolveStarted = resolve;
26051
26199
  });
26052
26200
  const finished = new Promise((resolve) => {
@@ -26058,7 +26206,18 @@ function createActiveTurn(threadId) {
26058
26206
  turnId: null,
26059
26207
  threadId,
26060
26208
  abort: new AbortController,
26061
- started,
26209
+ stop: new AbortController,
26210
+ interrupts: new Map,
26211
+ get started() {
26212
+ return started;
26213
+ },
26214
+ resetStarted() {
26215
+ this.turnId = null;
26216
+ startedSettled = false;
26217
+ started = new Promise((resolve) => {
26218
+ resolveStarted = resolve;
26219
+ });
26220
+ },
26062
26221
  resolveStarted(turnId) {
26063
26222
  if (startedSettled)
26064
26223
  return;
@@ -26078,7 +26237,6 @@ function createActiveTurn(threadId) {
26078
26237
  // src/agent/CodexAgent.ts
26079
26238
  var CLOSE_TURN_GRACE_MS = 5000;
26080
26239
  var HISTORY_PAGE_SIZE = 50;
26081
- var ERROR_STOP_REASON = "_error";
26082
26240
  var IMPLEMENT_PLAN_OPTION = "implement_plan";
26083
26241
  var REVISE_PLAN_OPTION = "revise_plan";
26084
26242
 
@@ -26087,13 +26245,27 @@ class CodexAgent {
26087
26245
  codex;
26088
26246
  process;
26089
26247
  providers;
26248
+ switching = false;
26249
+ admissions = 0;
26250
+ pendingUnsubscribes = new Map;
26251
+ sessionMutations = new Set;
26252
+ providerQueue = Promise.resolve();
26090
26253
  info;
26091
26254
  env;
26092
26255
  closeGraceMs;
26093
26256
  sessions = new Map;
26094
26257
  capabilities = null;
26095
26258
  codexInitialized = false;
26259
+ initializing = null;
26260
+ terminalTurns = new WeakSet;
26261
+ completedTurns = new WeakMap;
26096
26262
  skillRoots = [];
26263
+ skillsGeneration = 0;
26264
+ changingSkillRoots = false;
26265
+ publishedSkills = new WeakMap;
26266
+ skillSnapshots = new Map;
26267
+ skillsQueue = Promise.resolve();
26268
+ accountGeneration = 0;
26097
26269
  constructor(link, options) {
26098
26270
  this.link = link;
26099
26271
  this.codex = options.codex;
@@ -26104,22 +26276,40 @@ class CodexAgent {
26104
26276
  this.closeGraceMs = options.closeGraceMs ?? CLOSE_TURN_GRACE_MS;
26105
26277
  this.process?.exited.then(() => this.handleCodexExit());
26106
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 });
26107
26291
  }
26108
26292
  async initialize(params) {
26109
26293
  if (params.protocolVersion !== PROTOCOL_VERSION) {
26110
26294
  throw RequestError.invalidParams({ protocolVersion: params.protocolVersion }, `Unsupported protocol version ${params.protocolVersion}; this agent speaks ACP v${PROTOCOL_VERSION}`);
26111
26295
  }
26112
- this.capabilities = {
26296
+ const capabilities = {
26113
26297
  formElicitation: params.capabilities?.elicitation?.form != null,
26114
26298
  urlElicitation: params.capabilities?.elicitation?.url != null
26115
26299
  };
26116
26300
  if (!this.codexInitialized) {
26117
- await this.withCodex(() => this.codex.initialize({
26118
- clientInfo: { name: params.info.name, title: params.info.title ?? null, version: params.info.version },
26119
- capabilities: { experimentalApi: true, requestAttestation: false }
26120
- }));
26121
- this.codexInitialized = true;
26301
+ this.initializing ??= this.withCodex(async () => {
26302
+ await this.codex.initialize({
26303
+ clientInfo: { name: params.info.name, title: params.info.title ?? null, version: params.info.version },
26304
+ capabilities: { experimentalApi: true, requestAttestation: false }
26305
+ });
26306
+ this.codexInitialized = true;
26307
+ }).finally(() => {
26308
+ this.initializing = null;
26309
+ });
26310
+ await this.initializing;
26122
26311
  }
26312
+ this.capabilities = capabilities;
26123
26313
  return {
26124
26314
  protocolVersion: PROTOCOL_VERSION,
26125
26315
  info: this.info,
@@ -26156,7 +26346,11 @@ class CodexAgent {
26156
26346
  return {};
26157
26347
  }
26158
26348
  async refreshAccounts() {
26349
+ this.codex.invalidateModels();
26350
+ const generation = ++this.accountGeneration;
26159
26351
  const account = (await this.codex.accountRead({ refreshToken: false })).account;
26352
+ if (generation !== this.accountGeneration)
26353
+ return;
26160
26354
  for (const runtime of this.sessions.values())
26161
26355
  runtime.session.account = account;
26162
26356
  }
@@ -26166,16 +26360,14 @@ class CodexAgent {
26166
26360
  }
26167
26361
  async setProvider(params) {
26168
26362
  this.requireInitialized("providers/set");
26169
- this.assertNoActiveTurns("providers/set");
26170
- this.providers.set(params);
26171
- await this.rebindSessions();
26363
+ await this.changeProvider((candidate) => candidate.set(params));
26172
26364
  return {};
26173
26365
  }
26174
26366
  async disableProvider(params) {
26175
26367
  this.requireInitialized("providers/disable");
26176
- this.assertNoActiveTurns("providers/disable");
26177
- this.providers.disable(params);
26178
- await this.rebindSessions();
26368
+ if (params.providerId !== OPENAI_PROVIDER_ID)
26369
+ return {};
26370
+ await this.changeProvider((candidate) => candidate.disable(params));
26179
26371
  return {};
26180
26372
  }
26181
26373
  assertNoActiveTurns(method) {
@@ -26184,57 +26376,173 @@ class CodexAgent {
26184
26376
  throw RequestError.invalidRequest({ sessions: busy }, `${method} cannot change routing while a turn is running; cancel it or wait for idle`);
26185
26377
  }
26186
26378
  }
26187
- async rebindSessions() {
26188
- const gateway = this.providers.active;
26189
- for (const runtime of this.sessions.values()) {
26190
- const { session } = runtime;
26191
- const config2 = buildThreadConfig(this.providers.threadConfig(), session.cwd, session.additionalDirectories, [], new Set);
26192
- const thread = await this.withCodex(() => this.codex.threadResume({
26193
- threadId: session.id,
26194
- cwd: session.cwd,
26195
- config: config2,
26196
- modelProvider: this.providers.modelProvider(),
26197
- excludeTurns: true
26198
- }));
26199
- const codexCatalog = await this.withCodex(() => this.codex.allModels());
26200
- session.catalog = this.providers.catalog(codexCatalog);
26201
- session.model = resolveModelSelection(session.catalog, gateway?.model ?? thread.model, thread.reasoningEffort);
26202
- await runtime.client.update({ sessionUpdate: "config_option_update", configOptions: sessionConfigOptions(session) });
26203
- }
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;
26204
26478
  }
26205
- async newSession(params) {
26206
- 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);
26207
26481
  return { sessionId: runtime.session.id, configOptions: sessionConfigOptions(runtime.session) };
26208
26482
  }
26209
- async resumeSession(params) {
26210
- 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);
26211
26485
  return { configOptions: sessionConfigOptions(runtime.session) };
26212
26486
  }
26213
- async forkSession(params) {
26214
- 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);
26215
26489
  return { sessionId: runtime.session.id, configOptions: sessionConfigOptions(runtime.session) };
26216
26490
  }
26217
- 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) {
26218
26516
  const method = { new: "session/new", resume: "session/resume", fork: "session/fork" }[open2.kind];
26219
26517
  const capabilities = this.requireInitialized(method);
26518
+ if (open2.kind !== "new")
26519
+ this.assertUnsubscribeSettled(open2.request.sessionId);
26220
26520
  const { request } = open2;
26221
26521
  if (typeof request.cwd !== "string" || !path6.isAbsolute(request.cwd)) {
26222
26522
  throw RequestError.invalidParams({ cwd: request.cwd }, "cwd must be an absolute path");
26223
26523
  }
26224
- if (open2.kind !== "new" && this.sessions.has(open2.request.sessionId)) {
26225
- await this.closeRuntime(open2.request.sessionId);
26524
+ if (open2.kind === "resume" && open2.request.replayFrom != null && open2.request.replayFrom.type !== "start") {
26525
+ throw RequestError.invalidParams({ replayFrom: open2.request.replayFrom }, 'Only replayFrom {type: "start"} is supported');
26226
26526
  }
26527
+ const seed = open2.kind === "new" ? seedHistoryOf(open2.request._meta) : [];
26227
26528
  const additionalDirectories = readAdditionalDirectories(request.cwd, request.additionalDirectories);
26529
+ if (open2.kind === "resume" && this.sessions.has(open2.request.sessionId)) {
26530
+ await this.closeRuntime(open2.request.sessionId);
26531
+ this.assertUnsubscribeSettled(open2.request.sessionId);
26532
+ }
26228
26533
  const mcpServers = request.mcpServers ?? [];
26229
- 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) {
26230
26537
  throw RequestError.authRequired(undefined, "Log in to Codex first (auth/login)");
26231
26538
  }
26232
- await this.withCodex(() => this.refreshSkills(request.cwd, additionalDirectories));
26233
26539
  const existingMcp = mcpServers.length > 0 ? await this.withCodex(() => this.configuredMcpServerNames(request.cwd)) : new Set;
26234
26540
  const config2 = buildThreadConfig(this.providers.threadConfig(), request.cwd, additionalDirectories, mcpServers, existingMcp);
26235
26541
  const mcpStartupGeneration = this.codex.mcpStartupGeneration;
26236
26542
  const modelProvider = await this.withCodex(() => this.resolveModelProvider());
26237
- 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");
26238
26546
  switch (open2.kind) {
26239
26547
  case "new":
26240
26548
  return await this.codex.threadStart({ config: config2, cwd: request.cwd, modelProvider });
@@ -26243,13 +26551,32 @@ class CodexAgent {
26243
26551
  case "fork":
26244
26552
  return await this.codex.threadFork({ threadId: open2.request.sessionId, config: config2, cwd: request.cwd, modelProvider, excludeTurns: true });
26245
26553
  }
26246
- });
26554
+ }) }));
26247
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
+ });
26248
26571
  try {
26249
- const [codexCatalog, account] = await this.withCodex(() => Promise.all([
26250
- this.codex.allModels(),
26251
- this.codex.accountRead({ refreshToken: false })
26252
- ]));
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
+ }
26253
26580
  const gateway = this.providers.active;
26254
26581
  const catalog = this.providers.catalog(codexCatalog);
26255
26582
  const model = resolveModelSelection(catalog, gateway?.model ?? thread.model, thread.reasoningEffort);
@@ -26257,13 +26584,13 @@ class CodexAgent {
26257
26584
  id: sessionId,
26258
26585
  cwd: request.cwd,
26259
26586
  additionalDirectories,
26260
- mcpServerNames: mcpServerNames(mcpServers),
26587
+ mcpServerNames: mcpServerNames(mcpServers).filter((name) => !existingMcp.has(name)),
26261
26588
  catalog,
26262
26589
  model,
26263
26590
  mode: initialAgentMode(this.env),
26264
26591
  collaborationMode: DEFAULT_COLLABORATION_MODE,
26265
26592
  fastMode: thread.serviceTier === FAST_SERVICE_TIER,
26266
- account: account.account,
26593
+ account: openedAccount,
26267
26594
  title: null,
26268
26595
  titleIsExplicit: false,
26269
26596
  activeTurn: null,
@@ -26271,40 +26598,66 @@ class CodexAgent {
26271
26598
  contextWindow: null,
26272
26599
  closed: false
26273
26600
  };
26274
- 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();
26275
26611
  const replay = open2.kind === "fork" || open2.kind === "resume" && open2.request.replayFrom?.type === "start";
26276
- if (open2.kind === "resume" && open2.request.replayFrom != null && open2.request.replayFrom.type !== "start") {
26277
- throw RequestError.invalidParams({ replayFrom: open2.request.replayFrom }, 'Only replayFrom {type: "start"} is supported');
26278
- }
26279
26612
  if (open2.kind !== "new") {
26280
26613
  await this.replayHistory(runtime, replay, thread.thread);
26281
26614
  } else {
26282
- const seed = seedHistoryOf(open2.request._meta);
26283
26615
  if (seed.length > 0) {
26284
26616
  await this.withCodex(() => this.codex.threadInjectItems({ threadId: sessionId, items: seed.map(seedItem) }));
26285
26617
  }
26286
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
+ }
26287
26627
  if (mcpServers.length > 0)
26288
26628
  this.reportMcpStartup(runtime, mcpStartupGeneration);
26289
- this.publishAvailableCommands(runtime);
26629
+ this.publishAvailableCommands(runtime, skills);
26290
26630
  return runtime;
26291
26631
  } catch (error40) {
26632
+ const runtime = this.sessions.get(sessionId);
26633
+ if (runtime) {
26634
+ runtime.session.closed = true;
26635
+ runtime.lifetime.abort();
26636
+ runtime.bridge.dispose();
26637
+ runtime.client.dispose();
26638
+ }
26292
26639
  this.sessions.delete(sessionId);
26293
26640
  this.codex.detachThread(sessionId);
26294
- await this.codex.threadUnsubscribe({ threadId: sessionId }).catch(() => {});
26641
+ await within(this.unsubscribeThread(sessionId), this.closeGraceMs).catch(() => {});
26295
26642
  throw error40;
26643
+ } finally {
26644
+ removeCancellation();
26645
+ stopObservingOpen();
26296
26646
  }
26297
26647
  }
26298
- installRuntime(session, capabilities) {
26648
+ installRuntime(session, capabilities, config2, modelProvider) {
26299
26649
  const client = new ClientSession(session.id, this.link, capabilities);
26300
26650
  const bridge = new EventBridge(client, session);
26301
26651
  const turnContext = new TurnContext(session.id);
26302
- const signal = () => session.activeTurn?.abort.signal;
26652
+ const signal = () => {
26653
+ const turn = session.activeTurn;
26654
+ return AbortSignal.any([this.codex.disconnectSignal, ...turn ? [turn.abort.signal, turn.stop.signal] : []]);
26655
+ };
26303
26656
  const approval = new CodexApprovalHandler(client, turnContext, signal);
26304
26657
  const elicitation = new CodexElicitationHandler(client, turnContext, signal);
26305
- const runtime = { session, client, bridge, turnContext, elicitation, queue: Promise.resolve() };
26658
+ const runtime = { config: config2, modelProvider, stale: false, session, client, bridge, turnContext, elicitation, lifetime: new AbortController, queue: Promise.resolve() };
26306
26659
  const drained = (operation) => async (params) => {
26307
- await this.drain(runtime);
26660
+ await abortable(this.drain(runtime), this.codex.disconnectSignal);
26308
26661
  return await operation(params);
26309
26662
  };
26310
26663
  this.codex.attachThread(session.id, {
@@ -26323,7 +26676,23 @@ class CodexAgent {
26323
26676
  return runtime;
26324
26677
  }
26325
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;
26326
26693
  const run = async () => {
26694
+ if (runtime.lifetime.signal.aborted)
26695
+ return;
26327
26696
  try {
26328
26697
  runtime.turnContext.observe(notification);
26329
26698
  await runtime.elicitation.observe(notification);
@@ -26334,11 +26703,21 @@ class CodexAgent {
26334
26703
  };
26335
26704
  runtime.queue = runtime.queue.then(run, run);
26336
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
+ }
26337
26716
  async drain(runtime) {
26338
26717
  let current;
26339
26718
  do {
26340
26719
  current = runtime.queue;
26341
- await current;
26720
+ await abortable(current, runtime.client.signal);
26342
26721
  } while (runtime.queue !== current);
26343
26722
  }
26344
26723
  async replayHistory(runtime, replay, thread) {
@@ -26361,12 +26740,18 @@ class CodexAgent {
26361
26740
  await publishTitle(firstPage.data);
26362
26741
  return;
26363
26742
  }
26743
+ const cursors = new Set;
26364
26744
  let cursor = null;
26365
26745
  do {
26366
26746
  const page = await this.turnPage(session.id, cursor);
26367
26747
  await publishTitle(page.data);
26368
26748
  await client.updateAll(historyUpdates(page.data));
26369
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
+ }
26370
26755
  } while (cursor !== null && !session.closed);
26371
26756
  await publishTitle([]);
26372
26757
  }
@@ -26376,7 +26761,7 @@ class CodexAgent {
26376
26761
  }
26377
26762
  async reportMcpStartup(runtime, afterGeneration) {
26378
26763
  try {
26379
- const result = await this.codex.awaitMcpStartup(runtime.session.mcpServerNames, afterGeneration);
26764
+ const result = await this.codex.awaitMcpStartup(runtime.session.mcpServerNames, afterGeneration, runtime.lifetime.signal, runtime.session.id);
26380
26765
  if (runtime.session.closed)
26381
26766
  return;
26382
26767
  for (const failure of result.failed) {
@@ -26386,15 +26771,18 @@ class CodexAgent {
26386
26771
  await runtime.client.update(mcpStartupFailed(server, `MCP server "${server}" startup was cancelled.`));
26387
26772
  }
26388
26773
  } catch (error40) {
26774
+ if (runtime.lifetime.signal.aborted)
26775
+ return;
26389
26776
  logger.error("MCP startup reporting failed", error40, { sessionId: runtime.session.id });
26390
26777
  }
26391
26778
  }
26392
- async publishAvailableCommands(runtime) {
26779
+ async publishAvailableCommands(runtime, snapshot) {
26393
26780
  try {
26394
- 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] });
26395
26782
  if (runtime.session.closed)
26396
26783
  return;
26397
26784
  await runtime.client.update({ sessionUpdate: "available_commands_update", availableCommands: availableCommands(skills.data) });
26785
+ this.publishedSkills.set(runtime, skills);
26398
26786
  } catch (error40) {
26399
26787
  logger.error("publishing available commands failed", error40, { sessionId: runtime.session.id });
26400
26788
  }
@@ -26421,49 +26809,92 @@ class CodexAgent {
26421
26809
  };
26422
26810
  }
26423
26811
  async closeSession(params) {
26812
+ return await this.admission(() => this.closeSessionInternal(params), params.sessionId);
26813
+ }
26814
+ async closeSessionInternal(params) {
26424
26815
  this.requireInitialized("session/close");
26425
- await this.closeRuntime(params.sessionId);
26426
- return {};
26816
+ return await this.closeRuntime(params.sessionId);
26427
26817
  }
26428
26818
  async deleteSession(params) {
26819
+ return await this.admission(() => this.deleteSessionInternal(params), params.sessionId);
26820
+ }
26821
+ async deleteSessionInternal(params) {
26429
26822
  this.requireInitialized("session/delete");
26430
26823
  await this.closeRuntime(params.sessionId);
26431
26824
  await this.withCodex(() => this.codex.threadDelete({ threadId: params.sessionId }));
26432
26825
  return {};
26433
26826
  }
26434
26827
  async archiveSession(params) {
26828
+ return await this.admission(() => this.archiveSessionInternal(params), params.sessionId);
26829
+ }
26830
+ async archiveSessionInternal(params) {
26435
26831
  this.requireInitialized("_codex/session_archive");
26436
26832
  await this.closeRuntime(params.sessionId);
26437
26833
  await this.withCodex(() => this.codex.threadArchive({ threadId: params.sessionId }));
26438
26834
  return {};
26439
26835
  }
26440
26836
  async unarchiveSession(params) {
26837
+ return await this.admission(() => this.unarchiveSessionInternal(params), params.sessionId);
26838
+ }
26839
+ async unarchiveSessionInternal(params) {
26441
26840
  this.requireInitialized("_codex/session_unarchive");
26442
26841
  await this.withCodex(() => this.codex.threadUnarchive({ threadId: params.sessionId }));
26443
26842
  return {};
26444
26843
  }
26445
26844
  async closeRuntime(sessionId) {
26845
+ const deadline = performance.now() + this.closeGraceMs;
26446
26846
  const runtime = this.sessions.get(sessionId);
26447
26847
  if (!runtime)
26448
- return;
26848
+ return {};
26449
26849
  runtime.session.closed = true;
26850
+ runtime.lifetime.abort();
26450
26851
  const turn = runtime.session.activeTurn;
26451
26852
  if (turn) {
26452
- await this.interruptTurn(runtime, turn);
26453
- const timeout = new Promise((resolve) => setTimeout(() => resolve("timeout"), this.closeGraceMs).unref());
26454
- if (await Promise.race([turn.finished.then(() => "finished"), timeout]) === "timeout" && turn.turnId) {
26455
- this.codex.resolveTurnInterrupted(turn.threadId, turn.turnId);
26456
- await turn.finished;
26853
+ this.interruptTurn(runtime, turn);
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(() => {});
26457
26857
  }
26858
+ turn.stop.abort();
26458
26859
  }
26459
- this.sessions.delete(sessionId);
26860
+ if (this.sessions.get(sessionId) === runtime)
26861
+ this.sessions.delete(sessionId);
26460
26862
  this.codex.detachThread(sessionId);
26863
+ runtime.client.dispose();
26461
26864
  runtime.bridge.dispose();
26462
- await this.codex.threadUnsubscribe({ threadId: sessionId }).catch((error40) => {
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";
26463
26878
  logger.error("thread/unsubscribe failed", error40, { sessionId });
26464
- });
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;
26465
26893
  }
26466
26894
  async setSessionConfigOption(params) {
26895
+ return await this.admission(() => this.setSessionConfigOptionInternal(params), params.sessionId);
26896
+ }
26897
+ async setSessionConfigOptionInternal(params) {
26467
26898
  const runtime = this.runtime(params.sessionId, "session/set_config_option");
26468
26899
  await this.withCodex(() => applyConfigOption(runtime.session, this.codex, params));
26469
26900
  const configOptions = sessionConfigOptions(runtime.session);
@@ -26473,23 +26904,28 @@ class CodexAgent {
26473
26904
  runtime(sessionId, method) {
26474
26905
  this.requireInitialized(method);
26475
26906
  const runtime = this.sessions.get(sessionId);
26476
- if (!runtime)
26477
- throw RequestError.invalidParams({ sessionId }, `Unknown session "${sessionId}"`);
26907
+ if (!runtime || runtime.session.closed)
26908
+ throw RequestError.invalidParams({ sessionId }, `Unknown session "${sessionId}" (missing or closed); create or resume it first`);
26478
26909
  return runtime;
26479
26910
  }
26480
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");
26481
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");
26482
26918
  const { session } = runtime;
26483
26919
  if (!Array.isArray(params.prompt) || params.prompt.length === 0) {
26484
26920
  throw RequestError.invalidParams(undefined, "prompt must contain at least one content block");
26485
26921
  }
26486
- if (session.activeTurn) {
26487
- return await this.steerActiveTurn(runtime, session.activeTurn, params);
26488
- }
26489
26922
  const model = findModel(session.catalog, session.model.model);
26490
26923
  if (!modelSupportsImages(model) && params.prompt.some((block) => block.type === "image")) {
26491
26924
  throw RequestError.invalidParams({ model: session.model.model }, "The current model does not support image input");
26492
26925
  }
26926
+ if (session.activeTurn) {
26927
+ return await this.steerActiveTurn(runtime, session.activeTurn, params);
26928
+ }
26493
26929
  const turn = createActiveTurn(session.id);
26494
26930
  session.activeTurn = turn;
26495
26931
  runtime.client.reportRunning();
@@ -26505,71 +26941,55 @@ class CodexAgent {
26505
26941
  await this.codex.turnSteer({ threadId: runtime.session.id, expectedTurnId: turnId, input: toUserInput(params.prompt) });
26506
26942
  return { _meta: { codex: { steered: turnId } } };
26507
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
+ }
26508
26948
  throw RequestError.invalidRequest({ sessionId: params.sessionId, turnId }, `Could not steer the running turn: ${errorMessage(error40)}`);
26509
26949
  }
26510
26950
  }
26511
- async runPrompt(runtime, turn, params) {
26512
- const { session, client, bridge } = runtime;
26951
+ async runPrompt(runtime, turn, params, observed, items = []) {
26952
+ const { session, bridge } = runtime;
26513
26953
  bridge.beginTurn();
26954
+ if (items.length)
26955
+ runtime.queue = runtime.queue.then(() => bridge.restore(items));
26514
26956
  try {
26515
26957
  const command = parseCommand(params.prompt);
26516
26958
  const outcome = command ? resolveCommand(command, session) : { kind: "prompt" };
26517
- let completed = null;
26518
- switch (outcome.kind) {
26519
- case "prompt":
26520
- completed = await this.runCodexTurn(runtime, turn, params.prompt);
26521
- break;
26522
- case "message":
26523
- await client.update(agentMessage(`command:${command?.name}:${Date.now()}`, await this.commandText(runtime, command?.name ?? "", outcome.text)));
26524
- break;
26525
- case "config":
26526
- await this.setSessionConfigOption({ sessionId: session.id, configId: outcome.configId, type: "id", value: outcome.value });
26527
- break;
26528
- case "compact":
26529
- await this.runCompaction(runtime, turn);
26530
- break;
26531
- case "review":
26532
- completed = await this.withCodex(() => this.codex.runReview({ threadId: session.id, target: outcome.target, delivery: "inline" }, (turnId, threadId) => {
26533
- turn.turnId = turnId;
26534
- turn.threadId = threadId;
26535
- turn.resolveStarted(turnId);
26536
- }));
26537
- break;
26538
- case "logout":
26539
- await this.withCodex(() => logout(this.codex));
26540
- await this.refreshAccounts();
26541
- await client.update(agentMessage(`command:logout:${Date.now()}`, "Logged out of the Codex account."));
26542
- break;
26543
- }
26959
+ let completed = await abortable(observed ?? this.executePrompt(runtime, turn, command, outcome, params), turn.stop.signal);
26544
26960
  await this.drain(runtime);
26545
26961
  await bridge.flush();
26546
- if (completed?.turn.status === "completed" && !turn.abort.signal.aborted) {
26547
- completed = await this.maybeImplementPlan(runtime, turn, completed);
26962
+ if (completed?.turn.status === "completed" && !turn.abort.signal.aborted && !this.exitHandled) {
26963
+ completed = await abortable(this.maybeImplementPlan(runtime, turn, completed), turn.stop.signal);
26548
26964
  }
26549
26965
  if (completed?.turn.status === "interrupted" || turn.abort.signal.aborted) {
26550
- await bridge.cancelOpenToolCalls();
26551
- await client.reportIdle("cancelled", { usage: usageOf(session) });
26966
+ await this.reportIdle(runtime, turn, "cancelled", { usage: usageOf(session) });
26552
26967
  return;
26553
26968
  }
26554
26969
  if (completed?.turn.status === "failed") {
26555
- await this.reportTurnFailure(runtime, completed.turn.error ?? bridge.takeError() ?? { message: "Turn failed", codexErrorInfo: null, additionalDetails: null, misalignment: null });
26970
+ await this.reportTurnFailure(runtime, turn, completed.turn.error ?? bridge.takeError() ?? { message: "Turn failed", codexErrorInfo: null, additionalDetails: null, misalignment: null });
26556
26971
  return;
26557
26972
  }
26558
26973
  const pendingError = bridge.takeError();
26559
26974
  if (pendingError) {
26560
- await this.reportTurnFailure(runtime, pendingError);
26975
+ await this.reportTurnFailure(runtime, turn, pendingError);
26561
26976
  return;
26562
26977
  }
26563
26978
  await this.publishFallbackTitle(runtime, promptTitle(params.prompt));
26564
- await client.reportIdle("end_turn", { usage: usageOf(session) });
26979
+ await this.reportIdle(runtime, turn, "end_turn", { usage: usageOf(session) });
26565
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;
26566
26987
  if (turn.abort.signal.aborted || session.closed) {
26567
- await bridge.cancelOpenToolCalls().catch(() => {});
26568
- await client.reportIdle("cancelled", { usage: usageOf(session) }).catch(() => {});
26988
+ await this.reportIdle(runtime, turn, "cancelled", { usage: usageOf(session) }).catch(() => {});
26569
26989
  return;
26570
26990
  }
26571
26991
  logger.error("prompt failed", error40, { sessionId: session.id });
26572
- await this.reportTurnFailure(runtime, { message: this.describeFailure(error40), codexErrorInfo: null, additionalDetails: null, misalignment: null }).catch(() => {});
26992
+ await this.reportTurnFailure(runtime, turn, this.failureOf(error40)).catch(() => {});
26573
26993
  } finally {
26574
26994
  turn.resolveStarted(null);
26575
26995
  if (session.activeTurn === turn)
@@ -26577,49 +26997,106 @@ class CodexAgent {
26577
26997
  turn.resolveFinished();
26578
26998
  }
26579
26999
  }
27000
+ async executePrompt(runtime, turn, command, outcome, params) {
27001
+ const { session, client } = runtime;
27002
+ let completed = null;
27003
+ switch (outcome.kind) {
27004
+ case "prompt":
27005
+ completed = await this.runCodexTurn(runtime, turn, params.prompt);
27006
+ break;
27007
+ case "message":
27008
+ await client.update(agentMessage(`command:${command?.name}:${Date.now()}`, await this.commandText(runtime, command?.name ?? "", outcome.text)));
27009
+ break;
27010
+ case "config":
27011
+ await this.setSessionConfigOption({ sessionId: session.id, configId: outcome.configId, type: "id", value: outcome.value });
27012
+ break;
27013
+ case "compact":
27014
+ await this.runCompaction(runtime, turn);
27015
+ break;
27016
+ case "review":
27017
+ completed = await this.withCodex(() => this.codex.runReview({ threadId: session.id, target: outcome.target, delivery: "inline" }, (turnId, threadId) => {
27018
+ this.turnStarted(runtime, turn, turnId, threadId);
27019
+ }, turn.stop.signal));
27020
+ break;
27021
+ case "logout":
27022
+ await this.withCodex(() => logout(this.codex));
27023
+ await this.refreshAccounts();
27024
+ await client.update(agentMessage(`command:logout:${Date.now()}`, "Logged out of the Codex account."));
27025
+ break;
27026
+ }
27027
+ return completed;
27028
+ }
26580
27029
  async runCodexTurn(runtime, turn, prompt) {
26581
27030
  const { session } = runtime;
26582
27031
  const model = findModel(session.catalog, session.model.model);
26583
27032
  const disableSummary = session.account?.type === "apiKey" || modelLacksReasoning(model);
26584
- await this.withCodex(() => this.refreshSkills(session.cwd, session.additionalDirectories));
26585
- if (turn.abort.signal.aborted) {
26586
- return interruptedTurn(session.id);
26587
- }
26588
- return await this.withCodex(() => this.codex.runTurn({
26589
- threadId: session.id,
26590
- input: toUserInput(prompt),
26591
- approvalPolicy: session.mode.approvalPolicy,
26592
- approvalsReviewer: session.mode.approvalsReviewer,
26593
- sandboxPolicy: withWritableRoots(session.mode.sandboxPolicy, session.additionalDirectories),
26594
- model: session.model.model,
26595
- effort: session.model.effort,
26596
- summary: disableSummary ? "none" : "auto",
26597
- serviceTier: session.fastMode ? FAST_SERVICE_TIER : null
26598
- }, (turnId) => {
26599
- turn.turnId = turnId;
26600
- turn.resolveStarted(turnId);
26601
- }));
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
+ }
26602
27069
  }
26603
27070
  async runCompaction(runtime, turn) {
26604
27071
  const threadId = runtime.session.id;
27072
+ turn.resolveStarted(null);
27073
+ const pending = new AbortController;
27074
+ const signal = AbortSignal.any([pending.signal, turn.abort.signal]);
26605
27075
  const completed = Promise.race([
26606
- this.codex.awaitNotification("item/completed", (params) => params.threadId === threadId && params.item.type === "contextCompaction"),
26607
- this.codex.awaitNotification("thread/compacted", (params) => params.threadId === threadId)
27076
+ this.codex.awaitNotification("item/completed", (params) => params.threadId === threadId && params.item.type === "contextCompaction", signal),
27077
+ this.codex.awaitNotification("thread/compacted", (params) => params.threadId === threadId, signal)
26608
27078
  ]);
26609
- await this.withCodex(() => this.codex.threadCompactStart({ threadId }));
26610
- turn.resolveStarted(null);
26611
- await completed;
27079
+ completed.catch(() => {});
27080
+ try {
27081
+ signal.throwIfAborted();
27082
+ await abortable(this.withCodex(() => this.codex.threadCompactStart({ threadId })), signal);
27083
+ await completed;
27084
+ } finally {
27085
+ pending.abort();
27086
+ }
26612
27087
  }
26613
27088
  async maybeImplementPlan(runtime, turn, completed) {
26614
27089
  const plan = runtime.bridge.takeCompletedPlan();
26615
27090
  if (!plan || runtime.session.collaborationMode !== PLAN_COLLABORATION_MODE)
26616
27091
  return completed;
26617
- const approved = await this.requestPlanApproval(runtime, plan, turn.abort.signal);
26618
- if (!approved || turn.abort.signal.aborted)
27092
+ this.completedTurns.delete(turn);
27093
+ const signal = AbortSignal.any([turn.abort.signal, turn.stop.signal]);
27094
+ const approved = await this.requestPlanApproval(runtime, plan, signal);
27095
+ if (!approved || signal.aborted)
26619
27096
  return completed;
26620
27097
  await this.setSessionConfigOption({ sessionId: runtime.session.id, configId: "collaboration_mode", type: "id", value: DEFAULT_COLLABORATION_MODE });
26621
27098
  runtime.bridge.beginTurn();
26622
- turn.turnId = null;
27099
+ turn.resetStarted();
26623
27100
  const implementation = await this.runCodexTurn(runtime, turn, [{ type: "text", text: "Implement the approved plan." }]);
26624
27101
  await this.drain(runtime);
26625
27102
  await runtime.bridge.flush();
@@ -26627,13 +27104,12 @@ class CodexAgent {
26627
27104
  }
26628
27105
  async requestPlanApproval(runtime, plan, signal) {
26629
27106
  const toolCallId = `plan-review:${plan.itemId}`;
27107
+ const toolCall = { toolCallId, name: ToolName.PlanReview, title: "Implement this plan?", kind: "switch_mode", status: "pending", rawInput: { plan: plan.text } };
26630
27108
  try {
27109
+ await runtime.client.update({ sessionUpdate: "tool_call_update", ...toolCall });
26631
27110
  const response = await runtime.client.requestPermission({
26632
27111
  title: "Implement this plan?",
26633
- subject: {
26634
- type: "tool_call",
26635
- toolCall: { toolCallId, name: ToolName.PlanReview, title: "Implement this plan?", kind: "switch_mode", status: "pending", rawInput: { plan: plan.text } }
26636
- },
27112
+ subject: { type: "tool_call", toolCall },
26637
27113
  options: [
26638
27114
  { optionId: IMPLEMENT_PLAN_OPTION, name: "Yes, implement this plan", kind: "allow_once" },
26639
27115
  { optionId: REVISE_PLAN_OPTION, name: "No, and tell Codex what to do differently", kind: "reject_once" }
@@ -26650,6 +27126,7 @@ class CodexAgent {
26650
27126
  return approved;
26651
27127
  } catch (error40) {
26652
27128
  logger.error("plan approval failed", error40, { sessionId: runtime.session.id });
27129
+ await runtime.client.update({ sessionUpdate: "tool_call_update", toolCallId, status: signal.aborted ? "cancelled" : "failed" }).catch(() => {});
26653
27130
  return false;
26654
27131
  }
26655
27132
  }
@@ -26667,7 +27144,17 @@ class CodexAgent {
26667
27144
  return fallback;
26668
27145
  }
26669
27146
  }
26670
- async reportTurnFailure(runtime, error40) {
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");
27152
+ if (runtime.session.activeTurn === turn)
27153
+ runtime.session.activeTurn = null;
27154
+ await runtime.client.reportIdle(reason, extra);
27155
+ }
27156
+ async reportTurnFailure(runtime, turn, error40) {
27157
+ const { stopReason, ...classification } = classifyTurnError(error40.codexErrorInfo);
26671
27158
  const message = error40.additionalDetails ? `${error40.message}
26672
27159
 
26673
27160
  ${error40.additionalDetails}` : error40.message;
@@ -26675,11 +27162,11 @@ ${error40.additionalDetails}` : error40.message;
26675
27162
  sessionUpdate: "agent_message_chunk",
26676
27163
  messageId: `codex-error:${runtime.session.id}:${Date.now()}`,
26677
27164
  content: { type: "text", text: message },
26678
- _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 } } }
26679
27166
  });
26680
- await runtime.client.reportIdle(ERROR_STOP_REASON, {
27167
+ await this.reportIdle(runtime, turn, stopReason, {
26681
27168
  usage: usageOf(runtime.session),
26682
- _meta: { codex: { error: { message: error40.message, codexErrorInfo: error40.codexErrorInfo } } }
27169
+ _meta: { codex: { error: { ...classification, message: error40.message, codexErrorInfo: error40.codexErrorInfo } } }
26683
27170
  });
26684
27171
  }
26685
27172
  async publishFallbackTitle(runtime, title) {
@@ -26701,19 +27188,77 @@ ${error40.additionalDetails}` : error40.message;
26701
27188
  const turnId = turn.turnId ?? await turn.started;
26702
27189
  if (turnId === null)
26703
27190
  return;
26704
- try {
26705
- await this.codex.turnInterrupt({ threadId: turn.threadId, turnId });
26706
- } catch (error40) {
26707
- logger.error("turn/interrupt failed", error40, { sessionId: runtime.session.id, turnId });
27191
+ await this.sendInterrupt(runtime, turn, turnId);
27192
+ }
27193
+ sendInterrupt(runtime, turn, turnId) {
27194
+ const key = `${turn.threadId}:${turnId}`;
27195
+ let pending = turn.interrupts.get(key);
27196
+ if (!pending) {
27197
+ pending = this.codex.turnInterrupt({ threadId: turn.threadId, turnId }).then(() => {}, (error40) => {
27198
+ turn.interrupts.delete(key);
27199
+ logger.error("turn/interrupt failed", error40, { sessionId: runtime.session.id, turnId });
27200
+ });
27201
+ turn.interrupts.set(key, pending);
27202
+ }
27203
+ return pending;
27204
+ }
27205
+ turnStarted(runtime, turn, turnId, threadId) {
27206
+ turn.turnId = turnId;
27207
+ turn.threadId = threadId;
27208
+ turn.resolveStarted(turnId);
27209
+ if (turn.abort.signal.aborted)
27210
+ this.sendInterrupt(runtime, turn, turnId);
27211
+ }
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
+ }
26708
27243
  }
27244
+ return skills;
26709
27245
  }
26710
- async refreshSkills(cwd, additionalDirectories) {
26711
- const roots = additionalDirectories.map((root) => path6.join(root, ".agents", "skills"));
26712
- if (roots.length !== this.skillRoots.length || roots.some((root, index) => root !== this.skillRoots[index])) {
26713
- await this.codex.skillsExtraRootsSet({ extraRoots: roots });
26714
- this.skillRoots = roots;
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 });
26715
27261
  }
26716
- await this.codex.skillsList({ cwds: [cwd, ...additionalDirectories], forceReload: true });
26717
27262
  }
26718
27263
  async configuredMcpServerNames(cwd) {
26719
27264
  const response = await this.codex.configRead({ includeLayers: true, cwd });
@@ -26726,13 +27271,13 @@ ${error40.additionalDetails}` : error40.message;
26726
27271
  }
26727
27272
  return names;
26728
27273
  }
26729
- async resolveModelProvider() {
26730
- const routed = this.providers.modelProvider();
27274
+ async resolveModelProvider(routing = this.providers) {
27275
+ const routed = routing.modelProvider();
26731
27276
  if (routed)
26732
27277
  return routed;
26733
27278
  const config2 = await this.codex.configRead({ includeLayers: false });
26734
27279
  const provider = config2.config["model_provider"];
26735
- return typeof provider === "string" && provider.length > 0 ? provider : null;
27280
+ return typeof provider === "string" && provider.length > 0 ? provider : OPENAI_PROVIDER_ID;
26736
27281
  }
26737
27282
  async withCodex(operation) {
26738
27283
  try {
@@ -26749,6 +27294,15 @@ ${stderr}` : ""}`);
26749
27294
  throw RequestError.internalError({ details: errorMessage(error40) }, errorMessage(error40));
26750
27295
  }
26751
27296
  }
27297
+ failureOf(error40) {
27298
+ const details = error40 instanceof Error ? error40.additionalDetails : undefined;
27299
+ return {
27300
+ message: this.describeFailure(error40),
27301
+ codexErrorInfo: null,
27302
+ additionalDetails: typeof details === "string" && details.length > 0 ? details : null,
27303
+ misalignment: null
27304
+ };
27305
+ }
26752
27306
  describeFailure(error40) {
26753
27307
  if (error40 instanceof RequestError) {
26754
27308
  const details = error40.data?.details;
@@ -26763,14 +27317,16 @@ ${stderr}` : ""}`);
26763
27317
  this.exitHandled = true;
26764
27318
  for (const runtime of this.sessions.values()) {
26765
27319
  const turn = runtime.session.activeTurn;
26766
- if (!turn)
27320
+ if (!turn || this.completedTurns.get(turn) === turn.turnId)
26767
27321
  continue;
27322
+ const stderr = this.process?.recentStderr() || null;
27323
+ turn.stop.abort(Object.assign(new Error("Connection to Codex was lost"), { additionalDetails: stderr }));
26768
27324
  turn.resolveStarted(null);
26769
27325
  if (turn.turnId) {
26770
27326
  this.codex.failTurn(turn.threadId, turn.turnId, {
26771
27327
  message: "Connection to Codex was lost",
26772
27328
  codexErrorInfo: null,
26773
- additionalDetails: this.process?.recentStderr() || null,
27329
+ additionalDetails: stderr,
26774
27330
  misalignment: null
26775
27331
  });
26776
27332
  }
@@ -26825,6 +27381,19 @@ function seedItem(message) {
26825
27381
  content: [{ type: message.role === "user" ? "input_text" : "output_text", text: message.text }]
26826
27382
  };
26827
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
+ }
26828
27397
 
26829
27398
  // src/agent/createAgent.ts
26830
27399
  function createAgentApp(options) {
@@ -26836,7 +27405,7 @@ function createAgentApp(options) {
26836
27405
  };
26837
27406
  return agent({ name: options.info.name }).onConnect((connection) => {
26838
27407
  agent2 = new CodexAgent(connection.client, options);
26839
- }).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));
26840
27409
  }
26841
27410
 
26842
27411
  // src/codex/AppServerClient.ts
@@ -26854,12 +27423,18 @@ class AppServerClient {
26854
27423
  elicitationHandlers = new Map;
26855
27424
  turnCompletionWaiters = new Map;
26856
27425
  earlyTurnCompletions = new Map;
27426
+ startingThreads = new Map;
27427
+ startingReviews = 0;
27428
+ modelCatalog = null;
26857
27429
  mcpStartupStates = new Map;
26858
27430
  mcpStartupWaiters = [];
26859
27431
  notificationWaiters = [];
26860
27432
  mcpStartupVersion = 0;
27433
+ disconnected = new AbortController;
26861
27434
  constructor(connection) {
26862
27435
  this.connection = connection;
27436
+ connection.onClose(() => this.disconnected.abort(new Error("Connection to Codex was lost")));
27437
+ connection.onDispose(() => this.disconnected.abort(new Error("Connection to Codex was lost")));
26863
27438
  connection.onUnhandledNotification((message) => {
26864
27439
  this.dispatchNotification(message);
26865
27440
  });
@@ -26884,6 +27459,9 @@ class AppServerClient {
26884
27459
  return handler ? await handler.handleUserInput(params) : { answers: {} };
26885
27460
  });
26886
27461
  }
27462
+ get disconnectSignal() {
27463
+ return this.disconnected.signal;
27464
+ }
26887
27465
  attachThread(threadId, handlers) {
26888
27466
  this.threadHandlers.set(threadId, handlers.notification);
26889
27467
  this.approvalHandlers.set(threadId, handlers.approval);
@@ -26893,18 +27471,49 @@ class AppServerClient {
26893
27471
  this.threadHandlers.delete(threadId);
26894
27472
  this.approvalHandlers.delete(threadId);
26895
27473
  this.elicitationHandlers.delete(threadId);
27474
+ for (const key of this.mcpStartupStates.keys()) {
27475
+ if (key.startsWith(`${threadId}\x00`))
27476
+ this.mcpStartupStates.delete(key);
27477
+ }
26896
27478
  }
26897
- awaitNotification(method, predicate = () => true) {
27479
+ awaitNotification(method, predicate = () => true, signal) {
27480
+ let waiter;
26898
27481
  const settled = new Promise((resolve) => {
26899
- this.notificationWaiters.push({
27482
+ waiter = {
26900
27483
  method,
26901
27484
  matches: (params) => predicate(params),
26902
27485
  resolve
26903
- });
27486
+ };
27487
+ this.notificationWaiters.push(waiter);
26904
27488
  });
26905
- return settled;
27489
+ const result = abortable(settled, signal ? AbortSignal.any([signal, this.disconnected.signal]) : this.disconnected.signal).finally(() => {
27490
+ const index = this.notificationWaiters.indexOf(waiter);
27491
+ if (index >= 0)
27492
+ this.notificationWaiters.splice(index, 1);
27493
+ });
27494
+ result.catch(() => {});
27495
+ return result;
27496
+ }
27497
+ async withNotification(method, operation) {
27498
+ const scope = new AbortController;
27499
+ try {
27500
+ return await operation(this.awaitNotification(method, () => true, scope.signal));
27501
+ } finally {
27502
+ scope.abort();
27503
+ }
27504
+ }
27505
+ observers = new Set;
27506
+ observeNotifications(handler) {
27507
+ this.observers.add(handler);
27508
+ return () => {
27509
+ this.observers.delete(handler);
27510
+ };
26906
27511
  }
26907
27512
  dispatchNotification(notification) {
27513
+ if (notification.method === "account/updated")
27514
+ this.modelCatalog = null;
27515
+ for (const observer of this.observers)
27516
+ observer(notification);
26908
27517
  if (this.notificationWaiters.length > 0) {
26909
27518
  const remaining = [];
26910
27519
  for (const waiter of this.notificationWaiters) {
@@ -26917,7 +27526,7 @@ class AppServerClient {
26917
27526
  }
26918
27527
  if (notification.method === "mcpServer/startupStatus/updated") {
26919
27528
  this.mcpStartupVersion += 1;
26920
- this.mcpStartupStates.set(notification.params.name, {
27529
+ this.mcpStartupStates.set(mcpKey(notification.params.threadId, notification.params.name), {
26921
27530
  status: notification.params.status,
26922
27531
  error: notification.params.error,
26923
27532
  version: this.mcpStartupVersion
@@ -26936,25 +27545,62 @@ class AppServerClient {
26936
27545
  handler(notification);
26937
27546
  }
26938
27547
  }
26939
- async runTurn(params, onTurnStarted) {
26940
- const started = await this.turnStart(params);
26941
- onTurnStarted?.(started.turn.id);
26942
- return await this.awaitTurnCompleted(params.threadId, started.turn.id);
27548
+ async runTurn(params, onTurnStarted, signal) {
27549
+ this.startingThreads.set(params.threadId, (this.startingThreads.get(params.threadId) ?? 0) + 1);
27550
+ let completed;
27551
+ try {
27552
+ const started = await this.turnStart(params);
27553
+ onTurnStarted?.(started.turn.id);
27554
+ completed = this.awaitTurnCompleted(params.threadId, started.turn.id, signal);
27555
+ } finally {
27556
+ const remaining = (this.startingThreads.get(params.threadId) ?? 1) - 1;
27557
+ if (remaining === 0)
27558
+ this.startingThreads.delete(params.threadId);
27559
+ else
27560
+ this.startingThreads.set(params.threadId, remaining);
27561
+ this.pruneEarlyCompletions();
27562
+ }
27563
+ return await completed;
26943
27564
  }
26944
- async runReview(params, onTurnStarted) {
26945
- const started = await this.reviewStart(params);
26946
- onTurnStarted?.(started.turn.id, started.reviewThreadId);
26947
- return await this.awaitTurnCompleted(started.reviewThreadId, started.turn.id);
27565
+ async runReview(params, onTurnStarted, signal) {
27566
+ this.startingReviews += 1;
27567
+ let completed;
27568
+ try {
27569
+ const started = await this.reviewStart(params);
27570
+ onTurnStarted?.(started.turn.id, started.reviewThreadId);
27571
+ completed = this.awaitTurnCompleted(started.reviewThreadId, started.turn.id, signal);
27572
+ } finally {
27573
+ this.startingReviews -= 1;
27574
+ this.pruneEarlyCompletions();
27575
+ }
27576
+ return await completed;
26948
27577
  }
26949
- awaitTurnCompleted(threadId, turnId) {
27578
+ awaitTurnCompleted(threadId, turnId, signal) {
26950
27579
  const key = turnKey(threadId, turnId);
26951
27580
  const early = this.earlyTurnCompletions.get(key);
26952
27581
  if (early) {
26953
27582
  this.earlyTurnCompletions.delete(key);
26954
27583
  return Promise.resolve(early);
26955
27584
  }
26956
- return new Promise((resolve) => {
26957
- 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
+ });
26958
27604
  });
26959
27605
  }
26960
27606
  resolveTurnInterrupted(threadId, turnId) {
@@ -26986,26 +27632,42 @@ class AppServerClient {
26986
27632
  waiter(event);
26987
27633
  return;
26988
27634
  }
26989
- this.earlyTurnCompletions.set(key, event);
27635
+ if (this.startingThreads.has(event.threadId) || this.startingReviews > 0)
27636
+ this.earlyTurnCompletions.set(key, event);
27637
+ }
27638
+ pruneEarlyCompletions() {
27639
+ if (this.startingReviews > 0)
27640
+ return;
27641
+ for (const [key, event] of this.earlyTurnCompletions) {
27642
+ if (!this.startingThreads.has(event.threadId))
27643
+ this.earlyTurnCompletions.delete(key);
27644
+ }
26990
27645
  }
26991
27646
  get mcpStartupGeneration() {
26992
27647
  return this.mcpStartupVersion;
26993
27648
  }
26994
- awaitMcpStartup(serverNames, afterVersion) {
27649
+ awaitMcpStartup(serverNames, afterVersion, signal, threadId = null) {
26995
27650
  const names = [...new Set(serverNames.map((name) => name.trim()).filter((name) => name.length > 0))];
26996
27651
  if (names.length === 0)
26997
27652
  return Promise.resolve({ ready: [], failed: [], cancelled: [] });
26998
- const immediate = this.buildMcpStartupResult(names, afterVersion);
27653
+ const immediate = this.buildMcpStartupResult(names, afterVersion, threadId);
26999
27654
  if (immediate)
27000
27655
  return Promise.resolve(immediate);
27001
- return new Promise((resolve) => {
27002
- this.mcpStartupWaiters.push({ serverNames: names, afterVersion, resolve });
27656
+ let waiter;
27657
+ const completed = new Promise((resolve) => {
27658
+ waiter = { serverNames: names, afterVersion, threadId, resolve };
27659
+ this.mcpStartupWaiters.push(waiter);
27660
+ });
27661
+ return abortable(completed, signal ? AbortSignal.any([signal, this.disconnected.signal]) : this.disconnected.signal).finally(() => {
27662
+ const index = this.mcpStartupWaiters.indexOf(waiter);
27663
+ if (index >= 0)
27664
+ this.mcpStartupWaiters.splice(index, 1);
27003
27665
  });
27004
27666
  }
27005
27667
  settleMcpStartupWaiters() {
27006
27668
  const pending = [];
27007
27669
  for (const waiter of this.mcpStartupWaiters) {
27008
- const result = this.buildMcpStartupResult(waiter.serverNames, waiter.afterVersion);
27670
+ const result = this.buildMcpStartupResult(waiter.serverNames, waiter.afterVersion, waiter.threadId);
27009
27671
  if (result)
27010
27672
  waiter.resolve(result);
27011
27673
  else
@@ -27013,10 +27675,12 @@ class AppServerClient {
27013
27675
  }
27014
27676
  this.mcpStartupWaiters.splice(0, this.mcpStartupWaiters.length, ...pending);
27015
27677
  }
27016
- buildMcpStartupResult(serverNames, afterVersion) {
27678
+ buildMcpStartupResult(serverNames, afterVersion, threadId) {
27017
27679
  const result = { ready: [], failed: [], cancelled: [] };
27018
27680
  for (const name of serverNames) {
27019
- const state = this.mcpStartupStates.get(name);
27681
+ const scoped = this.mcpStartupStates.get(mcpKey(threadId, name));
27682
+ const global = this.mcpStartupStates.get(mcpKey(null, name));
27683
+ const state = scoped && scoped.version > afterVersion ? scoped : global;
27020
27684
  if (!state || state.version <= afterVersion || state.status === "starting")
27021
27685
  return null;
27022
27686
  if (state.status === "ready")
@@ -27068,8 +27732,9 @@ class AppServerClient {
27068
27732
  return this.send({ method: "thread/compact/start", params });
27069
27733
  }
27070
27734
  async threadSettingsUpdate(params) {
27735
+ this.disconnected.signal.throwIfAborted();
27071
27736
  logger.log("[codex request]", { method: "thread/settings/update" });
27072
- await this.connection.sendRequest("thread/settings/update", params);
27737
+ await abortable(this.connection.sendRequest("thread/settings/update", params), this.disconnected.signal);
27073
27738
  }
27074
27739
  turnStart(params) {
27075
27740
  return this.send({ method: "turn/start", params });
@@ -27086,13 +27751,34 @@ class AppServerClient {
27086
27751
  modelList(params) {
27087
27752
  return this.send({ method: "model/list", params });
27088
27753
  }
27089
- 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() {
27090
27770
  const models = [];
27771
+ const cursors = new Set;
27091
27772
  let cursor = null;
27092
27773
  do {
27093
27774
  const page = await this.modelList({ cursor, limit: null });
27094
27775
  models.push(...page.data);
27095
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
+ }
27096
27782
  } while (cursor);
27097
27783
  return models;
27098
27784
  }
@@ -27121,23 +27807,279 @@ class AppServerClient {
27121
27807
  return this.send({ method: "mcpServerStatus/list", params });
27122
27808
  }
27123
27809
  async send(request) {
27810
+ this.disconnected.signal.throwIfAborted();
27124
27811
  logger.log("[codex request]", { method: request.method });
27125
27812
  const params = "params" in request ? request.params : undefined;
27126
- return params === undefined ? await this.connection.sendRequest(request.method) : await this.connection.sendRequest(request.method, params);
27813
+ return await abortable(params === undefined ? this.connection.sendRequest(request.method) : this.connection.sendRequest(request.method, params), this.disconnected.signal);
27127
27814
  }
27128
27815
  }
27129
27816
  function turnKey(threadId, turnId) {
27130
27817
  return `${threadId}\x00${turnId}`;
27131
27818
  }
27819
+ function mcpKey(threadId, name) {
27820
+ return `${threadId ?? ""}\x00${name}`;
27821
+ }
27132
27822
  function threadIdOf(notification) {
27133
27823
  const params = notification.params;
27134
27824
  return params && typeof params.threadId === "string" ? params.threadId : null;
27135
27825
  }
27136
27826
 
27137
27827
  // src/codex/process.ts
27828
+ import { existsSync } from "fs";
27829
+ import path7 from "path";
27138
27830
  import { spawn, spawnSync } from "child_process";
27139
27831
  import { createRequire } from "module";
27140
- var rpc = __toESM(require_main(), 1);
27832
+
27833
+ // src/codex/transport.ts
27834
+ var import_node2 = __toESM(require_main(), 1);
27835
+ import { StringDecoder } from "string_decoder";
27836
+ function wireLog(value) {
27837
+ return JSON.stringify(value, (key, nested) => {
27838
+ const normalized = key.replace(/[_-]/g, "").toLowerCase();
27839
+ if (/^(apikey|accesskey|accesstoken|refreshtoken|idtoken|secretaccesskey|sessiontoken|authorization|proxyauthorization|password|secret|token|cookie|setcookie|headers|httpheaders|env|queryparams)$/.test(normalized))
27840
+ return "***";
27841
+ return nested;
27842
+ });
27843
+ }
27844
+ function createWriter(writable) {
27845
+ const errors3 = new import_node2.Emitter;
27846
+ const closed = new import_node2.Emitter;
27847
+ const pending = new Set;
27848
+ let ended = false;
27849
+ const onError = (error40) => {
27850
+ for (const reject of [...pending])
27851
+ reject(error40);
27852
+ errors3.fire([error40, undefined, undefined]);
27853
+ };
27854
+ const onClose = () => {
27855
+ if (ended)
27856
+ return;
27857
+ ended = true;
27858
+ for (const reject of [...pending])
27859
+ reject(new Error("Codex input stream closed"));
27860
+ closed.fire();
27861
+ };
27862
+ writable.on("error", onError);
27863
+ writable.on("close", onClose);
27864
+ writable.on("finish", onClose);
27865
+ return {
27866
+ write(message) {
27867
+ if (ended || writable.destroyed || writable.writableEnded)
27868
+ return Promise.reject(new Error("Codex input stream is closed"));
27869
+ const { jsonrpc: _jsonrpc, ...frame2 } = message;
27870
+ if (logger.enabled)
27871
+ logger.log("[codex <-]", { line: wireLog(frame2) });
27872
+ return new Promise((resolve, reject) => {
27873
+ const fail = (error40) => {
27874
+ pending.delete(fail);
27875
+ reject(error40);
27876
+ };
27877
+ pending.add(fail);
27878
+ try {
27879
+ writable.write(`${JSON.stringify(frame2)}
27880
+ `, (error40) => {
27881
+ pending.delete(fail);
27882
+ if (error40)
27883
+ reject(error40);
27884
+ else
27885
+ resolve();
27886
+ });
27887
+ } catch (error40) {
27888
+ fail(error40 instanceof Error ? error40 : new Error(String(error40)));
27889
+ }
27890
+ });
27891
+ },
27892
+ end() {
27893
+ writable.end();
27894
+ },
27895
+ onError: errors3.event,
27896
+ onClose: closed.event,
27897
+ dispose() {
27898
+ onClose();
27899
+ writable.off("error", onError);
27900
+ writable.off("close", onClose);
27901
+ writable.off("finish", onClose);
27902
+ errors3.dispose();
27903
+ closed.dispose();
27904
+ }
27905
+ };
27906
+ }
27907
+ function createReader(readable) {
27908
+ const errors3 = new import_node2.Emitter;
27909
+ const closed = new import_node2.Emitter;
27910
+ const partial2 = new import_node2.Emitter;
27911
+ const decoder = new StringDecoder("utf8");
27912
+ let fragments = [];
27913
+ let ended = false;
27914
+ let callback = null;
27915
+ const deliver = (line) => {
27916
+ if (!line.trim())
27917
+ return;
27918
+ let message;
27919
+ try {
27920
+ const parsed = JSON.parse(line);
27921
+ if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed))
27922
+ throw new Error("Expected an object");
27923
+ const frame2 = parsed;
27924
+ if (frame2["jsonrpc"] === undefined)
27925
+ frame2["jsonrpc"] = "2.0";
27926
+ if (logger.enabled)
27927
+ logger.log("[codex ->]", { line: wireLog(frame2) });
27928
+ message = frame2;
27929
+ } catch {
27930
+ const error40 = new Error("Malformed JSON-RPC frame from Codex");
27931
+ logger.error(error40.message, error40);
27932
+ errors3.fire(error40);
27933
+ return;
27934
+ }
27935
+ callback?.(message);
27936
+ };
27937
+ const onData = (chunk) => {
27938
+ const text2 = typeof chunk === "string" ? chunk : decoder.write(chunk);
27939
+ let offset = 0;
27940
+ for (;; ) {
27941
+ const newline2 = text2.indexOf(`
27942
+ `, offset);
27943
+ if (newline2 < 0)
27944
+ break;
27945
+ const part = text2.slice(offset, newline2);
27946
+ const line = fragments.length ? fragments.join("") + part : part;
27947
+ fragments = [];
27948
+ deliver(line);
27949
+ offset = newline2 + 1;
27950
+ }
27951
+ if (offset < text2.length)
27952
+ fragments.push(text2.slice(offset));
27953
+ };
27954
+ const onClose = () => {
27955
+ if (ended)
27956
+ return;
27957
+ ended = true;
27958
+ closed.fire();
27959
+ };
27960
+ const onEnd = () => {
27961
+ fragments.push(decoder.end());
27962
+ deliver(fragments.join(""));
27963
+ fragments = [];
27964
+ onClose();
27965
+ };
27966
+ const onError = (error40) => errors3.fire(error40);
27967
+ const stop = () => {
27968
+ readable.off("data", onData);
27969
+ readable.off("end", onEnd);
27970
+ readable.off("close", onClose);
27971
+ readable.off("error", onError);
27972
+ callback = null;
27973
+ fragments = [];
27974
+ };
27975
+ return {
27976
+ listen(receive) {
27977
+ callback = receive;
27978
+ readable.on("data", onData);
27979
+ readable.on("end", onEnd);
27980
+ readable.on("close", onClose);
27981
+ readable.on("error", onError);
27982
+ return { dispose: stop };
27983
+ },
27984
+ onError: errors3.event,
27985
+ onClose: closed.event,
27986
+ onPartialMessage: partial2.event,
27987
+ dispose() {
27988
+ stop();
27989
+ errors3.dispose();
27990
+ closed.dispose();
27991
+ partial2.dispose();
27992
+ }
27993
+ };
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
+ }
28081
+
28082
+ // src/codex/process.ts
27141
28083
  var STDERR_RETAIN_BYTES = 8 * 1024;
27142
28084
  var MINIMUM_CODEX_VERSION = package_default.optionalDependencies["@openai/codex"].replace(/^[\^~]/, "");
27143
28085
  function parseCodexVersion(output) {
@@ -27162,7 +28104,29 @@ function resolveCodexLauncher(codexPath) {
27162
28104
  return process.platform === "win32" ? { command: `"${codexPath}"`, prefixArgs: [], shell: true } : { command: codexPath, prefixArgs: [], shell: false };
27163
28105
  }
27164
28106
  try {
27165
- 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 };
27166
28130
  } catch (error40) {
27167
28131
  throw new Error("No Codex CLI: set CODEX_PATH to an installed Codex executable, or install the optional " + `@openai/codex dependency (${String(error40)})`);
27168
28132
  }
@@ -27179,11 +28143,13 @@ function startCodexProcess(codexPath, env = process.env) {
27179
28143
  stderr = (stderr + data.toString()).slice(-STDERR_RETAIN_BYTES);
27180
28144
  logger.log("[codex stderr]", { data: data.toString() });
27181
28145
  });
27182
- const connection = rpc.createMessageConnection(createReader(child.stdout), createWriter(child.stdin));
28146
+ const { connection, drained } = createCodexConnection(child.stdout, child.stdin);
27183
28147
  connection.listen();
28148
+ drained.then(() => connection.dispose());
27184
28149
  const exited = new Promise((resolve) => {
27185
- child.on("exit", (code, signal) => {
28150
+ child.on("close", async (code, signal) => {
27186
28151
  logger.log("codex exited", { code, signal });
28152
+ await drained;
27187
28153
  connection.dispose();
27188
28154
  resolve(code);
27189
28155
  });
@@ -27196,65 +28162,6 @@ function startCodexProcess(codexPath, env = process.env) {
27196
28162
  exitCode: () => child.exitCode
27197
28163
  };
27198
28164
  }
27199
- function redactSecrets(line) {
27200
- return line.replace(/"(apiKey|accessToken|secretAccessKey|sessionToken)":"[^"]*"/g, '"$1":"***"');
27201
- }
27202
- function createWriter(writable) {
27203
- return {
27204
- async write(message) {
27205
- const { jsonrpc: _jsonrpc, ...frame2 } = message;
27206
- const line = JSON.stringify(frame2);
27207
- logger.log("[codex <-]", { line: redactSecrets(line) });
27208
- writable.write(`${line}
27209
- `);
27210
- },
27211
- end() {
27212
- writable.end();
27213
- },
27214
- onError: new rpc.Emitter().event,
27215
- onClose: new rpc.Emitter().event,
27216
- dispose() {}
27217
- };
27218
- }
27219
- function createReader(readable) {
27220
- return {
27221
- listen(callback) {
27222
- let buffer = "";
27223
- const onData = (chunk) => {
27224
- buffer += chunk.toString();
27225
- for (;; ) {
27226
- const newline2 = buffer.indexOf(`
27227
- `);
27228
- if (newline2 < 0)
27229
- break;
27230
- const line = buffer.slice(0, newline2).trim();
27231
- buffer = buffer.slice(newline2 + 1);
27232
- if (line.length === 0)
27233
- continue;
27234
- logger.log("[codex ->]", { line: redactSecrets(line) });
27235
- try {
27236
- const message = JSON.parse(line);
27237
- if (message["jsonrpc"] === undefined)
27238
- message["jsonrpc"] = "2.0";
27239
- callback(message);
27240
- } catch (error40) {
27241
- logger.error("Malformed JSON-RPC frame from codex", error40, { line });
27242
- }
27243
- }
27244
- };
27245
- readable.on("data", onData);
27246
- return {
27247
- dispose() {
27248
- readable.off("data", onData);
27249
- }
27250
- };
27251
- },
27252
- onError: new rpc.Emitter().event,
27253
- onClose: new rpc.Emitter().event,
27254
- onPartialMessage: new rpc.Emitter().event,
27255
- dispose() {}
27256
- };
27257
- }
27258
28165
 
27259
28166
  // src/index.ts
27260
28167
  var CODEX_EXIT_GRACE_MS = 2000;