@bitkyc08/opencodex 2.82.0 → 2.84.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 (266) hide show
  1. package/gui/dist/assets/App-CZtqiGcw.js +56 -0
  2. package/gui/dist/assets/Tray-C9TGtwad.js +1 -0
  3. package/gui/dist/assets/{index-BAbcj51J.css → index-BB6uK6tU.css} +1 -1
  4. package/gui/dist/assets/index-CTm4l-HQ.js +94 -0
  5. package/gui/dist/assets/{quota-summary-DTBC8n7-.js → quota-summary-BbEeRfPY.js} +1 -1
  6. package/gui/dist/index.html +2 -2
  7. package/package.json +2 -2
  8. package/src/adapters/coding-agent/protocol.ts +15 -8
  9. package/src/adapters/cursor/catalog.ts +2 -2
  10. package/src/adapters/cursor/effort-map.ts +1 -1
  11. package/src/adapters/devin/live-models.ts +2 -2
  12. package/src/adapters/google-truncation.ts +22 -0
  13. package/src/adapters/google.ts +7 -3
  14. package/src/adapters/ollama-native.ts +47 -15
  15. package/src/adapters/openai-chat/messages.ts +8 -4
  16. package/src/adapters/openai-responses/empty-assistant.ts +32 -0
  17. package/src/adapters/openai-responses/passthrough.ts +5 -0
  18. package/src/adapters/openai-responses/plaintext-reasoning-notice.ts +85 -0
  19. package/src/adapters/openai-responses/reasoning.ts +11 -1
  20. package/src/adapters/openai-responses/request-strips.ts +10 -2
  21. package/src/adapters/qoder/adapter.ts +16 -14
  22. package/src/bridge/errors.ts +26 -1
  23. package/src/bridge.ts +1 -1
  24. package/src/claude/context-windows.ts +57 -4
  25. package/src/claude/desktop-picker.ts +1 -1
  26. package/src/claude/intercept/connect-proxy.ts +10 -3
  27. package/src/claude/intercept/picker-ca-persistence.ts +3 -1
  28. package/src/claude/intercept/picker-listener.ts +49 -1
  29. package/src/claude/intercept/settings-no-proxy.ts +146 -0
  30. package/src/claude/intercept/settings.ts +101 -9
  31. package/src/cli/account-api.ts +65 -0
  32. package/src/cli/account-auth.ts +44 -12
  33. package/src/cli/account.ts +41 -5
  34. package/src/cli/capabilities-access-remote.ts +8 -0
  35. package/src/cli/capabilities-base.ts +13 -1
  36. package/src/cli/catalog-command-result.ts +2 -1
  37. package/src/cli/claude.ts +15 -6
  38. package/src/cli/config-command.ts +8 -2
  39. package/src/cli/debug.ts +8 -0
  40. package/src/cli/dispatch.ts +16 -3
  41. package/src/cli/export-command.ts +14 -1
  42. package/src/cli/hub-disable.ts +99 -0
  43. package/src/cli/hub.ts +4 -2
  44. package/src/cli/index.ts +13 -4
  45. package/src/cli/link-child.ts +104 -0
  46. package/src/cli/link-enrollment.ts +96 -0
  47. package/src/cli/link.ts +39 -24
  48. package/src/cli/observe-stream.ts +6 -1
  49. package/src/cli/observe.ts +10 -2
  50. package/src/cli/provider.ts +12 -0
  51. package/src/cli/registry.ts +5 -4
  52. package/src/cli/resolve.ts +1 -1
  53. package/src/cli/runtime-api.ts +27 -2
  54. package/src/cli/status.ts +104 -20
  55. package/src/client/connect.ts +39 -4
  56. package/src/client/hub-client.ts +14 -7
  57. package/src/client/hub-state.ts +25 -14
  58. package/src/client/link-home.ts +248 -0
  59. package/src/client/link-management-relay.ts +61 -0
  60. package/src/client/link-relay.ts +6 -1
  61. package/src/client/link-status.ts +14 -0
  62. package/src/client/machine-api.ts +14 -7
  63. package/src/client/machine-listener.ts +29 -4
  64. package/src/clients/config-export/model-metadata.ts +16 -0
  65. package/src/clients/config-export/zcode-store.ts +46 -11
  66. package/src/clients/config-export/zcode.ts +3 -5
  67. package/src/clients/config-export.ts +47 -17
  68. package/src/clients/omo-role-models.ts +6 -3
  69. package/src/codex/agent-role-models.ts +26 -8
  70. package/src/codex/auth-api/account-list.ts +17 -2
  71. package/src/codex/auth-api/account-selection.ts +17 -7
  72. package/src/codex/auth-api/pool-mode-gate.ts +4 -5
  73. package/src/codex/auth-api/routes.ts +5 -1
  74. package/src/codex/auth-context.ts +8 -3
  75. package/src/codex/autostart-health.ts +3 -1
  76. package/src/codex/catalog/bundled.ts +5 -1
  77. package/src/codex/catalog/effort.ts +2 -2
  78. package/src/codex/catalog/metadata.ts +4 -2
  79. package/src/codex/catalog/native-models.ts +17 -3
  80. package/src/codex/catalog/provider-models.ts +25 -0
  81. package/src/codex/desktop-app/darwin.ts +1 -1
  82. package/src/codex/desktop-app/handoff.ts +1 -1
  83. package/src/codex/desktop-app/linux.ts +1 -1
  84. package/src/codex/desktop-app/lock.ts +1 -1
  85. package/src/codex/desktop-app/types.ts +1 -1
  86. package/src/codex/desktop-app/windows.ts +1 -1
  87. package/src/codex/desktop-app-restart.ts +1 -1
  88. package/src/codex/main-account.ts +6 -7
  89. package/src/codex/native-profile-startup.ts +4 -1
  90. package/src/codex/prompt-journal.ts +20 -2
  91. package/src/codex/prompt-layers/encoding.ts +49 -0
  92. package/src/codex/prompt-layers/import-source.ts +30 -0
  93. package/src/codex/prompt-layers/toml-edit.ts +269 -37
  94. package/src/codex/prompt-layers/toml-read.ts +52 -4
  95. package/src/codex/prompt-layers.ts +383 -20
  96. package/src/codex/prompt-lock-claim.ts +13 -3
  97. package/src/codex/prompt-lock-evidence.ts +3 -3
  98. package/src/codex/prompt-lock-owner.ts +14 -6
  99. package/src/codex/prompt-text-probe.ts +34 -5
  100. package/src/codex/quota.ts +13 -6
  101. package/src/codex/role-auto-assign.ts +2 -5
  102. package/src/codex/routing/active-account.ts +21 -0
  103. package/src/codex/routing.ts +1 -1
  104. package/src/codex/runtime.ts +196 -8
  105. package/src/codex/shim-overlay.ts +8 -4
  106. package/src/codex/shim.ts +1 -1
  107. package/src/combos/failover.ts +3 -0
  108. package/src/config/api-key-reconcile.ts +34 -0
  109. package/src/config/diagnostics.ts +16 -0
  110. package/src/config/feature-flags.ts +5 -0
  111. package/src/config/live-reconcile.ts +43 -29
  112. package/src/config/live-topology.ts +29 -0
  113. package/src/config/load-degrade.ts +2 -1
  114. package/src/config/paths.ts +3 -0
  115. package/src/config/salvage.ts +3 -1
  116. package/src/config/schema/config-schema.ts +2 -0
  117. package/src/generated/compatibility-version.json +373 -217
  118. package/src/grok/grpc-web.ts +23 -1
  119. package/src/grok/inject.ts +245 -221
  120. package/src/grok/reset-coupon-ledger.ts +293 -57
  121. package/src/grok/reset-coupons.ts +15 -6
  122. package/src/integrations/cursor-detect.ts +1 -1
  123. package/src/integrations/mutation-plan.ts +13 -7
  124. package/src/integrations/registry.ts +45 -11
  125. package/src/integrations/state.ts +19 -7
  126. package/src/integrations/target.ts +42 -5
  127. package/src/integrations/writer.ts +14 -6
  128. package/src/lib/app-owned-memory-stores.ts +4 -0
  129. package/src/lib/app-owned-memory.ts +11 -0
  130. package/src/lib/errors.ts +25 -0
  131. package/src/lib/json-byte-size.ts +31 -0
  132. package/src/lib/optional-shutdown-hooks.ts +35 -0
  133. package/src/lib/outbound-credential-registry.ts +210 -0
  134. package/src/lib/passive-read.ts +35 -0
  135. package/src/lib/redact.ts +2 -2
  136. package/src/lib/shadow-call.ts +44 -5
  137. package/src/link/codex-settings.ts +155 -0
  138. package/src/link/management-routes.ts +89 -0
  139. package/src/link/remote-probe.ts +38 -0
  140. package/src/link/ssh-argv.ts +26 -5
  141. package/src/link/ssh-runner.ts +58 -27
  142. package/src/link/status-projection.ts +22 -4
  143. package/src/link/supervisor.ts +144 -49
  144. package/src/link/tunnel-state.ts +11 -4
  145. package/src/oauth/anthropic-continuity.ts +4 -3
  146. package/src/oauth/anthropic-identity.ts +13 -2
  147. package/src/oauth/anthropic-model-quota.ts +13 -1
  148. package/src/oauth/anthropic-routing.ts +230 -23
  149. package/src/oauth/anthropic.ts +18 -4
  150. package/src/oauth/google-antigravity.ts +24 -7
  151. package/src/oauth/index.ts +25 -16
  152. package/src/oauth/kiro-account-identity.ts +106 -0
  153. package/src/oauth/kiro.ts +48 -5
  154. package/src/oauth/meta-muse-device.ts +1 -1
  155. package/src/oauth/plan.ts +29 -0
  156. package/src/oauth/store.ts +40 -3
  157. package/src/oauth/types.ts +3 -1
  158. package/src/plugins/loader.ts +11 -0
  159. package/src/providers/anthropic-billing-ownership.ts +74 -0
  160. package/src/providers/anthropic-billing-recovery.ts +31 -0
  161. package/src/providers/anthropic-billing-refusal.ts +55 -0
  162. package/src/providers/anthropic-fast.ts +1 -1
  163. package/src/providers/anthropic-reset-admission.ts +62 -0
  164. package/src/providers/anthropic-reset-grant-ledger.ts +29 -7
  165. package/src/providers/api-key-resolve.ts +17 -2
  166. package/src/providers/devin-cli-authmode-migration.ts +1 -1
  167. package/src/providers/devin-provider-merge-migration.ts +1 -1
  168. package/src/providers/github-copilot-auto.ts +169 -0
  169. package/src/providers/key-failover.ts +52 -5
  170. package/src/providers/model-presets.ts +1 -1
  171. package/src/providers/model-rename-startup.ts +5 -3
  172. package/src/providers/muse-subscription-usage.ts +1 -1
  173. package/src/providers/quota/account-cache.ts +11 -0
  174. package/src/providers/quota/anthropic-account-quota.ts +4 -2
  175. package/src/providers/quota/report-cache.ts +14 -0
  176. package/src/providers/quota/vendor-probes-oauth.ts +3 -3
  177. package/src/providers/quota.ts +4 -1
  178. package/src/providers/registry/entries-core.ts +20 -20
  179. package/src/providers/registry/entries-extended.ts +4 -1
  180. package/src/providers/registry/model-seeds.ts +20 -3
  181. package/src/providers/request-pacing.ts +9 -0
  182. package/src/providers/stale-no-vision-seed-migration.ts +72 -0
  183. package/src/providers/xai-fast-model.ts +1 -1
  184. package/src/responses/compaction.ts +14 -0
  185. package/src/responses/parser.ts +7 -1
  186. package/src/server/auth-cors.ts +42 -0
  187. package/src/server/configured-credentials.ts +194 -0
  188. package/src/server/inbound-body-admission.ts +24 -7
  189. package/src/server/index/link-listener.ts +30 -17
  190. package/src/server/index/link-management.ts +58 -0
  191. package/src/server/index/optional-listeners.ts +5 -1
  192. package/src/server/index/serve-options.ts +21 -2
  193. package/src/server/index.ts +1 -1
  194. package/src/server/lifecycle.ts +33 -5
  195. package/src/server/management/agent-settings-routes.ts +28 -10
  196. package/src/server/management/anthropic-reset-grant-routes.ts +17 -1
  197. package/src/server/management/codex-agent-role-routes.ts +19 -6
  198. package/src/server/management/codex-prompt-routes.ts +136 -5
  199. package/src/server/management/codex-role-auto-assign.ts +1 -1
  200. package/src/server/management/grok-coupon-routes.ts +333 -57
  201. package/src/server/management/link-enrollment.ts +192 -0
  202. package/src/server/management/link-route-state.ts +185 -0
  203. package/src/server/management/link-routes.ts +92 -390
  204. package/src/server/management/model-rows.ts +3 -1
  205. package/src/server/management/provider-overwrite-carry.ts +10 -0
  206. package/src/server/management/provider-routes.ts +1 -0
  207. package/src/server/management/route-registry.ts +7 -5
  208. package/src/server/management/shared.ts +3 -0
  209. package/src/server/management-api.ts +3 -1
  210. package/src/server/messages-native.ts +68 -42
  211. package/src/server/relay-eager.ts +4 -0
  212. package/src/server/relay.ts +18 -7
  213. package/src/server/request-log-failure-attribution.ts +3 -0
  214. package/src/server/request-log-filter.ts +6 -1
  215. package/src/server/request-log-terminal-status.ts +243 -0
  216. package/src/server/request-log.ts +15 -118
  217. package/src/server/responses/adapter-continuation.ts +9 -1
  218. package/src/server/responses/adapter-dispatch.ts +293 -81
  219. package/src/server/responses/code-mode-collab-surface.ts +205 -0
  220. package/src/server/responses/codex-ws-exchange.ts +147 -6
  221. package/src/server/responses/codex-ws-wire.ts +203 -0
  222. package/src/server/responses/collaboration.ts +36 -7
  223. package/src/server/responses/compact.ts +129 -50
  224. package/src/server/responses/context-overflow.ts +13 -1
  225. package/src/server/responses/core-codex-account.ts +105 -1
  226. package/src/server/responses/core-combo-failure.ts +14 -0
  227. package/src/server/responses/core-combo.ts +7 -2
  228. package/src/server/responses/core-errors.ts +29 -1
  229. package/src/server/responses/core-opaque-recovery.ts +5 -5
  230. package/src/server/responses/core-options.ts +2 -0
  231. package/src/server/responses/core.ts +6 -6
  232. package/src/server/responses/deferred-collab-surface.ts +158 -0
  233. package/src/server/responses/deferred-send-reporter.ts +25 -0
  234. package/src/server/responses/empty-completion-guard.ts +18 -2
  235. package/src/server/responses/fetch-helpers.ts +12 -1
  236. package/src/server/responses/jev-model-invoke.ts +1 -1
  237. package/src/server/responses/passthrough-delivery.ts +4 -1
  238. package/src/server/responses/passthrough-dispatch.ts +130 -64
  239. package/src/server/responses/passthrough-execution.ts +3 -3
  240. package/src/server/responses/request-prepare.ts +6 -4
  241. package/src/server/responses/request-transport.ts +131 -13
  242. package/src/server/responses/run-turn-execution.ts +13 -5
  243. package/src/server/responses/sidecar-execution.ts +16 -2
  244. package/src/server/responses/spend-storage-error.ts +33 -0
  245. package/src/server/responses/ws-upstream.ts +9 -1
  246. package/src/server/startup-action-control.ts +22 -2
  247. package/src/server/startup-health-cache.ts +4 -0
  248. package/src/server/system-env-shell.ts +7 -2
  249. package/src/server/system-env.ts +11 -1
  250. package/src/service/cli.ts +5 -1
  251. package/src/service/desktop-startup.ts +147 -34
  252. package/src/service/desktop-supervision.d.mts +27 -1
  253. package/src/service/desktop-supervision.mjs +111 -5
  254. package/src/service/health-sentinel-launch.ts +176 -0
  255. package/src/service/health-sentinel-main.ts +447 -0
  256. package/src/service/health-sentinel-policy.ts +241 -0
  257. package/src/service/health-sentinel-probe.ts +54 -0
  258. package/src/service/health-sentinel-windows.ts +87 -0
  259. package/src/service/linux-linger.ts +20 -0
  260. package/src/types/config.ts +5 -0
  261. package/src/types/request.ts +5 -0
  262. package/src/usage/expected-prices.ts +5 -1
  263. package/src/usage/log.ts +19 -1
  264. package/gui/dist/assets/App-Ds_qL7o8.js +0 -54
  265. package/gui/dist/assets/Tray-BqHF5Trd.js +0 -1
  266. package/gui/dist/assets/index-D3FBCNiP.js +0 -94
@@ -1,6 +1,6 @@
1
1
  // ZCode personal provider store export.
2
2
  import type { ExportContext, ManagedContribution, ManagedFragment } from "./contracts";
3
- import { authoritativeContextWindow, inputModalitiesForClient, normalizeExportModels } from "./model-metadata";
3
+ import { authoritativeContextWindow, inputModalitiesForClient, normalizeExportModels, zcodeSelectableEfforts } from "./model-metadata";
4
4
  import { OPENCODE_PROVIDER_ID, LOOPBACK_API_KEY_PLACEHOLDER } from "./constants";
5
5
  import { formatSelectorConjunction } from "../../integrations/merge";
6
6
 
@@ -49,10 +49,30 @@ export interface ZcodeStoreProviderRule {
49
49
  };
50
50
  }
51
51
 
52
+ /**
53
+ * One model rule, as the client's own store loader accepts it.
54
+ *
55
+ * The capability fields are the client's schema, not ours: ZCode 3.14's CLI
56
+ * bundle parses each `providerModelRules` entry with a strict object whose
57
+ * `config.properties` admits `inputFormat.{supportsText..supportsPdf}` (booleans)
58
+ * and whose `config.optionSpecs` admits `reasoningLevel.values` (non-empty,
59
+ * deduplicated) plus an optional compiled `map`. With no `map`, the chosen level
60
+ * is validated against `values` and forwarded verbatim to the wire field the
61
+ * `openai-responses` api type uses — `reasoning.effort`, which is exactly the
62
+ * Codex vocabulary our ladder already carries, so no mapping is emitted.
63
+ */
52
64
  export interface ZcodeStoreModelRule {
53
65
  providerId: string;
54
66
  modelId: string;
55
- config: { properties: { contextWindow: number } };
67
+ config: {
68
+ properties: {
69
+ contextWindow?: number;
70
+ inputFormat?: { supportsImage: true };
71
+ };
72
+ optionSpecs?: {
73
+ reasoningLevel: { values: string[] };
74
+ };
75
+ };
56
76
  }
57
77
 
58
78
  /**
@@ -77,7 +97,7 @@ export function zcodeStoreSchemaEstablished(parsed: unknown): boolean {
77
97
  * carrying only the model would match another provider's rule for the same
78
98
  * model. The conjunction grammar reserves `,` and `]`, and a model id holding
79
99
  * either cannot be addressed unambiguously — that row ships without its
80
- * context window rather than with a selector that points somewhere else.
100
+ * model rule rather than with a selector that points somewhere else.
81
101
  */
82
102
  function modelRuleSelector(modelId: string): string | null {
83
103
  return formatSelectorConjunction([
@@ -128,12 +148,18 @@ export function buildZcodeStoreProviderRule(ctx: ExportContext): ZcodeStoreProvi
128
148
 
129
149
  /**
130
150
  * Everything this project owns inside the store: one provider rule, plus one
131
- * model rule per row that has an authoritative context window.
151
+ * model rule per row that has anything authoritative to assert.
132
152
  *
133
- * A row without one ships no model rule at all, which is the same
134
- * authoritative-window rule the legacy block follows — an emitted stand-in
135
- * would be a guessed capability. Reasoning ladders have no counterpart here
136
- * that this project has observed the client produce, so none is written.
153
+ * A model rule now exists when the row has any of: an authoritative context
154
+ * window, image input, or a selectable effort ladder. A row with none of the
155
+ * three ships no rule at all, which is the same no-guessed-capability rule the
156
+ * legacy block follows. Image input is asserted only when the catalog declares
157
+ * it — `supportsImage: true` is what gates the client's attachment UI, and a
158
+ * text-only row keeps the client's own defaults rather than being pinned to
159
+ * `false`. `reasoningLevel.values` carries the same ladder (minus the `none`
160
+ * sentinel) the legacy block's `reasoning.variants` carries, so both surfaces
161
+ * offer the picker the catalog declared instead of the store silently losing
162
+ * reasoning and vision the way #5348 lost the provider itself.
137
163
  */
138
164
  export function buildZcodeStoreContribution(ctx: ExportContext): ManagedContribution {
139
165
  const fragments: ManagedFragment[] = [{
@@ -141,15 +167,24 @@ export function buildZcodeStoreContribution(ctx: ExportContext): ManagedContribu
141
167
  value: buildZcodeStoreProviderRule(ctx),
142
168
  }];
143
169
  for (const model of normalizeExportModels(ctx.models)) {
144
- if (inputModalitiesForClient("pi", model.inputModalities) === null) continue;
170
+ const input = inputModalitiesForClient("pi", model.inputModalities);
171
+ if (input === null) continue;
145
172
  const contextWindow = authoritativeContextWindow(model.contextWindow);
146
- if (contextWindow === undefined) continue;
173
+ const supportsImage = input.includes("image");
174
+ const efforts = zcodeSelectableEfforts(model.reasoningEfforts);
175
+ if (contextWindow === undefined && !supportsImage && efforts === undefined) continue;
147
176
  const selector = modelRuleSelector(model.namespaced);
148
177
  if (selector === null) continue;
149
178
  const rule: ZcodeStoreModelRule = {
150
179
  providerId: OPENCODE_PROVIDER_ID,
151
180
  modelId: model.namespaced,
152
- config: { properties: { contextWindow } },
181
+ config: {
182
+ properties: {
183
+ ...(contextWindow !== undefined ? { contextWindow } : {}),
184
+ ...(supportsImage ? { inputFormat: { supportsImage: true } } : {}),
185
+ },
186
+ ...(efforts !== undefined ? { optionSpecs: { reasoningLevel: { values: efforts } } } : {}),
187
+ },
153
188
  };
154
189
  fragments.push({ path: [...ZCODE_STORE_MODEL_RULES_PATH, selector], value: rule });
155
190
  }
@@ -1,7 +1,6 @@
1
1
  // ZCode config export.
2
2
  import type { ExportContext, ManagedContribution } from "./contracts";
3
- import { normalizeExportModels, inputModalitiesForClient, exportModelLabel, authoritativeContextWindow, singleFragment } from "./model-metadata";
4
- import { sanitizeCodexReasoningEfforts } from "../../reasoning-effort";
3
+ import { normalizeExportModels, inputModalitiesForClient, exportModelLabel, authoritativeContextWindow, singleFragment, zcodeSelectableEfforts } from "./model-metadata";
5
4
  import { OPENCODE_PROVIDER_ID, LOOPBACK_API_KEY_PLACEHOLDER } from "./constants";
6
5
 
7
6
 
@@ -86,9 +85,8 @@ export function buildZcodeClientConfig(ctx: ExportContext): ZcodeGeneratedConfig
86
85
  // `ultra` when present: ZCode forwards the selected variant to the wire field its
87
86
  // kind uses — `reasoning.effort` on `openai`, which is what `/v1/responses` reads
88
87
  // natively. Set `defaultVariant` only when it survives that filter.
89
- const efforts = sanitizeCodexReasoningEfforts(model.reasoningEfforts)
90
- ?.filter(effort => effort !== "none");
91
- if (efforts && efforts.length > 0) {
88
+ const efforts = zcodeSelectableEfforts(model.reasoningEfforts);
89
+ if (efforts !== undefined) {
92
90
  const defaultVariant = model.defaultReasoningEffort?.trim().toLowerCase();
93
91
  entry.reasoning = {
94
92
  enabled: true,
@@ -470,20 +470,51 @@ export function mcodeConfigPath(env: OpencodeLaunchEnv = process.env, home: stri
470
470
  return join(mcodeHomeDir(env, home), "config.yaml");
471
471
  }
472
472
 
473
- /**
474
- * ZCode (Z.ai's desktop client) keeps everything under `~/.zcode`; custom
475
- * providers live in `v2/config.json`. `ZCODE_DATA_DIR` mirrors the other
476
- * clients' override convention; relative overrides are refused for the same
477
- * reason as MCode's.
478
- */
479
- export function zcodeHomeDir(env: OpencodeLaunchEnv = process.env, home: string = homedir()): string {
480
- const override = env.ZCODE_DATA_DIR?.trim();
481
- if (override) return absoluteClientPath(override, home, "ZCODE_DATA_DIR");
482
- return join(home, ".zcode");
473
+ /** Bootstrap settings always stay at the default home, even after relocation. */
474
+ export type ZcodePathIO = {
475
+ readText: (path: string) => { kind: "text"; text: string } | { kind: "missing" } | { kind: "failed"; code?: string };
476
+ };
477
+ const zcodePathIO: ZcodePathIO = {
478
+ readText: path => {
479
+ try { return { kind: "text", text: readFileSync(path, "utf8") }; }
480
+ catch (error) { return (error as NodeJS.ErrnoException).code === "ENOENT" ? { kind: "missing" } : { kind: "failed" }; }
481
+ },
482
+ };
483
+ export class ZcodeSettingsPathError extends ClientPathError {}
484
+
485
+ export function zcodeBootstrapSettingsPath(home: string = homedir()): string {
486
+ return join(home, ".zcode", "v2", "setting.json");
483
487
  }
484
488
 
485
- export function zcodeConfigPath(env: OpencodeLaunchEnv = process.env, home: string = homedir()): string {
486
- return join(zcodeHomeDir(env, home), "v2", "config.json");
489
+ /** The pre-relocation path is retained solely to recognize historical ownership. */
490
+ export function zcodeLegacyHomeDir(env: OpencodeLaunchEnv = process.env, home: string = homedir()): string {
491
+ const override = env.ZCODE_DATA_DIR?.trim();
492
+ return override ? absoluteClientPath(override, home, "ZCODE_DATA_DIR") : join(home, ".zcode");
493
+ }
494
+
495
+ /** ZCODE_DATA_DIR names the .zcode root; dataBaseDir names its parent. */
496
+ export function zcodeHomeDir(env: OpencodeLaunchEnv = process.env, home: string = homedir(), io: ZcodePathIO = zcodePathIO): string {
497
+ if (env.ZCODE_DATA_DIR?.trim()) return zcodeLegacyHomeDir(env, home);
498
+ const bootstrap = zcodeBootstrapSettingsPath(home);
499
+ const read = io.readText(bootstrap);
500
+ if (read.kind === "missing") return join(home, ".zcode");
501
+ const unknown = () => new ZcodeSettingsPathError(
502
+ `ZCode's active data root is unknown: ${bootstrap} must be readable JSON with an absolute dataBaseDir (or no dataBaseDir). Repair the bootstrap setting before enabling or refreshing; no legacy provider will be applied.`,
503
+ );
504
+ if (read.kind !== "text") throw unknown();
505
+ let settings: unknown;
506
+ try { settings = JSON.parse(read.text); } catch { throw unknown(); }
507
+ if (settings === null || typeof settings !== "object" || Array.isArray(settings)) throw unknown();
508
+ const base = (settings as Record<string, unknown>).dataBaseDir;
509
+ if (base === undefined) return join(home, ".zcode");
510
+ // Match the native path's value, without cwd or tilde expansion. The native
511
+ // ZCODE_DATA_BASE_DIR selector is not an alias for our ZCODE_DATA_DIR.
512
+ if (typeof base !== "string" || !isAbsolute(base) || base.includes("\0")) throw unknown();
513
+ return join(base, ".zcode");
514
+ }
515
+
516
+ export function zcodeConfigPath(env: OpencodeLaunchEnv = process.env, home: string = homedir(), io?: ZcodePathIO): string {
517
+ return join(zcodeHomeDir(env, home, io), "v2", "config.json");
487
518
  }
488
519
 
489
520
  /**
@@ -532,8 +563,7 @@ export function commandCodeConfigPath(env: OpencodeLaunchEnv = process.env, home
532
563
  * an install that has ever run, the import has already happened and never runs
533
564
  * again — every later write to `v2/config.json` is read by nobody (#5348).
534
565
  *
535
- * This project does not write this file; it names it so the integration can
536
- * tell whether its own write can still reach the client. The env override is
566
+ * The integration writes this store through its existing schema-aware writer. The env override is
537
567
  * ZCode's own (`ZCODE_PERSONAL_PROVIDER_CONFIG_FILE`), so an operator who
538
568
  * relocated the store is measured against the file their client actually opens
539
569
  * rather than the default location. A relative override is refused for the same
@@ -541,10 +571,10 @@ export function commandCodeConfigPath(env: OpencodeLaunchEnv = process.env, home
541
571
  * which file it names, and here that disagreement decides whether an apply is
542
572
  * reported as effective.
543
573
  */
544
- export function zcodeProviderStorePath(env: OpencodeLaunchEnv = process.env, home: string = homedir()): string {
574
+ export function zcodeProviderStorePath(env: OpencodeLaunchEnv = process.env, home: string = homedir(), io?: ZcodePathIO): string {
545
575
  const override = env.ZCODE_PERSONAL_PROVIDER_CONFIG_FILE?.trim();
546
576
  if (override) return absoluteClientPath(override, home, "ZCODE_PERSONAL_PROVIDER_CONFIG_FILE");
547
- return join(zcodeHomeDir(env, home), "v2", "provider_config.json");
577
+ return join(zcodeHomeDir(env, home, io), "v2", "provider_config.json");
548
578
  }
549
579
 
550
580
  /**
@@ -1367,7 +1397,7 @@ function buildAsideContribution(ctx: ExportContext): ManagedContribution {
1367
1397
  * the keyed `providers`, the `models` ARRAY whose identity is `id`, the
1368
1398
  * `openai-completions` dialect, the loopback placeholder, and the
1369
1399
  * `thinkingLevelMap` levels. Evidence:
1370
- * `devlog/_plan/260912_omo_client_integration/001_omo_contract.md`.
1400
+ * `devlog/_fin/260912_omo_client_integration/001_omo_contract.md`.
1371
1401
  *
1372
1402
  * The flag is passed HERE as well as in the spec's `build`, which is the one
1373
1403
  * thing Prime and Aside do not do. They pass the default on both paths, so they
@@ -120,10 +120,13 @@ function serialize(text: string, doc: JsonObject): string {
120
120
  /**
121
121
  * `reasoning` left undefined keeps the entry's level; null removes it, so a level LazyCodex cannot
122
122
  * express does not leave a stale one behind for its next install to apply.
123
+ * `model` left undefined keeps the entry's model, so an effort-only save of a role that inherits
124
+ * its model never adds a pin here. LazyCodex declares both keys optional, so an entry may carry
125
+ * `reasoning` alone; an absent entry is created only when there is a value to put in it.
123
126
  */
124
127
  export function writeOmoRoleModel(
125
128
  role: string,
126
- model: string,
129
+ model: string | undefined,
127
130
  path: string = omoJsoncPath(),
128
131
  reasoning?: string | null,
129
132
  ): OmoRoleModelWriteStatus {
@@ -137,10 +140,10 @@ export function writeOmoRoleModel(
137
140
  if (!isObject(agents)) return "invalid";
138
141
  const entry = agents[role] === undefined ? {} : agents[role];
139
142
  if (!isObject(entry)) return "invalid";
140
- const next: JsonObject = { ...entry, model };
143
+ const next: JsonObject = model === undefined ? { ...entry } : { ...entry, model };
141
144
  if (reasoning === null) delete next.reasoning;
142
145
  else if (reasoning !== undefined) next.reasoning = reasoning;
143
- if (entry.model === model && entry.reasoning === next.reasoning) return "unchanged";
146
+ if (entry.model === next.model && entry.reasoning === next.reasoning) return "unchanged";
144
147
  agents[role] = next;
145
148
  codex.agents = agents;
146
149
  doc[CODEX_SECTION] = codex;
@@ -1,11 +1,12 @@
1
1
  /**
2
- * The dashboard's write of one Codex agent role's model, and optionally its reasoning effort.
2
+ * The dashboard's write of one Codex agent role's model, its reasoning effort, or both.
3
3
  *
4
4
  * Codex overrides the spawn-time model with the root `model` pin in
5
5
  * `$CODEX_HOME/agents/<role>.toml`, so that pin is what decides a child's model. This module
6
- * edits exactly that value, plus the root `model_reasoning_effort` when a caller passes one, and
7
- * nothing else: every other byte of the file, including the instructions multiline string that
8
- * usually mentions `model =` in prose, is left as it was.
6
+ * edits exactly that value when a caller passes a model, plus the root `model_reasoning_effort`
7
+ * when a caller passes an effort, and nothing else: every other byte of the file, including the
8
+ * instructions multiline string that usually mentions `model =` in prose, is left as it was. An
9
+ * omitted model leaves the pin as it is, so a role that inherits its model keeps inheriting it.
9
10
  * It runs only on an explicit user action; no sync or startup path calls it.
10
11
  */
11
12
  import { lstatSync, readFileSync } from "node:fs";
@@ -97,6 +98,21 @@ export function setTomlRootReasoningEffort(content: string, effort: string): str
97
98
  return setTomlRootString(content, locateTomlReasoningEffortKey(content), "model_reasoning_effort", effort);
98
99
  }
99
100
 
101
+ export function removeTomlRootReasoningEffort(content: string): string {
102
+ const location = locateTomlReasoningEffortKey(content);
103
+ if (!location?.inRootTable) return content;
104
+ if (!location.span) {
105
+ throw new AgentRoleModelError("unsupported_model_value", "the existing model_reasoning_effort value is not a one-line string");
106
+ }
107
+ const lines = content.split("\n");
108
+ // A terminated line goes with its own newline. An unterminated last line is emptied instead,
109
+ // so the previous line keeps its terminator (and its \r in a CRLF file).
110
+ if (location.line < lines.length - 1) lines.splice(location.line, 1);
111
+ else lines[location.line] = "";
112
+ const bom = location.line === 0 && content.startsWith("\ufeff") ? "\ufeff" : "";
113
+ return bom + lines.join("\n");
114
+ }
115
+
100
116
  export function readCodexAgentRoleEffort(role: string, codexHome: string): string | null {
101
117
  try {
102
118
  const location = locateTomlReasoningEffortKey(readFileSync(roleFile(role, codexHome), "utf8"));
@@ -148,9 +164,9 @@ function assertValidRoleToml(role: string, text: string, stage: "before" | "afte
148
164
 
149
165
  export function writeCodexAgentRoleModel(
150
166
  role: string,
151
- model: string,
167
+ model: string | undefined,
152
168
  codexHome: string,
153
- effort?: string,
169
+ effort?: string | null,
154
170
  ): { status: "written" | "unchanged" } {
155
171
  requireKnownRole(role, codexHome);
156
172
  const path = roleFile(role, codexHome);
@@ -159,10 +175,12 @@ export function writeCodexAgentRoleModel(
159
175
  }
160
176
  const before = readFileSync(path, "utf8");
161
177
  assertValidRoleToml(role, before, "before");
162
- const withModel = setTomlRootModel(before, validateAgentRoleModel(model));
178
+ const withModel = model === undefined ? before : setTomlRootModel(before, validateAgentRoleModel(model));
163
179
  const after = effort === undefined
164
180
  ? withModel
165
- : setTomlRootReasoningEffort(withModel, validateAgentRoleEffort(effort));
181
+ : effort === null
182
+ ? removeTomlRootReasoningEffort(withModel)
183
+ : setTomlRootReasoningEffort(withModel, validateAgentRoleEffort(effort));
166
184
  if (after === before) return { status: "unchanged" };
167
185
  assertValidRoleToml(role, after, "after");
168
186
  assertIntegrationWriteOwnership(path);
@@ -33,6 +33,7 @@ import { fetchMainAccountInfoAttempt, EMPTY_MAIN_ACCOUNT_INFO, mainResetCreditsF
33
33
  import { fetchPoolAccountQuota, PoolQuotaProbeBusyError, POOL_QUOTA_REFRESH_CONCURRENCY } from "./pool-quota-probe";
34
34
  import type { PoolQuotaResult } from "./pool-quota-probe";
35
35
  import { getRuntimeConfig, configuredPoolAccount, mapWithConcurrency } from "./runtime-config";
36
+ import { isPassiveRead } from "../../lib/passive-read";
36
37
 
37
38
  export function quotaForPlan<T extends Omit<StoredAccountQuota, "updatedAt"> | StoredAccountQuota | null>(
38
39
  quota: T,
@@ -274,13 +275,25 @@ export async function listCodexAuthAccountsSnapshot(
274
275
  pruneCodexCredits([MAIN_CODEX_ACCOUNT_ID, ...poolAccounts.map(account => account.id)]);
275
276
  // One redaction decision for the whole snapshot, read once from the operator's config (#3859).
276
277
  const maskEmails = emailMaskingEnabled(runtimeConfig);
277
- const mainResult = await fetchMainAccountInfoAttempt(forceRefresh, 1, undefined, false,
278
- forceRefresh, false, runtimeConfig);
278
+ // A passive read (a Remote Link Child's, #6959) reports what this machine already observed: no
279
+ // token refresh, no WHAM probe, and so none of the plan or credential writes they make.
280
+ const passive = isPassiveRead();
281
+ const mainResult: Awaited<ReturnType<typeof fetchMainAccountInfoAttempt>> = passive
282
+ ? { info: getMainAccountInfoCache() ?? EMPTY_MAIN_ACCOUNT_INFO, credentialChecked: false, hasCredential: false,
283
+ identityGeneration: captureMainAccountIdentityGeneration() }
284
+ : await fetchMainAccountInfoAttempt(forceRefresh, 1, undefined, false, forceRefresh, false, runtimeConfig);
279
285
  const refreshedPool = await mapWithConcurrency(poolAccounts, POOL_QUOTA_REFRESH_CONCURRENCY, async account => {
280
286
  const cred = getCodexAccountCredential(account.id);
281
287
  let quotaResult: PoolQuotaResult;
282
288
  if (!cred) {
283
289
  quotaResult = { quota: null, needsReauth: true };
290
+ } else if (passive) {
291
+ quotaResult = {
292
+ quota: getAccountQuota(account.id),
293
+ needsReauth: false,
294
+ credentialGeneration: readCodexAccountRecord(account.id)?.generation,
295
+ quotaProbeSkipped: true,
296
+ };
284
297
  } else {
285
298
  try {
286
299
  // Credits are process-local while pool quota is hydrated from disk, so right after a
@@ -410,6 +423,8 @@ export async function listCodexAuthAccountsSnapshot(
410
423
  };
411
424
  // Project the refusal and DTO in the same owned synchronous observation. A same-account grant
412
425
  // replacement during earlier pool probes must not inherit the previous grant's refusal.
426
+ // A passive read opens no claim database (its lock file is hardened on open).
427
+ if (passive) return projectSnapshot();
413
428
  const projectionLease = tryAcquireNativeMainProfileClaim();
414
429
  if (!projectionLease) return projectSnapshot();
415
430
  try {
@@ -6,11 +6,11 @@ import { deleteConfigTopLevelKey, fileBackedConfigPath, recordFileBackedConfig }
6
6
  import { MAIN_CODEX_ACCOUNT_ID, hasLegacyMainCodexPoolAccount, isSelectableCodexPoolAccount, isValidCodexAccountId } from "../account-id";
7
7
  import { isCodexAccountPaused } from "../account-pause";
8
8
  import { readCodexAccountRecord } from "../account-store";
9
- import { codexAccountPinDrainReason, resetCodexRoutingForManualSelection } from "../routing";
9
+ import { codexAccountPinDrainReason, resetCodexRoutingForClearedSelection, resetCodexRoutingForManualSelection } from "../routing";
10
10
  import { jsonResponse } from "./http";
11
11
 
12
12
  class SelectionRejected extends Error {
13
- constructor(message: string, readonly status: number) { super(message); }
13
+ constructor(message: string, readonly status: number, readonly code?: string) { super(message); }
14
14
  }
15
15
 
16
16
  function validateSelection(config: OcxConfig, accountId: string | null): void {
@@ -42,8 +42,14 @@ function applySelection(config: OcxConfig, accountId: string | null): boolean {
42
42
  return changed;
43
43
  }
44
44
 
45
+ function validateExpectedPin(config: OcxConfig, expectedPinnedAccountId: string | undefined): void {
46
+ if (expectedPinnedAccountId !== undefined && config.activeCodexAccountPinned !== expectedPinnedAccountId) {
47
+ throw new SelectionRejected("The pin changed; reload accounts before releasing it", 409, "account_pin_changed");
48
+ }
49
+ }
50
+
45
51
  /** A manual selection is an explicit command, even when equal to the live merge baseline. */
46
- export function persistCodexAccountSelection(config: OcxConfig, accountId: string | null): Response {
52
+ export function persistCodexAccountSelection(config: OcxConfig, accountId: string | null, expectedPinnedAccountId?: string): Response {
47
53
  const unknown = () => jsonResponse({
48
54
  error: "Account selection could not be confirmed; reload settings before retrying",
49
55
  code: "account_selection_unavailable",
@@ -63,7 +69,9 @@ export function persistCodexAccountSelection(config: OcxConfig, accountId: strin
63
69
  const normalized = validateConfigCandidate(mergeConfigDefaults(config));
64
70
  if (!normalized.ok) return unknown();
65
71
  const initial = normalized.config;
66
- applySelection(initial, accountId);
72
+ validateExpectedPin(initial, expectedPinnedAccountId);
73
+ // Conditional release must keep the baseline pin until the locked mutation checks it.
74
+ if (expectedPinnedAccountId === undefined) applySelection(initial, accountId);
67
75
  initializePersistedConfigIfMissing(initial);
68
76
  }
69
77
  const result = mutatePersistedConfig(persisted => {
@@ -71,12 +79,13 @@ export function persistCodexAccountSelection(config: OcxConfig, accountId: strin
71
79
  // A removed or paused account must not be resurrected by a stale UI snapshot.
72
80
  validateSelection(config, accountId);
73
81
  validateSelection(persisted, accountId);
82
+ validateExpectedPin(persisted, expectedPinnedAccountId);
74
83
  return { changed: applySelection(persisted, accountId), value: persisted };
75
84
  });
76
85
  if (result.status === "unavailable") return unknown();
77
86
  committed = result.value;
78
87
  } catch (error) {
79
- if (error instanceof SelectionRejected) return jsonResponse({ error: error.message }, error.status);
88
+ if (error instanceof SelectionRejected) return jsonResponse({ error: error.message, ...(error.code ? { code: error.code } : {}) }, error.status);
80
89
  // Atomic publication may succeed before generation/bookkeeping fails. Confirm the exact
81
90
  // durable selection instead of rolling live state back behind an already published write.
82
91
  const snapshot = readConfigFileSnapshot();
@@ -95,12 +104,13 @@ export function persistCodexAccountSelection(config: OcxConfig, accountId: strin
95
104
  adoptPersistedCodexAccountSelection(config, committed);
96
105
  recordFileBackedConfig(config, configPath);
97
106
  // No failed save may clear affinity, spend the manual preference, or move the RR cursor.
98
- resetCodexRoutingForManualSelection(accountId ?? MAIN_CODEX_ACCOUNT_ID);
107
+ if (accountId === null) resetCodexRoutingForClearedSelection();
108
+ else resetCodexRoutingForManualSelection(accountId);
99
109
  const pinDrainReason = accountId === null ? undefined : codexAccountPinDrainReason(config, accountId);
100
110
  return jsonResponse({
101
111
  ok: true,
102
112
  activeCodexAccountId: config.activeCodexAccountId ?? null,
103
- appliesImmediately: true,
113
+ appliesImmediately: accountId !== null,
104
114
  ...(pinDrainReason !== undefined ? { pinDrained: true, pinDrainReason } : {}),
105
115
  ...(warning ? { warning: "config_bookkeeping_failed" } : {}),
106
116
  });
@@ -88,14 +88,13 @@ export async function runMainAccountHardLockRecovery(config: OcxConfig): Promise
88
88
  const now = Date.now();
89
89
  const quota = getMainPolicyQuota();
90
90
  const observedAt = quota?.credits?.observedAt;
91
- // getMainPolicyQuota binds evidence to the observed physical identity. Only renew
92
- // valid formerly spendable funds; admission retains the actual five-minute clock.
91
+ // Positive evidence renews before expiry; unavailable evidence keeps discovery
92
+ // eligible under the same pacing. Admission still requires fresh spendable funds.
93
93
  return codexAccountUsesCreditsAfterLimit(config, MAIN_CODEX_ACCOUNT_ID)
94
94
  && !isCodexAccountPaused(config, MAIN_CODEX_ACCOUNT_ID)
95
95
  && codexUsageLimitResetAt(quota, undefined, now) !== undefined
96
- && observedAt !== undefined && observedAt > 0
97
- && now - observedAt >= MAIN_CREDIT_RENEWAL_AGE_MS
98
- && hasSpendableCodexCredits(quota, observedAt);
96
+ && (!hasSpendableCodexCredits(quota, now)
97
+ || (observedAt !== undefined && now - observedAt >= MAIN_CREDIT_RENEWAL_AGE_MS));
99
98
  };
100
99
  const status = getMainAccountHardLockStatus(config);
101
100
  if (status.state !== "blocked" && !creditRecoveryNeeded()) { mainHardLockRecoveryAttempt = undefined; return; }
@@ -254,7 +254,11 @@ export async function handleCodexAuthAPI(
254
254
  || !("accountId" in body) || (body.accountId !== null && typeof body.accountId !== "string")) {
255
255
  return jsonResponse({ error: "accountId must be an account id or null" }, 400);
256
256
  }
257
- return persistCodexAccountSelection(getRuntimeConfig(config), body.accountId);
257
+ const expectedPin = "expectedPinnedAccountId" in body ? body.expectedPinnedAccountId : undefined;
258
+ if (expectedPin !== undefined && (body.accountId !== null || !isCodexAccountPriorityKey(expectedPin))) {
259
+ return jsonResponse({ error: "expectedPinnedAccountId must be a valid account id for a release" }, 400);
260
+ }
261
+ return persistCodexAccountSelection(getRuntimeConfig(config), body.accountId, expectedPin as string | undefined);
258
262
  }
259
263
 
260
264
  if (url.pathname === "/api/codex-auth/active" && req.method === "GET") {
@@ -1,5 +1,6 @@
1
1
  import { bindPoolCreditPolicy, poolCreditHoldResetAt, poolContextCreditHoldResetAt } from "./pool-credit-policy";
2
2
  import { hasSpendableCodexCredits } from "./quota-types";
3
+ import { nextQuotaQueryDelay } from "./quota-query-backoff";
3
4
  import { noteMainAccountActivity } from "./main-account-external-usage";
4
5
  import { codexAccountPriorityFailbackEnabled } from "./account-priority";
5
6
  import type { PoolQuotaWriter } from "./quota-types";
@@ -553,8 +554,11 @@ function assertPoolAccountCredits(accountId: string, resetAt: number | undefined
553
554
  export class CodexMainAccountCreditsOffError extends CodexAccountCooldownError {
554
555
  readonly resetAt?: number;
555
556
 
556
- constructor(resetAt?: number) {
557
- super(MAIN_CODEX_ACCOUNT_ID, resetAt ?? 0);
557
+ constructor(resetAt?: number, creditRecovery = false) {
558
+ // Keep the included reset as policy evidence, but let consented callers retry
559
+ // within the base metadata recovery interval even when backoff grows longer.
560
+ super(MAIN_CODEX_ACCOUNT_ID, creditRecovery
561
+ ? Math.min(resetAt ?? Infinity, Date.now() + nextQuotaQueryDelay()) : resetAt ?? 0);
558
562
  this.name = "CodexMainAccountCreditsOffError";
559
563
  this.resetAt = resetAt;
560
564
  this.message = "Codex main account reached its usage limit, and spending ChatGPT credits is off or no fresh spendable balance is available."
@@ -778,7 +782,8 @@ function assertMainAccountPolicy(
778
782
  const status = getMainAccountHardLockStatus(config);
779
783
  if (status.state === "blocked") throw new CodexMainAccountHardLockError(status.resetAt, status.thresholds);
780
784
  const creditsResetAt = mainCreditsHoldResetAt(config);
781
- if (creditsResetAt !== undefined) throw new CodexMainAccountCreditsOffError(creditsResetAt);
785
+ if (creditsResetAt !== undefined) throw new CodexMainAccountCreditsOffError(creditsResetAt,
786
+ codexAccountUsesCreditsAfterLimit(config, MAIN_CODEX_ACCOUNT_ID));
782
787
  }
783
788
  // Only an admitted request is opencodex's own use of the main account. Counting a refused one
784
789
  // would hide outside usage from the warning exactly while the lock is holding.
@@ -215,7 +215,9 @@ function classifyStartupHealthSummary(health: StartupHealth): string {
215
215
  ? "custom remote Codex routing (no local restart dependency)"
216
216
  : "native Codex routing (no opencodex restart dependency)";
217
217
  if (health.protection === "service") return "protected by background service";
218
- if (health.protection === "desktop") return "protected by desktop app at login and its proxy supervisor";
218
+ if (health.protection === "desktop") return health.desktop?.loginRegistration === "registered-not-loaded"
219
+ ? "protected by desktop app at next login and its proxy supervisor (Start at Login registered; not loaded in this session)"
220
+ : "protected by desktop app at login and its proxy supervisor";
219
221
  const command = health.recommendedCommand ?? health.commands.restoreNative;
220
222
  if (health.routingKind === "unknown") return `AT RISK after restart (Codex routing could not be verified; run '${command}')`;
221
223
  if (health.routingKind === "custom-local") return `AT RISK after restart (custom local gateway lifecycle is not managed by opencodex; run '${command}')`;
@@ -28,6 +28,7 @@ import {
28
28
  import type { NormalizedComboConfig } from "../../combos/types";
29
29
  import { providerDestinationResolvedError } from "../../lib/destination-policy";
30
30
  import { redactSecretString } from "../../lib/redact";
31
+ import { isPassiveRead } from "../../lib/passive-read";
31
32
  import upstreamModelsSnapshot from "../data/upstream-models.json";
32
33
  import { decodeOverlayState } from "../shim-state-file";
33
34
 
@@ -313,7 +314,8 @@ function* bundledCatalogSteps(
313
314
  const output = yield command;
314
315
  if (!current()) return null;
315
316
  const catalog = output === null ? null : parseCatalogJson(output);
316
- if (catalog && findNativeTemplate(catalog)) {
317
+ if (catalog && catalog.models?.every(row => row !== null && typeof row === "object" && !Array.isArray(row))
318
+ && findNativeTemplate(catalog)) {
317
319
  if (cacheKey) {
318
320
  publishBundledCatalogCache(
319
321
  cacheKey,
@@ -377,6 +379,8 @@ export function loadBundledCodexCatalogAsync(
377
379
  if (useCache && bundledCatalogFlight?.inputs === inputs && bundledCatalogFlight.epoch === epoch) {
378
380
  return view(bundledCatalogFlight.promise);
379
381
  }
382
+ // A passive read (a Remote Link Child's, #6959) runs no Codex binary and persists no runtime.
383
+ if (isPassiveRead()) return Promise.resolve(null);
380
384
  const controller = new AbortController();
381
385
  const flight = { inputs, epoch, controller, promise: Promise.resolve<ReadonlyRawCatalog | null>(null) };
382
386
  if (useCache && !signal && isCurrent === catalogAlwaysCurrent) {
@@ -620,8 +620,8 @@ export function clampCatalogModelsToCodexSupport(models: RawEntry[], deps: Bundl
620
620
  removedEfforts: [...clamp.removedEfforts],
621
621
  affectedModels: [...clamp.affectedModels],
622
622
  };
623
- for (const line of formatClampLogLines(diagnostic)) console.warn(line);
624
- if (!deps.commandCandidates) persistEffortClamp(diagnostic, { configDir: deps.configDir });
623
+ const changed = deps.commandCandidates ? true : persistEffortClamp(diagnostic, { configDir: deps.configDir });
624
+ if (changed) for (const line of formatClampLogLines(diagnostic)) console.warn(line);
625
625
  deps.onEffortClamp?.(diagnostic);
626
626
  } else if (!deps.commandCandidates) {
627
627
  persistEffortClamp(null, { configDir: deps.configDir });
@@ -42,6 +42,7 @@ import { RESERVE_METADATA_SOURCE_FIELD } from "./reserve";
42
42
  import {
43
43
  ACCOUNT_GATED_NATIVE_OPENAI_MODELS,
44
44
  NATIVE_GPT6_CONTEXT,
45
+ NATIVE_GPT61_SOL_CONTEXT,
45
46
  NATIVE_DAYBREAK_BLUE_MODEL,
46
47
  NATIVE_GPT6_ASTRA_MINOR_MODEL,
47
48
  NATIVE_GPT6_ASTRA_MODEL,
@@ -202,8 +203,9 @@ export const NATIVE_OPENAI_CONTEXT_OVERRIDES: Record<string, { contextWindow?: n
202
203
  // the row's own ceiling rather than the GPT-5.6 family's measured 922,000.
203
204
  [NATIVE_GPT6_SOL_MODEL]: { ...NATIVE_GPT6_CONTEXT },
204
205
  [NATIVE_GPT6_LUNA_MODEL]: { ...NATIVE_GPT6_CONTEXT },
205
- // GPT-6.1 Sol's row (openai/codex models.json after #49318) ships the same 272,000 / 872,000 pair.
206
- [NATIVE_GPT61_SOL_MODEL]: { ...NATIVE_GPT6_CONTEXT },
206
+ // GPT-6.1 Sol's row (openai/codex models.json after #49318) ships 272,000 / 872,000, but OpenAI
207
+ // raised its Codex & Work default to 500,000 on 2026-10-11; the 872,000 ceiling is unchanged.
208
+ [NATIVE_GPT61_SOL_MODEL]: { ...NATIVE_GPT61_SOL_CONTEXT },
207
209
  // Astra Minor borrows Astra's row, so it inherits Astra's numbers. No account we hold can reach
208
210
  // it, so this is inheritance, not a measurement.
209
211
  [NATIVE_GPT6_ASTRA_MINOR_MODEL]: { ...NATIVE_GPT6_CONTEXT },
@@ -44,7 +44,8 @@ export const NATIVE_GPT6_LUNA_MODEL = "gpt-6-luna";
44
44
  * SELF-DESCRIBED: its row is pinned verbatim in `src/codex/data/roster-pinned-models.json` from
45
45
  * openai/codex `codex-rs/models-manager/models.json` after #49318, which also made it the Codex
46
46
  * catalog default (priority 1). Ladder low..ultra with default effort low, and the GPT-6
47
- * 272,000 / 872,000 context pair. Ungated for the same owner decision as Sol and Luna.
47
+ * 272,000 / 872,000 context pair in that pinned row; the effective native default is now 500,000
48
+ * (NATIVE_GPT61_SOL_CONTEXT). Ungated for the same owner decision as Sol and Luna.
48
49
  */
49
50
  export const NATIVE_GPT61_SOL_MODEL = "gpt-6.1-sol";
50
51
 
@@ -62,11 +63,24 @@ export const NATIVE_GPT6_ASTRA_MINOR_MODEL = "gpt-6-astra-minor";
62
63
  /**
63
64
  * Context pair every GPT-6 native ships in its upstream row: a 272,000-token default window
64
65
  * against an 872,000-token ceiling (and input limit) reached only through the long-window opt-in.
65
- * The built-in GPT-6 rows and every configured native (below) inherit this one value.
66
+ * The built-in GPT-6 rows and every configured native (below) inherit this one value, except
67
+ * GPT-6.1 Sol, whose default window is NATIVE_GPT61_SOL_CONTEXT.
66
68
  */
67
69
  export const NATIVE_GPT6_CONTEXT: Readonly<{ contextWindow: number; maxContextWindow: number; maxInputTokens: number }> =
68
70
  Object.freeze({ contextWindow: 272_000, maxContextWindow: 872_000, maxInputTokens: 872_000 });
69
71
 
72
+ /**
73
+ * GPT-6.1 Sol through Codex & Work: OpenAI raised the default context from 272k to 500k
74
+ * (Tibo Sottiaux, 2026-10-11, https://x.com/thsottiaux/status/2109430343346581527), while
75
+ * overrides stay allowed and the model maximum is described as 1M. The bundled codex-rs row
76
+ * still reads 272,000 / 872,000, and built-in natives never take roster values, so the default
77
+ * lives here. The 872,000 ceiling (and input limit) is kept on purpose: the announcement does
78
+ * not say how "1M" maps onto the native window and input fields, and raising maxInputTokens
79
+ * would assert an input allowance nobody has measured. Overrides therefore clamp to 872,000.
80
+ */
81
+ export const NATIVE_GPT61_SOL_CONTEXT: Readonly<{ contextWindow: number; maxContextWindow: number; maxInputTokens: number }> =
82
+ Object.freeze({ ...NATIVE_GPT6_CONTEXT, contextWindow: 500_000 });
83
+
70
84
  /** Pinned row a configured native borrows its capability metadata from: the current Sol. */
71
85
  export const CONFIGURED_NATIVE_OPENAI_TEMPLATE_MODEL = NATIVE_GPT61_SOL_MODEL;
72
86
 
@@ -93,7 +107,7 @@ export const CONFIGURED_NATIVE_OPENAI_TEMPLATE_MODEL = NATIVE_GPT61_SOL_MODEL;
93
107
  *
94
108
  * `gpt-daybreak-blue-latest` stays gated. It has no shipped catalog row anywhere, so absence is
95
109
  * the only signal that exists for it, and the ungating decision was scoped to the flagships.
96
- * Evidence: devlog/_plan/260904_flagship_native_always_visible/.
110
+ * Evidence: devlog/_fin/260904_flagship_native_always_visible/.
97
111
  */
98
112
  export const ACCOUNT_GATED_NATIVE_OPENAI_MODELS: ReadonlySet<string> = new Set([
99
113
  NATIVE_DAYBREAK_BLUE_MODEL,