@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,7 @@
1
1
  import { ensureManagementClaudeIntercept, interceptStartRefusal, interceptStatus } from "./claude-intercept-routes";
2
2
  import { persistCommittedDesktopGateway } from "../../claude/desktop-gateway-state";
3
3
  import { captureDesktopAppliedMarker, commitDesktopAppliedMarker } from "../../claude/desktop-applied-marker";
4
+ import { isPassiveRead } from "../../lib/passive-read";
4
5
  import { randomUUID } from "node:crypto";
5
6
  import { readFileSync } from "node:fs";
6
7
  import { dirname } from "node:path";
@@ -230,6 +231,32 @@ async function injectionModelOptions(config: OcxConfig, models: readonly Catalog
230
231
  return [...nativeModels, ...routedModels];
231
232
  }
232
233
 
234
+ /**
235
+ * The GET /api/injection-model body. `routed` is the discovered routed catalog, or `null` in a
236
+ * passive read (a Remote Link Child's, #6959). That body lists the native
237
+ * options and the current pick, which is all a read-only Child shows.
238
+ */
239
+ export async function injectionModelView(config: OcxConfig, routed: readonly CatalogModel[] | null) {
240
+ const { CODEX_REASONING_LEVELS } = await import("../../reasoning-effort");
241
+ const available = await injectionModelOptions(config, routed ?? []);
242
+ const picked = config.injectionModel;
243
+ if (routed === null && picked && !available.some(option => option.namespaced === picked)) {
244
+ const slash = picked.indexOf("/");
245
+ available.push(slash > 0
246
+ ? { provider: picked.slice(0, slash), model: picked.slice(slash + 1), namespaced: picked }
247
+ : { provider: "openai", model: picked, namespaced: picked });
248
+ }
249
+ return {
250
+ multiAgentGuidanceEnabled: multiAgentGuidanceEnabled(config),
251
+ syncCodexSubagentDefaults: subagentDefaultSyncEffective(config),
252
+ model: config.injectionModel ?? null,
253
+ effort: config.injectionEffort ?? null,
254
+ prompt: config.injectionPrompt ?? null,
255
+ efforts: CODEX_REASONING_LEVELS.map(l => l.effort),
256
+ available,
257
+ };
258
+ }
259
+
233
260
  export async function handleAgentSettingsRoutes(ctx: ManagementContext): Promise<Response | null> {
234
261
  const { req, url, config, deps, convergeCodexCatalog, syncClaudeAgentDefsBestEffort } = ctx;
235
262
 
@@ -620,16 +647,7 @@ export async function handleAgentSettingsRoutes(ctx: ManagementContext): Promise
620
647
  // effort the prompt tells the agent to pass to spawn_agent. GET returns the current
621
648
  // picks + available models/efforts; PUT sets or clears them.
622
649
  if (url.pathname === "/api/injection-model" && req.method === "GET") {
623
- const { CODEX_REASONING_LEVELS } = await import("../../reasoning-effort");
624
- return jsonResponse({
625
- multiAgentGuidanceEnabled: multiAgentGuidanceEnabled(config),
626
- syncCodexSubagentDefaults: subagentDefaultSyncEffective(config),
627
- model: config.injectionModel ?? null,
628
- effort: config.injectionEffort ?? null,
629
- prompt: config.injectionPrompt ?? null,
630
- efforts: CODEX_REASONING_LEVELS.map(l => l.effort),
631
- available: await injectionModelOptions(config, await fetchAllModels(config)),
632
- });
650
+ return jsonResponse(await injectionModelView(config, isPassiveRead() ? null : await fetchAllModels(config)));
633
651
  }
634
652
  // Read-only: sizes the described delegated work and proposes a model and effort from the
635
653
  // same options GET offers. The page applies an accepted proposal through the PUT below.
@@ -19,6 +19,7 @@ import { captureAnthropicPhysicalSendOwnership, anthropicPhysicalSendOwnershipIs
19
19
  import { anthropicCooldownRecoveryFor } from "../../providers/quota/anthropic-cooldown-recovery";
20
20
  import { captureProviderAccountQuotaEpoch } from "../../providers/quota/account-cache";
21
21
  import { captureOAuthAccountSelection, getAccountSet, getAccountCredentialWithStatus, listAccounts } from "../../oauth/store";
22
+ import { admitAnthropicAccountAfterReset, type AnthropicResetAdmissionEvidence } from "../../providers/anthropic-reset-admission";
22
23
  import {
23
24
  ANTHROPIC_RESET_GRANT_ID_RE,
24
25
  AnthropicResetGrantError,
@@ -74,6 +75,8 @@ export interface AnthropicResetGrantRouteDeps {
74
75
  listAccountIds: () => string[];
75
76
  activeAccountId: () => string | null;
76
77
  accessTokenFor: (accountId: string) => Promise<string>;
78
+ /** Lift local admission after a journaled reset, bounded by its journal evidence. Tests omit it. */
79
+ admitReset?: (accountId: string, evidence: AnthropicResetAdmissionEvidence) => Promise<void>;
77
80
  }
78
81
 
79
82
  function defaultDepsFor(ctx: ManagementContext, provider: AnthropicInstanceId): AnthropicResetGrantRouteDeps | null {
@@ -109,6 +112,7 @@ function defaultDepsFor(ctx: ManagementContext, provider: AnthropicInstanceId):
109
112
  if (!targetCurrent() || !owner) throw new Error("reset owner changed");
110
113
  return snapshot.accessToken;
111
114
  },
115
+ admitReset: (accountId, evidence) => admitAnthropicAccountAfterReset(provider, accountId, evidence),
112
116
  };
113
117
  }
114
118
 
@@ -209,6 +213,16 @@ async function handleConsume(ctx: ManagementContext, injected?: AnthropicResetGr
209
213
  }
210
214
  if (deps.isCurrent?.() === false) return scopedFail(401, "auth_failed");
211
215
  const ledger = { journalPath: deps.journalPath, now: deps.now?.() };
216
+ // Retry-after cooldowns survive a usage probe. A journaled reset, including a
217
+ // replay after the process died before admission, has to lift this account,
218
+ // but only refusals recorded before the claim was journaled and only for the
219
+ // windows the journal says were cleared. A failed reread must not turn a
220
+ // spent grant into an error.
221
+ const admitJournaledReset = async (code: string, evidence: AnthropicResetAdmissionEvidence): Promise<void> => {
222
+ if (code !== "reset" || deps.isCurrent?.() === false) return;
223
+ try { await deps.admitReset?.(accountId, evidence); }
224
+ catch { /* The grant is already spent. */ }
225
+ };
212
226
 
213
227
  // A new operation must pass the spend gate on a fresh read. A same-id retry of
214
228
  // an open record skips it: that attempt was already gated, and upstream
@@ -242,6 +256,7 @@ async function handleConsume(ctx: ManagementContext, injected?: AnthropicResetGr
242
256
  }
243
257
  switch (begin.kind) {
244
258
  case "replay":
259
+ await admitJournaledReset(begin.code, { claimedAt: begin.claimedAt, cleared: begin.cleared });
245
260
  return jsonResponse(
246
261
  { provider, code: begin.code, replayed: true, resetsLeft: begin.resetsLeft, accountId, grantId, operationId },
247
262
  200, ctx.req, ctx.config,
@@ -268,7 +283,7 @@ async function handleConsume(ctx: ManagementContext, injected?: AnthropicResetGr
268
283
  let settled;
269
284
  try {
270
285
  settled = settleAnthropicResetOperation(
271
- { operationId, code: answer.code, resetsLeft: answer.resetsLeft },
286
+ { operationId, code: answer.code, resetsLeft: answer.resetsLeft, cleared: answer.cleared },
272
287
  { journalPath: deps.journalPath, now: deps.now?.() },
273
288
  );
274
289
  } catch {
@@ -278,6 +293,7 @@ async function handleConsume(ctx: ManagementContext, injected?: AnthropicResetGr
278
293
  // Report what the journal holds. An overlapping same-id attempt may have
279
294
  // settled first; its answer is the canonical one.
280
295
  const replayed = settled.code !== answer.code || settled.resetsLeft !== answer.resetsLeft;
296
+ await admitJournaledReset(settled.code, { claimedAt: settled.claimedAt, cleared: settled.cleared });
281
297
  return jsonResponse(
282
298
  {
283
299
  provider,
@@ -96,12 +96,18 @@ export async function handleCodexAgentRoleRoutes(ctx: ManagementContext): Promis
96
96
  return jsonResponse({ error: "invalid JSON body" }, 400, req, config);
97
97
  }
98
98
 
99
- let model: string;
100
- let effort: string | undefined;
99
+ // An omitted model leaves the role's model pin (or its inheritance) alone, an omitted effort
100
+ // keeps the effort, and an effort of null clears it. A body with neither changes nothing, which
101
+ // is a client mistake rather than a save, so it is refused before any role file is read.
102
+ if (body?.model === undefined && body?.effort === undefined) {
103
+ return jsonResponse({ error: "send a model, an effort, or both", code: "empty_update" }, 400, req, config);
104
+ }
105
+ let model: string | undefined;
106
+ let effort: string | null | undefined;
101
107
  let toml: { status: "written" | "unchanged" };
102
108
  try {
103
- model = roles.validateAgentRoleModel(body?.model);
104
- effort = body?.effort === undefined || body.effort === null ? undefined : roles.validateAgentRoleEffort(body.effort);
109
+ model = body?.model === undefined ? undefined : roles.validateAgentRoleModel(body.model);
110
+ effort = body?.effort === undefined ? undefined : body.effort === null ? null : roles.validateAgentRoleEffort(body.effort);
105
111
  toml = roles.writeCodexAgentRoleModel(role, model, codexHome, effort);
106
112
  } catch (error) {
107
113
  if (error instanceof roles.AgentRoleModelError) {
@@ -115,10 +121,17 @@ export async function handleCodexAgentRoleRoutes(ctx: ManagementContext): Promis
115
121
  // The role TOML is what Codex obeys, so its write stands even when the omo mirror cannot follow.
116
122
  let omoStatus: ReturnType<typeof omo.writeOmoRoleModel> | "write_failed";
117
123
  try {
118
- const reasoning = effort === undefined ? undefined : omo.omoReasoningFor(effort);
124
+ const reasoning = effort === undefined ? undefined : effort === null ? null : omo.omoReasoningFor(effort);
119
125
  omoStatus = omo.writeOmoRoleModel(role, model, omo.omoJsoncPath(), reasoning);
120
126
  } catch {
121
127
  omoStatus = "write_failed";
122
128
  }
123
- return jsonResponse({ ok: true, role, model, ...(effort ? { effort } : {}), toml, omoJsonc: { status: omoStatus } }, 200, req, config);
129
+ return jsonResponse({
130
+ ok: true,
131
+ role,
132
+ ...(model !== undefined ? { model } : {}),
133
+ ...(effort !== undefined ? { effort } : {}),
134
+ toml,
135
+ omoJsonc: { status: omoStatus },
136
+ }, 200, req, config);
124
137
  }
@@ -30,11 +30,14 @@ import {
30
30
  composeProjection,
31
31
  computePromptProbeStateFingerprint,
32
32
  findInvalidCharacter,
33
+ importBaseVariant,
33
34
  inspectOwnership,
34
35
  normalizeBody,
35
36
  previewAdopt,
37
+ previewBaseImport,
36
38
  previewSalvage,
37
39
  readPromptLayers,
40
+ recoverPromptJournal,
38
41
  salvageProjection,
39
42
  selectBaseVariant,
40
43
  setToggle,
@@ -76,6 +79,7 @@ const WRITE_ERROR_STATUS: Record<WriteError, number> = {
76
79
  unknown_layer: 400,
77
80
  store_unreadable: 409,
78
81
  invalid_characters: 400,
82
+ body_too_large: 400,
79
83
  write_superseded: 409,
80
84
  // Not the caller's fault and not a race: the filesystem refused the write and the
81
85
  // transaction rolled itself back. 500 rather than 409 — retrying the same request
@@ -84,6 +88,8 @@ const WRITE_ERROR_STATUS: Record<WriteError, number> = {
84
88
  recovery_required: 409,
85
89
  locked: 409,
86
90
  unsafe: 422,
91
+ import_body_changed: 409,
92
+ unsupported_form: 409,
87
93
  };
88
94
 
89
95
  /** Read-only view for the route test that asserts every mapping is a client error. */
@@ -335,7 +341,12 @@ export async function handleCodexPromptRoutes(ctx: ManagementContext): Promise<R
335
341
  if (!body) return fail(ctx, "invalid_body", 400, "expected a JSON object");
336
342
  const id = body.id;
337
343
  if (typeof id !== "string") return fail(ctx, "invalid_body", 400, "id must be a string");
338
- if (typeof body.enabled !== "boolean") return fail(ctx, "invalid_body", 400, "enabled must be a boolean");
344
+ // `null` is the restore-the-default verb: it deletes the line instead of
345
+ // writing the documented default back as a literal, so a later upstream
346
+ // change to the default still reaches this file.
347
+ if (typeof body.enabled !== "boolean" && body.enabled !== null) {
348
+ return fail(ctx, "invalid_body", 400, "enabled must be a boolean or null");
349
+ }
339
350
  const revision = revisionOf(body);
340
351
  if (!revision) return fail(ctx, "stale_revision", 409, "revision required");
341
352
 
@@ -388,6 +399,109 @@ export async function handleCodexPromptRoutes(ctx: ManagementContext): Promise<R
388
399
  return settle(ctx, selectBaseVariant(selection, revision, paths(ctx)));
389
400
  }
390
401
 
402
+ if (url.pathname === "/api/codex-prompt/base/import" && req.method === "POST") {
403
+ // The adopt flow for `model_instructions_file`: preview first, write only on
404
+ // confirmation. The verb exists because /base/select refuses the `external`
405
+ // state by design — that refusal is about SILENT retargeting, and an
406
+ // explicit, previewed import is precisely the opt-in it protects.
407
+ //
408
+ // The preview carries the SERIALIZED variant file — `# {title}\n{body}` —
409
+ // not just the body: the heading is part of what Codex will read, so the
410
+ // thing confirmed must be the whole stored text, title included. Both byte
411
+ // counts are reported and separately named: `bodyBytes` is the budget the
412
+ // 64 KiB cap applies to, `serializedBytes` is the complete file.
413
+ const body = await readBody(ctx);
414
+ if (!body) return fail(ctx, "invalid_body", 400, "expected a JSON object");
415
+ const readState = readPromptLayers(paths(ctx));
416
+ if (!readState.readable) {
417
+ return fail(ctx, "config_unreadable", 409, "the configuration file exists but could not be read", { path: readState.configPath });
418
+ }
419
+ // The title is validated before anything is previewed or written: a caller
420
+ // that supplies one gets the same contract as a custom-layer title —
421
+ // 1-80 characters on a single line.
422
+ if (body.title !== undefined) {
423
+ const t = body.title;
424
+ if (typeof t !== "string" || t.trim().length === 0
425
+ || t.length > MAX_TITLE_CHARS || /[\r\n]/.test(t)) {
426
+ return fail(ctx, "invalid_title", 400, "title must be 1-80 characters on a single line");
427
+ }
428
+ }
429
+ const preview = previewBaseImport(paths(ctx), typeof body.title === "string" ? body.title : undefined);
430
+ if (preview.reason === "nothing_to_import") {
431
+ return fail(ctx, "nothing_to_import", 409,
432
+ "model_instructions_file is absent or already points at a managed variant");
433
+ }
434
+ if (preview.reason === "unsupported_form") {
435
+ return fail(ctx, "import_unsupported_form", 409, preview.detail, {
436
+ path: preview.rawPath,
437
+ });
438
+ }
439
+ if (preview.reason === "file_unreadable") {
440
+ return fail(ctx, "import_file_unreadable", 409, preview.detail, {
441
+ path: preview.resolvedPath ?? preview.rawPath,
442
+ });
443
+ }
444
+ if (preview.reason === "invalid_characters") {
445
+ return fail(ctx, "invalid_characters", 400, preview.detail, { path: preview.resolvedPath });
446
+ }
447
+ // Same boundary as every other write to this key: the 64 KiB cap applies to
448
+ // the BODY alone — the title heading lives outside that budget, unchanged
449
+ // from the managed-variant contract.
450
+ if (preview.reason === "body_too_large" || utf8Bytes(preview.body!) > MAX_BODY_BYTES) {
451
+ return fail(ctx, "body_too_large", 400, `the file body exceeds ${MAX_BODY_BYTES} bytes`, {
452
+ path: preview.resolvedPath,
453
+ });
454
+ }
455
+ if (preview.reason === "slots_full") {
456
+ return fail(ctx, "variant_slots_full", 409,
457
+ `at most ${MAX_BASE_VARIANTS} base variants; delete one first`, {
458
+ maxBaseVariants: MAX_BASE_VARIANTS,
459
+ serialized: preview.serialized,
460
+ resolvedPath: preview.resolvedPath,
461
+ });
462
+ }
463
+ if (body.confirm !== true) {
464
+ // Preview writes nothing, by construction: previewBaseImport is a pure read.
465
+ return jsonResponse({
466
+ ok: true,
467
+ changed: false,
468
+ preview: {
469
+ rawPath: preview.rawPath,
470
+ resolvedPath: preview.resolvedPath,
471
+ serialized: preview.serialized,
472
+ serializedBytes: utf8Bytes(preview.serialized!),
473
+ bodyBytes: utf8Bytes(preview.body!),
474
+ suggestedTitle: preview.suggestedTitle,
475
+ effectiveTitle: preview.effectiveTitle,
476
+ previewSha256: preview.previewSha256,
477
+ },
478
+ }, 200, req, ctx.config);
479
+ }
480
+ const revision = revisionOf(body);
481
+ if (!revision) return fail(ctx, "stale_revision", 409, "revision required");
482
+ // The preview hash is REQUIRED — a confirm that does not name exactly what
483
+ // was previewed (serialized text, title included) cannot be checked against
484
+ // it and is refused without writing.
485
+ const previewSha256 = body.previewSha256;
486
+ if (typeof previewSha256 !== "string" || !/^[0-9a-f]{64}$/.test(previewSha256)) {
487
+ return fail(ctx, "import_preview_required", 400,
488
+ "confirm requires the previewSha256 returned by a preview of the same title");
489
+ }
490
+ if (previewSha256 !== preview.previewSha256) {
491
+ // The file or the selected title moved between preview and confirm. Refuse
492
+ // rather than import a serialization nobody previewed.
493
+ return fail(ctx, "import_body_changed", 409,
494
+ "the preview no longer matches; preview it again", {
495
+ resolvedPath: preview.resolvedPath,
496
+ });
497
+ }
498
+ return settle(ctx, importBaseVariant(
499
+ { title: typeof body.title === "string" ? body.title : undefined, previewSha256 },
500
+ revision,
501
+ paths(ctx),
502
+ ));
503
+ }
504
+
391
505
  if (url.pathname === "/api/codex-prompt/base" && req.method === "PUT") {
392
506
  const body = await readBody(ctx);
393
507
  if (!body) return fail(ctx, "invalid_body", 400, "expected a JSON object");
@@ -568,11 +682,28 @@ export async function handleCodexPromptRoutes(ctx: ManagementContext): Promise<R
568
682
  return settle(ctx, adoptDeveloperInstructions(revision, paths(ctx)));
569
683
  }
570
684
 
571
- // journal-present: recovery lives inside WP1's commit and is not exported.
572
- // Any ordinary mutation replays it on its own path, so the honest answer is
573
- // to name the state rather than duplicate the transaction here.
685
+ if (drift === "journal-present") {
686
+ // Recovery as its own locked operation — never a disguised layers write.
687
+ // The GUI used to replay recovery through a byte-identical custom PUT,
688
+ // which projected custom=[] over a config still carrying instructions
689
+ // whenever the store file was missing.
690
+ if (!confirm) {
691
+ return jsonResponse({
692
+ ok: true,
693
+ changed: false,
694
+ preview: { drift, storePath: snapshot.storePath },
695
+ }, 200, req, ctx.config);
696
+ }
697
+ if (!revision) return fail(ctx, "stale_revision", 409, "revision required");
698
+ if (revision !== snapshot.revision) {
699
+ return fail(ctx, "stale_revision", 409, "the configuration moved since it was read");
700
+ }
701
+ return settle(ctx, recoverPromptJournal(paths(ctx)));
702
+ }
703
+
704
+ // A drift kind this build does not repair: name it rather than guess.
574
705
  return fail(ctx, "repair_unsupported", 409,
575
- "a write journal is present; recovery runs automatically on the next write", {
706
+ "this drift has no repair on this route", {
576
707
  drift, storePath: snapshot.storePath,
577
708
  });
578
709
  }
@@ -209,7 +209,7 @@ export async function proposeDelegationModel(options: {
209
209
  role: DELEGATED_WORK_ROLE,
210
210
  model: options.config.injectionModel ?? null,
211
211
  effort: options.config.injectionEffort ?? null,
212
- }], outcomes, classified, { alwaysProposeEffort: true });
212
+ }], outcomes, classified);
213
213
  return {
214
214
  sizingModel,
215
215
  sizingError,