@bitkyc08/opencodex 2.81.0 → 2.82.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (212) hide show
  1. package/bin/ocx.mjs +166 -73
  2. package/gui/dist/assets/App-Ds_qL7o8.js +54 -0
  3. package/gui/dist/assets/{Tray-EwAOsP0S.js → Tray-BqHF5Trd.js} +1 -1
  4. package/gui/dist/assets/{index-Brk6F3fn.css → index-BAbcj51J.css} +1 -1
  5. package/gui/dist/assets/{index-RsoaAxXY.js → index-D3FBCNiP.js} +13 -13
  6. package/gui/dist/assets/{quota-summary-D2lN8tDm.js → quota-summary-DTBC8n7-.js} +1 -1
  7. package/gui/dist/index.html +2 -2
  8. package/package.json +1 -1
  9. package/src/adapters/anthropic.ts +1 -1
  10. package/src/adapters/openai-responses/compaction-search-history.ts +109 -0
  11. package/src/adapters/openai-responses/passthrough.ts +9 -4
  12. package/src/adapters/openai-responses/tool-output-recovery.ts +20 -0
  13. package/src/claude/agents-inject.ts +14 -3
  14. package/src/claude/context-windows.ts +19 -17
  15. package/src/claude/desktop-3p.ts +88 -12
  16. package/src/claude/desktop-profile.ts +2 -1
  17. package/src/claude/gateway-cache.ts +18 -1
  18. package/src/claude/inbound.ts +77 -3
  19. package/src/claude/intercept/cli-catalog.ts +3 -0
  20. package/src/claude/intercept/picker-ca-persistence.ts +7 -6
  21. package/src/claude/intercept/picker-models.ts +14 -6
  22. package/src/claude/intercept/settings.ts +3 -0
  23. package/src/claude/long-context.ts +66 -0
  24. package/src/claude/model-info.ts +19 -16
  25. package/src/claude/outbound.ts +94 -9
  26. package/src/claude/subagent-model.ts +40 -11
  27. package/src/cli/agent.ts +9 -5
  28. package/src/cli/aside-profile-recovery.ts +33 -0
  29. package/src/cli/capabilities-base.ts +5 -5
  30. package/src/cli/capabilities-integrations.ts +2 -2
  31. package/src/cli/cli-path-diagnostics.ts +132 -0
  32. package/src/cli/desktop-runtime-guidance.ts +65 -0
  33. package/src/cli/dispatch.ts +5 -0
  34. package/src/cli/doctor.ts +16 -3
  35. package/src/cli/index.ts +14 -9
  36. package/src/cli/integration-aside-sync.ts +2 -1
  37. package/src/cli/integrations.ts +16 -3
  38. package/src/cli/resolve.ts +25 -2
  39. package/src/cli/root.ts +4 -2
  40. package/src/cli/status.ts +64 -12
  41. package/src/cli/update-restart-child.ts +4 -2
  42. package/src/cli/update-restart-home.ts +28 -10
  43. package/src/cli/update-restart-service-record.ts +116 -0
  44. package/src/cli/update-restart-supervision.ts +96 -0
  45. package/src/cli/update-restart.ts +59 -13
  46. package/src/cli/v2.ts +27 -20
  47. package/src/cli/version-skew-notice.ts +77 -0
  48. package/src/clients/lazycodex.ts +1 -1
  49. package/src/clients/omo-role-models.ts +32 -7
  50. package/src/codex/auth-api/account-selection.ts +107 -0
  51. package/src/codex/auth-api/routes.ts +10 -49
  52. package/src/codex/auth-context.ts +20 -1
  53. package/src/codex/autostart-health.ts +12 -3
  54. package/src/codex/catalog/build-entries.ts +18 -0
  55. package/src/codex/catalog/bundled.ts +155 -26
  56. package/src/codex/catalog/combo-member.ts +3 -2
  57. package/src/codex/catalog/effort.ts +26 -5
  58. package/src/codex/catalog/gather-capture.ts +5 -0
  59. package/src/codex/catalog/model-hints.ts +15 -8
  60. package/src/codex/catalog/model-visibility.ts +3 -1
  61. package/src/codex/catalog/operator-tiers.ts +38 -0
  62. package/src/codex/catalog/provider-models.ts +43 -35
  63. package/src/codex/catalog/retained-sync.ts +7 -5
  64. package/src/codex/catalog/routed-gather.ts +15 -1
  65. package/src/codex/catalog-auto-refresh-sources.ts +3 -4
  66. package/src/codex/catalog-auto-refresh.ts +19 -0
  67. package/src/codex/catalog.ts +1 -1
  68. package/src/codex/codex-home-owner.ts +3 -0
  69. package/src/codex/config-write-lock.ts +333 -0
  70. package/src/codex/convergence.ts +2 -1
  71. package/src/codex/features.ts +257 -200
  72. package/src/codex/inject/config-write-section.ts +34 -0
  73. package/src/codex/inject/multi-agent-v2.ts +14 -6
  74. package/src/codex/inject/remove.ts +36 -11
  75. package/src/codex/inject/restore.ts +85 -20
  76. package/src/codex/inject-coordination.ts +15 -6
  77. package/src/codex/inject.ts +62 -20
  78. package/src/codex/journal.ts +62 -17
  79. package/src/codex/main-account-cache.ts +43 -0
  80. package/src/codex/paths.ts +2 -0
  81. package/src/codex/prepared-home.ts +18 -0
  82. package/src/codex/prompt-journal.ts +25 -12
  83. package/src/codex/prompt-layers.ts +49 -26
  84. package/src/codex/prompt-lock-claim.ts +134 -0
  85. package/src/codex/prompt-lock-evidence.ts +26 -0
  86. package/src/codex/prompt-lock-io.ts +16 -0
  87. package/src/codex/prompt-lock-owner.ts +130 -0
  88. package/src/codex/prompt-lock.ts +84 -53
  89. package/src/codex/routing/failure-window.ts +146 -0
  90. package/src/codex/routing/selection.ts +4 -1
  91. package/src/codex/routing.ts +13 -17
  92. package/src/codex/runtime.ts +106 -18
  93. package/src/codex/shim.ts +45 -19
  94. package/src/combos/jev-service-exchange.ts +281 -0
  95. package/src/combos/jev.ts +34 -220
  96. package/src/config/atomic-write.ts +1 -1
  97. package/src/config/codex-ws-reuse-setting.ts +10 -0
  98. package/src/config/live-reconcile.ts +17 -0
  99. package/src/config/load-degrade.ts +8 -0
  100. package/src/config/rebase-provenance.ts +11 -0
  101. package/src/config/schema/config-schema.ts +4 -0
  102. package/src/generated/compatibility-version.json +303 -171
  103. package/src/generated/model-metadata.ts +2 -2
  104. package/src/grok/inject.ts +50 -4
  105. package/src/grok/sync.ts +2 -1
  106. package/src/images/loop.ts +29 -11
  107. package/src/integrations/aside-profiles.ts +10 -3
  108. package/src/integrations/catalog-refresh.ts +18 -2
  109. package/src/integrations/writer.ts +9 -3
  110. package/src/lib/bun-path-runtime.d.mts +32 -0
  111. package/src/lib/bun-path-runtime.mjs +95 -0
  112. package/src/lib/bun-runtime-preflight.ts +140 -0
  113. package/src/lib/desktop-cli-handoff.d.mts +20 -0
  114. package/src/lib/desktop-cli-handoff.mjs +92 -0
  115. package/src/lib/desktop-cli-record.d.mts +29 -0
  116. package/src/lib/desktop-cli-record.mjs +102 -0
  117. package/src/lib/request-execution-budget.ts +8 -0
  118. package/src/lib/synced-state-location.ts +46 -6
  119. package/src/lib/test-home-guard.ts +87 -1
  120. package/src/lib/translator-budget.ts +17 -4
  121. package/src/lib/windows-atomic-replace.ts +38 -16
  122. package/src/lib/windows-elevation.ts +6 -0
  123. package/src/lib/windows-secret-acl.ts +92 -25
  124. package/src/lib/winsw.ts +2 -3
  125. package/src/oauth/anthropic-account-refusal.ts +39 -1
  126. package/src/oauth/anthropic-routing.ts +1 -1
  127. package/src/oauth/anthropic-send-ownership.ts +3 -3
  128. package/src/oauth/chatgpt.ts +113 -41
  129. package/src/oauth/index.ts +6 -2
  130. package/src/oauth/store.ts +1 -1
  131. package/src/providers/azure-model-metadata.ts +115 -0
  132. package/src/providers/derive.ts +21 -0
  133. package/src/providers/quota/vendor-probes-key.ts +127 -17
  134. package/src/providers/quota.ts +1 -1
  135. package/src/responses/reasoning-envelope.ts +29 -1
  136. package/src/responses/reasoning-replay-cache.ts +17 -1
  137. package/src/responses/spill-store.ts +15 -2
  138. package/src/responses/state/metrics.ts +8 -0
  139. package/src/responses/state/spill-failure.ts +12 -0
  140. package/src/responses/state/spill-queue.ts +12 -5
  141. package/src/responses/state.ts +18 -10
  142. package/src/responses/task-input.ts +17 -0
  143. package/src/server/background-lifecycle.ts +27 -0
  144. package/src/server/chat-completions.ts +1 -0
  145. package/src/server/chat-native.ts +28 -12
  146. package/src/server/claude-messages.ts +43 -6
  147. package/src/server/inbound-body-admission.ts +1 -0
  148. package/src/server/index/claude-intercept-lifecycle.ts +3 -1
  149. package/src/server/index/serve-options.ts +10 -2
  150. package/src/server/index/spend-ledger-lifecycle.ts +2 -1
  151. package/src/server/index/startup-warnings.ts +19 -0
  152. package/src/server/index.ts +1 -0
  153. package/src/server/management/agent-settings-routes.ts +93 -32
  154. package/src/server/management/codex-agent-role-routes.ts +4 -2
  155. package/src/server/management/codex-prompt-routes.ts +1 -0
  156. package/src/server/management/codex-role-auto-assign.ts +13 -2
  157. package/src/server/management/config-routes.ts +59 -8
  158. package/src/server/management/context.ts +2 -2
  159. package/src/server/management/shared.ts +3 -3
  160. package/src/server/messages-native-oauth.ts +9 -8
  161. package/src/server/messages-native.ts +49 -13
  162. package/src/server/messages-response-headers.ts +14 -0
  163. package/src/server/request-log-conversation.ts +48 -5
  164. package/src/server/request-log.ts +31 -2
  165. package/src/server/responses/adapter-continuation.ts +2 -2
  166. package/src/server/responses/adapter-dispatch.ts +34 -13
  167. package/src/server/responses/codex-ws-exchange.ts +17 -2
  168. package/src/server/responses/codex-ws-pool.ts +100 -13
  169. package/src/server/responses/codex-ws-wire.ts +9 -0
  170. package/src/server/responses/combo-requested-effort.ts +3 -1
  171. package/src/server/responses/core-codex-account.ts +22 -1
  172. package/src/server/responses/core-combo-native.ts +20 -3
  173. package/src/server/responses/core-combo.ts +22 -14
  174. package/src/server/responses/core-lifetime.ts +3 -36
  175. package/src/server/responses/core-options.ts +5 -0
  176. package/src/server/responses/core-replay.ts +73 -0
  177. package/src/server/responses/core.ts +3 -3
  178. package/src/server/responses/fetch-helpers.ts +26 -3
  179. package/src/server/responses/passthrough-delivery.ts +24 -2
  180. package/src/server/responses/passthrough-dispatch.ts +19 -2
  181. package/src/server/responses/request-prepare.ts +4 -0
  182. package/src/server/responses/request-send-budget.ts +112 -49
  183. package/src/server/responses/response-effects.ts +1 -0
  184. package/src/server/responses/run-turn-execution.ts +2 -0
  185. package/src/server/responses/sidecar-execution.ts +25 -16
  186. package/src/server/responses/sidecar-send-budget.ts +58 -0
  187. package/src/server/responses/ws-upstream.ts +19 -6
  188. package/src/server/startup-health-cache.ts +2 -1
  189. package/src/server/workflow-refusal.ts +3 -0
  190. package/src/service/cli.ts +20 -4
  191. package/src/service/desktop-command-guard.ts +32 -0
  192. package/src/service/desktop-startup.ts +66 -40
  193. package/src/service/desktop-supervision.d.mts +26 -0
  194. package/src/service/desktop-supervision.mjs +140 -0
  195. package/src/service/managing-cli.ts +1 -0
  196. package/src/service/orchestration.ts +46 -20
  197. package/src/service/repair.ts +25 -9
  198. package/src/service/state.ts +4 -3
  199. package/src/service/systemd.ts +17 -8
  200. package/src/service/windows-ops.ts +61 -22
  201. package/src/storage/policy-job.ts +5 -1
  202. package/src/storage/policy-worker.ts +3 -1
  203. package/src/storage/policy.ts +9 -0
  204. package/src/types/config.ts +21 -0
  205. package/src/types/request.ts +5 -0
  206. package/src/types/wire.ts +1 -0
  207. package/src/update/index.ts +57 -9
  208. package/src/update/restart-ownership.ts +10 -3
  209. package/src/update/runtime-ownership.d.mts +6 -0
  210. package/src/update/runtime-ownership.mjs +23 -7
  211. package/src/web-search/loop.ts +5 -2
  212. package/gui/dist/assets/App-7yhoys8a.js +0 -54
@@ -31,7 +31,7 @@ import { recoverStaleResponseStateTemps } from "./state/temp-recovery";
31
31
  export type { ResponseSpillWriteFailureCode, ResponseSpillWriteStatus, ResponseSpillWriteFailureOrigin } from "./state/spill-failure";
32
32
  import type { ResponseSpillWriteFailureCode, ResponseSpillWriteStatus, ResponseSpillWriteFailureOrigin } from "./state/spill-failure";
33
33
  export { responseAdmissionCountersForTests } from "./state/spill-failure";
34
- import { admissionCounters, noteSpillWriteFailure, noteSpillWriteSuccess, spillCounters, spillWriteHealth } from "./state/spill-failure";
34
+ import { admissionCounters, noteSpillWriteFailure, noteSpillWriteSuccess, resetSpillCountersForTests, spillCounters, spillWriteHealth } from "./state/spill-failure";
35
35
  import { loadSnapshotEntry } from "./state/snapshot-codec";
36
36
  import { isBodyNonPersistable } from "./state/body-policy";
37
37
  export { isBodyNonPersistable, markBodyNonPersistable } from "./state/body-policy";
@@ -876,20 +876,29 @@ export function replayOverlapSkipsForTests(): number {
876
876
  * This ceiling therefore bounds what the store owns, which is every file it can
877
877
  * account for, and not the directory as a whole.
878
878
  */
879
- function enforceSpilledResponseBudget(): number {
879
+ function enforceSpilledResponseBudget(requiredHeadroomBytes?: number): number {
880
880
  // Price in-flight publications too: a file being created by
881
881
  // `writeResponseSpillDurablyAsync` occupies the volume before it reaches `states`.
882
882
  let spilledBytes = accountedResponseSpillBytes();
883
- if (spilledBytes <= spillByteCap()) return 0;
883
+ // Admission (#6747) passes the room its publication needs, 0 included. Evict only while that
884
+ // room is still reachable: re-read accounting after each attempt, since a failed unlink stays
885
+ // charged, and stop the moment the publication cannot fit so no unrelated continuation pays.
886
+ const admission = requiredHeadroomBytes !== undefined;
887
+ const headroom = admission && requiredHeadroomBytes > 0 ? requiredHeadroomBytes : 0;
888
+ const feasible = (): boolean => !admission
889
+ || (spilledBytes = accountedResponseSpillBytes()) - spilledResponseBytes() + headroom <= spillByteCap();
890
+ if (!feasible() || spilledBytes <= spillByteCap() - headroom) return 0;
884
891
  const before = spilledBytes;
885
892
  // Deferred generations go first. They are already superseded, so releasing one
886
893
  // costs only the crash window the queue exists to cover — the same trade
887
894
  // PENDING_SPILL_UNLINKS_MAX already makes against unbounded disk. Evicting a
888
895
  // live continuation to make room for a dead file would be the wrong order.
889
- while (spilledBytes > spillByteCap() && pendingSpillUnlinks.length > 0) {
896
+ while (spilledBytes > spillByteCap() - headroom && pendingSpillUnlinks.length > 0) {
890
897
  const ref = pendingSpillUnlinks.shift()!;
891
898
  spilledBytes -= ref.payloadBytes;
892
899
  deleteResponseSpill(ref);
900
+ if (admission) spillCounters.headroomEvictions += 1;
901
+ if (!feasible()) return before - spilledBytes;
893
902
  }
894
903
  // Ordered by createdAt, not by map order. `states` is not an age index:
895
904
  // demotion and spill replacement delete and reinsert entries, and
@@ -906,9 +915,11 @@ function enforceSpilledResponseBudget(): number {
906
915
  .sort((a, b) => a[1].createdAt - b[1].createdAt
907
916
  || (a[0] < b[0] ? -1 : a[0] > b[0] ? 1 : 0));
908
917
  for (const [id, entry] of spilled) {
909
- if (spilledBytes <= spillByteCap()) break;
918
+ if (spilledBytes <= spillByteCap() - headroom) break;
910
919
  spilledBytes -= entry.spill.payloadBytes;
911
920
  deleteEntry(id);
921
+ if (admission) spillCounters.headroomEvictions += 1;
922
+ if (!feasible()) return before - spilledBytes;
912
923
  }
913
924
  return before - spilledBytes;
914
925
  }
@@ -1210,6 +1221,7 @@ export function responseStateMetrics(): ResponseStateMetrics {
1210
1221
  spillLastSuccessAt: spillWriteHealth.lastSuccessAt,
1211
1222
  spillAclRetryReturnedTimeouts: spillCounters.aclRetryReturnedTimeouts,
1212
1223
  spillAclTimeoutMemoRefusals: spillCounters.aclTimeoutMemoRefusals,
1224
+ spillCapacityRefusals: spillCounters.capacityRefusals, spillHeadroomEvictions: spillCounters.headroomEvictions,
1213
1225
  spillLastFailureAt: spillWriteHealth.lastFailureAt,
1214
1226
  spillReadFailures: spillCounters.readFailures,
1215
1227
  replayScopeMismatchDrops,
@@ -1321,11 +1333,7 @@ export function clearResponseStateMemoryForTests(): void {
1321
1333
  oldestResidentAt = null;
1322
1334
  stateRevision = 0;
1323
1335
  pendingSpillUnlinks.length = 0;
1324
- spillCounters.writes = 0;
1325
- spillCounters.writeFailures = 0;
1326
- spillCounters.readFailures = 0;
1327
- spillCounters.aclRetryReturnedTimeouts = 0;
1328
- spillCounters.aclTimeoutMemoRefusals = 0;
1336
+ resetSpillCountersForTests();
1329
1337
  spillWriteHealth.consecutiveFailures = 0;
1330
1338
  spillWriteHealth.lastFailureCode = null;
1331
1339
  spillWriteHealth.lastFailureOrigin = null;
@@ -54,3 +54,20 @@ export function externalTaskInputContent(item: unknown): string | OcxContentPart
54
54
  block.type === "output_text" ? { ...block, type: "input_text" } : block,
55
55
  ));
56
56
  }
57
+
58
+ /**
59
+ * The same recognition, emitted as Responses-wire user content for the raw-body repairs (#6764).
60
+ *
61
+ * The passthrough and routed-compaction paths never read the parsed context, so they used to
62
+ * send this envelope through the generic orphan repair, which prefixes
63
+ * "[tool output for unknown call]". A summarizer then read an authorized handover as a stray tool
64
+ * result. Recognition is shared with the parser so both paths agree on what is task input.
65
+ */
66
+ export function externalTaskInputResponsesContent(item: unknown): Record<string, unknown>[] | undefined {
67
+ if (externalTaskInputContent(item) === undefined) return undefined;
68
+ const output = (item as { output: string | TaskInputBlock[] }).output;
69
+ if (typeof output === "string") return [{ type: "input_text", text: output }];
70
+ return output.map(block => block.type === "input_image"
71
+ ? { type: "input_image", image_url: block.image_url, ...(block.detail ? { detail: block.detail === "original" ? "high" : block.detail } : {}) }
72
+ : { type: "input_text", text: block.text });
73
+ }
@@ -14,6 +14,7 @@ import {
14
14
  } from "../storage/policy-scheduler";
15
15
  import { startQuotaResetPoller, stopQuotaResetPoller } from "../quota/reset-poller";
16
16
  import {
17
+ setCatalogAutoRefreshClientFanout,
17
18
  startCatalogAutoRefresh,
18
19
  stopCatalogAutoRefresh,
19
20
  syncCatalogAutoRefreshCadence,
@@ -40,6 +41,7 @@ type ProcessLoops = {
40
41
 
41
42
  type LeaseOwner = {
42
43
  token: symbol;
44
+ fanoutRevoked: boolean;
43
45
  applyPolicy: PolicyApply;
44
46
  resources: ServerResourceOwnerLease;
45
47
  lowQuota: LowQuotaRegistration;
@@ -48,6 +50,7 @@ type LeaseOwner = {
48
50
  export type ServerBackgroundLifecycleLease = {
49
51
  scheduleStartupRun(): void;
50
52
  listLowQuotaEvents(limit?: number): LowQuotaEvent[];
53
+ revokeClientFanout(): void;
51
54
  release(): Promise<void>;
52
55
  releaseAfterFailedStart(): void;
53
56
  };
@@ -61,6 +64,25 @@ function setLivePolicyOwner(applyPolicy: PolicyApply | null): void {
61
64
  setStorageCleanupPolicyJobLiveApply(applyPolicy);
62
65
  }
63
66
 
67
+ /** Resolve the current process owner at delivery time so lease hand-over cannot retain an old port. */
68
+ async function refreshConnectedClientsAfterCatalogChange(isCurrent: () => boolean) {
69
+ const captured = owners.findLast(owner => !owner.fanoutRevoked);
70
+ if (!captured) return [];
71
+ const current = () => isCurrent() && !captured.fanoutRevoked && owners.includes(captured);
72
+ const [{ readRuntimePort }, { loadConfig }, { localClientSyncAllowed }, { syncEnabledClientIntegrations }] =
73
+ await Promise.all([
74
+ import("../config/process-state"),
75
+ import("../config"),
76
+ import("../codex/desired-state"),
77
+ import("./management/config-routes"),
78
+ ]);
79
+ const runtime = readRuntimePort(process.pid);
80
+ const config = loadConfig();
81
+ // An explicit catalog tick on a hub is not permission to enroll its local clients.
82
+ if (!runtime || !current() || !localClientSyncAllowed(config)) return [];
83
+ return syncEnabledClientIntegrations(runtime.port, config, {}, { unattended: { isCurrent: current } });
84
+ }
85
+
64
86
  function startProcessLoops(applyPolicy: PolicyApply): ProcessLoops {
65
87
  let memoryWatchdog: MemoryWatchdog | null = null;
66
88
  let stateStoreSweeper: ReturnType<typeof startStateStoreSweeper> | null = null;
@@ -83,6 +105,7 @@ function startProcessLoops(applyPolicy: PolicyApply): ProcessLoops {
83
105
  // and interval timers are unref'd. The scheduler
84
106
  // module keeps every heavy import inside its tick, so naming it statically here
85
107
  // costs a module record and nothing else.
108
+ setCatalogAutoRefreshClientFanout(refreshConnectedClientsAfterCatalogChange);
86
109
  startCatalogAutoRefresh();
87
110
  // The scheduler starts at its default cadence because resolving the operator's value
88
111
  // reads the config barrel. Fire-and-forget: startup must not await an optional
@@ -106,6 +129,7 @@ function startProcessLoops(applyPolicy: PolicyApply): ProcessLoops {
106
129
  stopStorageCleanupScheduler();
107
130
  stopQuotaResetPoller();
108
131
  stopCatalogAutoRefresh();
132
+ setCatalogAutoRefreshClientFanout(null);
109
133
  setLivePolicyOwner(null);
110
134
  throw error;
111
135
  }
@@ -119,6 +143,7 @@ function stopProcessLoops(): void {
119
143
  stopStorageCleanupScheduler();
120
144
  stopQuotaResetPoller();
121
145
  stopCatalogAutoRefresh();
146
+ setCatalogAutoRefreshClientFanout(null);
122
147
  setLivePolicyOwner(null);
123
148
  }
124
149
 
@@ -181,6 +206,7 @@ export function acquireServerBackgroundLifecycle(
181
206
 
182
207
  const owner: LeaseOwner = {
183
208
  token: Symbol("server-background-lifecycle"),
209
+ fanoutRevoked: false,
184
210
  applyPolicy,
185
211
  resources: acquireServerResourceOwner(),
186
212
  lowQuota: registerCodexLowQuotaProtection(config),
@@ -206,6 +232,7 @@ export function acquireServerBackgroundLifecycle(
206
232
  return stopStoragePolicyWorker().finally(() => { cleanupInProgress = false; });
207
233
  }
208
234
  return {
235
+ revokeClientFanout() { owner.fanoutRevoked = true; },
209
236
  listLowQuotaEvents(limit) { return owner.lowQuota.listEvents(limit); },
210
237
  scheduleStartupRun() {
211
238
  if (owners.some(candidate => candidate.token === owner.token)) {
@@ -113,6 +113,7 @@ export async function handleChatCompletions(
113
113
  return finalizeTranslatorBudgetResponse(
114
114
  await handleChatCompletionsWithBudget(req, config, logCtx, translatorBudget, logIds),
115
115
  translatorBudget,
116
+ req.signal,
116
117
  );
117
118
  } catch (error) {
118
119
  translatorBudget.dispose();
@@ -361,14 +361,16 @@ export async function runNativeChatAttempt(
361
361
  // key rotation so recovery cannot replace the ceiling along with the active credential.
362
362
  const requestTransientPolicy = transientRetryPolicyFor(activeProvider);
363
363
  let transientSendsUsed = 0;
364
- // A combo child also answers to the request's shared base allowance, at the cap its own ladder
365
- // uses. Its first send is exempt: the combo reserved it before dispatching this target.
364
+ // A Combo child's initial send is prepaid, not exempt from the shared ceiling. Only its
365
+ // owning permit exposes that one booking in addition to the still-unspent allowance, and only
366
+ // until this child's first physical send has started and settled the booking.
366
367
  const sharedSendCap = requestTransientPolicy?.attempts ?? TRANSIENT_RETRY_MAX_ATTEMPTS;
367
368
  let physicalSends = 0;
369
+ let initialSendStarted = false;
368
370
  const remainingSharedSends = (): number => {
369
371
  if (!sendBudget) return Number.POSITIVE_INFINITY;
370
372
  const remaining = sendBudget.remainingBaseSends(sharedSendCap);
371
- return physicalSends === 0 ? Math.max(1, remaining) : remaining;
373
+ return Math.min(sharedSendCap, remaining + (!initialSendStarted && execution.comboDispatchPermit ? 1 : 0));
372
374
  };
373
375
  const remainingTransientSends = (): number => Math.min(
374
376
  requestTransientPolicy
@@ -404,6 +406,21 @@ export async function runNativeChatAttempt(
404
406
  claimAmbiguousResend,
405
407
  );
406
408
 
409
+ // Non-enforced accounting for one physical send. Enforced spend is owned by the reporter.
410
+ const comboReceipt = Boolean(sendBudget && execution.comboDispatchPermit);
411
+ const settleNonEnforcedSend = (url: string): void => {
412
+ if (sendBudget) {
413
+ // Backstop for sends the helper cannot see coming (a reset replay). The first
414
+ // report settles the combo's booking; each later one is charged and booked.
415
+ if (physicalSends > 0 && sendBudget.remainingBaseSends(sharedSendCap) <= 0) {
416
+ throw new SendBudgetExhaustedError(safeHostLabel(url));
417
+ }
418
+ physicalSends += 1;
419
+ reportDispatchSends(sendBudget, 1, execution.comboDispatchPermit);
420
+ initialSendStarted = true;
421
+ } else if (!spendTracker?.charge()) throw new NativeChatSpendRefusal();
422
+ };
423
+
407
424
  const send = async (
408
425
  request: AdapterRequest,
409
426
  recovery?: "rate-limit-429" | "key-429" | UpstreamSendRecovery,
@@ -433,6 +450,9 @@ export async function runNativeChatAttempt(
433
450
  providerFetch(activeProvider, undefined, {
434
451
  providerName: route.providerName,
435
452
  modelId: route.modelId,
453
+ onPhysicalDispatch: comboReceipt
454
+ ? () => { if (!physicalBudget.spendEnforced) settleNonEnforcedSend(request.url); }
455
+ : undefined,
436
456
  dispatchOverride: async (_input, init, execute) => {
437
457
  if (!providerApiKeySelectionIsCurrent(config, route.providerName, activeProvider)) {
438
458
  const current = resolveCurrentProviderApiKeyTransport(config, route.providerName, activeProvider);
@@ -466,15 +486,11 @@ export async function runNativeChatAttempt(
466
486
  if ((logCtx.spendTracker as { refusals?: number } | undefined)?.refusals) throw new NativeChatSpendRefusal();
467
487
  throw new SendBudgetExhaustedError();
468
488
  }
469
- if (!physicalBudget.spendEnforced && sendBudget) {
470
- // Backstop for sends the helper cannot see coming (a reset replay). The first
471
- // report settles the combo's booking; each later one is charged and booked.
472
- if (physicalSends > 0 && sendBudget.remainingBaseSends(sharedSendCap) <= 0) {
473
- throw new SendBudgetExhaustedError(safeHostLabel(request.url));
474
- }
475
- physicalSends += 1;
476
- reportDispatchSends(sendBudget, 1, execution.comboDispatchPermit);
477
- } else if (!physicalBudget.spendEnforced && !spendTracker?.charge()) throw new NativeChatSpendRefusal();
489
+ // Enforced spend started this send at `beforeSend`. A Combo child's non-enforced
490
+ // send is booked by the physical-dispatch receipt instead, so a local refusal
491
+ // before the wire refunds the booking rather than charging it.
492
+ if (physicalBudget.spendEnforced) initialSendStarted = true;
493
+ else if (!comboReceipt) settleNonEnforcedSend(request.url);
478
494
  noteProviderAttemptSend(logCtx, route.providerName, activeProvider, logCtx.usageLogInputTokens, transportRecovery ?? recovery);
479
495
  // A reselected provider transport is still a physical send: the connection policy
480
496
  // and manual-redirect ownership wrap the selected implementation (#4992).
@@ -21,7 +21,8 @@ import { normalizeAnthropicImages } from "../adapters/anthropic-image-normalize"
21
21
  import { createToolCallIdAllocator } from "../adapters/tool-call-id";
22
22
  import { openAIChatSerializesThinking } from "../adapters/openai-chat/messages";
23
23
  import { messagesToResponsesTranslation } from "../protocols/codecs/messages";
24
- import { AnthropicRequestError, DesktopModelMappingUnavailableError, extractOcxEffortDirective, extractOcxRouteDirective, resolveInboundModel, type ClaudeCacheKeySource } from "../claude/inbound";
24
+ import { AnthropicRequestError, DesktopModelMappingUnavailableError, extractOcxEffortDirective, extractOcxRouteDirective, nativeAnthropicProjection, resolveInboundModel, type ClaudeCacheKeySource } from "../claude/inbound";
25
+ import { nativeReasoningTag, type NativeReasoningOwner } from "../responses/reasoning-replay-cache";
25
26
  import { isKnownDesktop3pModelId, resolveDesktop3pAlias } from "../claude/desktop-3p";
26
27
  import { resolveAlias, claudeCodeNativeAlias, legacyAliasForNative } from "../claude/alias";
27
28
  import { recordDesktopRequest } from "../claude/desktop-health";
@@ -41,6 +42,7 @@ import {
41
42
  } from "../lib/upstream-retry";
42
43
  import { resolveClientRetryAfter } from "../lib/retry-after";
43
44
  import { anthropicRateLimitHeaders } from "./anthropic-rate-limit-headers";
45
+ import { upstreamMessagesRequestIdHeaders } from "./messages-response-headers";
44
46
  import {
45
47
  anthropicErrorBody,
46
48
  anthropicErrorResponse,
@@ -634,7 +636,10 @@ async function anthropicNativePassthrough(
634
636
  const upstream = result.upstream;
635
637
 
636
638
  const contentType = upstream.headers.get("content-type") ?? "application/json";
637
- const rateLimitHeaders = anthropicRateLimitHeaders(upstream.headers);
639
+ const rateLimitHeaders = {
640
+ ...anthropicRateLimitHeaders(upstream.headers),
641
+ ...(pathname === "/v1/messages" ? upstreamMessagesRequestIdHeaders(upstream.headers) : {}),
642
+ };
638
643
  const bodyGuard = resolvePassthroughBodyGuard(config, req.signal);
639
644
  if (upstream.ok && contentType.includes("text/event-stream") && upstream.body) {
640
645
  return new Response(tapAnthropicSseForLog(upstream.body, logCtx, finalize, bodyGuard), {
@@ -837,6 +842,7 @@ export async function handleClaudeMessages(
837
842
  return finalizeTranslatorBudgetResponse(
838
843
  await handleClaudeMessagesWithBudget(req, config, logCtx, translatorBudget, logIds, requestPolicy, ingress),
839
844
  translatorBudget,
845
+ req.signal,
840
846
  );
841
847
  } catch (error) {
842
848
  translatorBudget.dispose();
@@ -872,6 +878,9 @@ async function handleClaudeMessagesWithBudget(
872
878
  let anthropicBody: unknown;
873
879
  let internalBody: Rec;
874
880
  let cacheKeySource: ClaudeCacheKeySource = null;
881
+ let nativeReasoningReplay: ReadonlyMap<string, string> | undefined;
882
+ const nativeReasoningMint: { owner?: NativeReasoningOwner } = {};
883
+ let nativeProjectionOnlyError: AnthropicRequestError | undefined;
875
884
  let effortOverride: string | null = null;
876
885
  let effortRow: ParsedEffortRowId | null = null;
877
886
  let fastRow: ParsedFastRowId | null = null;
@@ -962,7 +971,15 @@ async function handleClaudeMessagesWithBudget(
962
971
  if (!effortRow && !fastRow && isRec(anthropicBody) && wantsNativePassthrough(req, config, requestPolicy, anthropicBody.model, cc)) {
963
972
  markProtocolEntry(logCtx, { inbound: "messages", lane: "native", features: messagesFeatures });
964
973
  recordProtocolShadowPlan(logCtx, config, { inbound: "messages", model: requestedModel });
965
- return await anthropicNativePassthrough(req, config, logCtx, logIds, anthropicBody, "/v1/messages");
974
+ let projectedBody: Rec;
975
+ try {
976
+ projectedBody = nativeAnthropicProjection(anthropicBody, translatorBudget);
977
+ } catch (err) {
978
+ if (!isTranslatorBudgetExceededError(err)) throw err;
979
+ if (logIds) addFinalRequestLog(logIds.requestId, logIds.start, logCtx, 413, { closeReason: "non_stream" });
980
+ return anthropicErrorResponse(413, "request translation buffer exceeded the safe limit", "request_too_large", "translation_buffer_limit");
981
+ }
982
+ return await anthropicNativePassthrough(req, config, logCtx, logIds, projectedBody, "/v1/messages");
966
983
  }
967
984
  if (isRec(anthropicBody) && typeof anthropicBody.model === "string"
968
985
  && messagesSecondaryInstanceUnavailable(config, anthropicBody.model, cc)) {
@@ -1017,7 +1034,19 @@ async function handleClaudeMessagesWithBudget(
1017
1034
  };
1018
1035
  delete anthropicBody.thinking;
1019
1036
  }
1020
- const translation = messagesToResponsesTranslation(anthropicBody, cc, translatorBudget);
1037
+ let translation;
1038
+ try {
1039
+ translation = messagesToResponsesTranslation(anthropicBody, cc, translatorBudget);
1040
+ } catch (err) {
1041
+ // A native Messages route can discard an undecodable proxy envelope. Keep the
1042
+ // original error for any route that ultimately translates to Responses.
1043
+ if (!(err instanceof AnthropicRequestError) || err.message !== "malformed ocxr1 reasoning signature"
1044
+ || !isRec(anthropicBody)) throw err;
1045
+ const projected = nativeAnthropicProjection(anthropicBody, translatorBudget);
1046
+ if (projected === anthropicBody) throw err;
1047
+ translation = messagesToResponsesTranslation(projected, cc, translatorBudget);
1048
+ nativeProjectionOnlyError = err;
1049
+ }
1021
1050
  internalBody = translation.body;
1022
1051
  // The Anthropic translator builds its body from model/input/store/stream plus sampling
1023
1052
  // fields only, so the caller intent is applied to the TRANSLATED body rather than the
@@ -1025,6 +1054,7 @@ async function handleClaudeMessagesWithBudget(
1025
1054
  if (fastRow) internalBody.service_tier = "priority";
1026
1055
  translatorBudget.chargeRetained(jsonUtf8Bytes(internalBody), { kind: "request_copies" });
1027
1056
  cacheKeySource = translation.cacheKeySource;
1057
+ nativeReasoningReplay = translation.nativeReasoningReplay;
1028
1058
  } catch (err) {
1029
1059
  const overflow = isTranslatorBudgetExceededError(err);
1030
1060
  const unavailable = err instanceof DesktopModelMappingUnavailableError;
@@ -1171,6 +1201,10 @@ async function handleClaudeMessagesWithBudget(
1171
1201
  ? nativeMessagesDeclineReason(settledRoute, anthropicBody, config, nativeSelector)
1172
1202
  : "rollout-disabled";
1173
1203
  const nativeMessagesRoute = settledRoute && nativeDecline === undefined ? settledRoute : undefined;
1204
+ if (nativeProjectionOnlyError && !nativeMessagesRoute) {
1205
+ if (logIds) addFinalRequestLog(logIds.requestId, logIds.start, logCtx, 400, { closeReason: "non_stream" });
1206
+ return anthropicErrorResponse(400, nativeProjectionOnlyError.message, "invalid_request_error");
1207
+ }
1174
1208
  // With the switch on, a declined route says why on its bridge mark, as native Chat does.
1175
1209
  if (nativeDecline && nativeDecline !== "rollout-disabled") {
1176
1210
  markProtocolEntry(logCtx, {
@@ -1209,7 +1243,7 @@ async function handleClaudeMessagesWithBudget(
1209
1243
  let nativeBody: Rec;
1210
1244
  try {
1211
1245
  // Built from the source envelope when there is one, after the managed-client steps above.
1212
- nativeBody = envelope ? envelope.freshBody() : anthropicBody as Rec;
1246
+ nativeBody = nativeAnthropicProjection(envelope ? envelope.freshBody() : anthropicBody as Rec, translatorBudget);
1213
1247
  } catch (err) {
1214
1248
  if (!isTranslatorBudgetExceededError(err)) throw err;
1215
1249
  if (logIds) addFinalRequestLog(logIds.requestId, logIds.start, logCtx, 413, { closeReason: "non_stream" });
@@ -1314,6 +1348,8 @@ async function handleClaudeMessagesWithBudget(
1314
1348
  // Without this the replay would look native and a Responses-scoped wire default
1315
1349
  // would fire, disagreeing with the pre-flight decision above.
1316
1350
  inboundWire: "anthropic",
1351
+ nativeReasoningReplay,
1352
+ nativeReasoningMint,
1317
1353
  claudeGoAffinity: { sessionLane: claudeGoSessionLane },
1318
1354
  claudeNativeSessionId,
1319
1355
  stripClaudeMainAuthForNoncanonicalForward: true,
@@ -1395,6 +1431,7 @@ async function handleClaudeMessagesWithBudget(
1395
1431
  if (contentType.includes("text/event-stream") && response.body) {
1396
1432
  const anthropicSse = responsesSseToAnthropicSse(response.body, requestedModel, {
1397
1433
  translatorBudget,
1434
+ nativeReasoningTagFor: blob => nativeReasoningTag(nativeReasoningMint.owner, blob),
1398
1435
  // Only a floor, and only for the first frame: an upstream that reports usage early wins
1399
1436
  // over it inside the translator, and the terminal `message_delta` carries the
1400
1437
  // authoritative count either way (#4857).
@@ -1464,7 +1501,7 @@ async function handleClaudeMessagesWithBudget(
1464
1501
  }
1465
1502
  let message: Rec;
1466
1503
  try {
1467
- message = responsesJsonToAnthropicMessage(json, requestedModel, translatorBudget);
1504
+ message = responsesJsonToAnthropicMessage(json, requestedModel, translatorBudget, blob => nativeReasoningTag(nativeReasoningMint.owner, blob));
1468
1505
  } catch (err) {
1469
1506
  if (!isTranslatorBudgetExceededError(err)) throw err;
1470
1507
  return anthropicErrorResponse(413, "upstream translation buffer exceeded the safe limit", "request_too_large", "translation_buffer_limit");
@@ -181,6 +181,7 @@ export function runAdmittedBodyWork(
181
181
  addFinalRequestLog(refusalLog.requestId, refusalLog.start, refusalLog.logCtx, refused.status, {
182
182
  closeReason: "terminal",
183
183
  });
184
+ refusalLog.onLogged?.();
184
185
  }
185
186
  return withCors(refused, req, policy);
186
187
  });
@@ -41,10 +41,11 @@ export function buildInterceptDesiredClients(config: OcxConfig, observed: Claude
41
41
  * imports keep the catalog and discovery off the synchronous startup path.
42
42
  */
43
43
  export async function loadPickerRoutesFromCatalog(): Promise<PickerRouteInput> {
44
- const [{ loadConfig }, { fetchAllModels }, catalog] = await Promise.all([
44
+ const [{ loadConfig }, { fetchAllModels }, catalog, { resolveAutoContext }] = await Promise.all([
45
45
  import("../../config"),
46
46
  import("../management-api"),
47
47
  import("../../codex/catalog"),
48
+ import("../../claude/context-windows"),
48
49
  ]);
49
50
  const config = loadConfig();
50
51
  const models = await fetchAllModels(config);
@@ -54,6 +55,7 @@ export async function loadPickerRoutesFromCatalog(): Promise<PickerRouteInput> {
54
55
  .map(model => ({ provider: model.provider, id: model.id, contextWindow: model.contextWindow })),
55
56
  ...(config.claudeCode?.desktopProfile ? { profile: config.claudeCode.desktopProfile } : {}),
56
57
  nativeContextCap: catalog.nativeContextLimits(config),
58
+ auto: resolveAutoContext(config.claudeCode),
57
59
  };
58
60
  }
59
61
 
@@ -21,6 +21,7 @@ import {
21
21
  } from "./live-sideband";
22
22
  import {
23
23
  withRequestLogId,
24
+ withMessagesRequestLogId,
24
25
  } from "./startup-warnings";
25
26
 
26
27
  import { remoteWorkspaceEnabled } from "../../remote-control/workspace-activation";
@@ -118,6 +119,7 @@ import { handleChatCompletions } from "../chat-completions";
118
119
  import { anthropicErrorResponse } from "../../claude/outbound";
119
120
  import {
120
121
  buildDesktop3pRegistry,
122
+ desktop3pModelOptions,
121
123
  generateDesktop3pModels,
122
124
  } from "../../claude/desktop-3p";
123
125
  import { buildDesktopDiscoveryInputs } from "../../claude/desktop-discovery-inputs";
@@ -1024,6 +1026,7 @@ export function createServeOptions(ctx: ServeOptionsContext) {
1024
1026
  const models = config.claudeCode?.enabled === false ? [] : generateDesktop3pModels(
1025
1027
  desktopInputs.nativeSlugs, desktopInputs.routedModels,
1026
1028
  config.claudeCode?.desktopProfile, desktopInputs.nativeContextCap,
1029
+ desktop3pModelOptions(config.claudeCode),
1027
1030
  );
1028
1031
  const response = jsonResponse({ version: 1, models }, 200, req, policy);
1029
1032
  response.headers.set("Cache-Control", "no-store");
@@ -1556,11 +1559,16 @@ export function createServeOptions(ctx: ServeOptionsContext) {
1556
1559
  // pre-translation stream + native passthrough callbacks) — do not re-wrap the
1557
1560
  // translated Anthropic stream here.
1558
1561
  const sessionReq = withCallerSessionIdentity(req, admission);
1559
- return runAdmittedHttpTurn(sessionReq, policy, async turnAdmissionLease => withCors(
1562
+ // Entering the handler, or a logged refusal, means the final request log owns requestId.
1563
+ let ownsRequestLog = false;
1564
+ const claimRequestLog = (work: (lease: ActiveTurnLease) => Promise<Response>) =>
1565
+ (lease: ActiveTurnLease) => { ownsRequestLog = true; return work(lease); };
1566
+ const response = await runAdmittedHttpTurn(sessionReq, policy, claimRequestLog(async turnAdmissionLease => withCors(
1560
1567
  await handleClaudeMessages(sessionReq, config, logCtx, { requestId, start, turnAdmissionLease, admission }, policy, { claudeIntercept: ingress === "claude-intercept" }),
1561
1568
  req,
1562
1569
  policy,
1563
- ), { requestId, start, logCtx });
1570
+ )), { requestId, start, logCtx, onLogged: () => { ownsRequestLog = true; } });
1571
+ return ownsRequestLog ? withMessagesRequestLogId(response, requestId) : response;
1564
1572
  }
1565
1573
 
1566
1574
  // OpenAI Chat Completions inbound (GitHub Copilot App / OpenAI-compatible clients).
@@ -37,7 +37,8 @@ export interface SpendLedgerServerLifecycle {
37
37
  export function acquireSpendLedgerServerLifecycle(configDir: string): SpendLedgerServerLifecycle {
38
38
  const owner: SpendLedgerOwnerLease = acquireSpendLedgerOwner(configDir);
39
39
  // Advisory: a synced state directory makes the journal's hard-link guard refuse intermittently
40
- // (#6314). Said once at startup instead of being discovered from a 502.
40
+ // (#6314), including Google Drive syncing the native Desktop or Documents folder. Said once
41
+ // at startup instead of being discovered from a 502. The nlink guard itself is unchanged.
41
42
  warnIfSyncedStateDirectory(configDir);
42
43
  // Each entry returns whatever the listener's own stop returned. Typed as void-or-promise
43
44
  // because the rollback below has to WAIT on it: declaring it `() => void` let the call site
@@ -100,6 +100,25 @@ import {
100
100
 
101
101
  const REQUEST_LOG_ID_RESPONSE_HEADER = "x-opencodex-request-id";
102
102
 
103
+ /** Messages clients persist request-id; the upstream diagnostic id has a separate namespace. */
104
+ export function withMessagesRequestLogId(response: Response, requestId: string): Response {
105
+ const upstreamId = response.headers.get("request-id");
106
+ const correlated = withRequestLogId(response, requestId);
107
+ correlated.headers.delete("x-opencodex-upstream-request-id");
108
+ if (upstreamId && /^req_[A-Za-z0-9_-]{1,128}$/.test(upstreamId)) {
109
+ correlated.headers.set("x-opencodex-upstream-request-id", upstreamId);
110
+ }
111
+ correlated.headers.set("request-id", requestId);
112
+ const exposed = correlated.headers.get("Access-Control-Expose-Headers") ?? "";
113
+ const names = exposed.split(",").map(name => name.trim().toLowerCase());
114
+ const additions = ["request-id", ...(correlated.headers.has("x-opencodex-upstream-request-id")
115
+ ? ["x-opencodex-upstream-request-id"] : [])].filter(name => !names.includes(name));
116
+ if (additions.length) {
117
+ correlated.headers.set("Access-Control-Expose-Headers", [exposed, ...additions].filter(Boolean).join(", "));
118
+ }
119
+ return correlated;
120
+ }
121
+
103
122
  export function withRequestLogId(response: Response, requestId: string): Response {
104
123
  const headers = new Headers(response.headers);
105
124
  headers.set(REQUEST_LOG_ID_RESPONSE_HEADER, requestId);
@@ -750,6 +750,7 @@ function startServerWithSpendLedgerOwner(port: number | undefined, deps: StartSe
750
750
  Object.defineProperty(server, "stop", {
751
751
  configurable: true,
752
752
  value: async (closeActiveConnections?: boolean): Promise<void> => {
753
+ backgroundLifecycle?.revokeClientFanout();
753
754
  remoteWorkspaceStopping = true;
754
755
  liveCallBindings.clear();
755
756
  // Disarm the package-tree restart timer before teardown can schedule another restart.