@bitkyc08/opencodex 2.56.0 → 2.58.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 (206) hide show
  1. package/bin/ocx.mjs +10 -0
  2. package/gui/dist/assets/{index-D4zuyIxQ.js → index-BbrHOIY0.js} +21 -21
  3. package/gui/dist/assets/{index-BBOZWGB6.css → index-C5-RdDmD.css} +1 -1
  4. package/gui/dist/index.html +2 -2
  5. package/package.json +4 -4
  6. package/src/adapters/codebuddy/adapter.ts +2 -1
  7. package/src/adapters/codebuddy/scaffold-guard.ts +249 -0
  8. package/src/adapters/command-code.ts +12 -3
  9. package/src/adapters/cursor/cursor-errors.ts +15 -0
  10. package/src/adapters/cursor/discovery.ts +65 -1
  11. package/src/adapters/cursor/envelope-echo.ts +8 -2
  12. package/src/adapters/cursor/live-transport.ts +5 -1
  13. package/src/adapters/cursor/protobuf-events.ts +110 -11
  14. package/src/adapters/cursor/protobuf-request.ts +19 -1
  15. package/src/adapters/cursor/text-toolcall.ts +230 -0
  16. package/src/adapters/cursor/thread-continuity.ts +67 -0
  17. package/src/adapters/cursor/types.ts +5 -0
  18. package/src/adapters/cursor.ts +55 -5
  19. package/src/adapters/google-http.ts +38 -13
  20. package/src/adapters/google.ts +7 -7
  21. package/src/adapters/kiro/payload.ts +17 -3
  22. package/src/adapters/kiro/reasoning.ts +70 -7
  23. package/src/adapters/kiro/stream.ts +8 -2
  24. package/src/adapters/kiro/wire.ts +2 -1
  25. package/src/adapters/kiro-events.ts +21 -13
  26. package/src/adapters/mimo-free.ts +32 -17
  27. package/src/adapters/ollama-native.ts +42 -8
  28. package/src/adapters/openai-chat/tool-name-registry.ts +166 -0
  29. package/src/adapters/openai-chat/tool-schema.ts +25 -7
  30. package/src/adapters/openai-chat.ts +8 -8
  31. package/src/adapters/openai-responses/passthrough.ts +62 -5
  32. package/src/adapters/openai-responses/request-strips.ts +43 -0
  33. package/src/adapters/physical-send.ts +50 -0
  34. package/src/bridge/errors.ts +26 -2
  35. package/src/bridge/response-json.ts +8 -2
  36. package/src/bridge/sse.ts +20 -2
  37. package/src/claude/desktop-profile.ts +66 -9
  38. package/src/claude/outbound.ts +32 -4
  39. package/src/cli/account-main.ts +1 -1
  40. package/src/cli/capabilities.ts +2 -2
  41. package/src/cli/combo.ts +10 -1
  42. package/src/cli/config-command.ts +35 -18
  43. package/src/cli/dispatch.ts +17 -4
  44. package/src/cli/index.ts +92 -7
  45. package/src/cli/registry.ts +2 -1
  46. package/src/cli/system-command.ts +74 -5
  47. package/src/cli/uninstall-client-state.ts +12 -0
  48. package/src/clients/config-export.ts +7 -3
  49. package/src/codex/account-label.ts +14 -3
  50. package/src/codex/account-store.ts +113 -26
  51. package/src/codex/account-usability.ts +21 -0
  52. package/src/codex/auth-api/login-flow.ts +14 -2
  53. package/src/codex/auth-api/reset-credit-service.ts +11 -2
  54. package/src/codex/auth-context.ts +199 -15
  55. package/src/codex/catalog/aggregation.ts +80 -1
  56. package/src/codex/catalog/model-visibility.ts +1 -0
  57. package/src/codex/catalog/remote.ts +30 -0
  58. package/src/codex/catalog/retained-sync.ts +9 -1
  59. package/src/codex/catalog/routed-gather.ts +38 -1
  60. package/src/codex/cli-install-provenance.ts +7 -1
  61. package/src/codex/convergence.ts +7 -2
  62. package/src/codex/desktop-app/types.ts +11 -2
  63. package/src/codex/desktop-app/windows.ts +5 -5
  64. package/src/codex/desktop-switches.ts +145 -0
  65. package/src/codex/history-job.ts +5 -1
  66. package/src/codex/history-provider.ts +33 -4
  67. package/src/codex/history-worker.ts +14 -1
  68. package/src/codex/inject/remove.ts +145 -7
  69. package/src/codex/inject/restore.ts +231 -32
  70. package/src/codex/inject.ts +12 -16
  71. package/src/codex/loopback-target.ts +9 -0
  72. package/src/codex/model-entitlements.ts +152 -15
  73. package/src/codex/native-profile-startup.ts +64 -20
  74. package/src/codex/pool-refresh-backoff.ts +12 -3
  75. package/src/codex/quota-rejection.ts +104 -15
  76. package/src/codex/routing/cache-affinity.ts +70 -0
  77. package/src/codex/routing/cooldown-math.ts +10 -0
  78. package/src/codex/routing/selection.ts +79 -2
  79. package/src/codex/routing/thread-affinity.ts +50 -2
  80. package/src/codex/routing/transient-hold-dispatch.ts +141 -0
  81. package/src/codex/routing.ts +29 -49
  82. package/src/codex/warmup.ts +1 -1
  83. package/src/combos/failover.ts +85 -0
  84. package/src/combos/request.ts +17 -10
  85. package/src/combos/types.ts +23 -2
  86. package/src/config/atomic-write.ts +83 -8
  87. package/src/config/pending-teardown.ts +31 -0
  88. package/src/config/schema/config-schema.ts +2 -0
  89. package/src/config/schema/leaf-validators.ts +1 -0
  90. package/src/generated/compatibility-version.json +272 -180
  91. package/src/images/loop.ts +1 -1
  92. package/src/lib/bounded-subprocess.ts +62 -10
  93. package/src/lib/errors.ts +17 -0
  94. package/src/lib/request-execution-budget.ts +147 -21
  95. package/src/lib/spend-reservation-ledger.ts +18 -0
  96. package/src/lib/state-store-registrations.ts +6 -2
  97. package/src/lib/test-home-guard.ts +85 -1
  98. package/src/lib/upstream-retry.ts +77 -10
  99. package/src/lib/windows-elevation.ts +76 -14
  100. package/src/lib/windows-secret-acl.ts +151 -15
  101. package/src/lib/windows-user-principal.ts +5 -1
  102. package/src/oauth/index.ts +2 -2
  103. package/src/oauth/key-providers.ts +2 -2
  104. package/src/providers/derive.ts +6 -0
  105. package/src/providers/kiro-models.ts +4 -3
  106. package/src/providers/label.ts +19 -1
  107. package/src/providers/model-discovery.ts +35 -7
  108. package/src/providers/registry/entries-core.ts +18 -0
  109. package/src/providers/registry/entries-extended.ts +59 -28
  110. package/src/providers/registry/model-seeds.ts +71 -17
  111. package/src/providers/registry/types.ts +9 -0
  112. package/src/responses/reasoning-envelope.ts +6 -3
  113. package/src/responses/spill-store.ts +17 -0
  114. package/src/responses/state/body-policy.ts +25 -0
  115. package/src/responses/state/spill-queue.ts +8 -6
  116. package/src/responses/state.ts +3 -22
  117. package/src/router.ts +4 -0
  118. package/src/routing/identity-domains.ts +21 -14
  119. package/src/routing/probe-lease.ts +103 -1
  120. package/src/server/auth-cors.ts +1 -0
  121. package/src/server/chat-completions.ts +3 -1
  122. package/src/server/chat-native.ts +37 -9
  123. package/src/server/index/live-sideband.ts +37 -1
  124. package/src/server/index/websocket-handler.ts +54 -3
  125. package/src/server/index.ts +5 -5
  126. package/src/server/inspection-tee.ts +107 -0
  127. package/src/server/live.ts +46 -1
  128. package/src/server/management/combo-routes.ts +10 -1
  129. package/src/server/management/config-routes.ts +27 -5
  130. package/src/server/models-capabilities.ts +24 -3
  131. package/src/server/relay-eager.ts +2 -0
  132. package/src/server/relay.ts +14 -19
  133. package/src/server/request-log.ts +127 -3
  134. package/src/server/response-log-body.ts +153 -0
  135. package/src/server/responses/account-change-state.ts +74 -0
  136. package/src/server/responses/adapter-continuation.ts +33 -7
  137. package/src/server/responses/adapter-delivery.ts +5 -11
  138. package/src/server/responses/adapter-dispatch.ts +84 -13
  139. package/src/server/responses/codex-ws-exchange.ts +65 -4
  140. package/src/server/responses/codex-ws-wire.ts +5 -0
  141. package/src/server/responses/collaboration.ts +74 -4
  142. package/src/server/responses/combo-session-recall.ts +68 -8
  143. package/src/server/responses/combo-stream-preflight.ts +68 -5
  144. package/src/server/responses/compact.ts +54 -13
  145. package/src/server/responses/core-auth.ts +2 -0
  146. package/src/server/responses/core-codex-account.ts +51 -3
  147. package/src/server/responses/core-combo.ts +129 -23
  148. package/src/server/responses/core-errors.ts +18 -0
  149. package/src/server/responses/core-options.ts +3 -0
  150. package/src/server/responses/core-replay.ts +105 -32
  151. package/src/server/responses/core.ts +3 -3
  152. package/src/server/responses/encrypted-payload.ts +0 -1
  153. package/src/server/responses/fetch-helpers.ts +4 -1
  154. package/src/server/responses/input-admission.ts +126 -6
  155. package/src/server/responses/native-injection-protocol.ts +42 -0
  156. package/src/server/responses/native-injection-replay.ts +105 -0
  157. package/src/server/responses/native-injection.ts +242 -0
  158. package/src/server/responses/native-response-control.ts +56 -0
  159. package/src/server/responses/native-response-json.ts +14 -0
  160. package/src/server/responses/native-response-output.ts +37 -0
  161. package/src/server/responses/native-steering-log.ts +44 -0
  162. package/src/server/responses/native-steering-policy.ts +49 -0
  163. package/src/server/responses/native-steering-replay.ts +126 -0
  164. package/src/server/responses/native-steering-settings.ts +76 -0
  165. package/src/server/responses/native-steering.ts +400 -0
  166. package/src/server/responses/native-tool-results.ts +130 -0
  167. package/src/server/responses/passthrough-delivery.ts +30 -6
  168. package/src/server/responses/passthrough-dispatch.ts +61 -11
  169. package/src/server/responses/passthrough-error.ts +38 -2
  170. package/src/server/responses/request-prepare.ts +173 -22
  171. package/src/server/responses/request-send-budget.ts +97 -2
  172. package/src/server/responses/request-spend.ts +147 -0
  173. package/src/server/responses/request-transport.ts +62 -3
  174. package/src/server/responses/run-turn-execution.ts +59 -31
  175. package/src/server/responses/sidecar-execution.ts +7 -13
  176. package/src/server/responses/terminal-guard.ts +65 -4
  177. package/src/server/responses/ws-upstream.ts +21 -1
  178. package/src/server/responses-undeclared-tool-guard.ts +9 -5
  179. package/src/server/stop-teardown.ts +8 -1
  180. package/src/server/ws-bridge.ts +16 -1
  181. package/src/service/cli.ts +13 -1
  182. package/src/service/windows-ops.ts +210 -16
  183. package/src/service/windows-scheduler.ts +28 -21
  184. package/src/service.ts +1 -1
  185. package/src/types/config.ts +8 -1
  186. package/src/types/provider.ts +13 -0
  187. package/src/types/request.ts +8 -5
  188. package/src/types/tools.ts +24 -0
  189. package/src/types.ts +2 -0
  190. package/src/update/index.ts +10 -0
  191. package/src/update/stop-contract.d.mts +1 -0
  192. package/src/update/stop-contract.mjs +19 -0
  193. package/src/update/stop-decision.d.mts +1 -1
  194. package/src/update/stop-decision.mjs +12 -3
  195. package/src/usage/log.ts +1 -1
  196. package/src/vision/anthropic-describe.ts +1 -1
  197. package/src/vision/describe.ts +5 -5
  198. package/src/web-search/anthropic-executor.ts +1 -1
  199. package/src/web-search/exa-executor.ts +1 -1
  200. package/src/web-search/executor.ts +1 -1
  201. package/src/web-search/gemini-executor.ts +1 -1
  202. package/src/web-search/loop.ts +1 -1
  203. package/src/web-search/ollama-executor.ts +1 -1
  204. package/src/web-search/parse.ts +67 -14
  205. package/src/web-search/passthrough-bridge.ts +64 -31
  206. package/src/web-search/xai-executor.ts +1 -1
@@ -22,8 +22,34 @@ export interface RenderedDesktopModel extends DesktopProfileModel {
22
22
  supports1m: boolean;
23
23
  }
24
24
 
25
- const DATE_ALIAS = /^claude-opus-4-8-(2026\d{4})$/;
26
- const DAY_COUNT_2026 = 365;
25
+ // Managed-namespace date aliases run 2026-2035. The original 2026-only
26
+ // (365 slots) design failed with "all 365 encoded date slots are occupied"
27
+ // once a catalog exceeded 365 routes (stale assignments are retained by
28
+ // design, so the set only grows). Years before 2026 stay rejected: dated
29
+ // ids like `claude-opus-4-8-20250201` are real model snapshot ids, not
30
+ // managed aliases, and the inbound decoder relies on that distinction.
31
+ // Every emitted suffix stays 8 digits so modelMap date-stripping keeps
32
+ // working.
33
+ const DATE_ALIAS = /^claude-opus-4-8-(202[6-9]\d{4}|203[0-5]\d{4})$/;
34
+ const LEGACY_YEAR = 2026;
35
+ const LEGACY_DAY_COUNT = 365;
36
+ const ALIAS_FIRST_YEAR = 2026;
37
+ const ALIAS_LAST_YEAR = 2035;
38
+
39
+ function isLeapYear(year: number): boolean {
40
+ return year % 4 === 0 && (year % 100 !== 0 || year % 400 === 0);
41
+ }
42
+
43
+ function daysInAliasYear(year: number): number {
44
+ return isLeapYear(year) ? 366 : 365;
45
+ }
46
+
47
+ export const TOTAL_ALIAS_SLOTS = (() => {
48
+ let total = 0;
49
+ for (let year = ALIAS_FIRST_YEAR; year <= ALIAS_LAST_YEAR; year += 1) total += daysInAliasYear(year);
50
+ return total;
51
+ })();
52
+ export const ALIAS_YEAR_RANGE = { first: ALIAS_FIRST_YEAR, last: ALIAS_LAST_YEAR } as const;
27
53
 
28
54
  export class DesktopProfileError extends Error {
29
55
  constructor(message: string, readonly path = "profile") {
@@ -119,7 +145,7 @@ export function parseDesktopProfile(value: unknown): DesktopProfile {
119
145
  if (isRealAnthropicRoute(route)) {
120
146
  if (raw.alias !== routeModelId(route)) throw new DesktopProfileError("real Anthropic routes must keep their exact model id", `profile.assignments.${route}.alias`);
121
147
  } else if (!validDateAlias(raw.alias)) {
122
- throw new DesktopProfileError("must be a valid claude-opus-4-8-2026MMDD alias", `profile.assignments.${route}.alias`);
148
+ throw new DesktopProfileError("must be a valid claude-opus-4-8-YYYYMMDD alias", `profile.assignments.${route}.alias`);
123
149
  }
124
150
  if (aliases.has(raw.alias)) throw new DesktopProfileError(`duplicate alias "${raw.alias}"`, `profile.assignments.${route}.alias`);
125
151
  aliases.add(raw.alias);
@@ -144,26 +170,57 @@ export function parseDesktopProfile(value: unknown): DesktopProfile {
144
170
  return { version: 1, assignments, defaults, ...appliedMarkers(value) };
145
171
  }
146
172
 
147
- function dayOfYearAlias(dayIndex: number): string {
148
- const date = new Date(Date.UTC(2026, 0, dayIndex + 1));
173
+ function formatSlotDate(year: number, dayOfYear: number): string {
174
+ const date = new Date(Date.UTC(year, 0, dayOfYear));
149
175
  const y = date.getUTCFullYear();
150
176
  const m = String(date.getUTCMonth() + 1).padStart(2, "0");
151
177
  const d = String(date.getUTCDate()).padStart(2, "0");
152
178
  return `claude-opus-4-8-${y}${m}${d}`;
153
179
  }
154
180
 
181
+ // Legacy 2026 ring, byte-identical to the original allocator: the same route
182
+ // must keep resolving to the same 2026 alias it always had, and a probe over
183
+ // a nearly-full 2026 set must land on the same free date as before.
184
+ function legacyDayAlias(dayIndex: number): string {
185
+ return formatSlotDate(LEGACY_YEAR, dayIndex + 1);
186
+ }
187
+
188
+ // Overflow ring for catalogs past 365 routes (2027-2035). Probed only after
189
+ // every legacy slot is taken, so existing profiles never shift into it.
190
+ const OVERFLOW_FIRST_YEAR = LEGACY_YEAR + 1;
191
+ const OVERFLOW_SLOT_COUNT = TOTAL_ALIAS_SLOTS - LEGACY_DAY_COUNT;
192
+
193
+ function overflowSlotAlias(slotIndex: number): string {
194
+ let remaining = ((slotIndex % OVERFLOW_SLOT_COUNT) + OVERFLOW_SLOT_COUNT) % OVERFLOW_SLOT_COUNT;
195
+ for (let year = OVERFLOW_FIRST_YEAR; year <= ALIAS_LAST_YEAR; year += 1) {
196
+ const days = daysInAliasYear(year);
197
+ if (remaining < days) return formatSlotDate(year, remaining + 1);
198
+ remaining -= days;
199
+ }
200
+ throw new DesktopProfileError("slot index out of range", "profile.assignments");
201
+ }
202
+
155
203
  function routeStartDay(route: string): number {
156
- return createHash("sha256").update(route).digest().readUInt32BE(0) % DAY_COUNT_2026;
204
+ return createHash("sha256").update(route).digest().readUInt32BE(0) % LEGACY_DAY_COUNT;
205
+ }
206
+
207
+ function routeOverflowStart(route: string): number {
208
+ return createHash("sha256").update(route).digest().readUInt32BE(4) % OVERFLOW_SLOT_COUNT;
157
209
  }
158
210
 
159
211
  function allocateAlias(route: string, used: Set<string>): string {
160
212
  if (isRealAnthropicRoute(route)) return routeModelId(route);
161
213
  const start = routeStartDay(route);
162
- for (let offset = 0; offset < DAY_COUNT_2026; offset += 1) {
163
- const alias = dayOfYearAlias((start + offset) % DAY_COUNT_2026);
214
+ for (let offset = 0; offset < LEGACY_DAY_COUNT; offset += 1) {
215
+ const alias = legacyDayAlias((start + offset) % LEGACY_DAY_COUNT);
216
+ if (!used.has(alias)) return alias;
217
+ }
218
+ const overflowStart = routeOverflowStart(route);
219
+ for (let offset = 0; offset < OVERFLOW_SLOT_COUNT; offset += 1) {
220
+ const alias = overflowSlotAlias((overflowStart + offset) % OVERFLOW_SLOT_COUNT);
164
221
  if (!used.has(alias)) return alias;
165
222
  }
166
- throw new DesktopProfileError("all 365 encoded date slots are occupied", `profile.assignments.${route}.alias`);
223
+ throw new DesktopProfileError(`all ${TOTAL_ALIAS_SLOTS} encoded date slots are occupied`, `profile.assignments.${route}.alias`);
167
224
  }
168
225
 
169
226
  export function reconcileDesktopProfile(
@@ -189,7 +189,7 @@ function webSearchPairFromItem(item: Rec): { id: string; input: Rec; resultConte
189
189
  return { id, input, resultContent, completed };
190
190
  }
191
191
 
192
- function messageSnapshot(model: string): Rec {
192
+ function messageSnapshot(model: string, confirmedUsage?: Rec): Rec {
193
193
  return {
194
194
  id: `msg_${uuid()}`,
195
195
  type: "message",
@@ -198,7 +198,7 @@ function messageSnapshot(model: string): Rec {
198
198
  model,
199
199
  stop_reason: null,
200
200
  stop_sequence: null,
201
- usage: { input_tokens: 0, output_tokens: 0 },
201
+ usage: confirmedUsage ?? { input_tokens: 0, output_tokens: 0 },
202
202
  };
203
203
  }
204
204
 
@@ -212,6 +212,8 @@ interface OpenBlock {
212
212
  argsBuf?: string;
213
213
  argsBufBytes?: number;
214
214
  webSearchArgsEmitted?: boolean;
215
+ /** True once ordinary function-call arguments were emitted to Anthropic SSE. */
216
+ toolArgsEmitted?: boolean;
215
217
  callId?: string;
216
218
  /** Last fixed-size reasoning identity (item + summary/content index) seen by this block. */
217
219
  reasoningPartKey?: string;
@@ -244,6 +246,7 @@ export function responsesSseToAnthropicSse(
244
246
  let open: OpenBlock | null = null;
245
247
  let sawToolUse = false;
246
248
  let webSearchRequests = 0;
249
+ let earlyAnthropicUsage: Rec | undefined;
247
250
  let pingTimer: ReturnType<typeof setInterval> | undefined;
248
251
  let reader: ReadableStreamDefaultReader<Uint8Array> | undefined;
249
252
  const utf8SliceBytes = (value: string, start: number, end: number): number => {
@@ -280,7 +283,7 @@ export function responsesSseToAnthropicSse(
280
283
  const ensureStarted = () => {
281
284
  if (started) return;
282
285
  started = true;
283
- emit("message_start", { type: "message_start", message: messageSnapshot(model) });
286
+ emit("message_start", { type: "message_start", message: messageSnapshot(model, earlyAnthropicUsage) });
284
287
  emit("ping", { type: "ping" });
285
288
  };
286
289
  // Keepalive pings protect remote deployments behind LB/NAT idle timeouts even
@@ -403,8 +406,17 @@ export function responsesSseToAnthropicSse(
403
406
  const handleFrame = (eventName: string, data: Rec) => {
404
407
  switch (eventName) {
405
408
  case "response.created":
406
- // Transport prelude only. Start Anthropic framing on semantic output or completion.
409
+ case "response.in_progress": {
410
+ // Lifecycle preludes do not start Anthropic framing, but some upstreams attach
411
+ // confirmed input usage before semantic output. Retain only its bounded Anthropic
412
+ // projection so message_start can report measurements that already arrived.
413
+ const response = isRec(data.response) ? data.response : {};
414
+ const usage = isRec(response.usage) ? response.usage : undefined;
415
+ if (!started && usage && typeof usage.input_tokens === "number") {
416
+ earlyAnthropicUsage = anthropicUsage(usage);
417
+ }
407
418
  break;
419
+ }
408
420
  case "response.heartbeat":
409
421
  if ((controller.desiredSize ?? 0) > 0) emit("ping", { type: "ping" });
410
422
  break;
@@ -488,6 +500,7 @@ export function responsesSseToAnthropicSse(
488
500
  argsBuf: "",
489
501
  argsBufBytes: 0,
490
502
  webSearchArgsEmitted: false,
503
+ toolArgsEmitted: false,
491
504
  };
492
505
  break;
493
506
  }
@@ -518,6 +531,14 @@ export function responsesSseToAnthropicSse(
518
531
  type: "content_block_delta", index: open.index,
519
532
  delta: { type: "input_json_delta", partial_json: data.delta },
520
533
  });
534
+ open.toolArgsEmitted = true;
535
+ break;
536
+ }
537
+ case "response.function_call_arguments.done": {
538
+ if (!open || open.kind !== "tool_use" || open.bufferWebSearchArgs || open.toolArgsEmitted) break;
539
+ if (typeof data.arguments !== "string" || data.arguments.length === 0) break;
540
+ emit("content_block_delta", { type: "content_block_delta", index: open.index, delta: { type: "input_json_delta", partial_json: data.arguments } });
541
+ open.toolArgsEmitted = true;
521
542
  break;
522
543
  }
523
544
  case "response.output_item.done": {
@@ -565,6 +586,13 @@ export function responsesSseToAnthropicSse(
565
586
  delta: { type: "input_json_delta", partial_json: JSON.stringify(sanitizeWebSearchInput(parsed)) },
566
587
  });
567
588
  open.webSearchArgsEmitted = true;
589
+ } else if (!open.bufferWebSearchArgs && !open.toolArgsEmitted
590
+ && typeof item.arguments === "string" && item.arguments.length > 0) {
591
+ emit("content_block_delta", {
592
+ type: "content_block_delta", index: open.index,
593
+ delta: { type: "input_json_delta", partial_json: item.arguments },
594
+ });
595
+ open.toolArgsEmitted = true;
568
596
  }
569
597
  closeOpenBlock();
570
598
  }
@@ -245,7 +245,7 @@ export async function cmdNativeMainAccount(args: string[], deps: AccountDeps): P
245
245
  }
246
246
  if (noWait) {
247
247
  printStatus({ flowId: startFlowId, ...pending });
248
- console.log("follow up: ocx account main reauth status --flow " + startFlowId);
248
+ if (!wantsJson) console.log("follow up: ocx account main reauth status --flow " + startFlowId);
249
249
  return 0;
250
250
  }
251
251
  // Blocking wait bounded by the service flow expiry (15-minute grant + margin).
@@ -710,10 +710,10 @@ export const CAPABILITIES: readonly Capability[] = [
710
710
  },
711
711
  {
712
712
  command: ["system", "codex-restart"],
713
- summary: "Restart the Codex app-server.",
713
+ summary: "Restart the Codex desktop app and app-servers.",
714
714
  routes: [{ method: "POST", path: "/api/system/codex-restart" }],
715
715
  flags: [
716
- { name: "--yes", value: "boolean", summary: "Required: restarts the operator's running Codex app-server." },
716
+ { name: "--yes", value: "boolean", summary: "Required: fully quits and relaunches the operator's Codex desktop app and restarts its app-servers." },
717
717
  { name: "--json", value: "boolean", summary: "Emit the restart result as JSON." },
718
718
  ],
719
719
  mutates: true,
package/src/cli/combo.ts CHANGED
@@ -15,7 +15,8 @@ const USAGE = `Usage:
15
15
  ocx combo show <id> [--json]
16
16
  ocx combo set <id> --targets <provider/model[:weight],...>
17
17
  [--strategy <failover|round-robin|random|least-used|reset-window>] [--sticky <1-100>]
18
- [--effort <low|medium|high|xhigh|max|ultra|->] [--alias <name|->]
18
+ [--effort <low|medium|high|xhigh|max|ultra|->] [--effort-mode <fallback|force>]
19
+ (force overrides valid client effort and can increase cost/latency) [--alias <name|->]
19
20
  [--native-alias] [--display-name <label|->]
20
21
  [--rename-from <id>] [--json]
21
22
  ocx combo remove <id> --yes [--json]`;
@@ -80,6 +81,10 @@ async function set(argv: string[], deps: RuntimeApiDeps): Promise<void> {
80
81
  if (strategy !== "round-robin") throw new CliUsageError("--sticky applies only to round-robin", USAGE);
81
82
  }
82
83
  const effort = takeOption(args, "--effort");
84
+ const effortMode = takeOption(args, "--effort-mode");
85
+ if (effortMode !== undefined && effortMode !== "fallback" && effortMode !== "force") {
86
+ throw new CliUsageError("--effort-mode must be fallback or force", USAGE);
87
+ }
83
88
  const alias = takeOption(args, "--alias");
84
89
  const nativeAlias = takeFlag(args, "--native-alias");
85
90
  const displayName = takeOption(args, "--display-name");
@@ -91,12 +96,16 @@ async function set(argv: string[], deps: RuntimeApiDeps): Promise<void> {
91
96
  targets: parseTargets(targetsRaw),
92
97
  };
93
98
  if (effort !== undefined) combo.defaultEffort = effort === "-" ? null : effort;
99
+ if (effortMode !== undefined) combo.defaultEffortMode = effortMode;
94
100
  if (alias !== undefined) combo.alias = alias === "-" ? "" : alias;
95
101
  if (nativeAlias) combo.nativeAlias = true;
96
102
  if (displayName !== undefined) combo.displayName = displayName === "-" ? "" : displayName;
97
103
  const current = await runtimeRequest<{ combos?: ComboRow[] }>("/api/combos", {}, deps);
98
104
  const existing = (current.combos ?? []).find(row => row.id === (renameFrom ?? id));
99
105
  if (existing?.imageInput === "disabled") combo.imageInput = "disabled";
106
+ if (effortMode === undefined && existing?.defaultEffortMode === "force") {
107
+ combo.defaultEffortMode = effort === "-" ? "fallback" : "force";
108
+ }
100
109
  const result = await runtimeRequest("/api/combos", {
101
110
  method: "PUT",
102
111
  body: JSON.stringify({ id, combo, ...(renameFrom ? { renameFrom } : {}) }),
@@ -4,7 +4,7 @@ import { getConfigPath, mutatePersistedConfig, readConfigDiagnostics, sanitizeMo
4
4
  import { VISION_REASONING_EFFORTS, isVisionReasoningEffort } from "../reasoning-effort";
5
5
  import type { OcxConfig } from "../types";
6
6
  import { normalizeVisionReasoningForModel } from "../vision/reasoning";
7
- import type { ClientConnectionStatus } from "./connect";
7
+ import type { ServiceApiTokenState } from "../lib/service-secrets";
8
8
  import { CliUsageError, printData, rejectArgs, runCliAction, takeFlag } from "./runtime-api";
9
9
 
10
10
  const USAGE = `Usage:
@@ -38,9 +38,8 @@ const BLOCKED_SEGMENTS = new Set(["__proto__", "prototype", "constructor"]);
38
38
  * `client` block, which is the same defect in miniature: the presence of configuration is not
39
39
  * evidence that the connection works, and a machine whose data-plane token was revoked, rotated
40
40
  * away or deleted would have been labelled `connected: true` while it could not reach the hub at
41
- * all. `collectClientConnectionStatus` is the one reader that knows — it compares the token file's
42
- * fingerprint against the connection record — so the caller passes its answer in and this stays
43
- * pure and testable.
41
+ * all. The read-only projection compares the bounded token file's fingerprint against the
42
+ * connection record, and passes that answer in so this formatter stays pure and testable.
44
43
  *
45
44
  * Synthetic and NOT persisted, for two reasons. `clientConnectionSchema` is `.strict()`, so a
46
45
  * `client.note` field would not validate; and persisted prose drifts from the behaviour it
@@ -49,7 +48,7 @@ const BLOCKED_SEGMENTS = new Set(["__proto__", "prototype", "constructor"]);
49
48
  */
50
49
  export function remoteHubConfigNote(
51
50
  config: OcxConfig,
52
- readConnection: () => Pick<ClientConnectionStatus, "state" | "reason" | "token">,
51
+ readConnection: () => RemoteHubConnectionObservation,
53
52
  ): { connected: boolean; origin: string; note: string } | null {
54
53
  if (config.runtimeRole !== "client" || !config.client) return null;
55
54
  // A thunk, so a standalone or hub install pays nothing: the guard above returns first and the
@@ -67,6 +66,36 @@ export function remoteHubConfigNote(
67
66
  return { connected, origin: config.client.serverUrl, note };
68
67
  }
69
68
 
69
+ export type RemoteHubConnectionObservation = {
70
+ state: "disconnected" | "connected" | "invalid" | "mismatched";
71
+ reason?: string;
72
+ token: "owned" | "missing" | "changed" | "unsafe";
73
+ };
74
+
75
+ export function remoteHubConnectionFromTokenState(
76
+ config: Pick<OcxConfig, "client">,
77
+ tokenState: ServiceApiTokenState,
78
+ ): RemoteHubConnectionObservation {
79
+ const token = tokenState.kind === "absent"
80
+ ? "missing"
81
+ : tokenState.kind === "unsafe"
82
+ ? "unsafe"
83
+ : tokenState.fingerprint === config.client?.tokenFingerprint ? "owned" : "changed";
84
+ return { state: "connected", token };
85
+ }
86
+
87
+ async function readRemoteHubConfigNote(config: OcxConfig): Promise<ReturnType<typeof remoteHubConfigNote>> {
88
+ if (config.runtimeRole !== "client" || !config.client) return null;
89
+ // This display command needs only connection ownership, not lifecycle recovery, catalog
90
+ // readiness, or any write-capable connect machinery. Keep the read on the bounded token
91
+ // observer so a cold `config show` never imports the full connect command graph.
92
+ const { readServiceApiTokenState } = await import("../lib/service-secrets");
93
+ return remoteHubConfigNote(
94
+ config,
95
+ () => remoteHubConnectionFromTokenState(config, readServiceApiTokenState()),
96
+ );
97
+ }
98
+
70
99
  function redact(value: unknown, key = ""): unknown {
71
100
  if (SECRET_KEYS.test(key) && typeof value === "string") return value ? "********" : value;
72
101
  // `client.priorCatalog` is the base64 catalog snapshot connect took before overwriting the
@@ -168,19 +197,7 @@ export async function handleConfigCommand(argv: string[]): Promise<number> {
168
197
  rejectArgs(args, USAGE);
169
198
  const diagnostics = readConfigDiagnostics();
170
199
  const redacted = redact(diagnostics.config);
171
- // Imported here rather than at module scope: `./connect` pulls the whole client lifecycle
172
- // in, and `ocx config get/set` has no use for it.
173
- const { collectClientConnectionStatus } = await import("./connect");
174
- // The readiness probe is declined explicitly. `collectClientConnectionStatus` observes the
175
- // local Codex ladder for a connected client, and observing it spawns `codex debug models`
176
- // under a 45s budget. `ocx config show` reads only `state`, `reason` and `token` from the
177
- // result, so paying for a subprocess here would buy nothing and would quietly turn a
178
- // read-only config dump into a runtime probe. Returning no ladder resolves readiness to
179
- // `unverified`, which is the honest answer for a caller that never asked.
180
- const note = remoteHubConfigNote(
181
- diagnostics.config,
182
- () => collectClientConnectionStatus(undefined, undefined, { supportedEfforts: () => null }),
183
- );
200
+ const note = await readRemoteHubConfigNote(diagnostics.config);
184
201
  // First key, not last: it has to be read before the empty `providers` map that misled a
185
202
  // reader into concluding nothing was configured anywhere.
186
203
  const config = note && redacted && typeof redacted === "object" && !Array.isArray(redacted)
@@ -24,7 +24,7 @@ import {
24
24
  } from "../codex/desired-state";
25
25
  import { syncModelsToCodex } from "../codex/sync";
26
26
  import { collectOrcaCodexHomeDiagnostic } from "../codex/home";
27
- import { restoreNativeCodexAsync } from "../codex/inject";
27
+ import { restoreNativeCodexAsync, type CodexNativeRestoreResult } from "../codex/inject";
28
28
  import { stripGrokConfig } from "../grok/inject";
29
29
  import { handleRestartScopeAfterWrite, readRestartScope, type RestartScope } from "./restart-scope";
30
30
  import { normalizeUpdateChannel, runGuiUpdateWorker } from "../update/job";
@@ -106,6 +106,7 @@ const commandRunners: Record<string, CommandRunner> = {
106
106
  restore: async deps => {
107
107
  const restoreArgs = deps.args.slice(1);
108
108
  const restoreJson = takeFlag(restoreArgs, "--json");
109
+ const removeProviderTable = takeFlag(restoreArgs, "--remove-codex-provider-table");
109
110
  if (restoreArgs[0] === "back") {
110
111
  // Reverse switch: re-point plain `codex` at the RUNNING proxy without touching its
111
112
  // lifecycle — the counterpart of `ocx restore`. Start/stop triggers are unchanged;
@@ -146,6 +147,9 @@ const commandRunners: Record<string, CommandRunner> = {
146
147
  const target = collectOrcaCodexHomeDiagnostic();
147
148
  return emitBack(true, `Plain \`codex\` now routes through opencodex in ${target.effectiveCodexHome} (undo with: ocx restore).`, 0);
148
149
  }
150
+ if (removeProviderTable && !restoreJson) {
151
+ console.log("⚠️ Removing [model_providers.opencodex] means conversations already tagged opencodex will stop opening.");
152
+ }
149
153
  const desired = setIntegrationEnabled("codex", false);
150
154
  if (!desired.ok) {
151
155
  if (restoreJson) {
@@ -191,9 +195,9 @@ const commandRunners: Record<string, CommandRunner> = {
191
195
  return grokCode;
192
196
  }
193
197
  }
194
- let r: { success: boolean; message: string };
198
+ let r: CodexNativeRestoreResult | Pick<CodexNativeRestoreResult, "success" | "message">;
195
199
  try {
196
- r = await restoreNativeCodexAsync({ revalidateDesiredState: true });
200
+ r = await restoreNativeCodexAsync({ revalidateDesiredState: true, removeProviderTable });
197
201
  } catch (err) {
198
202
  r = { success: false, message: err instanceof Error ? err.message : String(err) };
199
203
  }
@@ -232,7 +236,16 @@ const commandRunners: Record<string, CommandRunner> = {
232
236
  code = 1;
233
237
  }
234
238
  if (r.success) {
235
- console.log("Codex integration is OFF and plain `codex` now runs natively. Switch back with: ocx restore back");
239
+ const retained = "retainedCodexProviderTable" in r ? r.retainedCodexProviderTable : undefined;
240
+ if (retained) {
241
+ console.log("Codex integration is OFF and plain `codex` now runs natively.");
242
+ console.log("The following lines remain in $CODEX_HOME/config.toml because conversations already tagged opencodex resolve their provider only through this table:");
243
+ console.log(retained.lines.join("\n"));
244
+ console.log(`Follow-up: ${retained.followUp}`);
245
+ console.log("Switch back with: ocx restore back");
246
+ } else {
247
+ console.log("Codex integration is OFF and plain `codex` now runs natively. Switch back with: ocx restore back");
248
+ }
236
249
  console.log(`Note: ${OCX_NATIVE_REPLAY_RECOVERY_NOTE}`);
237
250
  } else {
238
251
  console.error("Plain `codex` was not fully restored. Inspect $CODEX_HOME/config.toml before using native Codex.");
package/src/cli/index.ts CHANGED
@@ -13,9 +13,22 @@ try {
13
13
  /* best-effort */
14
14
  }
15
15
  }
16
- import { currentExternalCodexModelProvider, restoreNativeCodex, restoreNativeCodexAsync, shouldInjectApiAuthHeader } from "../codex/inject";
16
+ import {
17
+ currentExternalCodexModelProvider,
18
+ restoreNativeCodex,
19
+ restoreNativeCodexAsync,
20
+ shouldInjectApiAuthHeader,
21
+ } from "../codex/inject";
22
+ // Straight from the owning modules rather than the facade: these are teardown-reporting
23
+ // helpers, not part of the injection surface, and `inject.ts` sits under a size cap that
24
+ // exists to stop it collecting exactly this kind of passthrough.
25
+ import { readOcxProviderTableBlock } from "../codex/inject/remove";
26
+ import {
27
+ describeRetainedCodexProviderTable,
28
+ type RetainedCodexProviderTable,
29
+ } from "../codex/inject/restore";
17
30
  import { stripGrokConfig } from "../grok/inject";
18
- import { STOP_HISTORY_INCOMPLETE_EXIT_CODE } from "../update/stop-contract.mjs";
31
+ import { STOP_HISTORY_DEFERRED_EXIT_CODE, STOP_HISTORY_INCOMPLETE_EXIT_CODE } from "../update/stop-contract.mjs";
19
32
  import {
20
33
  describeHistoryJobFailure,
21
34
  resolveCodexHistoryJobTarget,
@@ -46,6 +59,7 @@ import {
46
59
  isPendingTeardownAbandoned,
47
60
  listPendingTeardowns,
48
61
  pendingTeardownPathFor,
62
+ pendingTeardownsAreExactly,
49
63
  quarantinePendingTeardown,
50
64
  } from "../config/pending-teardown";
51
65
  import { collectStatus, hubStatusLines, remoteHubBannerLine, remoteHubStatusLines, unusedProxyWarningLines } from "./status";
@@ -118,6 +132,12 @@ function reportShellHookFailure(result: { state: "installed" | "absent" | "faile
118
132
  console.warn(" Check ~/.zshrc for the '# opencodex claude-env hook' block.");
119
133
  }
120
134
 
135
+ function reportRetainedCodexProviderTable(retained: RetainedCodexProviderTable): void {
136
+ console.log(` ${describeRetainedCodexProviderTable(retained)}`);
137
+ console.log(" Retained config lines:");
138
+ for (const line of retained.lines) console.log(` ${line}`);
139
+ }
140
+
121
141
  async function refreshOwnedRaycastCatalog(
122
142
  config: ReturnType<typeof loadConfig>,
123
143
  port: number,
@@ -785,20 +805,44 @@ async function handleRestartStartWhenStopped(): Promise<boolean | "skipped"> {
785
805
  *
786
806
  * The distinction exists because `ocx update` must proceed for the first and abort for the
787
807
  * second, and it can only see an exit code (#3008).
808
+ *
809
+ * `historyDeferred` is the third kind (#4718). The Codex history preflight refuses BEFORE
810
+ * the config half runs, so nothing was restored at all: config, catalog, history and
811
+ * provenance are untouched and the client is still routed at the proxy that just stopped.
812
+ * Like `historyOnly` the proxy is genuinely down, so an update may replace package files.
813
+ * Unlike `historyOnly` the obligation was not performed, so the receipt must survive.
788
814
  */
789
- async function restoreSharedClientStateAfterStop(): Promise<{ historyOnly: boolean; other: boolean }> {
815
+ async function restoreSharedClientStateAfterStop(): Promise<{ historyOnly: boolean; historyDeferred: boolean; other: boolean }> {
790
816
  let historyOnly = false;
817
+ let historyDeferred = false;
791
818
  let other = false;
792
819
  try {
793
820
  const result = await restoreNativeCodexAsync();
794
- if (result.success) console.log(`↩️ ${result.message}`);
821
+ if (result.success) {
822
+ console.log(`↩️ ${result.message}`);
823
+ if (result.retainedCodexProviderTable) {
824
+ reportRetainedCodexProviderTable(result.retainedCodexProviderTable);
825
+ }
826
+ }
795
827
  else {
796
828
  // Codex history is the one restore whose failure leaves the runtime consistent: the
797
829
  // manifest is retained and the routed metadata is untouched. Config and catalog are
798
830
  // not — a client reads those, so their failure is a real teardown failure.
799
831
  const artifacts = result.artifacts;
800
832
  const configOrCatalogFailed = artifacts.config.state === "failed" || artifacts.catalog.state === "failed";
801
- if (!configOrCatalogFailed && artifacts.history.state === "failed") historyOnly = true;
833
+ // A preflight refusal reports every artifact as `skipped` because none of them were
834
+ // attempted. Reading the states alone cannot tell that apart from an ownership
835
+ // refusal, so the structured reason carries it and the states are still required to
836
+ // agree — a refusal that somehow reports a failed artifact is not this case.
837
+ // A degraded restore has no refusal reason and reports config as partial, so it cannot
838
+ // enter this branch: its config obligation was discharged and the stop receipt must be
839
+ // released rather than preserved.
840
+ const preflightRefused = result.historyPreflightRefusal !== undefined
841
+ && artifacts.config.state === "skipped"
842
+ && artifacts.catalog.state === "skipped"
843
+ && artifacts.history.state === "skipped";
844
+ if (preflightRefused) historyDeferred = true;
845
+ else if (!configOrCatalogFailed && artifacts.history.state === "failed") historyOnly = true;
802
846
  else other = true;
803
847
  console.error(`⚠️ ${result.message}`);
804
848
  }
@@ -816,7 +860,7 @@ async function restoreSharedClientStateAfterStop(): Promise<{ historyOnly: boole
816
860
  other = true;
817
861
  console.error(`⚠️ Grok config restore failed: ${error instanceof Error ? error.message : String(error)}`);
818
862
  }
819
- return { historyOnly, other };
863
+ return { historyOnly, historyDeferred, other };
820
864
  }
821
865
 
822
866
  async function handleStop() {
@@ -860,6 +904,11 @@ async function handleStop() {
860
904
  };
861
905
  let stopFailed = false;
862
906
  let historyOnlyFailure = false;
907
+ /**
908
+ * Obligations this run deliberately kept because the Codex history preflight refused
909
+ * before restoring anything (#4718). Non-null selects the deferred exit code.
910
+ */
911
+ let historyDeferredNonces: string[] | null = null;
863
912
  // Only Task Scheduler respawns after a successful stop (#764), so only it earns the
864
913
  // restart-window wait; launchd, systemd and WinSW are down when they say so.
865
914
  let schedulerCanRespawn = false;
@@ -1138,6 +1187,7 @@ async function handleStop() {
1138
1187
  }
1139
1188
  const restore = await restoreSharedClientStateAfterStop();
1140
1189
  if (restore.other) stopFailed = true;
1190
+ else if (restore.historyDeferred) historyDeferredNonces = teardownNonce ? [teardownNonce, ...recoveredNonces] : recoveredNonces;
1141
1191
  else if (restore.historyOnly) historyOnlyFailure = true;
1142
1192
  // The obligation is discharged whether or not history metadata finalized: config and
1143
1193
  // catalog are what a client reads, and `restore.other` already fails the stop.
@@ -1145,7 +1195,16 @@ async function handleStop() {
1145
1195
  // Each nonce names its own file, so a clear can only ever remove the obligation it
1146
1196
  // names — never one a concurrent stop wrote. Both this run's claim and every inherited
1147
1197
  // receipt it proved discharged are released together.
1148
- if (!restore.other) {
1198
+ //
1199
+ // A history-preflight refusal is the exception: it restored nothing, so there is
1200
+ // nothing to discharge. Clearing here would drop a real obligation on the floor and
1201
+ // leave the client config pointing at a proxy that is gone, with nothing on disk
1202
+ // saying so — which is the whole failure the receipt exists to prevent (#4718).
1203
+ if (restore.historyDeferred) {
1204
+ console.error(" The shared teardown was refused before it changed anything, so it is still owed.");
1205
+ console.error(" Its receipt is preserved; run 'ocx stop' again once Codex is closed to retry the restore.");
1206
+ }
1207
+ if (!restore.other && !restore.historyDeferred) {
1149
1208
  const discharged = teardownNonce ? [teardownNonce, ...recoveredNonces] : recoveredNonces;
1150
1209
  for (const nonce of discharged) {
1151
1210
  // A receipt that survives its discharge re-triggers recovery forever, so a failed
@@ -1185,6 +1244,17 @@ async function handleStop() {
1185
1244
  // still wins: it is the stronger signal.
1186
1245
  if (stopFailed) process.exitCode = 1;
1187
1246
  else if (historyOnlyFailure) process.exitCode = STOP_HISTORY_INCOMPLETE_EXIT_CODE;
1247
+ // The deferred code says "the only obligations left are the ones I just decided to
1248
+ // keep". It is read across a process boundary by an updater that will replace package
1249
+ // files on the strength of it, so this run has to be able to prove the claim: if any
1250
+ // other obligation is sitting in the home — quarantined, or a concurrent stop's — the
1251
+ // claim is false and the ordinary failure code is the honest answer. That is also the
1252
+ // behaviour before #4718, so the fallback loses nothing that used to work.
1253
+ else if (historyDeferredNonces) {
1254
+ process.exitCode = pendingTeardownsAreExactly(historyDeferredNonces)
1255
+ ? STOP_HISTORY_DEFERRED_EXIT_CODE
1256
+ : 1;
1257
+ }
1188
1258
  return !stopFailed;
1189
1259
  }
1190
1260
 
@@ -1314,6 +1384,9 @@ async function handleUninstall() {
1314
1384
  await runStep("native Codex restored", async () => {
1315
1385
  const r = await restoreNativeCodexAsync();
1316
1386
  if (!r.success) throw new Error(r.message);
1387
+ if (r.retainedCodexProviderTable) {
1388
+ reportRetainedCodexProviderTable(r.retainedCodexProviderTable);
1389
+ }
1317
1390
  });
1318
1391
 
1319
1392
  await runStep("Grok Build config restored", () => {
@@ -1460,6 +1533,18 @@ async function handleStatus() {
1460
1533
  console.log(` Codex autostart: ${status.json.codexAutostart ? "enabled" : "disabled"}${local}`);
1461
1534
  console.log(` Restart safety: ${startupHealthSummary(status.json.startup)}${local}`);
1462
1535
  console.log(` ${formatStartupRoutingDetail(status.json.startup)}${local}`);
1536
+ if (status.json.startup.routingKind === "native") {
1537
+ let retainedProviderTable = false;
1538
+ try {
1539
+ retainedProviderTable = readOcxProviderTableBlock() !== null;
1540
+ } catch {
1541
+ // The routing snapshot owns unreadable-config reporting. A later read race must not
1542
+ // turn this diagnostic command into a teardown failure.
1543
+ }
1544
+ if (retainedProviderTable) {
1545
+ console.log(` ⚠️ Codex provider table retained${local}: [model_providers.opencodex] remains while root routing is native. Remove with 'ocx restore --remove-codex-provider-table'; tagged conversations will stop opening.`);
1546
+ }
1547
+ }
1463
1548
  console.log(` Service: ${status.json.service.summary}${local}`);
1464
1549
  console.log(` ${status.json.codexShim.summary}${local}`);
1465
1550
  console.log(` Codex runtime: ${status.json.codexRuntime.path}${local}`);
@@ -407,7 +407,8 @@ export const CLI_COMMANDS: CliCommandEntry[] = [
407
407
  "Ensures the proxy is running, then execs `claude` with ANTHROPIC_BASE_URL/ANTHROPIC_AUTH_TOKEN,",
408
408
  "CLAUDE_CODE_ENABLE_GATEWAY_MODEL_DISCOVERY=1 and model slots from config.claudeCode.",
409
409
  "When Claude routing is explicitly disabled, it launches natively after removing proven OpenCodex-owned proxy state.",
410
- "Routed models appear in the native /model picker with stable claude-opus-4-8-2026MMDD slot aliases (Claude Code >= 2.1.129).",
410
+ "Routed models appear in the native /model picker with stable claude-opus-4-8-YYYYMMDD slot aliases,",
411
+ "where the year runs 2026-2035 and 2026 slots are allocated first (Claude Code >= 2.1.129).",
411
412
  "Older versions: pick models via ANTHROPIC_MODEL or /model <id> directly (any string passes through).",
412
413
  "User-exported ANTHROPIC_* variables take precedence for routed launches; native fallback removes only proven OpenCodex-owned proxy values.",
413
414
  "",