@bitkyc08/opencodex 2.58.0 → 2.60.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 (303) hide show
  1. package/README.md +28 -10
  2. package/gui/dist/assets/index-BTuCbqQd.css +1 -0
  3. package/gui/dist/assets/index-DoBVdPHP.js +134 -0
  4. package/gui/dist/index.html +2 -2
  5. package/gui/dist/provider-icons/crusoe.svg +1 -0
  6. package/gui/dist/provider-icons/opper.svg +3 -0
  7. package/package.json +4 -1
  8. package/src/adapters/anthropic-image-codec.ts +16 -2
  9. package/src/adapters/anthropic-image-normalize.ts +49 -2
  10. package/src/adapters/anthropic.ts +4 -1
  11. package/src/adapters/base.ts +34 -1
  12. package/src/adapters/coding-agent/turn.ts +22 -2
  13. package/src/adapters/command-code.ts +50 -3
  14. package/src/adapters/cursor/catalog.ts +11 -0
  15. package/src/adapters/cursor/checkpoint-store.ts +3 -0
  16. package/src/adapters/cursor/discovery.ts +11 -8
  17. package/src/adapters/cursor/effort-map.ts +16 -2
  18. package/src/adapters/cursor/envelope-echo.ts +55 -2
  19. package/src/adapters/cursor/live-transport.ts +26 -9
  20. package/src/adapters/cursor/message-mapper.ts +3 -2
  21. package/src/adapters/cursor/protobuf-request.ts +8 -5
  22. package/src/adapters/cursor/request-builder.ts +21 -4
  23. package/src/adapters/cursor/thread-continuity.ts +105 -31
  24. package/src/adapters/cursor/tool-guidance.ts +5 -4
  25. package/src/adapters/cursor/transport.ts +19 -0
  26. package/src/adapters/cursor.ts +45 -2
  27. package/src/adapters/devin/cloud-direct/chat.ts +14 -3
  28. package/src/adapters/devin/cloud-direct/index.ts +7 -0
  29. package/src/adapters/devin/cloud-direct/stated-reset-retry.ts +140 -0
  30. package/src/adapters/devin.ts +125 -26
  31. package/src/adapters/google-antigravity-replay.ts +1 -1
  32. package/src/adapters/google-antigravity-wire.ts +55 -4
  33. package/src/adapters/google-http.ts +57 -11
  34. package/src/adapters/google-tool-schema.ts +595 -31
  35. package/src/adapters/google-wire-compiler.ts +93 -10
  36. package/src/adapters/google-wire-shape.ts +461 -0
  37. package/src/adapters/google.ts +60 -10
  38. package/src/adapters/openai-chat/response-events.ts +61 -0
  39. package/src/adapters/openai-chat-images.ts +3 -1
  40. package/src/adapters/openai-chat.ts +10 -11
  41. package/src/adapters/openai-responses/image-gen.ts +8 -6
  42. package/src/adapters/openai-responses/passthrough.ts +25 -4
  43. package/src/adapters/openai-responses/reasoning.ts +7 -0
  44. package/src/adapters/openai-responses/tool-output-recovery.ts +75 -0
  45. package/src/adapters/openai-responses/tool-schema.ts +19 -7
  46. package/src/adapters/opencode-go-additional-tools.ts +12 -2
  47. package/src/adapters/responses-tool-schema.ts +76 -46
  48. package/src/adapters/run-turn-queue.ts +17 -4
  49. package/src/bridge/response-json.ts +1 -1
  50. package/src/bridge/sse.ts +28 -35
  51. package/src/claude/context-windows.ts +22 -0
  52. package/src/claude/outbound.ts +35 -4
  53. package/src/cli/account-api.ts +4 -3
  54. package/src/cli/account-extended.ts +26 -6
  55. package/src/cli/account-orca-import.ts +63 -0
  56. package/src/cli/account.ts +32 -4
  57. package/src/cli/capabilities.ts +40 -0
  58. package/src/cli/claude.ts +29 -1
  59. package/src/cli/codex-cli-update.ts +97 -2
  60. package/src/cli/dispatch.ts +57 -3
  61. package/src/cli/doctor.ts +218 -4
  62. package/src/cli/help.ts +4 -1
  63. package/src/cli/hub.ts +3 -2
  64. package/src/cli/index.ts +95 -22
  65. package/src/cli/models-runtime.ts +33 -4
  66. package/src/cli/opencode.ts +2 -2
  67. package/src/cli/provider.ts +13 -1
  68. package/src/cli/registry.ts +11 -1
  69. package/src/cli/runtime-api.ts +44 -0
  70. package/src/cli/start-args.ts +94 -0
  71. package/src/cli/system-command.ts +2 -0
  72. package/src/client/machine-api.ts +6 -5
  73. package/src/client/machine-listener.ts +16 -3
  74. package/src/client/runtime.ts +26 -2
  75. package/src/clients/config-export/constants.ts +2 -3
  76. package/src/clients/config-export.ts +5 -5
  77. package/src/codex/account-store.ts +146 -5
  78. package/src/codex/auth-api/account-list.ts +19 -11
  79. package/src/codex/auth-api/pool-quota-probe.ts +44 -10
  80. package/src/codex/auth-api/routes.ts +17 -2
  81. package/src/codex/auth-context.ts +16 -12
  82. package/src/codex/catalog/build-entries.ts +25 -4
  83. package/src/codex/catalog/derive-entry.ts +8 -1
  84. package/src/codex/catalog/effort.ts +10 -6
  85. package/src/codex/catalog/gather-capture.ts +22 -2
  86. package/src/codex/catalog/model-hints.ts +66 -33
  87. package/src/codex/catalog/parsing.ts +90 -5
  88. package/src/codex/catalog/provider-models.ts +19 -2
  89. package/src/codex/catalog/reserve-warn.ts +96 -0
  90. package/src/codex/catalog/retained-sync.ts +41 -26
  91. package/src/codex/catalog/routed-gather.ts +61 -3
  92. package/src/codex/cli-installation-identity.ts +210 -0
  93. package/src/codex/cli-installation-targets.ts +158 -0
  94. package/src/codex/context-compat.ts +5 -2
  95. package/src/codex/convergence.ts +5 -0
  96. package/src/codex/desired-state.ts +4 -1
  97. package/src/codex/history-job.ts +6 -6
  98. package/src/codex/history-provider.ts +24 -167
  99. package/src/codex/history-rollout-read.ts +174 -0
  100. package/src/codex/history-state-open.ts +105 -0
  101. package/src/codex/inject/config-toml.ts +44 -2
  102. package/src/codex/inject.ts +3 -2
  103. package/src/codex/internal/catalog-writer.ts +33 -1
  104. package/src/codex/lineage.ts +83 -32
  105. package/src/codex/loopback-target.ts +31 -0
  106. package/src/codex/main-account-hard-lock.ts +2 -1
  107. package/src/codex/main-account.ts +10 -3
  108. package/src/codex/main-device-reauth.ts +17 -9
  109. package/src/codex/model-cache.ts +47 -0
  110. package/src/codex/model-entitlements.ts +80 -2
  111. package/src/codex/observed-model-denials.ts +230 -0
  112. package/src/codex/orca-auth-source.ts +94 -0
  113. package/src/codex/orca-import.ts +219 -0
  114. package/src/codex/prompt-text-probe.ts +289 -16
  115. package/src/codex/quota-401-recovery.ts +12 -0
  116. package/src/codex/quota-types.ts +65 -0
  117. package/src/codex/quota.ts +24 -19
  118. package/src/codex/routing/cooldown-math.ts +8 -47
  119. package/src/codex/routing/pin-drain.ts +57 -0
  120. package/src/codex/routing.ts +20 -16
  121. package/src/codex/shim.ts +1 -1
  122. package/src/codex/subagent-model-fallback.ts +114 -2
  123. package/src/codex/windows-installation-files.ts +224 -0
  124. package/src/combos/failover.ts +125 -5
  125. package/src/config/admitted-identity.ts +222 -0
  126. package/src/config/diagnostics.ts +43 -1
  127. package/src/config/feature-flags.ts +5 -0
  128. package/src/config/load-degrade.ts +33 -0
  129. package/src/config/pending-teardown.ts +8 -0
  130. package/src/config/process-state.ts +36 -3
  131. package/src/config/provider-relative-send-path.ts +16 -0
  132. package/src/config/proxy-env.ts +31 -7
  133. package/src/config/schema/compaction-triggers.ts +11 -0
  134. package/src/config/schema/config-schema.ts +25 -0
  135. package/src/config/schema/leaf-validators.ts +76 -17
  136. package/src/config.ts +2 -2
  137. package/src/generated/compatibility-version.json +410 -258
  138. package/src/generated/model-metadata.ts +1 -1
  139. package/src/grok/reset-coupons.ts +38 -19
  140. package/src/images/loop.ts +6 -1
  141. package/src/integrations/aside-profile-context.ts +37 -3
  142. package/src/integrations/aside-profile-journal.ts +68 -3
  143. package/src/integrations/aside-profiles.ts +128 -3
  144. package/src/integrations/mutation-plan.ts +815 -0
  145. package/src/integrations/writer.ts +85 -99
  146. package/src/lab/live/transport.ts +4 -0
  147. package/src/lab/live/types.ts +5 -0
  148. package/src/lab/subject/behavior-fingerprint.ts +1 -1
  149. package/src/lib/admin-secrets.ts +9 -1
  150. package/src/lib/bounded-body.ts +4 -2
  151. package/src/lib/debug-log-buffer.ts +6 -1
  152. package/src/lib/debug.ts +23 -0
  153. package/src/lib/destination-policy.ts +48 -6
  154. package/src/lib/errors.ts +82 -15
  155. package/src/lib/http-response-semantics.ts +57 -0
  156. package/src/lib/lab-live-pinned-sender.ts +26 -12
  157. package/src/lib/local-destinations.ts +32 -5
  158. package/src/lib/pinned-http.ts +142 -2
  159. package/src/lib/plain-data.ts +103 -0
  160. package/src/lib/process-control.ts +13 -5
  161. package/src/lib/provider-outbound.ts +53 -5
  162. package/src/lib/proxy-env.ts +70 -3
  163. package/src/lib/request-execution-budget.ts +11 -3
  164. package/src/lib/response-body-inactivity.ts +193 -0
  165. package/src/lib/retry-delay.ts +69 -0
  166. package/src/lib/socks5-fetch.ts +741 -0
  167. package/src/lib/spend-ledger-owner.ts +364 -0
  168. package/src/lib/spend-reservation-ledger.ts +332 -35
  169. package/src/lib/windows-system-proxy.ts +16 -11
  170. package/src/lib/workflow-budget.ts +145 -8
  171. package/src/oauth/account-quota-rank.ts +72 -15
  172. package/src/oauth/callback-server.ts +4 -3
  173. package/src/oauth/generic-account-failover.ts +41 -27
  174. package/src/oauth/health.ts +12 -1
  175. package/src/oauth/index.ts +3 -107
  176. package/src/oauth/login-flow-state.ts +127 -0
  177. package/src/oauth/orcarouter.ts +15 -2
  178. package/src/oauth/store.ts +8 -0
  179. package/src/providers/codex-capacity.ts +9 -0
  180. package/src/providers/derive.ts +34 -17
  181. package/src/providers/devin-cli-authmode-migration.ts +14 -10
  182. package/src/providers/devin-provider-merge-migration.ts +33 -12
  183. package/src/providers/free-directory.ts +20 -2
  184. package/src/providers/key-failover.ts +327 -25
  185. package/src/providers/model-rename-migration.ts +56 -1
  186. package/src/providers/model-rename-startup.ts +7 -5
  187. package/src/providers/openai-sidecar.ts +4 -0
  188. package/src/providers/openai-virtual-models.ts +42 -2
  189. package/src/providers/opencode-go-transport.ts +14 -5
  190. package/src/providers/quota/antigravity.ts +22 -2
  191. package/src/providers/quota/report-cache.ts +3 -0
  192. package/src/providers/quota/vendor-probes-key.ts +1 -1
  193. package/src/providers/registry/entries-core.ts +39 -17
  194. package/src/providers/registry/entries-extended.ts +131 -1
  195. package/src/providers/registry/model-ids.ts +168 -0
  196. package/src/providers/registry/model-seeds.ts +87 -21
  197. package/src/providers/registry/types.ts +2 -0
  198. package/src/providers/resolved-model-policy-merge.ts +167 -0
  199. package/src/providers/resolved-model-policy.ts +406 -0
  200. package/src/providers/stale-vision-classification-migration.ts +137 -0
  201. package/src/responses/apply-patch-envelope.ts +32 -11
  202. package/src/responses/bridge-search-replay-cache.ts +152 -0
  203. package/src/responses/code-mode-helper-compat.ts +26 -16
  204. package/src/responses/custom-tool-compat.ts +1 -1
  205. package/src/responses/freeform-wrapper-scan.ts +279 -0
  206. package/src/responses/hosted-tool-policy.ts +85 -2
  207. package/src/responses/legacy-dotted-tool-name-repair.ts +134 -0
  208. package/src/responses/progressive-freeform-input.ts +130 -0
  209. package/src/responses/reasoning-envelope.ts +30 -0
  210. package/src/responses/schema.ts +9 -2
  211. package/src/responses/state.ts +5 -12
  212. package/src/responses/tool-name-aliases.ts +15 -1
  213. package/src/router.ts +91 -115
  214. package/src/routing/compatibility/behavior.ts +9 -0
  215. package/src/routing/compatibility/subject.ts +16 -1
  216. package/src/server/adapter-resolve.ts +9 -0
  217. package/src/server/auth-cors.ts +29 -0
  218. package/src/server/chat-completions.ts +13 -5
  219. package/src/server/chat-native-sse.ts +26 -9
  220. package/src/server/chat-native.ts +10 -4
  221. package/src/server/claude-messages.ts +30 -5
  222. package/src/server/effort-row.ts +11 -3
  223. package/src/server/grok-responses-control-frame.ts +160 -1
  224. package/src/server/gui-static.ts +36 -2
  225. package/src/server/inbound-body-admission.ts +187 -0
  226. package/src/server/index/serve-options.ts +86 -29
  227. package/src/server/index/spend-ledger-lifecycle.ts +66 -0
  228. package/src/server/index/websocket-handler.ts +6 -1
  229. package/src/server/index.ts +24 -25
  230. package/src/server/management/api-access.ts +3 -4
  231. package/src/server/management/aside-profile-routes.ts +266 -7
  232. package/src/server/management/config-routes.ts +48 -7
  233. package/src/server/management/context.ts +3 -0
  234. package/src/server/management/integration-routes.ts +287 -5
  235. package/src/server/management/metrics-routes.ts +20 -0
  236. package/src/server/management/model-rows.ts +224 -12
  237. package/src/server/management/provider-capability-config.ts +35 -7
  238. package/src/server/management/provider-routes.ts +70 -18
  239. package/src/server/management/route-registry.ts +14 -0
  240. package/src/server/management/shared.ts +10 -3
  241. package/src/server/management/system-restart.ts +7 -2
  242. package/src/server/management/system-routes.ts +2 -0
  243. package/src/server/management/usage-aggregate-cache.ts +4 -0
  244. package/src/server/management-api.ts +2 -0
  245. package/src/server/management-auth.ts +15 -1
  246. package/src/server/proxy-liveness.ts +97 -2
  247. package/src/server/readiness.ts +29 -10
  248. package/src/server/relay-eager.ts +24 -2
  249. package/src/server/relay.ts +136 -34
  250. package/src/server/request-log.ts +80 -3
  251. package/src/server/request-metrics.ts +236 -0
  252. package/src/server/responses/adapter-continuation.ts +74 -30
  253. package/src/server/responses/adapter-delivery.ts +39 -8
  254. package/src/server/responses/adapter-dispatch.ts +63 -30
  255. package/src/server/responses/compact.ts +89 -18
  256. package/src/server/responses/compaction-routing.ts +111 -0
  257. package/src/server/responses/core-codex-account.ts +90 -24
  258. package/src/server/responses/core-combo.ts +7 -7
  259. package/src/server/responses/core-normalize.ts +16 -15
  260. package/src/server/responses/core-opaque-recovery.ts +1 -0
  261. package/src/server/responses/core-options.ts +4 -0
  262. package/src/server/responses/encrypted-payload.ts +20 -2
  263. package/src/server/responses/fetch-helpers.ts +68 -2
  264. package/src/server/responses/passthrough-delivery.ts +41 -15
  265. package/src/server/responses/passthrough-dispatch.ts +145 -53
  266. package/src/server/responses/passthrough-execution.ts +11 -1
  267. package/src/server/responses/policy-fallback.ts +5 -13
  268. package/src/server/responses/request-prepare.ts +96 -17
  269. package/src/server/responses/request-send-budget.ts +89 -8
  270. package/src/server/responses/request-sidecar-auth.ts +17 -9
  271. package/src/server/responses/request-spend.ts +38 -9
  272. package/src/server/responses/request-transport.ts +15 -12
  273. package/src/server/responses/run-turn-execution.ts +45 -8
  274. package/src/server/responses/sidecar-execution.ts +19 -2
  275. package/src/server/responses/ws-upstream.ts +16 -28
  276. package/src/server/responses-custom-tool-repair.ts +29 -56
  277. package/src/server/responses-undeclared-tool-guard.ts +31 -1
  278. package/src/server/sse-frame-buffer.ts +12 -10
  279. package/src/server/sse-payload-rewrite.ts +37 -10
  280. package/src/server/system-env-shell.ts +5 -1
  281. package/src/server/system-env.ts +7 -1
  282. package/src/server/workflow-refusal.ts +56 -2
  283. package/src/service/cli.ts +16 -6
  284. package/src/service/guards.ts +10 -0
  285. package/src/service/health.ts +43 -0
  286. package/src/service/state.ts +7 -2
  287. package/src/tray/windows-tray.ps1 +155 -3
  288. package/src/types/accounts.ts +4 -0
  289. package/src/types/config.ts +111 -6
  290. package/src/types/provider.ts +37 -0
  291. package/src/types/request.ts +9 -1
  292. package/src/types/tools.ts +14 -0
  293. package/src/types/wire.ts +9 -1
  294. package/src/types.ts +1 -0
  295. package/src/usage/expected-prices.ts +28 -0
  296. package/src/usage/log.ts +87 -4
  297. package/src/vision/eligibility.ts +88 -9
  298. package/src/vision/plan.ts +34 -10
  299. package/src/web-search/executor.ts +41 -2
  300. package/src/web-search/loop.ts +6 -1
  301. package/src/web-search/passthrough-bridge.ts +39 -5
  302. package/gui/dist/assets/index-BbrHOIY0.js +0 -128
  303. package/gui/dist/assets/index-C5-RdDmD.css +0 -1
@@ -3,14 +3,19 @@ import { ClientPathError } from "../../clients/config-export";
3
3
  import { IntegrationMutationBusyError } from "../../integrations/mutation-flight";
4
4
  import { IntegrationWriterLockBusyError } from "../../integrations/writer-lock";
5
5
  import {
6
- getAsideProfileState, listAsideProfileStates, mutateAsideProfiles, refreshAsideProfiles,
7
- type AsideProfilesInput,
6
+ getAsideProfileState, listAsideProfileStates, mutateAsideProfiles, previewAsideProfile, refreshAsideProfiles,
7
+ type AsideProfilesInput, type AsideProfileWriteOutcome,
8
8
  } from "../../integrations/aside-profiles";
9
9
  import {
10
10
  listAsideOperations, findAsideOperation, restoreAsideProfile, deleteAsideOperation,
11
11
  asideOperationMatchesCurrent,
12
12
  } from "../../integrations/aside-profile-journal";
13
+ import type { AsideOperation } from "../../integrations/aside-profile-journal";
13
14
  import type { WriteRefused } from "../../integrations/writer";
15
+ import type { IntegrationWriteInput } from "../../integrations/writer";
16
+ import type { IntegrationMutationPlan, IntegrationPlanOperation } from "../../integrations/mutation-plan";
17
+ import { previewIntegration } from "../../integrations/mutation-plan";
18
+ import { exportSnapshotIdentity, previewExportSnapshot } from "./model-rows";
14
19
  import type { ManagementContext } from "./context";
15
20
  import { readManagementJsonBody, readOptionalManagementJsonBody, rethrowManagementBodyTooLarge } from "./body";
16
21
  import { jsonResponse } from "../auth-cors";
@@ -22,6 +27,142 @@ export interface AsideProfileRouteOptions {
22
27
 
23
28
  class ProfileQueryError extends Error { readonly status = 400; readonly code = "invalid_aside_profile"; }
24
29
 
30
+ /**
31
+ * The confirmed plan carried with this request, or null when there is none.
32
+ *
33
+ * Both fields or neither, and the operation must be the one being requested. Dropping a supplied
34
+ * binding would be the worst available behaviour: the caller believes their confirmation is being
35
+ * checked while the mutation proceeds unchecked.
36
+ */
37
+ function asideBinding(
38
+ body: { operation?: unknown; planFingerprint?: unknown },
39
+ expected: IntegrationPlanOperation,
40
+ ): { operation: IntegrationPlanOperation; fingerprint: string } | null {
41
+ const { operation, planFingerprint } = body;
42
+ if (operation === undefined && planFingerprint === undefined) return null;
43
+ if (operation === undefined || typeof planFingerprint !== "string" || planFingerprint.length === 0) {
44
+ throw new ProfileQueryError("operation and planFingerprint must be sent together");
45
+ }
46
+ if (operation !== expected) throw new ProfileQueryError("operation does not match the requested change");
47
+ return { operation: expected, fingerprint: planFingerprint };
48
+ }
49
+
50
+ /**
51
+ * The guard a bound profile change runs before it writes anything.
52
+ *
53
+ * It re-plans the same profile scope and compares. The hooks that call it sit ahead of the
54
+ * preference write and the journal import, which is the only placement that helps here: those two
55
+ * happen before any writer lock is taken, so a check under that lock would fire after the thing it
56
+ * was meant to prevent.
57
+ */
58
+ /**
59
+ * The check a bound Aside change runs, against the input that change is about to write from.
60
+ *
61
+ * It plans the prepared input it is given rather than building one of its own. Rebuilding meant
62
+ * reading the live configuration a second time, so a configuration edited to something else and
63
+ * back again while this action was in flight produced a check that agreed with a plan the write
64
+ * never described.
65
+ *
66
+ * Exported so the check itself can be exercised with a real prepared input and a roster that
67
+ * actually moved. The window it closes needs an ordinary load to complete while a mutation is
68
+ * preparing, which no test seam reaches from outside; a copy of the guard would prove nothing
69
+ * about the one the route installs. It takes only the configuration from the request context.
70
+ */
71
+ export function asideGuardFor(
72
+ ctx: Pick<ManagementContext, "config">,
73
+ capturedIdentity: string,
74
+ profileIdValue: number,
75
+ binding: { operation: IntegrationPlanOperation; fingerprint: string },
76
+ request: { opId?: string; confirmDrift?: boolean; resolved?: AsideOperation },
77
+ capture: { plan: IntegrationMutationPlan | null },
78
+ ): (prepared: IntegrationWriteInput) => Promise<AsideProfileWriteOutcome | null> {
79
+ return async prepared => {
80
+ const plan = planFor(prepared, profileIdValue, binding, request);
81
+ /*
82
+ * The roster this confirmation was planned against has to still be the retained one. Only its
83
+ * rows were carried into the mutation, so an ordinary load completing while this action
84
+ * prepared could have replaced or retired the snapshot, and fingerprinting the carried rows
85
+ * would then accept a confirmation for a roster the operator no longer has. The non-Aside
86
+ * guard verifies the same identity; this one verifies it before the preference write, which is
87
+ * the last moment that still precedes every effect this action has.
88
+ */
89
+ const rosterMoved = exportSnapshotIdentity(ctx.config) !== capturedIdentity;
90
+ if (!rosterMoved && plan.canApply && plan.fingerprint === binding.fingerprint) return null;
91
+ capture.plan = plan;
92
+ return {
93
+ ok: false,
94
+ reason: "conflict",
95
+ state: plan.state,
96
+ clientId: "aside",
97
+ message: rosterMoved
98
+ ? "the model roster changed while confirming"
99
+ : "that confirmation no longer describes this profile",
100
+ profileId: profileIdValue,
101
+ };
102
+ };
103
+ }
104
+
105
+ /**
106
+ * The same comparison, run again immediately before the document is written.
107
+ *
108
+ * Aside takes no writer lock, and its preference write and journal import sit between the first
109
+ * check and the write that check authorizes. A target edited in that window is read fresh by the
110
+ * writer, which then compares it against itself and finds nothing to object to, so a confirmation
111
+ * about the earlier file would still overwrite the later one.
112
+ *
113
+ * The roster is deliberately not re-examined here. This action has just written the operator's
114
+ * preference into the configuration, which retires the retained roster by design, so asking that
115
+ * question at this point would refuse every confirmation on principle. What is asked is the one
116
+ * thing this moment can answer: does the plan the operator confirmed still describe this file.
117
+ */
118
+ export function asideLateGuardFor(
119
+ profileIdValue: number,
120
+ binding: { operation: IntegrationPlanOperation; fingerprint: string },
121
+ request: { opId?: string; confirmDrift?: boolean; resolved?: AsideOperation },
122
+ capture: { plan: IntegrationMutationPlan | null },
123
+ ): (prepared: IntegrationWriteInput) => Promise<AsideProfileWriteOutcome | null> {
124
+ return async prepared => {
125
+ const plan = planFor(prepared, profileIdValue, binding, request);
126
+ if (plan.canApply && plan.fingerprint === binding.fingerprint) return null;
127
+ capture.plan = plan;
128
+ return {
129
+ ok: false,
130
+ reason: "conflict",
131
+ state: plan.state,
132
+ clientId: "aside",
133
+ message: "that confirmation no longer describes this profile",
134
+ profileId: profileIdValue,
135
+ };
136
+ };
137
+ }
138
+
139
+ function planFor(
140
+ prepared: IntegrationWriteInput,
141
+ profileIdValue: number,
142
+ binding: { operation: IntegrationPlanOperation; fingerprint: string },
143
+ request: { opId?: string; confirmDrift?: boolean; resolved?: AsideOperation },
144
+ ): IntegrationMutationPlan {
145
+ return previewIntegration(prepared, {
146
+ profileId: profileIdValue,
147
+ operation: binding.operation,
148
+ ...(request.opId === undefined ? {} : { opId: request.opId }),
149
+ ...(request.confirmDrift === undefined ? {} : { confirmDrift: request.confirmDrift }),
150
+ // The row the route selected, so the guard and the mutation mean the same operation even
151
+ // when more than one valid copy exists.
152
+ ...(request.resolved === undefined
153
+ ? {}
154
+ : { resolved: { entry: request.resolved.entry, store: request.resolved.store } }),
155
+ });
156
+ }
157
+
158
+ function stalePlanResponse(ctx: ManagementContext, plan: IntegrationMutationPlan): Response {
159
+ return jsonResponse({
160
+ error: "integration preview is stale",
161
+ code: "integration_preview_stale",
162
+ plan,
163
+ }, 409, ctx.req, ctx.config);
164
+ }
165
+
25
166
  const ASIDE_INTEGRATION_PATH = "/api/client-integrations/aside";
26
167
  const ASIDE_PROFILES_PATH = "/api/client-integrations/aside/profiles";
27
168
 
@@ -75,7 +216,7 @@ function nestedProfileContext(ctx: ManagementContext): { ctx: ManagementContext;
75
216
  url.searchParams.set("client", "aside");
76
217
  return { ctx: { ...ctx, url }, action: "journal" };
77
218
  }
78
- if (parts.length > 2 || !parts[0] || (parts[1] !== undefined && !["journal", "restore"].includes(parts[1]))) {
219
+ if (parts.length > 2 || !parts[0] || (parts[1] !== undefined && !["journal", "restore", "preview"].includes(parts[1]))) {
79
220
  throw new ProfileQueryError("Invalid Aside profile path");
80
221
  }
81
222
  const prior = url.searchParams.get("profile");
@@ -107,12 +248,62 @@ export async function handleAsideProfileRoutes(
107
248
  }
108
249
  return null;
109
250
  }
251
+ if (normalized.action === "preview") {
252
+ if (req.method !== "POST") return null;
253
+ // A plan describes one profile, so an unscoped preview has nothing to describe.
254
+ if (id === undefined) throw new ProfileQueryError("a plan applies to one profile");
255
+ const body = await readProfileBody(req);
256
+ if (!isObject(body)) throw new ProfileQueryError("preview body must be an object");
257
+ const operation = body.operation;
258
+ if (operation !== "apply" && operation !== "overwrite" && operation !== "disable" && operation !== "restore") {
259
+ throw new ProfileQueryError("operation must be apply, overwrite, disable or restore");
260
+ }
261
+ if (operation === "restore" && (typeof body.opId !== "string" || !body.opId.trim())) {
262
+ throw new ProfileQueryError("opId must be a non-empty string");
263
+ }
264
+ // The preview resolves the row the same way the mutation will, once, here.
265
+ const selected = operation === "restore"
266
+ ? findAsideOperation(options.input(), String(body.opId).trim(), id)
267
+ : null;
268
+ if (operation === "restore" && selected === null) {
269
+ return jsonResponse({
270
+ error: "integration operation not found",
271
+ code: "integration_operation_not_found",
272
+ }, 404, req, ctx.config);
273
+ }
274
+ const roster = previewExportSnapshot(ctx.config);
275
+ if (roster === null) {
276
+ return jsonResponse({
277
+ error: "no model roster is cached yet, so this change cannot be planned",
278
+ code: "integration_preview_unavailable",
279
+ }, 409, req, ctx.config);
280
+ }
281
+ const plan = await previewAsideProfile({ ...options.input(), models: roster.models }, {
282
+ profileId: id,
283
+ operation,
284
+ ...(operation === "restore"
285
+ ? {
286
+ opId: String(body.opId).trim(),
287
+ confirmDrift: body.confirmDrift === true,
288
+ ...(selected === null ? {} : { resolved: { entry: selected.entry, store: selected.store } }),
289
+ }
290
+ : {}),
291
+ });
292
+ return jsonResponse(plan, 200, req, ctx.config);
293
+ }
110
294
  if (normalized.action === "restore") {
111
295
  if (req.method !== "POST") return null;
112
296
  const body = await readProfileBody(req);
113
297
  if (!isObject(body) || typeof body.opId !== "string" || !body.opId.trim()
114
298
  || (body.confirmDrift !== undefined && typeof body.confirmDrift !== "boolean")) throw new ProfileQueryError("Invalid Aside restore request");
115
- return asideRestoreResponse(ctx, { opId: body.opId.trim(), confirmDrift: body.confirmDrift === true }, options);
299
+ // Rebuilding the body here previously dropped any binding, so a bound nested restore
300
+ // reached the writer with its confirmation unexamined. Forward the fields as sent.
301
+ return asideRestoreResponse(ctx, {
302
+ opId: body.opId.trim(),
303
+ confirmDrift: body.confirmDrift === true,
304
+ ...(body.operation === undefined ? {} : { operation: body.operation }),
305
+ ...(body.planFingerprint === undefined ? {} : { planFingerprint: body.planFingerprint }),
306
+ }, options);
116
307
  }
117
308
  if (url.pathname === "/api/client-integrations/aside/sync") {
118
309
  if (req.method !== "POST") return null;
@@ -134,7 +325,38 @@ export async function handleAsideProfileRoutes(
134
325
  if (!isObject(body) || typeof body.enabled !== "boolean") throw new ProfileQueryError("enabled must be a boolean");
135
326
  if (body.overwriteConflict !== undefined && typeof body.overwriteConflict !== "boolean") throw new ProfileQueryError("overwriteConflict must be a boolean");
136
327
  if (body.overwriteConflict === true && !body.enabled) throw new ProfileQueryError("overwriteConflict applies only to enabling an integration");
137
- const batch = await mutateAsideProfiles(options.input(), { enabled: body.enabled, profileId: id, overwriteConflict: body.overwriteConflict === true });
328
+ // An exact profile is required before a binding could ever mean anything: one fingerprint
329
+ // cannot honestly describe several independently changing files.
330
+ const expected: IntegrationPlanOperation = body.enabled
331
+ ? (body.overwriteConflict === true ? "overwrite" : "apply")
332
+ : "disable";
333
+ if ((body.operation !== undefined || body.planFingerprint !== undefined) && id === undefined) {
334
+ throw new ProfileQueryError("a confirmed plan applies to one profile");
335
+ }
336
+ const binding = asideBinding(body, expected);
337
+ const capture: { plan: IntegrationMutationPlan | null } = { plan: null };
338
+ let mutationInput = options.input();
339
+ let revalidate: ((prepared: IntegrationWriteInput) => Promise<AsideProfileWriteOutcome | null>) | undefined;
340
+ let revalidateBeforeWrite: ((prepared: IntegrationWriteInput) => Promise<AsideProfileWriteOutcome | null>) | undefined;
341
+ if (binding !== null && id !== undefined) {
342
+ const roster = previewExportSnapshot(ctx.config);
343
+ if (roster === null) {
344
+ return jsonResponse({
345
+ error: "no model roster is cached yet, so this change cannot be planned",
346
+ code: "integration_preview_unavailable",
347
+ }, 409, req, ctx.config);
348
+ }
349
+ // One roster for the guard and the mutation, so they cannot disagree by construction.
350
+ mutationInput = { ...mutationInput, models: roster.models };
351
+ revalidate = asideGuardFor(ctx, roster.identity, id, binding, {}, capture);
352
+ revalidateBeforeWrite = asideLateGuardFor(id, binding, {}, capture);
353
+ }
354
+ const batch = await mutateAsideProfiles(
355
+ mutationInput,
356
+ { enabled: body.enabled, profileId: id, overwriteConflict: body.overwriteConflict === true },
357
+ revalidate ? { revalidate, ...(revalidateBeforeWrite ? { revalidateBeforeWrite } : {}) } : undefined,
358
+ );
359
+ if (capture.plan) return stalePlanResponse(ctx, capture.plan);
138
360
  if (id !== undefined) {
139
361
  const result = batch.results[0];
140
362
  if (!result) throw new Error("Aside profile mutation returned no result");
@@ -180,7 +402,9 @@ export async function asideJournalResponse(
180
402
  }
181
403
 
182
404
  export async function asideRestoreResponse(
183
- ctx: ManagementContext, body: { opId: string; confirmDrift?: boolean }, options: AsideProfileRouteOptions,
405
+ ctx: ManagementContext,
406
+ body: { opId: string; confirmDrift?: boolean; operation?: unknown; planFingerprint?: unknown },
407
+ options: AsideProfileRouteOptions,
184
408
  ): Promise<Response | null> {
185
409
  try {
186
410
  validateClientSelector(ctx);
@@ -196,7 +420,42 @@ export async function asideRestoreResponse(
196
420
  if (id === undefined) return null;
197
421
  return jsonResponse({ error: "integration operation not found", code: "integration_operation_not_found", opId: body.opId }, 404, ctx.req, ctx.config);
198
422
  }
199
- const result = await restoreAsideProfile(input, { ...body, profileId: operation.profileId });
423
+ if ((body.operation !== undefined || body.planFingerprint !== undefined) && id === undefined) {
424
+ throw new ProfileQueryError("a confirmed plan applies to one profile");
425
+ }
426
+ const restoreBinding = asideBinding(body, "restore");
427
+ const capture: { plan: IntegrationMutationPlan | null } = { plan: null };
428
+ let restoreInput = input;
429
+ let revalidate: ((prepared: IntegrationWriteInput) => Promise<AsideProfileWriteOutcome | null>) | undefined;
430
+ let revalidateBeforeWrite: ((prepared: IntegrationWriteInput) => Promise<AsideProfileWriteOutcome | null>) | undefined;
431
+ if (restoreBinding !== null) {
432
+ const roster = previewExportSnapshot(ctx.config);
433
+ if (roster === null) {
434
+ return jsonResponse({
435
+ error: "no model roster is cached yet, so this change cannot be planned",
436
+ code: "integration_preview_unavailable",
437
+ }, 409, ctx.req, ctx.config);
438
+ }
439
+ restoreInput = { ...restoreInput, models: roster.models };
440
+ revalidate = asideGuardFor(ctx, roster.identity, operation.profileId, restoreBinding, {
441
+ opId: body.opId,
442
+ confirmDrift: body.confirmDrift === true,
443
+ resolved: operation,
444
+ }, capture);
445
+ revalidateBeforeWrite = asideLateGuardFor(operation.profileId, restoreBinding, {
446
+ opId: body.opId,
447
+ confirmDrift: body.confirmDrift === true,
448
+ resolved: operation,
449
+ }, capture);
450
+ }
451
+ const result = await restoreAsideProfile(
452
+ restoreInput,
453
+ // The same row the preview and guard used. Letting the mutation resolve its own copy is how
454
+ // a confirmation ends up bound to an operation other than the one that runs.
455
+ { ...body, profileId: operation.profileId, selectedOperation: operation },
456
+ revalidate ? { revalidate, ...(revalidateBeforeWrite ? { revalidateBeforeWrite } : {}) } : undefined,
457
+ );
458
+ if (capture.plan) return stalePlanResponse(ctx, capture.plan);
200
459
  return result.ok ? jsonResponse(result, 200, ctx.req, ctx.config) : options.failure(result);
201
460
  } catch (error) {
202
461
  if (error instanceof ClientPathError && !ctx.url.searchParams.has("profile")
@@ -1,3 +1,5 @@
1
+ import { compactionRoutingSchema } from "../../config/schema/leaf-validators";
2
+ import { captureConfigTopLevelRollback } from "../../config/rebase-provenance";
1
3
  import type { IntegrationClientId } from "../../integrations/registry";
2
4
  import { randomUUID } from "node:crypto";
3
5
  import { readFileSync } from "node:fs";
@@ -110,7 +112,7 @@ import { filterRequestLogs, getRequestLogEntries, type RequestLogEntry } from ".
110
112
  import { estimateComboCost, estimateRequestCost, normalizeCostTokens, tokensPerSecond } from "../../usage/cost";
111
113
  import type { PersistedUsageAttempt } from "../../usage/log";
112
114
  import { isAllowedRequestOrigin, jsonResponse, providerManagementConfigError, publicProviderBaseUrl, safeConfigDTO } from "../auth-cors";
113
- import { withProviderServiceTierDTO } from "./provider-capability-config";
115
+ import { withProviderCatalogCapabilityDTO } from "./provider-capability-config";
114
116
  import { applySystemEnvToggle } from "../system-env";
115
117
  import { getCachedStartupHealth, invalidateStartupHealthCache } from "../startup-health-cache";
116
118
  import { runWindowsTrayAction } from "../windows-tray-control";
@@ -186,7 +188,8 @@ interface ClientIntegrationSyncOutcome {
186
188
  export async function syncEnabledClientIntegrations(
187
189
  port: number | undefined,
188
190
  config: OcxConfig,
189
- deps: Pick<ManagementContext["deps"], "fetchAllModels" | "writeDesktop3pConfig"> = {},
191
+ deps: Pick<ManagementContext["deps"],
192
+ "fetchAllModels" | "refreshOwnedCatalogIntegrations" | "writeDesktop3pConfig"> = {},
190
193
  ): Promise<ClientIntegrationSyncOutcome[]> {
191
194
  if (port === undefined) return [];
192
195
  const { claudeDesktopIntegrationEnabled, grokIntegrationEnabled } = await import("../../codex/desired-state");
@@ -235,7 +238,8 @@ export async function syncEnabledClientIntegrations(
235
238
  }
236
239
 
237
240
  const { refreshOwnedCatalogIntegrations } = await import("../../integrations/catalog-refresh");
238
- out.push(...await refreshOwnedCatalogIntegrations({
241
+ const refreshOwned = deps.refreshOwnedCatalogIntegrations ?? refreshOwnedCatalogIntegrations;
242
+ out.push(...await refreshOwned({
239
243
  models: async () => {
240
244
  const { loadExportModels } = await import("./model-rows");
241
245
  return loadExportModels(config);
@@ -266,7 +270,7 @@ export async function handleConfigRoutes(ctx: ManagementContext): Promise<Respon
266
270
  const { req, url, config, deps, convergeCodexCatalog, syncClaudeAgentDefsBestEffort } = ctx;
267
271
  const readStartupHealth = deps.getCachedStartupHealth ?? getCachedStartupHealth;
268
272
  if (url.pathname === "/api/config" && req.method === "GET") {
269
- return jsonResponse(withProviderServiceTierDTO(safeConfigDTO(config), config));
273
+ return jsonResponse(withProviderCatalogCapabilityDTO(safeConfigDTO(config), config));
270
274
  }
271
275
 
272
276
  if (url.pathname === "/api/config" && req.method === "PUT") {
@@ -324,6 +328,8 @@ export async function handleConfigRoutes(ctx: ManagementContext): Promise<Respon
324
328
  // Absent means off, same convention: the GUI renders a plain switch without
325
329
  // needing to know that `undefined` and `false` mean the same thing here.
326
330
  ultraFastTier: config.ultraFastTier === true,
331
+ // Absent means on by default: the GUI renders a switch enabled unless explicit false.
332
+ fastRows: config.fastRows !== false,
327
333
  codexMainAccountHardLock: config.codexMainAccountHardLock === true,
328
334
  mainAccountHardLock: getMainAccountHardLockStatus(config),
329
335
  // Absent means the historical auto-open, so the GUI can render the toggle
@@ -338,6 +344,7 @@ export async function handleConfigRoutes(ctx: ManagementContext): Promise<Respon
338
344
  reason: "not_requested",
339
345
  retryable: false,
340
346
  }),
347
+ compactionRouting: config.compactionRouting ?? null,
341
348
  startupHealth: await readStartupHealth(config),
342
349
  codexRuntime: {
343
350
  path: displayCodexRuntimePath(resolved.runtime.command),
@@ -425,9 +432,11 @@ export async function handleConfigRoutes(ctx: ManagementContext): Promise<Respon
425
432
  codexQuotaAutoRefresh?: unknown;
426
433
  oauthOpenBrowser?: unknown;
427
434
  ultraFastTier?: unknown;
435
+ fastRows?: unknown;
428
436
  codexMainAccountHardLock?: unknown;
429
437
  codexDesktopAuthless?: unknown;
430
438
  codexClientCompaction?: unknown;
439
+ compactionRouting?: unknown;
431
440
  };
432
441
  if (body.codexAutoStart === undefined
433
442
  && body.streamMode === undefined
@@ -436,10 +445,12 @@ export async function handleConfigRoutes(ctx: ManagementContext): Promise<Respon
436
445
  && body.codexQuotaAutoRefresh === undefined
437
446
  && body.oauthOpenBrowser === undefined
438
447
  && body.ultraFastTier === undefined
448
+ && body.fastRows === undefined
439
449
  && body.codexMainAccountHardLock === undefined
440
450
  && body.codexDesktopAuthless === undefined
441
- && body.codexClientCompaction === undefined) {
442
- return jsonResponse({ error: "provide codexAutoStart, streamMode, appOwnedMemoryBudgetMb, codexAccountPickerEnabled, codexQuotaAutoRefresh, oauthOpenBrowser, ultraFastTier, codexMainAccountHardLock, codexDesktopAuthless, or codexClientCompaction" }, 400);
451
+ && body.codexClientCompaction === undefined
452
+ && body.compactionRouting === undefined) {
453
+ return jsonResponse({ error: "provide codexAutoStart, streamMode, appOwnedMemoryBudgetMb, codexAccountPickerEnabled, codexQuotaAutoRefresh, oauthOpenBrowser, ultraFastTier, fastRows, codexMainAccountHardLock, codexDesktopAuthless, codexClientCompaction, or compactionRouting" }, 400);
443
454
  }
444
455
  if (body.codexAutoStart !== undefined && typeof body.codexAutoStart !== "boolean") {
445
456
  return jsonResponse({ error: "codexAutoStart boolean is required" }, 400);
@@ -457,6 +468,9 @@ export async function handleConfigRoutes(ctx: ManagementContext): Promise<Respon
457
468
  if (body.ultraFastTier !== undefined && typeof body.ultraFastTier !== "boolean") {
458
469
  return jsonResponse({ error: "ultraFastTier boolean is required" }, 400);
459
470
  }
471
+ if (body.fastRows !== undefined && typeof body.fastRows !== "boolean") {
472
+ return jsonResponse({ error: "fastRows boolean is required" }, 400);
473
+ }
460
474
  if (body.codexMainAccountHardLock !== undefined && typeof body.codexMainAccountHardLock !== "boolean") {
461
475
  return jsonResponse({ error: "codexMainAccountHardLock boolean is required" }, 400);
462
476
  }
@@ -466,6 +480,12 @@ export async function handleConfigRoutes(ctx: ManagementContext): Promise<Respon
466
480
  if (body.codexClientCompaction !== undefined && typeof body.codexClientCompaction !== "boolean") {
467
481
  return jsonResponse({ error: "codexClientCompaction boolean is required" }, 400);
468
482
  }
483
+ const compactionRouting = body.compactionRouting == null
484
+ ? body.compactionRouting
485
+ : compactionRoutingSchema.safeParse(body.compactionRouting);
486
+ if (compactionRouting != null && !compactionRouting.success) {
487
+ return jsonResponse({ error: "compactionRouting requires a model, an optional valid reasoningEffort, and optional non-repeating triggers drawn from \"manual\" and \"auto\"" }, 400);
488
+ }
469
489
  let quotaAutoRefreshChange: { id: string; window: "fiveHour" | "weekly"; enabled: boolean } | undefined;
470
490
  if (body.codexQuotaAutoRefresh !== undefined) {
471
491
  if (!isPlainRecord(body.codexQuotaAutoRefresh)) {
@@ -495,6 +515,7 @@ export async function handleConfigRoutes(ctx: ManagementContext): Promise<Respon
495
515
  )) {
496
516
  return jsonResponse({ error: `appOwnedMemoryBudgetMb must be an integer from ${MIN_APP_OWNED_MEMORY_BUDGET_MB} to ${MAX_APP_OWNED_MEMORY_BUDGET_MB}` }, 400);
497
517
  }
518
+ const restoreCompactionRouting = captureConfigTopLevelRollback(config, ["compactionRouting"]);
498
519
  const previousSettings = {
499
520
  codexAutoStart: config.codexAutoStart,
500
521
  hasCodexAutoStart: Object.hasOwn(config, "codexAutoStart"),
@@ -512,6 +533,8 @@ export async function handleConfigRoutes(ctx: ManagementContext): Promise<Respon
512
533
  hasOauthOpenBrowser: Object.hasOwn(config, "oauthOpenBrowser"),
513
534
  ultraFastTier: config.ultraFastTier,
514
535
  hasUltraFastTier: Object.hasOwn(config, "ultraFastTier"),
536
+ fastRows: config.fastRows,
537
+ hasFastRows: Object.hasOwn(config, "fastRows"),
515
538
  codexMainAccountHardLock: config.codexMainAccountHardLock,
516
539
  hasCodexMainAccountHardLock: Object.hasOwn(config, "codexMainAccountHardLock"),
517
540
  codexDesktopAuthless: config.codexDesktopAuthless,
@@ -523,6 +546,7 @@ export async function handleConfigRoutes(ctx: ManagementContext): Promise<Respon
523
546
  let pickerIsEnabled = pickerWasEnabled;
524
547
  const authlessWasEnabled = config.codexDesktopAuthless === true;
525
548
  const clientCompactionWasEnabled = config.codexClientCompaction === true;
549
+ const fastRowsWasEnabled = config.fastRows !== false;
526
550
  try {
527
551
  if (typeof body.codexAutoStart === "boolean") {
528
552
  config.codexAutoStart = body.codexAutoStart;
@@ -550,12 +574,16 @@ export async function handleConfigRoutes(ctx: ManagementContext): Promise<Respon
550
574
  // default, and a written `false` would survive as a decision nobody made.
551
575
  if (body.ultraFastTier === true) config.ultraFastTier = true;
552
576
  else if (body.ultraFastTier === false) deleteConfigTopLevelKey(config, "ultraFastTier");
577
+ if (body.fastRows === false) config.fastRows = false;
578
+ else if (body.fastRows === true) deleteConfigTopLevelKey(config, "fastRows");
553
579
  if (body.codexMainAccountHardLock === true) config.codexMainAccountHardLock = true;
554
580
  else if (body.codexMainAccountHardLock === false) deleteConfigTopLevelKey(config, "codexMainAccountHardLock");
555
581
  if (body.codexDesktopAuthless === true) config.codexDesktopAuthless = true;
556
582
  else if (body.codexDesktopAuthless === false) deleteConfigTopLevelKey(config, "codexDesktopAuthless");
557
583
  if (body.codexClientCompaction === true) config.codexClientCompaction = true;
558
584
  else if (body.codexClientCompaction === false) deleteConfigTopLevelKey(config, "codexClientCompaction");
585
+ if (compactionRouting === null) deleteConfigTopLevelKey(config, "compactionRouting");
586
+ else if (compactionRouting?.success) config.compactionRouting = compactionRouting.data;
559
587
  if (quotaAutoRefreshChange) {
560
588
  const { id, window, enabled } = quotaAutoRefreshChange;
561
589
  const setting = { ...(config.codexQuotaAutoRefresh?.[id] ?? {}) };
@@ -592,6 +620,9 @@ export async function handleConfigRoutes(ctx: ManagementContext): Promise<Respon
592
620
  if (previousSettings.hasUltraFastTier) {
593
621
  config.ultraFastTier = previousSettings.ultraFastTier;
594
622
  } else deleteConfigTopLevelKey(config, "ultraFastTier");
623
+ if (previousSettings.hasFastRows) {
624
+ config.fastRows = previousSettings.fastRows;
625
+ } else deleteConfigTopLevelKey(config, "fastRows");
595
626
  if (previousSettings.hasCodexMainAccountHardLock) {
596
627
  config.codexMainAccountHardLock = previousSettings.codexMainAccountHardLock;
597
628
  } else deleteConfigTopLevelKey(config, "codexMainAccountHardLock");
@@ -601,6 +632,7 @@ export async function handleConfigRoutes(ctx: ManagementContext): Promise<Respon
601
632
  if (previousSettings.hasCodexClientCompaction) {
602
633
  config.codexClientCompaction = previousSettings.codexClientCompaction;
603
634
  } else deleteConfigTopLevelKey(config, "codexClientCompaction");
635
+ restoreCompactionRouting();
604
636
  throw error;
605
637
  }
606
638
  if (typeof body.appOwnedMemoryBudgetMb === "number") {
@@ -609,6 +641,8 @@ export async function handleConfigRoutes(ctx: ManagementContext): Promise<Respon
609
641
  }
610
642
  const authlessIsEnabled = config.codexDesktopAuthless === true;
611
643
  const clientCompactionIsEnabled = config.codexClientCompaction === true;
644
+ const fastRowsIsEnabled = config.fastRows !== false;
645
+ const fastRowsChanged = fastRowsWasEnabled !== fastRowsIsEnabled;
612
646
  const desktopSwitchesChanged = authlessWasEnabled !== authlessIsEnabled
613
647
  || clientCompactionWasEnabled !== clientCompactionIsEnabled;
614
648
  // Catalog convergence is not config injection, and the comment that used to sit here said
@@ -616,9 +650,14 @@ export async function handleConfigRoutes(ctx: ManagementContext): Promise<Respon
616
650
  // injector, which is why flipping either switch left `config.toml` in its old shape until
617
651
  // a separate `ocx sync` (#4809). Both halves are needed when a Desktop switch changes; a
618
652
  // picker-only update still refreshes just the catalog.
619
- const catalogRefresh = pickerWasEnabled !== pickerIsEnabled || desktopSwitchesChanged
653
+ const catalogRefresh = pickerWasEnabled !== pickerIsEnabled || desktopSwitchesChanged || fastRowsChanged
620
654
  ? await convergeCodexCatalog()
621
655
  : undefined;
656
+ if (fastRowsChanged) {
657
+ const { readRuntimePort } = await import("../../config/process-state");
658
+ const runtime = (deps.readRuntimePort ?? readRuntimePort)(process.pid);
659
+ await syncEnabledClientIntegrations(runtime?.port, config, deps);
660
+ }
622
661
  // Injection second, matching `syncModelsToCodex`: the injected `model_catalog_json` should
623
662
  // point at a catalog that has already settled. And it runs here rather than inside the save
624
663
  // because coordinated Codex writes acquire the Codex write lock N before the config mutation
@@ -641,9 +680,11 @@ export async function handleConfigRoutes(ctx: ManagementContext): Promise<Respon
641
680
  codexQuotaAutoRefresh: quotaAutoRefreshSettings(config),
642
681
  oauthOpenBrowser: config.oauthOpenBrowser !== false,
643
682
  catalogRefreshPending,
683
+ fastRows: config.fastRows !== false,
644
684
  codexDesktopAuthless: authlessIsEnabled,
645
685
  codexClientCompaction: clientCompactionIsEnabled,
646
686
  codexDesktopSwitches,
687
+ compactionRouting: config.compactionRouting ?? null,
647
688
  codexMainAccountHardLock: config.codexMainAccountHardLock === true,
648
689
  mainAccountHardLock: getMainAccountHardLockStatus(config),
649
690
  startupHealth: await readStartupHealth(config),
@@ -19,6 +19,7 @@ import type {
19
19
  performCodexRestart,
20
20
  readCodexAppServerState,
21
21
  } from "../../codex/app-server-restart-service";
22
+ import type { RequestMetricsSnapshotter } from "../request-metrics";
22
23
 
23
24
  import type { RemoteWorkspaceHub } from "../../remote-control/workspace-hub";
24
25
  import type { RemoteWorkspaceSessionService } from "../../remote-control/workspace-sessions";
@@ -31,6 +32,8 @@ export type RemoteWorkspaceSessionsApi = Pick<RemoteWorkspaceSessionService,
31
32
  "availability" | "list" | "create" | "prompt" | "submitPrompt" | "stop" | "shutdown">;
32
33
 
33
34
  export interface ManagementApiDeps {
35
+ /** Read-only process-local aggregate metrics; absent keeps the scrape route unavailable. */
36
+ requestMetrics?: RequestMetricsSnapshotter;
34
37
  remoteWorkspaceHub?: RemoteWorkspaceHubApi;
35
38
  remoteWorkspaceSessions?: RemoteWorkspaceSessionsApi;
36
39
  /** The listener retains and awaits teardown only after this optional subsystem activates. */