@agentclientprotocol/codex-acp 0.0.45 → 0.0.46

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 (2) hide show
  1. package/dist/index.js +765 -165
  2. package/package.json +2 -2
package/dist/index.js CHANGED
@@ -17319,6 +17319,30 @@ function applyStructuredPatch(source, patch, options = {}) {
17319
17319
  return resultLines.join("\n");
17320
17320
  }
17321
17321
 
17322
+ // node_modules/diff/libesm/patch/reverse.js
17323
+ function reversePatch(structuredPatch) {
17324
+ if (Array.isArray(structuredPatch)) {
17325
+ return structuredPatch.map((patch) => reversePatch(patch)).reverse();
17326
+ }
17327
+ return Object.assign(Object.assign({}, structuredPatch), { oldFileName: structuredPatch.newFileName, oldHeader: structuredPatch.newHeader, newFileName: structuredPatch.oldFileName, newHeader: structuredPatch.oldHeader, hunks: structuredPatch.hunks.map((hunk) => {
17328
+ return {
17329
+ oldLines: hunk.newLines,
17330
+ oldStart: hunk.newStart,
17331
+ newLines: hunk.oldLines,
17332
+ newStart: hunk.oldStart,
17333
+ lines: hunk.lines.map((l) => {
17334
+ if (l.startsWith("-")) {
17335
+ return `+${l.slice(1)}`;
17336
+ }
17337
+ if (l.startsWith("+")) {
17338
+ return `-${l.slice(1)}`;
17339
+ }
17340
+ return l;
17341
+ })
17342
+ };
17343
+ }) });
17344
+ }
17345
+
17322
17346
  // src/CodexToolCallMapper.ts
17323
17347
  import { readFile } from "node:fs/promises";
17324
17348
  import path2 from "node:path";
@@ -17521,80 +17545,93 @@ function createSearchTitle(query, path5) {
17521
17545
  return "Search";
17522
17546
  }
17523
17547
  async function createPatchContent(change) {
17524
- if (change.kind.type === "add" && !isUnifiedDiff(change.diff)) {
17525
- return {
17526
- type: "diff",
17527
- oldText: null,
17528
- newText: change.diff,
17529
- path: change.path,
17530
- _meta: {
17531
- kind: "add"
17532
- }
17533
- };
17534
- }
17535
- if (change.kind.type === "delete") {
17536
- const oldContent2 = await readFile(change.path, { encoding: "utf8" }).catch(
17537
- () => isUnifiedDiff(change.diff) ? patchToDeletedContent(change.diff) : change.diff
17538
- );
17539
- return {
17540
- type: "diff",
17541
- oldText: oldContent2,
17542
- newText: "",
17543
- path: change.path,
17544
- _meta: {
17545
- kind: "delete"
17546
- }
17547
- };
17548
- }
17549
- const oldContent = change.kind.type === "add" ? "" : await readFile(change.path, { encoding: "utf8" }).catch(() => null);
17550
- if (oldContent === null) {
17551
- return null;
17552
- }
17553
- const newContent = applyPatch(oldContent, change.diff);
17554
- if (newContent === false) {
17548
+ try {
17549
+ switch (change.kind.type) {
17550
+ case "add":
17551
+ return await createAddFileContent(change);
17552
+ case "delete":
17553
+ return await createDeleteFileContent(change);
17554
+ case "update":
17555
+ return await createUpdateFileContent(change);
17556
+ }
17557
+ } catch (error40) {
17558
+ logger.log(`Error processing file update change: ${error40}`);
17555
17559
  return null;
17556
17560
  }
17561
+ }
17562
+ async function createAddFileContent(change) {
17557
17563
  return {
17558
17564
  type: "diff",
17559
- oldText: change.kind.type === "add" ? null : oldContent,
17560
- newText: newContent,
17565
+ oldText: null,
17566
+ newText: change.diff,
17567
+ // app-server always returns file content instead of diff
17561
17568
  path: change.path,
17562
17569
  _meta: {
17563
- kind: change.kind.type
17570
+ kind: "add"
17564
17571
  }
17565
17572
  };
17566
17573
  }
17567
- function isUnifiedDiff(content) {
17568
- return content.startsWith("--- ") || content.includes("\n--- ");
17569
- }
17570
- function patchToDeletedContent(unifiedDiff) {
17571
- try {
17572
- const [patch] = parsePatch(unifiedDiff);
17573
- if (!patch || patch.hunks.length === 0) {
17574
- return null;
17575
- }
17576
- const oldLines = [];
17577
- let hasNoTrailingNewlineMarker = false;
17578
- for (const hunk of patch.hunks) {
17579
- for (const line of hunk.lines) {
17580
- if (line === "\") {
17581
- hasNoTrailingNewlineMarker = true;
17582
- continue;
17583
- }
17584
- if (line.startsWith("-") || line.startsWith(" ")) {
17585
- oldLines.push(line.slice(1));
17574
+ async function createUpdateFileContent(change) {
17575
+ if (change.kind.type !== "update") return null;
17576
+ const unifiedDiff = recoverCorruptedDiff(change.diff);
17577
+ const movePath = change.kind.move_path;
17578
+ const oldContent = await readFileContent(change.path);
17579
+ if (oldContent !== null) {
17580
+ const patchedContent = applyPatch(oldContent, unifiedDiff);
17581
+ if (patchedContent === false) {
17582
+ const revertedPatch2 = revertPatch(unifiedDiff);
17583
+ if (revertedPatch2) {
17584
+ const revertedContent2 = applyPatch(oldContent, revertedPatch2);
17585
+ if (revertedContent2 !== false) {
17586
+ return createUpdateDiffContent(change.path, revertedContent2, oldContent);
17586
17587
  }
17587
17588
  }
17589
+ return null;
17588
17590
  }
17589
- if (oldLines.length === 0) {
17590
- return "";
17591
- }
17592
- const oldText = oldLines.join("\n");
17593
- return hasNoTrailingNewlineMarker || !unifiedDiff.endsWith("\n") ? oldText : `${oldText}
17594
- `;
17595
- } catch {
17596
- return null;
17591
+ return createUpdateDiffContent(movePath ?? change.path, oldContent, patchedContent);
17597
17592
  }
17593
+ if (!movePath) return null;
17594
+ const newContent = await readFileContent(movePath);
17595
+ if (newContent === null) return null;
17596
+ const revertedPatch = revertPatch(unifiedDiff);
17597
+ if (!revertedPatch) return null;
17598
+ const revertedContent = applyPatch(newContent, revertedPatch);
17599
+ if (revertedContent === false) return null;
17600
+ return createUpdateDiffContent(movePath, revertedContent, newContent);
17601
+ }
17602
+ function revertPatch(unifiedDiff) {
17603
+ const [patch] = parsePatch(unifiedDiff);
17604
+ if (!patch) return null;
17605
+ return reversePatch(patch);
17606
+ }
17607
+ function createUpdateDiffContent(path5, oldText, newText) {
17608
+ return {
17609
+ type: "diff",
17610
+ oldText,
17611
+ newText,
17612
+ path: path5,
17613
+ _meta: {
17614
+ kind: "update"
17615
+ }
17616
+ };
17617
+ }
17618
+ async function createDeleteFileContent(change) {
17619
+ return {
17620
+ type: "diff",
17621
+ oldText: change.diff,
17622
+ // app-server always returns file content instead of diff
17623
+ newText: "",
17624
+ path: change.path,
17625
+ _meta: {
17626
+ kind: "delete"
17627
+ }
17628
+ };
17629
+ }
17630
+ async function readFileContent(filePath) {
17631
+ return await readFile(filePath, { encoding: "utf8" }).catch(() => null);
17632
+ }
17633
+ function recoverCorruptedDiff(diff) {
17634
+ return diff.replace(/\n\nMoved to: .*$/, "");
17598
17635
  }
17599
17636
 
17600
17637
  // src/CodexEventHandler.ts
@@ -17725,6 +17762,9 @@ var CodexEventHandler = class {
17725
17762
  case "thread/goal/cleared":
17726
17763
  case "remoteControl/status/changed":
17727
17764
  case "app/list/updated":
17765
+ case "thread/settings/updated":
17766
+ case "process/outputDelta":
17767
+ case "process/exited":
17728
17768
  return null;
17729
17769
  }
17730
17770
  }
@@ -18059,42 +18099,28 @@ var CodexApprovalHandler = class {
18059
18099
  }
18060
18100
  }
18061
18101
  buildCommandPermissionRequest(sessionId, params) {
18062
- const reasonContent = this.createContentFromReason(params.reason ?? null);
18063
18102
  return {
18064
18103
  sessionId,
18065
18104
  toolCall: {
18066
18105
  toolCallId: params.itemId,
18067
18106
  kind: "execute",
18068
18107
  status: "pending",
18069
- content: reasonContent ? [reasonContent] : null,
18070
18108
  rawInput: params.command ? { command: stripShellPrefix(params.command), cwd: params.cwd } : null
18071
18109
  },
18072
- options: APPROVAL_OPTIONS
18073
- };
18074
- }
18075
- createContentFromReason(reason) {
18076
- if (reason === null || reason === "") {
18077
- return null;
18078
- }
18079
- return {
18080
- type: "content",
18081
- content: {
18082
- type: "text",
18083
- text: reason
18084
- }
18110
+ options: APPROVAL_OPTIONS,
18111
+ _meta: { codex: { params } }
18085
18112
  };
18086
18113
  }
18087
18114
  buildFileChangePermissionRequest(sessionId, params) {
18088
- const reasonContent = this.createContentFromReason(params.reason ?? null);
18089
18115
  return {
18090
18116
  sessionId,
18091
18117
  toolCall: {
18092
18118
  toolCallId: params.itemId,
18093
18119
  kind: "edit",
18094
- status: "pending",
18095
- content: reasonContent ? [reasonContent] : null
18120
+ status: "pending"
18096
18121
  },
18097
- options: APPROVAL_OPTIONS
18122
+ options: APPROVAL_OPTIONS,
18123
+ _meta: { codex: { params } }
18098
18124
  };
18099
18125
  }
18100
18126
  convertCommandResponse(response) {
@@ -19002,6 +19028,7 @@ var ModelId = class _ModelId {
19002
19028
  };
19003
19029
 
19004
19030
  // src/AgentMode.ts
19031
+ var MODE_CONFIG_ID = "mode";
19005
19032
  var AgentMode = class _AgentMode {
19006
19033
  id;
19007
19034
  name;
@@ -19064,6 +19091,21 @@ var AgentMode = class _AgentMode {
19064
19091
  currentModeId: this.id
19065
19092
  };
19066
19093
  }
19094
+ toConfigOption() {
19095
+ return {
19096
+ id: MODE_CONFIG_ID,
19097
+ name: "Mode",
19098
+ description: "Approval and sandboxing preset for the session",
19099
+ category: "mode",
19100
+ type: "select",
19101
+ currentValue: this.id,
19102
+ options: _AgentMode.all().map((mode) => ({
19103
+ value: mode.id,
19104
+ name: mode.name,
19105
+ description: mode.description
19106
+ }))
19107
+ };
19108
+ }
19067
19109
  static all() {
19068
19110
  return [_AgentMode.ReadOnly, _AgentMode.Agent, _AgentMode.AgentFullAccess];
19069
19111
  }
@@ -19084,13 +19126,19 @@ var AgentMode = class _AgentMode {
19084
19126
  // src/CodexAcpClient.ts
19085
19127
  import path4 from "node:path";
19086
19128
 
19129
+ // src/McpServerName.ts
19130
+ var MCP_SERVER_NAME_WHITESPACE = /\p{White_Space}/gu;
19131
+ function sanitizeMcpServerName(name) {
19132
+ return name.replace(MCP_SERVER_NAME_WHITESPACE, "_");
19133
+ }
19134
+
19087
19135
  // package.json
19088
19136
  var package_default = {
19089
19137
  name: "@agentclientprotocol/codex-acp",
19090
19138
  publishConfig: {
19091
19139
  access: "public"
19092
19140
  },
19093
- version: "0.0.45",
19141
+ version: "0.0.46",
19094
19142
  description: "",
19095
19143
  main: "dist/index.js",
19096
19144
  bin: {
@@ -19149,7 +19197,7 @@ var package_default = {
19149
19197
  },
19150
19198
  dependencies: {
19151
19199
  "@agentclientprotocol/sdk": "^0.22.1",
19152
- "@openai/codex": "^0.128.0",
19200
+ "@openai/codex": "^0.137.0",
19153
19201
  diff: "^8.0.3",
19154
19202
  open: "^11.0.0",
19155
19203
  "vscode-jsonrpc": "^8.2.1"
@@ -19164,6 +19212,7 @@ var CodexAcpClient = class {
19164
19212
  gatewayConfig;
19165
19213
  pendingLoginCompleted = null;
19166
19214
  pendingAccountUpdated = null;
19215
+ sessionNotificationQueues = /* @__PURE__ */ new Map();
19167
19216
  constructor(codexClient, codexConfig, modelProvider) {
19168
19217
  this.codexClient = codexClient;
19169
19218
  this.config = codexConfig ?? {};
@@ -19289,7 +19338,7 @@ var CodexAcpClient = class {
19289
19338
  async getAccount() {
19290
19339
  return this.codexClient.accountRead({ refreshToken: false });
19291
19340
  }
19292
- async resumeSession(request) {
19341
+ async resumeSession(request, onSubscribed) {
19293
19342
  await this.refreshSkills(request.cwd, request._meta);
19294
19343
  const response = await this.codexClient.threadResume({
19295
19344
  config: await this.createSessionConfig(request.cwd, request.mcpServers ?? []),
@@ -19297,6 +19346,7 @@ var CodexAcpClient = class {
19297
19346
  modelProvider: this.getResumeModelProvider(),
19298
19347
  threadId: request.sessionId
19299
19348
  });
19349
+ onSubscribed?.();
19300
19350
  const codexModels = await this.fetchAvailableModels();
19301
19351
  const currentModelId = this.createModelId(codexModels, response.model, response.reasoningEffort).toString();
19302
19352
  return {
@@ -19306,13 +19356,14 @@ var CodexAcpClient = class {
19306
19356
  currentServiceTier: response.serviceTier ?? null
19307
19357
  };
19308
19358
  }
19309
- async loadSession(request) {
19359
+ async loadSession(request, onSubscribed) {
19310
19360
  const response = await this.codexClient.threadResume({
19311
19361
  config: await this.createSessionConfig(request.cwd, request.mcpServers ?? []),
19312
19362
  cwd: request.cwd,
19313
19363
  modelProvider: this.getResumeModelProvider(),
19314
19364
  threadId: request.sessionId
19315
19365
  });
19366
+ onSubscribed?.();
19316
19367
  const codexModels = await this.fetchAvailableModels();
19317
19368
  const currentModelId = this.createModelId(codexModels, response.model, response.reasoningEffort).toString();
19318
19369
  return {
@@ -19342,6 +19393,13 @@ var CodexAcpClient = class {
19342
19393
  currentServiceTier: response.serviceTier ?? null
19343
19394
  };
19344
19395
  }
19396
+ async closeSession(sessionId) {
19397
+ try {
19398
+ await this.codexClient.threadUnsubscribe({ threadId: sessionId });
19399
+ } finally {
19400
+ this.codexClient.clearThreadHandlers(sessionId);
19401
+ }
19402
+ }
19345
19403
  async awaitMcpServerStartup(serverNames, afterVersion) {
19346
19404
  return await this.codexClient.awaitMcpServerStartup(serverNames, afterVersion);
19347
19405
  }
@@ -19361,13 +19419,17 @@ var CodexAcpClient = class {
19361
19419
  return mergedConfig;
19362
19420
  }
19363
19421
  const existingNames = await this.getConfigMcpServerNames(projectPath);
19364
- const uniqueServers = mcpServers.filter((mcp) => !existingNames.has(mcp.name));
19422
+ const requestedServers = mcpServers.map((mcp) => ({
19423
+ name: sanitizeMcpServerName(mcp.name),
19424
+ server: mcp
19425
+ }));
19426
+ const uniqueServers = requestedServers.filter((mcp) => !existingNames.has(mcp.name));
19365
19427
  if (uniqueServers.length === 0) {
19366
19428
  return mergedConfig;
19367
19429
  }
19368
19430
  return {
19369
19431
  ...mergedConfig,
19370
- "mcp_servers": Object.fromEntries(uniqueServers.map((mcp) => [mcp.name, this.createMcpSeverConfig(mcp)]))
19432
+ "mcp_servers": Object.fromEntries(uniqueServers.map((mcp) => [mcp.name, this.createMcpSeverConfig(mcp.server)]))
19371
19433
  };
19372
19434
  }
19373
19435
  async getConfigMcpServerNames(projectPath) {
@@ -19388,14 +19450,13 @@ var CodexAcpClient = class {
19388
19450
  if (!cwd) {
19389
19451
  return;
19390
19452
  }
19391
- const additionalRoots = readAdditionalRoots(meta);
19453
+ const additionalRoots = readAdditionalRoots(meta).map((root) => path4.join(root, ".agents", "skills"));
19454
+ if (additionalRoots.length > 0) {
19455
+ await this.codexClient.skillsExtraRootsSet({ extraRoots: additionalRoots });
19456
+ }
19392
19457
  await this.codexClient.listSkills({
19393
19458
  cwds: [cwd],
19394
- forceReload: true,
19395
- perCwdExtraUserRoots: [{
19396
- cwd,
19397
- extraUserRoots: additionalRoots
19398
- }]
19459
+ forceReload: true
19399
19460
  });
19400
19461
  }
19401
19462
  /**
@@ -19433,14 +19494,57 @@ var CodexAcpClient = class {
19433
19494
  return ModelId.create(selectedModel.id, reasoningEffort ?? selectedModel.defaultReasoningEffort);
19434
19495
  }
19435
19496
  async subscribeToSessionEvents(sessionId, eventHandler, approvalHandler, elicitationHandler) {
19436
- this.codexClient.onServerNotification(sessionId, eventHandler);
19437
- this.codexClient.onApprovalRequest(sessionId, approvalHandler);
19438
- this.codexClient.onElicitationRequest(sessionId, elicitationHandler);
19497
+ this.codexClient.onServerNotification(sessionId, (event) => {
19498
+ this.enqueueSessionNotification(sessionId, () => eventHandler(event));
19499
+ });
19500
+ this.codexClient.onApprovalRequest(sessionId, {
19501
+ handleCommandExecution: async (params) => {
19502
+ await this.waitForSessionNotifications(sessionId);
19503
+ return await approvalHandler.handleCommandExecution(params);
19504
+ },
19505
+ handleFileChange: async (params) => {
19506
+ await this.waitForSessionNotifications(sessionId);
19507
+ return await approvalHandler.handleFileChange(params);
19508
+ }
19509
+ });
19510
+ this.codexClient.onElicitationRequest(sessionId, {
19511
+ handleElicitation: async (params) => {
19512
+ await this.waitForSessionNotifications(sessionId);
19513
+ return await elicitationHandler.handleElicitation(params);
19514
+ }
19515
+ });
19516
+ }
19517
+ async waitForSessionNotifications(sessionId) {
19518
+ while (true) {
19519
+ const queue = this.sessionNotificationQueues.get(sessionId);
19520
+ if (!queue) return;
19521
+ await queue;
19522
+ }
19523
+ }
19524
+ enqueueSessionNotification(sessionId, operation) {
19525
+ const run = async () => {
19526
+ try {
19527
+ await operation();
19528
+ } catch (error40) {
19529
+ logger.error("Error handling Codex session notification", error40);
19530
+ }
19531
+ };
19532
+ const previous = this.sessionNotificationQueues.get(sessionId);
19533
+ const next = previous ? previous.then(run, run) : run();
19534
+ this.sessionNotificationQueues.set(sessionId, next);
19535
+ void next.finally(() => {
19536
+ if (this.sessionNotificationQueues.get(sessionId) === next) {
19537
+ this.sessionNotificationQueues.delete(sessionId);
19538
+ }
19539
+ });
19439
19540
  }
19440
- async sendPrompt(request, agentMode, modelId, serviceTier, disableSummary, cwd) {
19541
+ async sendPrompt(request, agentMode, modelId, serviceTier, disableSummary, cwd, onTurnStarted, shouldCancel) {
19441
19542
  const input = buildPromptItems(request.prompt);
19442
19543
  const effort = modelId.effort;
19443
19544
  await this.refreshSkills(cwd, request._meta);
19545
+ if (shouldCancel?.()) {
19546
+ return null;
19547
+ }
19444
19548
  return await this.codexClient.runTurn({
19445
19549
  threadId: request.sessionId,
19446
19550
  input,
@@ -19450,7 +19554,13 @@ var CodexAcpClient = class {
19450
19554
  effort,
19451
19555
  model: modelId.model,
19452
19556
  serviceTier
19453
- });
19557
+ }, onTurnStarted);
19558
+ }
19559
+ resolveTurnInterrupted(params) {
19560
+ this.codexClient.resolveTurnInterrupted(params.threadId, params.turnId);
19561
+ }
19562
+ markTurnStale(params) {
19563
+ this.codexClient.markTurnStale(params.threadId, params.turnId);
19454
19564
  }
19455
19565
  async listSkills(params) {
19456
19566
  return this.codexClient.listSkills(params ?? {});
@@ -19651,6 +19761,44 @@ function mergeGatewayConfig(config2, gatewayConfig) {
19651
19761
  }
19652
19762
  }
19653
19763
 
19764
+ // src/ModelConfigOption.ts
19765
+ var MODEL_CONFIG_ID = "model";
19766
+ var REASONING_EFFORT_CONFIG_ID = "reasoning_effort";
19767
+ function findSupportedEffort(options, effort) {
19768
+ if (!effort) return void 0;
19769
+ return options.find((o) => o.reasoningEffort === effort)?.reasoningEffort;
19770
+ }
19771
+ function createModelConfigOption(availableModels, currentBaseModelId) {
19772
+ return {
19773
+ id: MODEL_CONFIG_ID,
19774
+ name: "Model",
19775
+ description: "Model Codex uses for the session",
19776
+ category: "model",
19777
+ type: "select",
19778
+ currentValue: currentBaseModelId,
19779
+ options: availableModels.map((model) => ({
19780
+ value: model.id,
19781
+ name: model.displayName,
19782
+ description: model.description
19783
+ }))
19784
+ };
19785
+ }
19786
+ function createReasoningEffortConfigOption(supportedReasoningEfforts, currentEffort) {
19787
+ return {
19788
+ id: REASONING_EFFORT_CONFIG_ID,
19789
+ name: "Reasoning effort",
19790
+ description: "How much reasoning effort the model should use",
19791
+ category: "thought_level",
19792
+ type: "select",
19793
+ currentValue: currentEffort,
19794
+ options: supportedReasoningEfforts.map((option) => ({
19795
+ value: option.reasoningEffort,
19796
+ name: option.reasoningEffort,
19797
+ description: option.description
19798
+ }))
19799
+ };
19800
+ }
19801
+
19654
19802
  // src/CodexCommands.ts
19655
19803
  var CodexCommands = class {
19656
19804
  connection;
@@ -19736,6 +19884,7 @@ var CodexCommands = class {
19736
19884
  async tryHandleCommand(prompt, sessionState) {
19737
19885
  const commandName = this.getCommandName(prompt);
19738
19886
  if (commandName === null) return false;
19887
+ if (commandName.startsWith("$")) return false;
19739
19888
  const sessionId = sessionState.sessionId;
19740
19889
  switch (commandName) {
19741
19890
  case "status": {
@@ -19980,6 +20129,17 @@ function createFastModeConfigOption(fastModeEnabled) {
19980
20129
  };
19981
20130
  }
19982
20131
 
20132
+ // src/JBUtils.ts
20133
+ function isJetBrains2026_1Client(clientInfo) {
20134
+ if (!clientInfo) {
20135
+ return false;
20136
+ }
20137
+ const platform2 = clientInfo._meta?.["platform"];
20138
+ const isIntelliJPlatform = platform2 === "intellij";
20139
+ const isJetBrainsClient = clientInfo.name.startsWith("JetBrains");
20140
+ return (isIntelliJPlatform || isJetBrainsClient) && clientInfo.version.startsWith("2026.1");
20141
+ }
20142
+
19983
20143
  // src/CodexAcpServer.ts
19984
20144
  var CodexAcpServer = class _CodexAcpServer {
19985
20145
  static MODEL_NAME_TOKEN_OVERRIDES = {
@@ -19992,15 +20152,27 @@ var CodexAcpServer = class _CodexAcpServer {
19992
20152
  defaultAuthRequest;
19993
20153
  getExitCode;
19994
20154
  availableCommands;
20155
+ clientInfo;
19995
20156
  sessions;
19996
20157
  pendingMcpStartupSessions;
20158
+ pendingTurnStarts;
20159
+ activePrompts;
20160
+ closingSessions;
20161
+ sessionGenerations;
20162
+ sessionOpenGenerations;
19997
20163
  constructor(connection, codexAcpClient, defaultAuthRequest, getExitCode) {
19998
20164
  this.sessions = /* @__PURE__ */ new Map();
19999
20165
  this.pendingMcpStartupSessions = /* @__PURE__ */ new Map();
20166
+ this.pendingTurnStarts = /* @__PURE__ */ new Map();
20167
+ this.activePrompts = /* @__PURE__ */ new Map();
20168
+ this.closingSessions = /* @__PURE__ */ new Map();
20169
+ this.sessionGenerations = /* @__PURE__ */ new Map();
20170
+ this.sessionOpenGenerations = /* @__PURE__ */ new Map();
20000
20171
  this.connection = connection;
20001
20172
  this.codexAcpClient = codexAcpClient;
20002
20173
  this.defaultAuthRequest = defaultAuthRequest ?? null;
20003
20174
  this.getExitCode = getExitCode ?? (() => null);
20175
+ this.clientInfo = null;
20004
20176
  this.availableCommands = new CodexCommands(
20005
20177
  connection,
20006
20178
  codexAcpClient,
@@ -20009,6 +20181,7 @@ var CodexAcpServer = class _CodexAcpServer {
20009
20181
  }
20010
20182
  async initialize(_params) {
20011
20183
  logger.log("Initialize request received");
20184
+ this.clientInfo = _params.clientInfo ?? null;
20012
20185
  await this.runWithProcessCheck(() => this.codexAcpClient.initialize(_params));
20013
20186
  return {
20014
20187
  protocolVersion: PROTOCOL_VERSION,
@@ -20028,7 +20201,8 @@ var CodexAcpServer = class _CodexAcpServer {
20028
20201
  },
20029
20202
  sessionCapabilities: {
20030
20203
  resume: {},
20031
- list: {}
20204
+ list: {},
20205
+ close: {}
20032
20206
  },
20033
20207
  mcpCapabilities: {
20034
20208
  acp: false,
@@ -20086,26 +20260,107 @@ var CodexAcpServer = class _CodexAcpServer {
20086
20260
  You have been logged out. Please try again.`);
20087
20261
  }
20088
20262
  }
20263
+ beginSessionOpen(sessionId) {
20264
+ const generation = this.getSessionGeneration(sessionId);
20265
+ if (this.sessionIsClosing(sessionId)) {
20266
+ throw RequestError.invalidRequest(`Session ${sessionId} is closing`);
20267
+ }
20268
+ this.sessionOpenGenerations.set(sessionId, generation);
20269
+ return generation;
20270
+ }
20271
+ sessionOpenCanInstall(sessionId, generation) {
20272
+ return !this.sessionIsClosing(sessionId) && this.getSessionGeneration(sessionId) === generation;
20273
+ }
20274
+ async cleanupStaleSessionOpen(sessionId, generation) {
20275
+ if (this.sessionOpenGenerations.get(sessionId) === generation) {
20276
+ if (!this.sessionIsClosing(sessionId)) {
20277
+ this.bumpSessionGeneration(sessionId);
20278
+ }
20279
+ this.beginSessionCloseFence(sessionId);
20280
+ try {
20281
+ await this.runWithProcessCheck(() => this.codexAcpClient.closeSession(sessionId));
20282
+ } catch (err) {
20283
+ logger.error(`Failed to close stale session open for ${sessionId}`, err);
20284
+ } finally {
20285
+ this.endSessionCloseFence(sessionId);
20286
+ }
20287
+ return true;
20288
+ }
20289
+ return false;
20290
+ }
20291
+ async closeStaleSessionOpen(sessionId, generation) {
20292
+ await this.cleanupStaleSessionOpen(sessionId, generation);
20293
+ throw RequestError.invalidRequest(`Session ${sessionId} is closing`);
20294
+ }
20295
+ sessionIsClosing(sessionId) {
20296
+ return (this.closingSessions.get(sessionId) ?? 0) > 0;
20297
+ }
20298
+ beginSessionCloseFence(sessionId) {
20299
+ this.closingSessions.set(sessionId, (this.closingSessions.get(sessionId) ?? 0) + 1);
20300
+ }
20301
+ endSessionCloseFence(sessionId) {
20302
+ const count = this.closingSessions.get(sessionId) ?? 0;
20303
+ if (count <= 1) {
20304
+ this.closingSessions.delete(sessionId);
20305
+ return;
20306
+ }
20307
+ this.closingSessions.set(sessionId, count - 1);
20308
+ }
20309
+ getSessionGeneration(sessionId) {
20310
+ return this.sessionGenerations.get(sessionId) ?? 0;
20311
+ }
20312
+ bumpSessionGeneration(sessionId) {
20313
+ const generation = this.getSessionGeneration(sessionId) + 1;
20314
+ this.sessionGenerations.set(sessionId, generation);
20315
+ return generation;
20316
+ }
20089
20317
  async tryCreateSession(request) {
20318
+ const requestedSessionGeneration = "sessionId" in request ? this.beginSessionOpen(request.sessionId) : null;
20090
20319
  await this.checkAuthorization();
20091
20320
  const requestedMcpServers = request.mcpServers ?? [];
20092
20321
  const mcpServerStartupVersion = requestedMcpServers.length > 0 ? this.codexAcpClient.getMcpServerStartupVersion() : null;
20093
20322
  let sessionMetadata;
20323
+ let resumeSubscribed = false;
20094
20324
  if ("sessionId" in request) {
20095
20325
  logger.log(`Resume existing session: ${request.sessionId}...`);
20096
- sessionMetadata = await this.runWithProcessCheck(() => this.codexAcpClient.resumeSession(request));
20326
+ try {
20327
+ sessionMetadata = await this.runWithProcessCheck(
20328
+ () => this.codexAcpClient.resumeSession(request, () => {
20329
+ resumeSubscribed = true;
20330
+ })
20331
+ );
20332
+ } catch (err) {
20333
+ if (resumeSubscribed && requestedSessionGeneration !== null) {
20334
+ await this.cleanupStaleSessionOpen(request.sessionId, requestedSessionGeneration);
20335
+ }
20336
+ throw err;
20337
+ }
20097
20338
  } else {
20098
20339
  logger.log(`Create new session...`);
20099
20340
  sessionMetadata = await this.runWithProcessCheck(() => this.codexAcpClient.newSession(request));
20100
20341
  }
20101
- const account = await this.getActiveAccount();
20102
20342
  const { sessionId, currentModelId, models } = sessionMetadata;
20343
+ let account;
20344
+ try {
20345
+ account = await this.getActiveAccount();
20346
+ } catch (err) {
20347
+ if (resumeSubscribed && requestedSessionGeneration !== null) {
20348
+ await this.cleanupStaleSessionOpen(sessionId, requestedSessionGeneration);
20349
+ }
20350
+ throw err;
20351
+ }
20352
+ const sessionGeneration = requestedSessionGeneration ?? this.beginSessionOpen(sessionId);
20353
+ if (!this.sessionOpenCanInstall(sessionId, sessionGeneration)) {
20354
+ resumeSubscribed = false;
20355
+ await this.closeStaleSessionOpen(sessionId, sessionGeneration);
20356
+ }
20103
20357
  const sessionMcpServers = this.resolveSessionMcpServers(requestedMcpServers, "sessionId" in request);
20104
20358
  const currentModel = this.findCurrentModel(models, currentModelId);
20105
20359
  const currentModelSupportsFast = modelSupportsFast(currentModel);
20106
20360
  const sessionState = {
20107
20361
  sessionId,
20108
20362
  currentModelId,
20363
+ availableModels: models,
20109
20364
  supportedReasoningEfforts: currentModel?.supportedReasoningEfforts ?? [],
20110
20365
  supportedInputModalities: currentModel?.inputModalities ?? ["text", "image"],
20111
20366
  agentMode: AgentMode.getInitialAgentMode(),
@@ -20121,9 +20376,10 @@ You have been logged out. Please try again.`);
20121
20376
  sessionMcpServers
20122
20377
  };
20123
20378
  this.sessions.set(sessionId, sessionState);
20379
+ resumeSubscribed = false;
20124
20380
  if (requestedMcpServers.length > 0 && mcpServerStartupVersion !== null) {
20125
20381
  this.pendingMcpStartupSessions.set(sessionId, {
20126
- requestedServers: new Set(requestedMcpServers.map((server) => server.name)),
20382
+ requestedServers: new Set(getRequestedMcpServerNames(requestedMcpServers)),
20127
20383
  afterVersion: mcpServerStartupVersion
20128
20384
  });
20129
20385
  this.publishMcpStartupStatusAsync(sessionId);
@@ -20157,7 +20413,7 @@ You have been logged out. Please try again.`);
20157
20413
  return {
20158
20414
  models: modelState,
20159
20415
  modes: modeState,
20160
- configOptions: this.createSessionConfigOptions(this.getSessionState(sessionId))
20416
+ ...this.createSessionConfigOptionsResponse(this.getSessionState(sessionId))
20161
20417
  };
20162
20418
  }
20163
20419
  async resumeSession(params) {
@@ -20171,7 +20427,7 @@ You have been logged out. Please try again.`);
20171
20427
  return {
20172
20428
  models: modelState,
20173
20429
  modes: modeState,
20174
- configOptions: this.createSessionConfigOptions(this.getSessionState(sessionId))
20430
+ ...this.createSessionConfigOptionsResponse(this.getSessionState(sessionId))
20175
20431
  };
20176
20432
  }
20177
20433
  async listSessions(params) {
@@ -20179,6 +20435,35 @@ You have been logged out. Please try again.`);
20179
20435
  await this.checkAuthorization();
20180
20436
  return await this.runWithProcessCheck(() => this.codexAcpClient.listSessions(params));
20181
20437
  }
20438
+ async closeSession(params) {
20439
+ logger.log("Closing session...", { sessionId: params.sessionId });
20440
+ const closeGeneration = this.bumpSessionGeneration(params.sessionId);
20441
+ const sessionState = this.sessions.get(params.sessionId);
20442
+ this.beginSessionCloseFence(params.sessionId);
20443
+ try {
20444
+ if (sessionState) {
20445
+ await this.interruptSessionTurn(sessionState, "Close", true);
20446
+ } else {
20447
+ logger.log("Close request received for unknown local session", { sessionId: params.sessionId });
20448
+ }
20449
+ const activePrompt = this.activePrompts.get(params.sessionId);
20450
+ if (activePrompt) {
20451
+ activePrompt.requestClose();
20452
+ await activePrompt.completion;
20453
+ }
20454
+ await this.runWithProcessCheck(() => this.codexAcpClient.closeSession(params.sessionId));
20455
+ logger.log("Session closed", { sessionId: params.sessionId });
20456
+ } finally {
20457
+ if (this.getSessionGeneration(params.sessionId) === closeGeneration) {
20458
+ this.sessions.delete(params.sessionId);
20459
+ this.pendingMcpStartupSessions.delete(params.sessionId);
20460
+ this.pendingTurnStarts.delete(params.sessionId);
20461
+ this.activePrompts.delete(params.sessionId);
20462
+ }
20463
+ this.endSessionCloseFence(params.sessionId);
20464
+ }
20465
+ return {};
20466
+ }
20182
20467
  async newSession(params) {
20183
20468
  logger.log("Starting new session...");
20184
20469
  const [sessionId, modelState, modeState] = await this.getOrCreateSession(params);
@@ -20191,7 +20476,7 @@ You have been logged out. Please try again.`);
20191
20476
  sessionId,
20192
20477
  models: modelState,
20193
20478
  modes: modeState,
20194
- configOptions: this.createSessionConfigOptions(this.getSessionState(sessionId))
20479
+ ...this.createSessionConfigOptionsResponse(this.getSessionState(sessionId))
20195
20480
  };
20196
20481
  }
20197
20482
  async authenticate(_params) {
@@ -20216,11 +20501,7 @@ You have been logged out. Please try again.`);
20216
20501
  });
20217
20502
  const sessionState = this.sessions.get(_params.sessionId);
20218
20503
  if (!sessionState) throw new Error(`Session ${_params.sessionId} not found`);
20219
- const newMode = AgentMode.find(_params.modeId);
20220
- if (!newMode) {
20221
- throw RequestError.invalidParams();
20222
- }
20223
- sessionState.agentMode = newMode;
20504
+ this.applyModeChange(sessionState, _params.modeId);
20224
20505
  return {};
20225
20506
  }
20226
20507
  async setSessionConfigOption(params) {
@@ -20230,17 +20511,66 @@ You have been logged out. Please try again.`);
20230
20511
  });
20231
20512
  const sessionState = this.sessions.get(params.sessionId);
20232
20513
  if (!sessionState) throw new Error(`Session ${params.sessionId} not found`);
20233
- if (params.configId !== FAST_MODE_CONFIG_ID || "type" in params && params.type === "boolean") {
20514
+ if (typeof params.value !== "string") {
20234
20515
  throw RequestError.invalidParams();
20235
20516
  }
20236
- if (params.value !== FAST_MODE_ON && params.value !== FAST_MODE_OFF) {
20237
- throw RequestError.invalidParams();
20517
+ const value = params.value;
20518
+ switch (params.configId) {
20519
+ case FAST_MODE_CONFIG_ID:
20520
+ this.applyFastModeChange(sessionState, value);
20521
+ break;
20522
+ case MODE_CONFIG_ID:
20523
+ this.applyModeChange(sessionState, value);
20524
+ break;
20525
+ case MODEL_CONFIG_ID:
20526
+ this.applyModelChange(sessionState, value);
20527
+ break;
20528
+ case REASONING_EFFORT_CONFIG_ID:
20529
+ this.applyReasoningEffortChange(sessionState, value);
20530
+ break;
20531
+ default:
20532
+ throw RequestError.invalidParams();
20238
20533
  }
20239
- sessionState.fastModeEnabled = params.value === FAST_MODE_ON;
20240
20534
  return {
20241
20535
  configOptions: this.createSessionConfigOptions(sessionState)
20242
20536
  };
20243
20537
  }
20538
+ applyFastModeChange(sessionState, value) {
20539
+ if (value !== FAST_MODE_ON && value !== FAST_MODE_OFF) {
20540
+ throw RequestError.invalidParams();
20541
+ }
20542
+ sessionState.fastModeEnabled = value === FAST_MODE_ON;
20543
+ }
20544
+ applyModeChange(sessionState, value) {
20545
+ const newMode = AgentMode.find(value);
20546
+ if (!newMode) {
20547
+ throw RequestError.invalidParams();
20548
+ }
20549
+ sessionState.agentMode = newMode;
20550
+ }
20551
+ applyModelChange(sessionState, value) {
20552
+ const model = sessionState.availableModels.find((m) => m.id === value);
20553
+ if (!model) {
20554
+ throw RequestError.invalidParams();
20555
+ }
20556
+ const currentEffort = ModelId.fromString(sessionState.currentModelId).effort;
20557
+ const effort = findSupportedEffort(model.supportedReasoningEfforts, currentEffort) ?? model.defaultReasoningEffort;
20558
+ this.applyModelAndEffort(sessionState, model, effort);
20559
+ }
20560
+ applyReasoningEffortChange(sessionState, value) {
20561
+ const effort = findSupportedEffort(sessionState.supportedReasoningEfforts, value);
20562
+ if (!effort) {
20563
+ throw RequestError.invalidParams();
20564
+ }
20565
+ const { model } = ModelId.fromString(sessionState.currentModelId);
20566
+ sessionState.currentModelId = ModelId.create(model, effort).toString();
20567
+ }
20568
+ applyModelAndEffort(sessionState, model, effort) {
20569
+ sessionState.currentModelId = ModelId.fromComponents(model, effort).toString();
20570
+ sessionState.supportedReasoningEfforts = model.supportedReasoningEfforts;
20571
+ sessionState.supportedInputModalities = model.inputModalities;
20572
+ sessionState.currentModelSupportsFast = modelSupportsFast(model);
20573
+ }
20244
20574
  async unstable_setSessionModel(params) {
20245
20575
  logger.log("Set session model requested", {
20246
20576
  sessionId: params.sessionId,
@@ -20248,36 +20578,44 @@ You have been logged out. Please try again.`);
20248
20578
  });
20249
20579
  const sessionState = this.sessions.get(params.sessionId);
20250
20580
  if (!sessionState) throw new Error(`Session ${params.sessionId} not found`);
20251
- const requestedModelId = ModelId.fromString(params.modelId);
20252
- const requestedModelName = requestedModelId.model;
20253
- const requestedEffort = requestedModelId.effort;
20581
+ const { model: requestedModelName, effort: requestedEffort } = ModelId.fromString(params.modelId);
20254
20582
  const models = await this.codexAcpClient.fetchAvailableModels();
20255
20583
  const model = models.find((m) => m.id === requestedModelName);
20256
20584
  if (!model) throw new Error(`Unknown model ${params.modelId}`);
20257
- const requestedEffortValue = requestedEffort;
20258
20585
  let reasoningEffort;
20259
- if (requestedEffortValue) {
20260
- const matchedEffort = model.supportedReasoningEfforts.find(
20261
- (option) => option.reasoningEffort === requestedEffortValue
20262
- )?.reasoningEffort;
20586
+ if (requestedEffort) {
20587
+ const matchedEffort = findSupportedEffort(model.supportedReasoningEfforts, requestedEffort);
20263
20588
  if (!matchedEffort) {
20264
- throw new Error(`Unsupported reasoning effort ${requestedEffortValue} for model ${requestedModelName}`);
20589
+ throw new Error(`Unsupported reasoning effort ${requestedEffort} for model ${requestedModelName}`);
20265
20590
  }
20266
20591
  reasoningEffort = matchedEffort;
20267
20592
  } else {
20268
20593
  reasoningEffort = model.defaultReasoningEffort;
20269
20594
  }
20270
- sessionState.currentModelId = ModelId.fromComponents(model, reasoningEffort).toString();
20271
- sessionState.supportedReasoningEfforts = model.supportedReasoningEfforts;
20272
- sessionState.supportedInputModalities = model.inputModalities;
20273
- sessionState.currentModelSupportsFast = modelSupportsFast(model);
20595
+ sessionState.availableModels = models;
20596
+ this.applyModelAndEffort(sessionState, model, reasoningEffort);
20274
20597
  return {};
20275
20598
  }
20276
20599
  createSessionConfigOptions(sessionState) {
20600
+ const currentModelId = ModelId.fromString(sessionState.currentModelId);
20277
20601
  return [
20602
+ sessionState.agentMode.toConfigOption(),
20603
+ createModelConfigOption(sessionState.availableModels, currentModelId.model),
20604
+ createReasoningEffortConfigOption(sessionState.supportedReasoningEfforts, currentModelId.effort),
20278
20605
  createFastModeConfigOption(sessionState.fastModeEnabled)
20279
20606
  ];
20280
20607
  }
20608
+ createSessionConfigOptionsResponse(sessionState) {
20609
+ if (!this.isSessionConfigEnabled()) {
20610
+ return {};
20611
+ }
20612
+ return {
20613
+ configOptions: this.createSessionConfigOptions(sessionState)
20614
+ };
20615
+ }
20616
+ isSessionConfigEnabled() {
20617
+ return !isJetBrains2026_1Client(this.clientInfo);
20618
+ }
20281
20619
  publishAvailableCommandsAsync(sessionId) {
20282
20620
  void this.availableCommands.publish(sessionId);
20283
20621
  }
@@ -20302,21 +20640,46 @@ You have been logged out. Please try again.`);
20302
20640
  };
20303
20641
  }
20304
20642
  async getOrCreateSessionWithHistory(request) {
20643
+ const requestedSessionGeneration = this.beginSessionOpen(request.sessionId);
20305
20644
  await this.checkAuthorization();
20306
20645
  const requestedMcpServers = request.mcpServers ?? [];
20307
20646
  const mcpServerStartupVersion = requestedMcpServers.length > 0 ? this.codexAcpClient.getMcpServerStartupVersion() : null;
20308
20647
  logger.log(`Load existing session: ${request.sessionId}...`);
20309
- const sessionMetadata = await this.runWithProcessCheck(
20310
- () => this.codexAcpClient.loadSession(request)
20311
- );
20312
- const account = await this.getActiveAccount();
20648
+ let subscribed = false;
20649
+ let sessionMetadata;
20650
+ try {
20651
+ sessionMetadata = await this.runWithProcessCheck(
20652
+ () => this.codexAcpClient.loadSession(request, () => {
20653
+ subscribed = true;
20654
+ })
20655
+ );
20656
+ } catch (err) {
20657
+ if (subscribed) {
20658
+ await this.cleanupStaleSessionOpen(request.sessionId, requestedSessionGeneration);
20659
+ }
20660
+ throw err;
20661
+ }
20313
20662
  const { sessionId, currentModelId, models, thread } = sessionMetadata;
20663
+ let account;
20664
+ try {
20665
+ account = await this.getActiveAccount();
20666
+ } catch (err) {
20667
+ if (subscribed) {
20668
+ await this.cleanupStaleSessionOpen(request.sessionId, requestedSessionGeneration);
20669
+ }
20670
+ throw err;
20671
+ }
20672
+ if (!this.sessionOpenCanInstall(sessionId, requestedSessionGeneration)) {
20673
+ subscribed = false;
20674
+ await this.closeStaleSessionOpen(sessionId, requestedSessionGeneration);
20675
+ }
20314
20676
  const sessionMcpServers = this.resolveSessionMcpServers(requestedMcpServers, true);
20315
20677
  const currentModel = this.findCurrentModel(models, currentModelId);
20316
20678
  const currentModelSupportsFast = modelSupportsFast(currentModel);
20317
20679
  const sessionState = {
20318
20680
  sessionId,
20319
20681
  currentModelId,
20682
+ availableModels: models,
20320
20683
  supportedReasoningEfforts: currentModel?.supportedReasoningEfforts ?? [],
20321
20684
  supportedInputModalities: currentModel?.inputModalities ?? ["text", "image"],
20322
20685
  agentMode: AgentMode.getInitialAgentMode(),
@@ -20332,9 +20695,10 @@ You have been logged out. Please try again.`);
20332
20695
  sessionMcpServers
20333
20696
  };
20334
20697
  this.sessions.set(sessionId, sessionState);
20698
+ subscribed = false;
20335
20699
  if (requestedMcpServers.length > 0 && mcpServerStartupVersion !== null) {
20336
20700
  this.pendingMcpStartupSessions.set(sessionId, {
20337
- requestedServers: new Set(requestedMcpServers.map((server) => server.name)),
20701
+ requestedServers: new Set(getRequestedMcpServerNames(requestedMcpServers)),
20338
20702
  afterVersion: mcpServerStartupVersion
20339
20703
  });
20340
20704
  this.publishMcpStartupStatusAsync(sessionId);
@@ -20580,11 +20944,16 @@ ${item.text}`
20580
20944
  pendingStartup.afterVersion
20581
20945
  )
20582
20946
  );
20947
+ if (!this.sessions.has(sessionId) || this.sessionIsClosing(sessionId) || this.pendingMcpStartupSessions.get(sessionId) !== pendingStartup) {
20948
+ return;
20949
+ }
20583
20950
  await this.publishMcpStartupStatus(sessionId, mcpStartup, pendingStartup.requestedServers);
20584
20951
  } catch (err) {
20585
20952
  logger.error(`Failed to publish MCP startup status for session ${sessionId}`, err);
20586
20953
  } finally {
20587
- this.pendingMcpStartupSessions.delete(sessionId);
20954
+ if (this.pendingMcpStartupSessions.get(sessionId) === pendingStartup) {
20955
+ this.pendingMcpStartupSessions.delete(sessionId);
20956
+ }
20588
20957
  }
20589
20958
  }
20590
20959
  async publishMcpStartupStatus(sessionId, mcpStartup, requestedServers) {
@@ -20600,6 +20969,125 @@ ${item.text}`
20600
20969
  });
20601
20970
  }
20602
20971
  }
20972
+ trackActivePrompt(sessionId) {
20973
+ let resolveCompletion = () => {
20974
+ };
20975
+ const completion = new Promise((resolve) => {
20976
+ resolveCompletion = resolve;
20977
+ });
20978
+ let resolveCloseSignal = () => {
20979
+ };
20980
+ const closeSignal = new Promise((resolve) => {
20981
+ resolveCloseSignal = resolve;
20982
+ });
20983
+ let completed = false;
20984
+ let closeRequested = false;
20985
+ const activePrompt = {
20986
+ completion,
20987
+ closeSignal,
20988
+ requestClose: () => {
20989
+ if (closeRequested) {
20990
+ return;
20991
+ }
20992
+ closeRequested = true;
20993
+ resolveCloseSignal(null);
20994
+ },
20995
+ complete: () => {
20996
+ if (completed) {
20997
+ return;
20998
+ }
20999
+ completed = true;
21000
+ if (this.activePrompts.get(sessionId) === activePrompt) {
21001
+ this.activePrompts.delete(sessionId);
21002
+ }
21003
+ resolveCompletion();
21004
+ }
21005
+ };
21006
+ this.activePrompts.set(sessionId, activePrompt);
21007
+ return activePrompt;
21008
+ }
21009
+ createPendingTurnStart() {
21010
+ let resolve = () => {
21011
+ };
21012
+ const promise2 = new Promise((innerResolve) => {
21013
+ resolve = innerResolve;
21014
+ });
21015
+ return { promise: promise2, resolve };
21016
+ }
21017
+ interruptLateStartedTurn(sessionId, turnId) {
21018
+ this.codexAcpClient.markTurnStale({
21019
+ threadId: sessionId,
21020
+ turnId
21021
+ });
21022
+ void this.runWithProcessCheck(() => this.codexAcpClient.turnInterrupt({
21023
+ threadId: sessionId,
21024
+ turnId
21025
+ })).catch((err) => {
21026
+ logger.error(`Close - late turnInterrupt failed`, err);
21027
+ }).finally(() => {
21028
+ this.codexAcpClient.resolveTurnInterrupted({
21029
+ threadId: sessionId,
21030
+ turnId
21031
+ });
21032
+ });
21033
+ }
21034
+ promptIsClosedOrStale(sessionId, activePrompt) {
21035
+ return this.activePrompts.get(sessionId) !== activePrompt || this.sessionIsClosing(sessionId);
21036
+ }
21037
+ async interruptSessionTurn(sessionState, requestName, resolveInterruptedTurn) {
21038
+ const turnId = await this.getInterruptibleTurnId(sessionState, requestName);
21039
+ if (!turnId) {
21040
+ return;
21041
+ }
21042
+ logger.log(`${requestName} session requested`, {
21043
+ sessionId: sessionState.sessionId,
21044
+ currentTurnId: turnId
21045
+ });
21046
+ if (resolveInterruptedTurn) {
21047
+ this.codexAcpClient.markTurnStale({
21048
+ threadId: sessionState.sessionId,
21049
+ turnId
21050
+ });
21051
+ }
21052
+ try {
21053
+ await this.runWithProcessCheck(() => this.codexAcpClient.turnInterrupt({
21054
+ threadId: sessionState.sessionId,
21055
+ turnId
21056
+ }));
21057
+ logger.log(`${requestName} - turnInterrupt succeeded`, {
21058
+ sessionId: sessionState.sessionId,
21059
+ currentTurnId: turnId
21060
+ });
21061
+ } catch (err) {
21062
+ logger.error(`${requestName} - turnInterrupt failed`, err);
21063
+ } finally {
21064
+ if (resolveInterruptedTurn) {
21065
+ this.codexAcpClient.resolveTurnInterrupted({
21066
+ threadId: sessionState.sessionId,
21067
+ turnId
21068
+ });
21069
+ }
21070
+ }
21071
+ }
21072
+ async getInterruptibleTurnId(sessionState, requestName) {
21073
+ if (sessionState.currentTurnId) {
21074
+ return sessionState.currentTurnId;
21075
+ }
21076
+ const pendingTurnStart = this.pendingTurnStarts.get(sessionState.sessionId);
21077
+ if (!pendingTurnStart) {
21078
+ logger.log(`${requestName} request rejected: no current turn`, { sessionId: sessionState.sessionId });
21079
+ return null;
21080
+ }
21081
+ if (requestName === "Close") {
21082
+ pendingTurnStart.resolve(null);
21083
+ return null;
21084
+ }
21085
+ const turnId = await pendingTurnStart.promise;
21086
+ if (!turnId) {
21087
+ logger.log(`${requestName} request rejected: no current turn`, { sessionId: sessionState.sessionId });
21088
+ }
21089
+ return turnId;
21090
+ }
20603
21091
  async prompt(params) {
20604
21092
  logger.log("Prompt received", {
20605
21093
  sessionId: params.sessionId,
@@ -20608,6 +21096,8 @@ ${item.text}`
20608
21096
  const sessionState = this.getSessionState(params.sessionId);
20609
21097
  sessionState.currentTurnId = null;
20610
21098
  sessionState.lastTokenUsage = null;
21099
+ const activePrompt = this.trackActivePrompt(params.sessionId);
21100
+ let pendingTurnStart = null;
20611
21101
  try {
20612
21102
  const eventHandler = new CodexEventHandler(this.connection, sessionState);
20613
21103
  const approvalHandler = new CodexApprovalHandler(this.connection, sessionState);
@@ -20629,6 +21119,13 @@ ${item.text}`
20629
21119
  _meta: this.buildQuotaMeta(sessionState)
20630
21120
  };
20631
21121
  }
21122
+ if (this.sessionIsClosing(params.sessionId)) {
21123
+ return {
21124
+ stopReason: "cancelled",
21125
+ usage: this.buildPromptUsage(sessionState.lastTokenUsage),
21126
+ _meta: this.buildQuotaMeta(sessionState)
21127
+ };
21128
+ }
20632
21129
  const modelId = ModelId.fromString(sessionState.currentModelId);
20633
21130
  const modelLacksReasoning = sessionState.supportedReasoningEfforts.length > 0 && sessionState.supportedReasoningEfforts.every((e) => e.reasoningEffort === "none");
20634
21131
  const disableSummary = sessionState.account?.type === "apiKey" || modelLacksReasoning;
@@ -20646,20 +21143,57 @@ ${item.text}`
20646
21143
  sessionState.fastModeEnabled,
20647
21144
  sessionState.currentModelSupportsFast
20648
21145
  );
20649
- const turnCompleted = await this.runWithProcessCheck(
20650
- () => this.codexAcpClient.sendPrompt(params, agentMode, modelId, serviceTier, disableSummary, sessionState.cwd)
21146
+ pendingTurnStart = this.createPendingTurnStart();
21147
+ this.pendingTurnStarts.set(params.sessionId, pendingTurnStart);
21148
+ const sendPromptPromise = this.runWithProcessCheck(
21149
+ () => this.codexAcpClient.sendPrompt(
21150
+ params,
21151
+ agentMode,
21152
+ modelId,
21153
+ serviceTier,
21154
+ disableSummary,
21155
+ sessionState.cwd,
21156
+ (turnId) => {
21157
+ if (this.promptIsClosedOrStale(params.sessionId, activePrompt)) {
21158
+ this.interruptLateStartedTurn(params.sessionId, turnId);
21159
+ return;
21160
+ }
21161
+ sessionState.currentTurnId = turnId;
21162
+ pendingTurnStart?.resolve(turnId);
21163
+ },
21164
+ () => this.promptIsClosedOrStale(params.sessionId, activePrompt)
21165
+ )
20651
21166
  );
21167
+ void sendPromptPromise.catch((err) => {
21168
+ if (this.activePrompts.get(params.sessionId) !== activePrompt) {
21169
+ logger.error(`Prompt for closed session ${params.sessionId} failed after close`, err);
21170
+ }
21171
+ });
21172
+ const turnCompleted = await Promise.race([
21173
+ sendPromptPromise,
21174
+ activePrompt.closeSignal
21175
+ ]);
21176
+ if (turnCompleted === null) {
21177
+ return {
21178
+ stopReason: "cancelled",
21179
+ usage: this.buildPromptUsage(sessionState.lastTokenUsage),
21180
+ _meta: this.buildQuotaMeta(sessionState)
21181
+ };
21182
+ }
21183
+ await this.codexAcpClient.waitForSessionNotifications(params.sessionId);
20652
21184
  if (turnCompleted.turn.status === "interrupted") {
20653
- await this.connection.sessionUpdate({
20654
- sessionId: params.sessionId,
20655
- update: {
20656
- sessionUpdate: "agent_message_chunk",
20657
- content: {
20658
- type: "text",
20659
- text: "*Conversation interrupted*"
21185
+ if (!this.sessionIsClosing(params.sessionId) && this.sessions.has(params.sessionId)) {
21186
+ await this.connection.sessionUpdate({
21187
+ sessionId: params.sessionId,
21188
+ update: {
21189
+ sessionUpdate: "agent_message_chunk",
21190
+ content: {
21191
+ type: "text",
21192
+ text: "*Conversation interrupted*"
21193
+ }
20660
21194
  }
20661
- }
20662
- });
21195
+ });
21196
+ }
20663
21197
  return {
20664
21198
  stopReason: "cancelled",
20665
21199
  usage: this.buildPromptUsage(sessionState.lastTokenUsage),
@@ -20681,6 +21215,11 @@ ${item.text}`
20681
21215
  } finally {
20682
21216
  logger.log("Prompt completed", { sessionId: params.sessionId });
20683
21217
  sessionState.currentTurnId = null;
21218
+ if (pendingTurnStart !== null && this.pendingTurnStarts.get(params.sessionId) === pendingTurnStart) {
21219
+ this.pendingTurnStarts.delete(params.sessionId);
21220
+ }
21221
+ pendingTurnStart?.resolve(null);
21222
+ activePrompt.complete();
20684
21223
  }
20685
21224
  }
20686
21225
  buildQuotaMeta(sessionState) {
@@ -20721,30 +21260,11 @@ ${item.text}`
20721
21260
  logger.log("Cancel request rejected: session not found", { sessionId: params.sessionId });
20722
21261
  return;
20723
21262
  }
20724
- if (!sessionState.currentTurnId) {
20725
- logger.log("Cancel request rejected: no current turn", { sessionId: params.sessionId });
20726
- return;
20727
- }
20728
- logger.log("Cancel session requested", {
20729
- sessionId: params.sessionId,
20730
- currentTurnId: sessionState.currentTurnId
20731
- });
20732
- try {
20733
- await this.codexAcpClient.turnInterrupt({
20734
- threadId: params.sessionId,
20735
- turnId: sessionState.currentTurnId
20736
- });
20737
- logger.log("Cancel - turnInterrupt succeeded", {
20738
- sessionId: params.sessionId,
20739
- currentTurnId: sessionState.currentTurnId
20740
- });
20741
- } catch (err) {
20742
- logger.error(`Cancel - turnInterrupt failed`, err);
20743
- }
21263
+ await this.interruptSessionTurn(sessionState, "Cancel", false);
20744
21264
  }
20745
21265
  };
20746
21266
  function getRequestedMcpServerNames(mcpServers) {
20747
- return Array.from(new Set(mcpServers.map((server) => server.name)));
21267
+ return Array.from(new Set(mcpServers.map((server) => sanitizeMcpServerName(server.name))));
20748
21268
  }
20749
21269
 
20750
21270
  // src/CodexAppServerClient.ts
@@ -20761,6 +21281,7 @@ var CodexAppServerClient = class {
20761
21281
  mcpServerStartupResolvers = [];
20762
21282
  pendingTurnCompletionResolvers = /* @__PURE__ */ new Map();
20763
21283
  turnCompletionCaptures = /* @__PURE__ */ new Map();
21284
+ staleTurnIds = /* @__PURE__ */ new Map();
20764
21285
  constructor(connection) {
20765
21286
  this.connection = connection;
20766
21287
  this.connection.onUnhandledNotification((data) => {
@@ -20777,12 +21298,26 @@ var CodexAppServerClient = class {
20777
21298
  if (isTurnCompletedNotification(serverNotification)) {
20778
21299
  this.recordTurnCompleted(serverNotification.params);
20779
21300
  }
21301
+ const routing = extractTurnRouting(serverNotification);
21302
+ const staleTurnNotification = this.isStaleTurn(routing.threadId, routing.turnId);
21303
+ if (staleTurnNotification) {
21304
+ if (isTurnCompletedNotification(serverNotification) && routing.threadId !== null && routing.turnId !== null) {
21305
+ this.clearStaleTurn(routing.threadId, routing.turnId);
21306
+ }
21307
+ for (const callback of this.codexEventHandlers) {
21308
+ callback({ eventType: "notification", ...serverNotification });
21309
+ }
21310
+ return;
21311
+ }
20780
21312
  this.notify(serverNotification);
20781
21313
  for (const callback of this.codexEventHandlers) {
20782
21314
  callback({ eventType: "notification", ...serverNotification });
20783
21315
  }
20784
21316
  });
20785
21317
  this.connection.onRequest(CommandExecutionApprovalRequest, async (params) => {
21318
+ if (this.isStaleTurn(params.threadId, params.turnId)) {
21319
+ return { decision: "cancel" };
21320
+ }
20786
21321
  const handler = this.approvalHandlers.get(params.threadId);
20787
21322
  if (!handler) {
20788
21323
  return { decision: "cancel" };
@@ -20790,6 +21325,9 @@ var CodexAppServerClient = class {
20790
21325
  return await handler.handleCommandExecution(params);
20791
21326
  });
20792
21327
  this.connection.onRequest(FileChangeApprovalRequest, async (params) => {
21328
+ if (this.isStaleTurn(params.threadId, params.turnId)) {
21329
+ return { decision: "cancel" };
21330
+ }
20793
21331
  const handler = this.approvalHandlers.get(params.threadId);
20794
21332
  if (!handler) {
20795
21333
  return { decision: "cancel" };
@@ -20797,6 +21335,9 @@ var CodexAppServerClient = class {
20797
21335
  return await handler.handleFileChange(params);
20798
21336
  });
20799
21337
  this.connection.onRequest(McpServerElicitationRequest, async (params) => {
21338
+ if (this.isStaleTurn(params.threadId, params.turnId)) {
21339
+ return { action: "cancel", content: null, _meta: null };
21340
+ }
20800
21341
  const handler = this.elicitationHandlers.get(params.threadId);
20801
21342
  if (!handler) {
20802
21343
  return { action: "cancel", content: null, _meta: null };
@@ -20810,19 +21351,25 @@ var CodexAppServerClient = class {
20810
21351
  onElicitationRequest(threadId, handler) {
20811
21352
  this.elicitationHandlers.set(threadId, handler);
20812
21353
  }
21354
+ clearThreadHandlers(threadId) {
21355
+ this.notificationHandlers.delete(threadId);
21356
+ this.approvalHandlers.delete(threadId);
21357
+ this.elicitationHandlers.delete(threadId);
21358
+ }
20813
21359
  async initialize(params) {
20814
21360
  return await this.sendRequest({ method: "initialize", params });
20815
21361
  }
20816
21362
  async turnStart(params) {
20817
21363
  return await this.sendRequest({ method: "turn/start", params });
20818
21364
  }
20819
- async runTurn(params) {
21365
+ async runTurn(params, onTurnStarted) {
20820
21366
  const capturedCompletions = [];
20821
21367
  const releaseCapture = this.captureTurnCompletions(params.threadId, (event) => {
20822
21368
  capturedCompletions.push(event);
20823
21369
  });
20824
21370
  try {
20825
21371
  const turnStarted = await this.turnStart(params);
21372
+ onTurnStarted?.(turnStarted.turn.id);
20826
21373
  const earlyCompletion = capturedCompletions.find((event) => event.turn.id === turnStarted.turn.id);
20827
21374
  releaseCapture();
20828
21375
  if (earlyCompletion) {
@@ -20836,6 +21383,11 @@ var CodexAppServerClient = class {
20836
21383
  async turnInterrupt(params) {
20837
21384
  return await this.sendRequest({ method: "turn/interrupt", params });
20838
21385
  }
21386
+ markTurnStale(threadId, turnId) {
21387
+ const threadStaleTurns = this.staleTurnIds.get(threadId) ?? /* @__PURE__ */ new Set();
21388
+ threadStaleTurns.add(turnId);
21389
+ this.staleTurnIds.set(threadId, threadStaleTurns);
21390
+ }
20839
21391
  async threadStart(params) {
20840
21392
  return await this.sendRequest({ method: "thread/start", params });
20841
21393
  }
@@ -20851,6 +21403,9 @@ var CodexAppServerClient = class {
20851
21403
  async threadRead(params) {
20852
21404
  return await this.sendRequest({ method: "thread/read", params });
20853
21405
  }
21406
+ async threadUnsubscribe(params) {
21407
+ return await this.sendRequest({ method: "thread/unsubscribe", params });
21408
+ }
20854
21409
  async listMcpServerStatus(params) {
20855
21410
  return await this.sendRequest({ method: "mcpServerStatus/list", params });
20856
21411
  }
@@ -20893,9 +21448,27 @@ var CodexAppServerClient = class {
20893
21448
  threadResolvers.set(turnId, resolve);
20894
21449
  });
20895
21450
  }
21451
+ resolveTurnInterrupted(threadId, turnId) {
21452
+ this.recordTurnCompleted({
21453
+ threadId,
21454
+ turn: {
21455
+ id: turnId,
21456
+ items: [],
21457
+ itemsView: "notLoaded",
21458
+ status: "interrupted",
21459
+ error: null,
21460
+ startedAt: null,
21461
+ completedAt: null,
21462
+ durationMs: null
21463
+ }
21464
+ });
21465
+ }
20896
21466
  async listModels(params) {
20897
21467
  return await this.sendRequest({ method: "model/list", params });
20898
21468
  }
21469
+ async skillsExtraRootsSet(params) {
21470
+ return await this.sendRequest({ method: "skills/extraRoots/set", params });
21471
+ }
20899
21472
  async listSkills(params) {
20900
21473
  return await this.sendRequest({ method: "skills/list", params });
20901
21474
  }
@@ -20943,6 +21516,22 @@ var CodexAppServerClient = class {
20943
21516
  capture(event);
20944
21517
  }
20945
21518
  }
21519
+ isStaleTurn(threadId, turnId) {
21520
+ if (threadId === null || turnId === null) {
21521
+ return false;
21522
+ }
21523
+ return this.staleTurnIds.get(threadId)?.has(turnId) ?? false;
21524
+ }
21525
+ clearStaleTurn(threadId, turnId) {
21526
+ const threadStaleTurns = this.staleTurnIds.get(threadId);
21527
+ if (!threadStaleTurns) {
21528
+ return;
21529
+ }
21530
+ threadStaleTurns.delete(turnId);
21531
+ if (threadStaleTurns.size === 0) {
21532
+ this.staleTurnIds.delete(threadId);
21533
+ }
21534
+ }
20946
21535
  getOrCreatePendingTurnCompletionResolvers(threadId) {
20947
21536
  const existing = this.pendingTurnCompletionResolvers.get(threadId);
20948
21537
  if (existing) {
@@ -21037,6 +21626,17 @@ function extractThreadId(notification) {
21037
21626
  }
21038
21627
  return null;
21039
21628
  }
21629
+ function extractTurnRouting(notification) {
21630
+ const params = notification.params;
21631
+ const threadId = extractThreadId(notification);
21632
+ if (params && typeof params.turnId === "string") {
21633
+ return { threadId, turnId: params.turnId };
21634
+ }
21635
+ if (params && typeof params.turn?.id === "string") {
21636
+ return { threadId, turnId: params.turn.id };
21637
+ }
21638
+ return { threadId, turnId: null };
21639
+ }
21040
21640
 
21041
21641
  // src/login.ts
21042
21642
  function parseArgs(args) {