@nyssance/codex-acp-v2 0.7.12 → 0.7.14

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,32 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.7.14 — 2026-10-16
4
+
5
+ ### Added
6
+
7
+ - Sub-agent activity carries `model` and `reasoningEffort` in `rawInput` and `_meta.codex.subagent`.
8
+
9
+ ### Changed
10
+
11
+ - Generated against Codex 0.163.0 (`@openai/codex` ^0.163.0); the version floor follows.
12
+ `generate-types` clears `src/app-server` first, so types Codex removed no longer linger.
13
+ - Command approvals offer exactly the `availableDecisions` Codex sends; a missing list, or one naming
14
+ an amendment Codex did not propose, is cancelled. The adapter no longer builds its own decision set.
15
+ - Review exits produce no update: Codex follows every exit with an assistant message carrying the
16
+ conclusion, and the adapter maps that message. Exit buffering and deduplication are removed.
17
+ - Fast mode availability reads `serviceTiers` only; the deprecated `additionalSpeedTiers` is ignored.
18
+ - `codexErrorInfo` values Codex has no name for classify as `unknown`.
19
+
20
+ ## 0.7.13 — 2026-10-13
21
+
22
+ ### Fixed
23
+
24
+ - Match gateway model capabilities by upstream model, including hidden native models,
25
+ and omit reasoning effort settings when the model declares no supported levels.
26
+ - Normalize unsupported reasoning efforts when selecting a model.
27
+ - Recognize local commands after leading file attachments and preserve attachments
28
+ when `/plan <task>` starts a turn.
29
+
3
30
  ## 0.7.12 — 2026-10-13
4
31
 
5
32
  ### Added
package/README.md CHANGED
@@ -59,7 +59,7 @@ See [`docs/protocol.md`](docs/protocol.md) for the exact wire contract.
59
59
 
60
60
  ## Requirements
61
61
 
62
- - Codex CLI **0.158.0 or newer** — the app-server types under `src/app-server` are generated
62
+ - Codex CLI **0.163.0 or newer** — the app-server types under `src/app-server` are generated
63
63
  from the `@openai/codex` release named in `optionalDependencies`, and the agent checks
64
64
  `codex --version` before starting the server; an older Codex fails at startup with an
65
65
  upgrade hint.
package/dist/index.js CHANGED
@@ -22168,7 +22168,7 @@ var package_default = {
22168
22168
  publishConfig: {
22169
22169
  access: "public"
22170
22170
  },
22171
- version: "0.7.12",
22171
+ version: "0.7.14",
22172
22172
  description: "Native ACP v2 agent for the OpenAI Codex app-server",
22173
22173
  packageManager: "bun@1.4.0",
22174
22174
  engines: {
@@ -22203,7 +22203,7 @@ var package_default = {
22203
22203
  "package:win-x64": "cd dist/bin && zip codex-acp-v2-x64-windows.zip codex-acp-v2-x64-windows.exe",
22204
22204
  "package:win-arm64": "cd dist/bin && zip codex-acp-v2-arm64-windows.zip codex-acp-v2-arm64-windows.exe",
22205
22205
  start: "bun src/index.ts",
22206
- "generate-types": "codex app-server generate-ts --experimental --out src/app-server",
22206
+ "generate-types": "rm -rf src/app-server && codex app-server generate-ts --experimental --out src/app-server",
22207
22207
  test: "vitest run --exclude src/__tests__/e2e.test.ts",
22208
22208
  "test:watch": "vitest",
22209
22209
  typecheck: "tsc --noEmit",
@@ -22241,7 +22241,7 @@ var package_default = {
22241
22241
  "vscode-jsonrpc": "^9.0.2"
22242
22242
  },
22243
22243
  optionalDependencies: {
22244
- "@openai/codex": "^0.159.2"
22244
+ "@openai/codex": "^0.163.0"
22245
22245
  }
22246
22246
  };
22247
22247
 
@@ -22385,6 +22385,7 @@ class ClientTools {
22385
22385
  }
22386
22386
 
22387
22387
  // src/agent/turnErrors.ts
22388
+ var CONNECTION_FAILURES = ["httpConnectionFailed", "responseStreamConnectionFailed", "responseStreamDisconnected", "responseTooManyFailedAttempts"];
22388
22389
  function classifyTurnError(info) {
22389
22390
  const failure2 = (category, retryable = false, stopReason = "error") => ({ category, retryable, stopReason });
22390
22391
  if (info === null)
@@ -22392,7 +22393,9 @@ function classifyTurnError(info) {
22392
22393
  if (typeof info === "object") {
22393
22394
  if ("activeTurnNotSteerable" in info)
22394
22395
  return failure2("turn_not_steerable");
22395
- const status = "httpConnectionFailed" in info ? info.httpConnectionFailed.httpStatusCode : ("responseStreamConnectionFailed" in info) ? info.responseStreamConnectionFailed.httpStatusCode : ("responseStreamDisconnected" in info) ? info.responseStreamDisconnected.httpStatusCode : info.responseTooManyFailedAttempts.httpStatusCode;
22396
+ const status = connectionStatus(info);
22397
+ if (status === undefined)
22398
+ return failure2("unknown");
22396
22399
  return failure2("connection", status === null || status === 408 || status === 429 || status >= 500);
22397
22400
  }
22398
22401
  switch (info) {
@@ -22426,6 +22429,14 @@ function classifyTurnError(info) {
22426
22429
  case "other":
22427
22430
  return failure2("unknown");
22428
22431
  }
22432
+ return failure2("unknown");
22433
+ }
22434
+ function connectionStatus(info) {
22435
+ for (const key of CONNECTION_FAILURES) {
22436
+ if (key in info)
22437
+ return info[key].httpStatusCode;
22438
+ }
22439
+ return;
22429
22440
  }
22430
22441
 
22431
22442
  // src/agent/CodexAgent.ts
@@ -23672,8 +23683,8 @@ function subAgentActivity(item, status, create) {
23672
23683
  toolCallId: item.id,
23673
23684
  ...create ? { name: ToolName.SubAgent, title: titles[item.kind], kind: "other" } : {},
23674
23685
  status,
23675
- rawInput: { agentThreadId: item.agentThreadId, agentPath: item.agentPath, activity: item.kind },
23676
- _meta: { codex: { subagent: { threadId: item.agentThreadId, path: item.agentPath, activity: item.kind } } }
23686
+ rawInput: { model: item.model, reasoningEffort: item.reasoningEffort, agentThreadId: item.agentThreadId, agentPath: item.agentPath, activity: item.kind },
23687
+ _meta: { codex: { subagent: { model: item.model, reasoningEffort: item.reasoningEffort, threadId: item.agentThreadId, path: item.agentPath, activity: item.kind } } }
23677
23688
  });
23678
23689
  }
23679
23690
  function collabToolCall(item, create) {
@@ -24263,23 +24274,14 @@ function itemSnapshot(item, command) {
24263
24274
  case "contextCompaction":
24264
24275
  return [compactionUpdate(item.id, "completed")];
24265
24276
  case "enteredReviewMode":
24277
+ case "exitedReviewMode":
24266
24278
  return [];
24267
- case "exitedReviewMode": {
24268
- const text = item.review.trim();
24269
- return text.length > 0 ? [{ sessionUpdate: "agent_message", messageId: item.id, content: [{ type: "text", text }] }] : [];
24270
- }
24271
24279
  case "hookPrompt":
24272
24280
  case "functionCallOutput":
24273
24281
  case "sleep":
24274
24282
  return [];
24275
24283
  }
24276
24284
  }
24277
- function isDuplicateReview(item, items) {
24278
- if (item.type !== "exitedReviewMode")
24279
- return false;
24280
- const text = item.review.trim();
24281
- return text.length > 0 && Array.from(items).some((candidate) => candidate.type === "agentMessage" && candidate.phase !== "commentary" && candidate.text.trim() === text);
24282
- }
24283
24285
 
24284
24286
  // src/bridge/turnMetadata.ts
24285
24287
  function withTurnId(update, turnId, startedAt) {
@@ -24321,8 +24323,6 @@ class EventBridge {
24321
24323
  turnPrompt = null;
24322
24324
  openToolCalls = new Set;
24323
24325
  announcedTools = new Set;
24324
- pendingReviews = new Map;
24325
- reviewMessages = new Map;
24326
24326
  messagePhases = new Map;
24327
24327
  promptMeta = new Map;
24328
24328
  reasoningWithDeltas = new Set;
@@ -24386,8 +24386,6 @@ class EventBridge {
24386
24386
  this.openToolCalls.clear();
24387
24387
  this.announcedTools.clear();
24388
24388
  this.messagePhases.clear();
24389
- this.reviewMessages.clear();
24390
- this.pendingReviews.clear();
24391
24389
  this.reasoningWithDeltas.clear();
24392
24390
  this.terminalItems.clear();
24393
24391
  this.imageGenerations.clear();
@@ -24410,8 +24408,6 @@ class EventBridge {
24410
24408
  for (const item of items) {
24411
24409
  if (item.type === "userMessage")
24412
24410
  this.unresolvedUserItems.add(item.id);
24413
- if (item.type === "agentMessage")
24414
- this.reviewMessages.set(item.id, item);
24415
24411
  if (!replay && itemSnapshot(item).some((update) => update.sessionUpdate === "tool_call_update")) {
24416
24412
  this.unannouncedItems.set(item.id, { item, turnId });
24417
24413
  }
@@ -24424,12 +24420,8 @@ class EventBridge {
24424
24420
  if (receipt)
24425
24421
  this.restoredCommands.set(item.id, receipt);
24426
24422
  this.unresolvedUserItems.delete(item.id);
24427
- if (!replay || isDuplicateReview(item, this.reviewMessages.values()))
24428
- continue;
24429
- if (item.type === "exitedReviewMode") {
24430
- this.pendingReviews.set(item.id, { item, turnId });
24423
+ if (!replay)
24431
24424
  continue;
24432
- }
24433
24425
  if (item.type === "commandExecution" && item.status === "inProgress" && usesTerminal(item))
24434
24426
  this.terminalItems.add(item.id);
24435
24427
  for (const update of itemSnapshot(item, receipt ?? undefined)) {
@@ -24549,7 +24541,6 @@ class EventBridge {
24549
24541
  }
24550
24542
  }
24551
24543
  async finishOpenToolCalls(status) {
24552
- await this.flushReviews();
24553
24544
  this.unannouncedItems.clear();
24554
24545
  const ids = [...this.openToolCalls];
24555
24546
  const terminals = [...this.terminalItems];
@@ -24574,18 +24565,6 @@ class EventBridge {
24574
24565
  }
24575
24566
  async flush() {
24576
24567
  await this.flushPlans();
24577
- await this.flushReviews();
24578
- }
24579
- async flushReviews() {
24580
- const pending = [...this.pendingReviews.values()];
24581
- this.pendingReviews.clear();
24582
- for (const { item, turnId } of pending) {
24583
- if (isDuplicateReview(item, this.reviewMessages.values()))
24584
- continue;
24585
- for (const update of itemSnapshot(item)) {
24586
- await this.client.update(this.withTiming(withTurnId(update, turnId, this.turnTime?.startedAt), turnId));
24587
- }
24588
- }
24589
24568
  }
24590
24569
  dispose() {
24591
24570
  this.beginTurn();
@@ -24709,6 +24688,8 @@ ${details}` : summary)];
24709
24688
  case "thread/settings/updated":
24710
24689
  case "thread/goal/updated":
24711
24690
  case "thread/goal/cleared":
24691
+ case "thread/readState/changed":
24692
+ case "thread/prediction/updated":
24712
24693
  case "thread/project/updated":
24713
24694
  case "thread/environment/connected":
24714
24695
  case "thread/environment/disconnected":
@@ -24825,7 +24806,6 @@ ${details}` : summary)];
24825
24806
  async itemCompleted(item, turnId) {
24826
24807
  switch (item.type) {
24827
24808
  case "agentMessage":
24828
- this.reviewMessages.set(item.id, item);
24829
24809
  this.messagePhases.delete(item.id);
24830
24810
  return itemSnapshot(item);
24831
24811
  case "reasoning": {
@@ -24864,15 +24844,12 @@ ${details}` : summary)];
24864
24844
  return [compactionUpdate(item.id, "completed")];
24865
24845
  case "plan":
24866
24846
  return await this.planCompleted(item);
24867
- case "exitedReviewMode": {
24868
- this.pendingReviews.set(item.id, { item, turnId });
24869
- return [];
24870
- }
24871
24847
  case "userMessage":
24872
24848
  case "hookPrompt":
24873
24849
  case "functionCallOutput":
24874
24850
  case "sleep":
24875
24851
  case "enteredReviewMode":
24852
+ case "exitedReviewMode":
24876
24853
  return [];
24877
24854
  }
24878
24855
  }
@@ -25224,6 +25201,7 @@ class AppServerClient {
25224
25201
  threadId,
25225
25202
  turn: {
25226
25203
  id: turnId,
25204
+ rootTurnId: null,
25227
25205
  items: [],
25228
25206
  itemsView: "notLoaded",
25229
25207
  status: "interrupted",
@@ -25237,7 +25215,7 @@ class AppServerClient {
25237
25215
  failTurn(threadId, turnId, error40) {
25238
25216
  this.recordTurnCompleted({
25239
25217
  threadId,
25240
- turn: { id: turnId, items: [], itemsView: "notLoaded", status: "failed", error: error40, startedAt: null, completedAt: null, durationMs: null }
25218
+ turn: { id: turnId, rootTurnId: null, items: [], itemsView: "notLoaded", status: "failed", error: error40, startedAt: null, completedAt: null, durationMs: null }
25241
25219
  });
25242
25220
  }
25243
25221
  recordTurnCompleted(event) {
@@ -25390,7 +25368,7 @@ class AppServerClient {
25390
25368
  const cursors = new Set;
25391
25369
  let cursor = null;
25392
25370
  do {
25393
- const page = await this.modelList({ cursor, limit: null });
25371
+ const page = await this.modelList({ cursor, limit: null, includeHidden: true });
25394
25372
  models.push(...page.data);
25395
25373
  cursor = page.nextCursor;
25396
25374
  if (cursor) {
@@ -25805,12 +25783,12 @@ class ProviderRouting {
25805
25783
  if (first.mode === "route") {
25806
25784
  if (first.models.length === 0)
25807
25785
  return codexCatalog;
25808
- return first.models.map((model, index) => gatewayModel(model, index === 0, codexCatalog[0], first.catalogEntries.get(model.id)));
25786
+ return first.models.map((model, index) => gatewayModel(model, index === 0, codexCatalog.find((entry) => entry.model === model.id), first.catalogEntries.get(model.id)));
25809
25787
  }
25810
25788
  const own = [];
25811
25789
  for (const gateway of this.gateways.values()) {
25812
25790
  for (const model of gateway.models)
25813
- own.push({ ...gatewayModel(model, false, codexCatalog[0], gateway.catalogEntries.get(model.id)), id: gatewayModelId(gateway.id, model.id) });
25791
+ own.push({ ...gatewayModel(model, false, codexCatalog.find((entry) => entry.model === model.id), gateway.catalogEntries.get(model.id)), id: gatewayModelId(gateway.id, model.id) });
25814
25792
  }
25815
25793
  return [...codexCatalog, ...own];
25816
25794
  }
@@ -25917,7 +25895,7 @@ function readCatalogFile(catalogPath) {
25917
25895
  return entries;
25918
25896
  }
25919
25897
  function gatewayModel(model, isDefault, template, entry) {
25920
- const efforts = entry && entry.efforts.length > 0 ? entry.efforts : template?.supportedReasoningEfforts ?? [];
25898
+ const efforts = entry ? entry.efforts : template?.supportedReasoningEfforts ?? [];
25921
25899
  const defaultEffort = entry?.defaultEffort ?? template?.defaultReasoningEffort ?? "medium";
25922
25900
  return {
25923
25901
  id: model.id,
@@ -25952,13 +25930,13 @@ var FAST_SERVICE_TIER = "fast";
25952
25930
  function resolveModelSelection(catalog, modelId, effort) {
25953
25931
  const listed = modelId === null ? undefined : catalog.find((model) => model.id === modelId);
25954
25932
  if (listed)
25955
- return { model: listed.id, effort: effort ?? listed.defaultReasoningEffort };
25933
+ return { model: listed.id, effort: coerceEffort(listed, effort ?? listed.defaultReasoningEffort) };
25956
25934
  if (modelId)
25957
25935
  return { model: modelId, effort: effort ?? DEFAULT_EFFORT };
25958
25936
  const fallback = catalog.find((model) => model.isDefault) ?? catalog[0];
25959
25937
  if (!fallback)
25960
25938
  throw new Error("Codex did not return any models");
25961
- return { model: fallback.id, effort: effort ?? fallback.defaultReasoningEffort };
25939
+ return { model: fallback.id, effort: coerceEffort(fallback, effort ?? fallback.defaultReasoningEffort) };
25962
25940
  }
25963
25941
  function findModel(catalog, modelId) {
25964
25942
  return catalog.find((model) => model.id === modelId);
@@ -25975,9 +25953,7 @@ function coerceEffort(model, effort) {
25975
25953
  return model?.defaultReasoningEffort ?? options[0].reasoningEffort;
25976
25954
  }
25977
25955
  function modelSupportsFast(model) {
25978
- if (!model)
25979
- return false;
25980
- return model.serviceTiers.some((tier) => tier.id === FAST_SERVICE_TIER) || model.additionalSpeedTiers.includes(FAST_SERVICE_TIER);
25956
+ return model?.serviceTiers.some((tier) => tier.id === FAST_SERVICE_TIER) ?? false;
25981
25957
  }
25982
25958
  function modelSupportsImages(model) {
25983
25959
  return model?.inputModalities.includes("image") ?? true;
@@ -26077,81 +26053,20 @@ function capitalize(value) {
26077
26053
 
26078
26054
  // src/permissions/commandDecisions.ts
26079
26055
  function availableCommandDecisions(params) {
26080
- if (params.availableDecisions === undefined || params.availableDecisions === null) {
26081
- return defaultDecisions(params);
26082
- }
26083
- if (!Array.isArray(params.availableDecisions) || params.availableDecisions.length === 0)
26056
+ const decisions = params.availableDecisions;
26057
+ if (!decisions || decisions.length === 0)
26084
26058
  return;
26085
- const decisions = [];
26086
- for (const candidate of params.availableDecisions) {
26087
- const decision = parseDecision(candidate, params);
26088
- if (!decision)
26089
- return;
26090
- decisions.push(decision);
26091
- }
26092
- return decisions;
26093
- }
26094
- function defaultDecisions(params) {
26095
- if (params.networkApprovalContext) {
26096
- const decisions2 = ["accept", "acceptForSession"];
26097
- for (const amendment of params.proposedNetworkPolicyAmendments ?? []) {
26098
- decisions2.push({ applyNetworkPolicyAmendment: { network_policy_amendment: amendment } });
26099
- }
26100
- decisions2.push("decline", "cancel");
26101
- return decisions2;
26102
- }
26103
- if (params.additionalPermissions)
26104
- return ["accept", "cancel"];
26105
- const decisions = ["accept", "acceptForSession"];
26106
- if (params.proposedExecpolicyAmendment && params.proposedExecpolicyAmendment.length > 0) {
26107
- decisions.push({ acceptWithExecpolicyAmendment: { execpolicy_amendment: params.proposedExecpolicyAmendment } });
26108
- }
26109
- decisions.push("decline", "cancel");
26110
- return decisions;
26059
+ return decisions.every((decision) => proposed(decision, params)) ? decisions : undefined;
26111
26060
  }
26112
- function parseDecision(candidate, params) {
26113
- if (candidate === "accept" || candidate === "acceptForSession" || candidate === "decline" || candidate === "cancel") {
26114
- return candidate;
26115
- }
26116
- if (!isRecord2(candidate))
26117
- return;
26118
- if ("acceptWithExecpolicyAmendment" in candidate) {
26119
- const value = candidate["acceptWithExecpolicyAmendment"];
26120
- if (!isRecord2(value))
26121
- return;
26122
- const amendment = value["execpolicy_amendment"];
26123
- if (!isStringArray(amendment) || amendment.length === 0)
26124
- return;
26125
- if (!sameStrings(amendment, params.proposedExecpolicyAmendment))
26126
- return;
26127
- return { acceptWithExecpolicyAmendment: { execpolicy_amendment: [...amendment] } };
26128
- }
26129
- if ("applyNetworkPolicyAmendment" in candidate) {
26130
- const value = candidate["applyNetworkPolicyAmendment"];
26131
- if (!isRecord2(value))
26132
- return;
26133
- const amendment = parseNetworkAmendment(value["network_policy_amendment"]);
26134
- if (!amendment || !params.networkApprovalContext)
26135
- return;
26136
- if (amendment.host !== params.networkApprovalContext.host)
26137
- return;
26138
- const proposed = params.proposedNetworkPolicyAmendments ?? [];
26139
- if (!proposed.some((entry) => entry.host === amendment.host && entry.action === amendment.action))
26140
- return;
26141
- return { applyNetworkPolicyAmendment: { network_policy_amendment: amendment } };
26061
+ function proposed(decision, params) {
26062
+ if (typeof decision === "string")
26063
+ return true;
26064
+ if ("acceptWithExecpolicyAmendment" in decision) {
26065
+ const amendment2 = decision.acceptWithExecpolicyAmendment.execpolicy_amendment;
26066
+ return amendment2.length > 0 && sameStrings(amendment2, params.proposedExecpolicyAmendment);
26142
26067
  }
26143
- return;
26144
- }
26145
- function parseNetworkAmendment(value) {
26146
- if (!isRecord2(value) || typeof value["host"] !== "string")
26147
- return;
26148
- const action = value["action"];
26149
- if (action !== "allow" && action !== "deny")
26150
- return;
26151
- return { host: value["host"], action };
26152
- }
26153
- function isStringArray(value) {
26154
- return Array.isArray(value) && value.every((entry) => typeof entry === "string");
26068
+ const amendment = decision.applyNetworkPolicyAmendment.network_policy_amendment;
26069
+ return amendment.host === params.networkApprovalContext?.host && (params.proposedNetworkPolicyAmendments ?? []).some((entry) => entry.host === amendment.host && entry.action === amendment.action);
26155
26070
  }
26156
26071
  function sameStrings(left, right) {
26157
26072
  return !!right && left.length === right.length && left.every((value, index) => value === right[index]);
@@ -26394,8 +26309,7 @@ class CodexApprovalHandler {
26394
26309
  this.signal = signal;
26395
26310
  }
26396
26311
  async handleCommandExecution(params) {
26397
- const approvalParams = params;
26398
- const decisions = commandDecisionOptions(approvalParams);
26312
+ const decisions = commandDecisionOptions(params);
26399
26313
  if (!decisions) {
26400
26314
  logger.log("cancelling command approval: no complete decision set", { itemId: params.itemId });
26401
26315
  return { decision: "cancel" };
@@ -26404,7 +26318,7 @@ class CodexApprovalHandler {
26404
26318
  const response = await this.client.requestPermission({
26405
26319
  title: params.networkApprovalContext ? NETWORK_TITLE : COMMAND_TITLE,
26406
26320
  description: nonBlank(params.reason),
26407
- subject: { type: "tool_call", toolCall: commandSubject(approvalParams) },
26321
+ subject: { type: "tool_call", toolCall: commandSubject(params) },
26408
26322
  options: decisions.map(({ option }) => option)
26409
26323
  }, this.signal());
26410
26324
  return { decision: selectedDecision(response, decisions) ?? "cancel" };
@@ -27814,7 +27728,7 @@ var BUILTIN_COMMANDS = [
27814
27728
  { name: "logout", description: "Sign out of the Codex account" }
27815
27729
  ];
27816
27730
  function parseCommand(prompt) {
27817
- const first = prompt[0];
27731
+ const first = prompt.find(ContentBlock.isText);
27818
27732
  if (!first || first.type !== "text")
27819
27733
  return null;
27820
27734
  const text2 = first.text.trim();
@@ -27899,7 +27813,7 @@ function statusMessage(session, rateLimits) {
27899
27813
  "**Session**",
27900
27814
  `**Session ID:** ${session.id}`,
27901
27815
  `**Working directory:** ${session.cwd}`,
27902
- `**Model:** ${session.model.model} (${session.model.effort})`,
27816
+ `**Model:** ${session.model.model}${supportedEfforts(findModel(session.catalog, session.model.model)).length > 0 ? ` (${session.model.effort})` : ""}`,
27903
27817
  `**Mode:** ${session.mode.name}`,
27904
27818
  `**Collaboration:** ${session.collaborationMode}`,
27905
27819
  `**Fast mode:** ${session.fastMode ? "on" : "off"}`,
@@ -27962,7 +27876,7 @@ function sessionConfigOptions(session) {
27962
27876
  const options = [
27963
27877
  modeConfigOption(session.mode),
27964
27878
  modelConfigOption(session.catalog, session.model.model, session.gatewayGroups),
27965
- effortConfigOption(supportedEfforts(model), session.model.effort),
27879
+ ...supportedEfforts(model).length > 0 ? [effortConfigOption(supportedEfforts(model), session.model.effort)] : [],
27966
27880
  collaborationModeConfigOption(session.collaborationMode)
27967
27881
  ];
27968
27882
  if (modelSupportsFast(model))
@@ -27990,7 +27904,7 @@ async function applyConfigOption(session, codex, request) {
27990
27904
  const effort = stringValue(request);
27991
27905
  const model = findModel(session.catalog, session.model.model);
27992
27906
  const efforts = supportedEfforts(model);
27993
- if (efforts.length > 0 && !efforts.some((option) => option.reasoningEffort === effort)) {
27907
+ if (!efforts.some((option) => option.reasoningEffort === effort)) {
27994
27908
  throw RequestError.invalidParams({ configId: request.configId }, `Model ${session.model.model} does not support effort "${effort}"`);
27995
27909
  }
27996
27910
  session.model = { ...session.model, effort };
@@ -28014,7 +27928,7 @@ async function applyConfigOption(session, codex, request) {
28014
27928
  threadId: session.id,
28015
27929
  collaborationMode: {
28016
27930
  mode,
28017
- settings: { model: findModel(session.catalog, session.model.model)?.model ?? session.model.model, reasoning_effort: session.model.effort, developer_instructions: null }
27931
+ settings: { model: findModel(session.catalog, session.model.model)?.model ?? session.model.model, reasoning_effort: supportedEfforts(findModel(session.catalog, session.model.model)).length > 0 ? session.model.effort : null, developer_instructions: null }
28018
27932
  }
28019
27933
  });
28020
27934
  session.collaborationMode = mode;
@@ -28037,8 +27951,6 @@ function historyUpdates(turns, commandReceipts = new Map) {
28037
27951
  for (const turn of turns) {
28038
27952
  const openingUser = turn.items.find((item) => item.type === "userMessage");
28039
27953
  for (const item of turn.items) {
28040
- if (isDuplicateReview(item, turn.items))
28041
- continue;
28042
27954
  const command = item.type === "userMessage" ? commandReceipts.get(item.id) : undefined;
28043
27955
  updates.push(...itemSnapshot(item, command).map((update) => {
28044
27956
  const replay = withTurnId(update, turn.id, turn.startedAt);
@@ -28302,7 +28214,7 @@ async function readSessionHistory(codex, raw, signal, commandReceipts) {
28302
28214
  if (view === "full")
28303
28215
  await read.call(() => assertCommandHistory(codex, start.thread, commandReceipts, signal));
28304
28216
  const turns = await read.call(() => Promise.all(page.data.map(async (turn) => {
28305
- const projections = view === "full" ? await Promise.all(turn.items.filter((item) => !isDuplicateReview(item, turn.items)).map(async (item) => projectHistoryItem(item, turn.id, turn.startedAt, item.type === "userMessage" ? await commandReceipts?.read(item.id) : null))) : [];
28217
+ const projections = view === "full" ? await Promise.all(turn.items.map(async (item) => projectHistoryItem(item, turn.id, turn.startedAt, item.type === "userMessage" ? await commandReceipts?.read(item.id) : null))) : [];
28306
28218
  return {
28307
28219
  turnId: turn.id,
28308
28220
  status: turn.status,
@@ -29664,7 +29576,9 @@ class CodexAgent {
29664
29576
  case "config":
29665
29577
  await this.setSessionConfigOption({ sessionId: session.id, configId: outcome.configId, type: "id", value: outcome.value });
29666
29578
  if (outcome.promptText) {
29667
- completed = await this.runCodexTurn(runtime, turn, [{ type: "text", text: outcome.promptText }, ...params.prompt.slice(1)]);
29579
+ const promptText = outcome.promptText;
29580
+ const commandText = params.prompt.find(ContentBlock.isText);
29581
+ completed = await this.runCodexTurn(runtime, turn, params.prompt.map((block) => block === commandText ? { ...block, type: "text", text: promptText } : block));
29668
29582
  } else {
29669
29583
  await client.update(agentMessage(`command:${turn.clientUserMessageId}`, `Plan mode ${outcome.value === PLAN_COLLABORATION_MODE ? "enabled" : "disabled"}.`));
29670
29584
  }
@@ -29717,7 +29631,7 @@ class CodexAgent {
29717
29631
  approvalsReviewer: session.mode.approvalsReviewer,
29718
29632
  sandboxPolicy: withWritableRoots(session.mode.sandboxPolicy, session.additionalDirectories),
29719
29633
  model: this.providers.upstreamModel(session.model.model),
29720
- effort: session.model.effort,
29634
+ effort: supportedEfforts(model).length > 0 ? session.model.effort : null,
29721
29635
  summary: disableSummary ? "none" : "auto",
29722
29636
  serviceTier: session.fastMode ? FAST_SERVICE_TIER : null
29723
29637
  }, (turnId) => {
@@ -30089,7 +30003,7 @@ function usageOf(session) {
30089
30003
  function interruptedTurn(threadId) {
30090
30004
  return {
30091
30005
  threadId,
30092
- turn: { id: "", items: [], itemsView: "notLoaded", status: "interrupted", error: null, startedAt: null, completedAt: null, durationMs: null }
30006
+ turn: { id: "", rootTurnId: null, items: [], itemsView: "notLoaded", status: "interrupted", error: null, startedAt: null, completedAt: null, durationMs: null }
30093
30007
  };
30094
30008
  }
30095
30009
  function parseSessionListParams(raw) {
package/docs/protocol.md CHANGED
@@ -309,7 +309,7 @@ perform the separate reversible `_codex/session_archive` operation.
309
309
  | --- | --- | --- | --- |
310
310
  | `mode` | `mode` | select | `read-only`, `agent`, `agent-full-access` |
311
311
  | `model` | `model` | select | Codex catalog ids (the current id is always listed) |
312
- | `effort` | `thought_level` | select | efforts supported by the current model |
312
+ | `effort` | `thought_level` | select | efforts supported by the current model; omitted when no levels are declared |
313
313
  | `collaboration_mode` | `model_config` | select | `default`, `plan` |
314
314
  | `fast_mode` | `model_config` | boolean | only when the model offers the `fast` service tier |
315
315
 
@@ -490,7 +490,7 @@ to status `cancelled`, and a cancelled turn marks every still-open tool call
490
490
  | `web_search` | `fetch` | web search |
491
491
  | `view_image` | `read` | image view |
492
492
  | `image_generation` | `other` | image generation; result as image content |
493
- | `subagent`, `collab` | `other` | sub-agent activity and collaboration calls; `_meta.codex.subagent` / `_meta.codex.collaboration` |
493
+ | `subagent`, `collab` | `other` | sub-agent activity and collaboration calls; `_meta.codex.subagent` (`model`, `reasoningEffort`, `threadId`, `path`, `activity`) / `_meta.codex.collaboration` |
494
494
  | `fuzzy_file_search` | `search` | Codex fuzzy file search sessions |
495
495
  | `guardian_review` | `think` | auto-approval reviews |
496
496
  | `mcp_startup` | `other` | failed MCP server startups (status `failed`) |
@@ -505,7 +505,7 @@ unknown ids, and transport errors fail closed.
505
505
 
506
506
  | Title | Options (`optionId` → Codex decision) |
507
507
  | --- | --- |
508
- | `Run command?` / `Allow network access?` | `allow_once` → accept, `allow_for_session` → acceptForSession, `accept_execpolicy_amendment` → acceptWithExecpolicyAmendment, `apply_network_policy_amendment:<n>` → applyNetworkPolicyAmendment, `decline`, `cancel`. When Codex sends `availableDecisions` that list is authoritative. |
508
+ | `Run command?` / `Allow network access?` | `allow_once` → accept, `allow_for_session` → acceptForSession, `accept_execpolicy_amendment` → acceptWithExecpolicyAmendment, `apply_network_policy_amendment:<n>` → applyNetworkPolicyAmendment, `decline`, `cancel`. Codex always sends `availableDecisions` and that list is authoritative; a missing list, or one naming an amendment Codex did not propose, fails closed with `cancel`. |
509
509
  | `Make edits?` | `allow_once` → accept, `allow_for_session` → acceptForSession, `cancel` |
510
510
  | `Grant permissions?` | `allow_permissions_turn`, `allow_permissions_turn_strict_auto_review`, `allow_permissions_session`, `reject_permissions` |
511
511
  | MCP approvals | `allow_once` / `accept`, `allow_session`, `allow_always` (only when Codex advertises `persist`), `decline`, `cancel` |
@@ -777,11 +777,9 @@ also contains ordered `{text, question?}` display parts. Receipt-adopting client
777
777
  that retain optimistic prompt content use these parts for display and copying;
778
778
  this does not alter the original input sent to Codex.
779
779
 
780
- Review results: when `exitedReviewMode.review` repeats a non-commentary agent message
781
- in the same turn, live updates and full-turn history omit the duplicate exit message.
782
- Review exits are buffered until the terminal drain to cover either native event order.
783
- A standalone or different review result remains visible. Item-level history retains
784
- its per-item projection because the matching answer may be outside the requested page.
780
+ Review results: Codex follows every `exitedReviewMode` item with an assistant message
781
+ carrying the same conclusion, including the notice for an interrupted review. The adapter
782
+ maps only that message; review entry and exit markers produce no updates, live or in history.
785
783
 
786
784
  ### Plugin enablement
787
785
 
package/package.json CHANGED
@@ -3,7 +3,7 @@
3
3
  "publishConfig": {
4
4
  "access": "public"
5
5
  },
6
- "version": "0.7.12",
6
+ "version": "0.7.14",
7
7
  "description": "Native ACP v2 agent for the OpenAI Codex app-server",
8
8
  "packageManager": "bun@1.4.0",
9
9
  "engines": {
@@ -38,7 +38,7 @@
38
38
  "package:win-x64": "cd dist/bin && zip codex-acp-v2-x64-windows.zip codex-acp-v2-x64-windows.exe",
39
39
  "package:win-arm64": "cd dist/bin && zip codex-acp-v2-arm64-windows.zip codex-acp-v2-arm64-windows.exe",
40
40
  "start": "bun src/index.ts",
41
- "generate-types": "codex app-server generate-ts --experimental --out src/app-server",
41
+ "generate-types": "rm -rf src/app-server && codex app-server generate-ts --experimental --out src/app-server",
42
42
  "test": "vitest run --exclude src/__tests__/e2e.test.ts",
43
43
  "test:watch": "vitest",
44
44
  "typecheck": "tsc --noEmit",
@@ -76,6 +76,6 @@
76
76
  "vscode-jsonrpc": "^9.0.2"
77
77
  },
78
78
  "optionalDependencies": {
79
- "@openai/codex": "^0.159.2"
79
+ "@openai/codex": "^0.163.0"
80
80
  }
81
81
  }