@bitkyc08/opencodex 2.81.0 → 2.83.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 (272) hide show
  1. package/bin/ocx.mjs +166 -73
  2. package/gui/dist/assets/App-Ba8uG696.js +54 -0
  3. package/gui/dist/assets/{Tray-EwAOsP0S.js → Tray-IImbNPRy.js} +1 -1
  4. package/gui/dist/assets/{index-Brk6F3fn.css → index-BAbcj51J.css} +1 -1
  5. package/gui/dist/assets/{index-RsoaAxXY.js → index-BHRZdJuv.js} +13 -13
  6. package/gui/dist/assets/{quota-summary-D2lN8tDm.js → quota-summary-DiJO6sYY.js} +1 -1
  7. package/gui/dist/index.html +2 -2
  8. package/package.json +1 -1
  9. package/src/adapters/anthropic.ts +1 -1
  10. package/src/adapters/cursor/catalog.ts +2 -2
  11. package/src/adapters/cursor/effort-map.ts +1 -1
  12. package/src/adapters/devin/live-models.ts +2 -2
  13. package/src/adapters/google-truncation.ts +22 -0
  14. package/src/adapters/google.ts +7 -3
  15. package/src/adapters/ollama-native.ts +18 -14
  16. package/src/adapters/openai-responses/compaction-search-history.ts +109 -0
  17. package/src/adapters/openai-responses/passthrough.ts +11 -4
  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/tool-output-recovery.ts +20 -0
  21. package/src/bridge/errors.ts +7 -1
  22. package/src/claude/agents-inject.ts +14 -3
  23. package/src/claude/context-windows.ts +22 -20
  24. package/src/claude/desktop-3p.ts +88 -12
  25. package/src/claude/desktop-picker.ts +1 -1
  26. package/src/claude/desktop-profile.ts +2 -1
  27. package/src/claude/gateway-cache.ts +18 -1
  28. package/src/claude/inbound.ts +77 -3
  29. package/src/claude/intercept/cli-catalog.ts +3 -0
  30. package/src/claude/intercept/picker-ca-persistence.ts +7 -6
  31. package/src/claude/intercept/picker-models.ts +14 -6
  32. package/src/claude/intercept/settings.ts +3 -0
  33. package/src/claude/long-context.ts +66 -0
  34. package/src/claude/model-info.ts +19 -16
  35. package/src/claude/outbound.ts +94 -9
  36. package/src/claude/subagent-model.ts +40 -11
  37. package/src/cli/account-auth.ts +20 -12
  38. package/src/cli/agent.ts +9 -5
  39. package/src/cli/aside-profile-recovery.ts +33 -0
  40. package/src/cli/capabilities-base.ts +5 -5
  41. package/src/cli/capabilities-integrations.ts +2 -2
  42. package/src/cli/cli-path-diagnostics.ts +132 -0
  43. package/src/cli/desktop-runtime-guidance.ts +65 -0
  44. package/src/cli/dispatch.ts +5 -0
  45. package/src/cli/doctor.ts +16 -3
  46. package/src/cli/index.ts +14 -9
  47. package/src/cli/integration-aside-sync.ts +2 -1
  48. package/src/cli/integrations.ts +16 -3
  49. package/src/cli/resolve.ts +26 -3
  50. package/src/cli/root.ts +4 -2
  51. package/src/cli/status.ts +64 -12
  52. package/src/cli/update-restart-child.ts +4 -2
  53. package/src/cli/update-restart-home.ts +28 -10
  54. package/src/cli/update-restart-service-record.ts +116 -0
  55. package/src/cli/update-restart-supervision.ts +96 -0
  56. package/src/cli/update-restart.ts +59 -13
  57. package/src/cli/v2.ts +27 -20
  58. package/src/cli/version-skew-notice.ts +77 -0
  59. package/src/clients/config-export/model-metadata.ts +16 -0
  60. package/src/clients/config-export/zcode-store.ts +46 -11
  61. package/src/clients/config-export/zcode.ts +3 -5
  62. package/src/clients/config-export.ts +1 -1
  63. package/src/clients/lazycodex.ts +1 -1
  64. package/src/clients/omo-role-models.ts +32 -7
  65. package/src/codex/auth-api/account-selection.ts +107 -0
  66. package/src/codex/auth-api/pool-mode-gate.ts +4 -5
  67. package/src/codex/auth-api/routes.ts +10 -49
  68. package/src/codex/auth-context.ts +28 -4
  69. package/src/codex/autostart-health.ts +12 -3
  70. package/src/codex/catalog/build-entries.ts +18 -0
  71. package/src/codex/catalog/bundled.ts +155 -26
  72. package/src/codex/catalog/combo-member.ts +3 -2
  73. package/src/codex/catalog/effort.ts +26 -5
  74. package/src/codex/catalog/gather-capture.ts +5 -0
  75. package/src/codex/catalog/model-hints.ts +15 -8
  76. package/src/codex/catalog/model-visibility.ts +3 -1
  77. package/src/codex/catalog/native-models.ts +1 -1
  78. package/src/codex/catalog/operator-tiers.ts +38 -0
  79. package/src/codex/catalog/provider-models.ts +68 -35
  80. package/src/codex/catalog/retained-sync.ts +7 -5
  81. package/src/codex/catalog/routed-gather.ts +15 -1
  82. package/src/codex/catalog-auto-refresh-sources.ts +3 -4
  83. package/src/codex/catalog-auto-refresh.ts +19 -0
  84. package/src/codex/catalog.ts +1 -1
  85. package/src/codex/codex-home-owner.ts +3 -0
  86. package/src/codex/config-write-lock.ts +333 -0
  87. package/src/codex/convergence.ts +2 -1
  88. package/src/codex/desktop-app/darwin.ts +1 -1
  89. package/src/codex/desktop-app/handoff.ts +1 -1
  90. package/src/codex/desktop-app/linux.ts +1 -1
  91. package/src/codex/desktop-app/lock.ts +1 -1
  92. package/src/codex/desktop-app/types.ts +1 -1
  93. package/src/codex/desktop-app/windows.ts +1 -1
  94. package/src/codex/desktop-app-restart.ts +1 -1
  95. package/src/codex/features.ts +257 -200
  96. package/src/codex/inject/config-write-section.ts +34 -0
  97. package/src/codex/inject/multi-agent-v2.ts +14 -6
  98. package/src/codex/inject/remove.ts +36 -11
  99. package/src/codex/inject/restore.ts +85 -20
  100. package/src/codex/inject-coordination.ts +15 -6
  101. package/src/codex/inject.ts +62 -20
  102. package/src/codex/journal.ts +62 -17
  103. package/src/codex/main-account-cache.ts +43 -0
  104. package/src/codex/main-account.ts +6 -7
  105. package/src/codex/paths.ts +2 -0
  106. package/src/codex/prepared-home.ts +18 -0
  107. package/src/codex/prompt-journal.ts +25 -12
  108. package/src/codex/prompt-layers.ts +49 -26
  109. package/src/codex/prompt-lock-claim.ts +134 -0
  110. package/src/codex/prompt-lock-evidence.ts +26 -0
  111. package/src/codex/prompt-lock-io.ts +16 -0
  112. package/src/codex/prompt-lock-owner.ts +130 -0
  113. package/src/codex/prompt-lock.ts +84 -53
  114. package/src/codex/routing/failure-window.ts +146 -0
  115. package/src/codex/routing/selection.ts +4 -1
  116. package/src/codex/routing.ts +13 -17
  117. package/src/codex/runtime.ts +106 -18
  118. package/src/codex/shim.ts +45 -19
  119. package/src/combos/jev-service-exchange.ts +281 -0
  120. package/src/combos/jev.ts +34 -220
  121. package/src/config/atomic-write.ts +1 -1
  122. package/src/config/codex-ws-reuse-setting.ts +10 -0
  123. package/src/config/live-reconcile.ts +17 -0
  124. package/src/config/load-degrade.ts +8 -0
  125. package/src/config/rebase-provenance.ts +11 -0
  126. package/src/config/schema/config-schema.ts +4 -0
  127. package/src/generated/compatibility-version.json +375 -227
  128. package/src/generated/model-metadata.ts +2 -2
  129. package/src/grok/inject.ts +276 -206
  130. package/src/grok/reset-coupon-ledger.ts +223 -57
  131. package/src/grok/reset-coupons.ts +13 -6
  132. package/src/grok/sync.ts +2 -1
  133. package/src/images/loop.ts +29 -11
  134. package/src/integrations/aside-profiles.ts +10 -3
  135. package/src/integrations/catalog-refresh.ts +18 -2
  136. package/src/integrations/cursor-detect.ts +1 -1
  137. package/src/integrations/writer.ts +9 -3
  138. package/src/lib/bun-path-runtime.d.mts +32 -0
  139. package/src/lib/bun-path-runtime.mjs +95 -0
  140. package/src/lib/bun-runtime-preflight.ts +140 -0
  141. package/src/lib/desktop-cli-handoff.d.mts +20 -0
  142. package/src/lib/desktop-cli-handoff.mjs +92 -0
  143. package/src/lib/desktop-cli-record.d.mts +29 -0
  144. package/src/lib/desktop-cli-record.mjs +102 -0
  145. package/src/lib/errors.ts +25 -0
  146. package/src/lib/redact.ts +2 -2
  147. package/src/lib/request-execution-budget.ts +8 -0
  148. package/src/lib/synced-state-location.ts +46 -6
  149. package/src/lib/test-home-guard.ts +87 -1
  150. package/src/lib/translator-budget.ts +17 -4
  151. package/src/lib/windows-atomic-replace.ts +38 -16
  152. package/src/lib/windows-elevation.ts +6 -0
  153. package/src/lib/windows-secret-acl.ts +92 -25
  154. package/src/lib/winsw.ts +2 -3
  155. package/src/oauth/anthropic-account-refusal.ts +39 -1
  156. package/src/oauth/anthropic-routing.ts +1 -1
  157. package/src/oauth/anthropic-send-ownership.ts +3 -3
  158. package/src/oauth/chatgpt.ts +113 -41
  159. package/src/oauth/index.ts +7 -3
  160. package/src/oauth/meta-muse-device.ts +1 -1
  161. package/src/oauth/store.ts +1 -1
  162. package/src/oauth/types.ts +1 -1
  163. package/src/providers/anthropic-fast.ts +1 -1
  164. package/src/providers/anthropic-reset-grant-ledger.ts +1 -1
  165. package/src/providers/azure-model-metadata.ts +115 -0
  166. package/src/providers/derive.ts +21 -0
  167. package/src/providers/devin-cli-authmode-migration.ts +1 -1
  168. package/src/providers/devin-provider-merge-migration.ts +1 -1
  169. package/src/providers/github-copilot-auto.ts +169 -0
  170. package/src/providers/model-presets.ts +1 -1
  171. package/src/providers/muse-subscription-usage.ts +1 -1
  172. package/src/providers/quota/vendor-probes-key.ts +127 -17
  173. package/src/providers/quota.ts +1 -1
  174. package/src/providers/registry/entries-core.ts +6 -6
  175. package/src/providers/registry/entries-extended.ts +4 -0
  176. package/src/providers/registry/model-seeds.ts +3 -3
  177. package/src/providers/xai-fast-model.ts +1 -1
  178. package/src/responses/reasoning-envelope.ts +29 -1
  179. package/src/responses/reasoning-replay-cache.ts +17 -1
  180. package/src/responses/spill-store.ts +15 -2
  181. package/src/responses/state/metrics.ts +8 -0
  182. package/src/responses/state/spill-failure.ts +12 -0
  183. package/src/responses/state/spill-queue.ts +12 -5
  184. package/src/responses/state.ts +18 -10
  185. package/src/responses/task-input.ts +17 -0
  186. package/src/server/background-lifecycle.ts +27 -0
  187. package/src/server/chat-completions.ts +1 -0
  188. package/src/server/chat-native.ts +28 -12
  189. package/src/server/claude-messages.ts +43 -6
  190. package/src/server/inbound-body-admission.ts +25 -7
  191. package/src/server/index/claude-intercept-lifecycle.ts +3 -1
  192. package/src/server/index/link-listener.ts +30 -17
  193. package/src/server/index/serve-options.ts +18 -4
  194. package/src/server/index/spend-ledger-lifecycle.ts +2 -1
  195. package/src/server/index/startup-warnings.ts +19 -0
  196. package/src/server/index.ts +2 -1
  197. package/src/server/lifecycle.ts +31 -4
  198. package/src/server/management/agent-settings-routes.ts +93 -32
  199. package/src/server/management/codex-agent-role-routes.ts +4 -2
  200. package/src/server/management/codex-prompt-routes.ts +1 -0
  201. package/src/server/management/codex-role-auto-assign.ts +13 -2
  202. package/src/server/management/config-routes.ts +59 -8
  203. package/src/server/management/context.ts +2 -2
  204. package/src/server/management/grok-coupon-routes.ts +277 -52
  205. package/src/server/management/link-routes.ts +42 -20
  206. package/src/server/management/route-registry.ts +2 -2
  207. package/src/server/management/shared.ts +3 -3
  208. package/src/server/messages-native-oauth.ts +9 -8
  209. package/src/server/messages-native.ts +49 -13
  210. package/src/server/messages-response-headers.ts +14 -0
  211. package/src/server/relay-eager.ts +2 -0
  212. package/src/server/relay.ts +18 -7
  213. package/src/server/request-log-conversation.ts +48 -5
  214. package/src/server/request-log-failure-attribution.ts +3 -0
  215. package/src/server/request-log-terminal-status.ts +186 -0
  216. package/src/server/request-log.ts +15 -89
  217. package/src/server/responses/adapter-continuation.ts +11 -3
  218. package/src/server/responses/adapter-dispatch.ts +99 -19
  219. package/src/server/responses/codex-ws-exchange.ts +17 -2
  220. package/src/server/responses/codex-ws-pool.ts +100 -13
  221. package/src/server/responses/codex-ws-wire.ts +9 -0
  222. package/src/server/responses/combo-requested-effort.ts +3 -1
  223. package/src/server/responses/compact.ts +3 -2
  224. package/src/server/responses/core-codex-account.ts +22 -1
  225. package/src/server/responses/core-combo-native.ts +20 -3
  226. package/src/server/responses/core-combo.ts +22 -14
  227. package/src/server/responses/core-lifetime.ts +3 -36
  228. package/src/server/responses/core-opaque-recovery.ts +5 -5
  229. package/src/server/responses/core-options.ts +5 -0
  230. package/src/server/responses/core-replay.ts +73 -0
  231. package/src/server/responses/core.ts +9 -9
  232. package/src/server/responses/empty-completion-guard.ts +18 -2
  233. package/src/server/responses/fetch-helpers.ts +26 -3
  234. package/src/server/responses/jev-model-invoke.ts +1 -1
  235. package/src/server/responses/passthrough-delivery.ts +24 -2
  236. package/src/server/responses/passthrough-dispatch.ts +134 -62
  237. package/src/server/responses/passthrough-execution.ts +3 -3
  238. package/src/server/responses/request-prepare.ts +4 -0
  239. package/src/server/responses/request-send-budget.ts +112 -49
  240. package/src/server/responses/request-transport.ts +125 -5
  241. package/src/server/responses/response-effects.ts +1 -0
  242. package/src/server/responses/run-turn-execution.ts +15 -5
  243. package/src/server/responses/sidecar-execution.ts +30 -16
  244. package/src/server/responses/sidecar-send-budget.ts +58 -0
  245. package/src/server/responses/spend-storage-error.ts +33 -0
  246. package/src/server/responses/ws-upstream.ts +19 -6
  247. package/src/server/startup-health-cache.ts +2 -1
  248. package/src/server/workflow-refusal.ts +3 -0
  249. package/src/service/cli.ts +20 -4
  250. package/src/service/desktop-command-guard.ts +32 -0
  251. package/src/service/desktop-startup.ts +66 -40
  252. package/src/service/desktop-supervision.d.mts +26 -0
  253. package/src/service/desktop-supervision.mjs +140 -0
  254. package/src/service/managing-cli.ts +1 -0
  255. package/src/service/orchestration.ts +46 -20
  256. package/src/service/repair.ts +25 -9
  257. package/src/service/state.ts +4 -3
  258. package/src/service/systemd.ts +17 -8
  259. package/src/service/windows-ops.ts +61 -22
  260. package/src/storage/policy-job.ts +5 -1
  261. package/src/storage/policy-worker.ts +3 -1
  262. package/src/storage/policy.ts +9 -0
  263. package/src/types/config.ts +21 -0
  264. package/src/types/request.ts +10 -0
  265. package/src/types/wire.ts +1 -0
  266. package/src/update/index.ts +57 -9
  267. package/src/update/restart-ownership.ts +10 -3
  268. package/src/update/runtime-ownership.d.mts +6 -0
  269. package/src/update/runtime-ownership.mjs +23 -7
  270. package/src/usage/expected-prices.ts +1 -1
  271. package/src/web-search/loop.ts +5 -2
  272. package/gui/dist/assets/App-7yhoys8a.js +0 -54
@@ -0,0 +1,281 @@
1
+ import { readBoundedResponseBytes } from "../lib/bounded-body";
2
+ import {
3
+ providerOutboundPost,
4
+ providerRedirectError,
5
+ } from "../lib/provider-outbound";
6
+ import {
7
+ isKeychainReference,
8
+ keychainReferenceBelongsToProvider,
9
+ resolveProviderApiKey,
10
+ } from "../providers/api-key-resolve";
11
+ import { providerMatchesRegistryTransport } from "../providers/registry";
12
+ import type { OcxConfig, OcxProviderConfig } from "../types";
13
+ import type { JevDecision, ResolveJevDecisionOptions } from "./jev";
14
+ import {
15
+ isSystemOneEndpoint,
16
+ jevDecisionEndpointUrl,
17
+ JEV_DECISION_TIMEOUT_DEFAULT_MS,
18
+ JEV_DECISION_TIMEOUT_MAX_MS,
19
+ JEV_DECISION_TIMEOUT_MIN_MS,
20
+ } from "./jev-decision-contract";
21
+
22
+ export const JEV_PROVIDER_ID = "jev";
23
+ export const JEV_API_URL = "https://api.typesafe.ai/v1/systemone";
24
+ export const JEV_MODEL = "jev-latest";
25
+
26
+ /** Environment names that hold TypeSafe credentials; a self-hosted row may never reference them. */
27
+ const TYPESAFE_ENV_KEYS = new Set(["TYPESAFE_API_KEY", "JEV_API_KEY"]);
28
+ export const JEV_MAX_REQUEST_BYTES = 65_536;
29
+ export const JEV_MAX_RESPONSE_BYTES = 65_536;
30
+ const JEV_OUTBOUND_DEPENDENCIES = {
31
+ isCanonicalUrl: (name: string, url: string) => name === JEV_PROVIDER_ID && url === JEV_API_URL,
32
+ // Self-hosted decision models (Ollama tev1) listen on plain HTTP loopback. The outbound wrapper
33
+ // still requires the row's own allowPrivateNetwork and a literal local address for that.
34
+ allowLocalCleartextPost: true,
35
+ };
36
+
37
+ /** Every non-applied outcome of one exchange: a local refusal, a transport failure or an invalid answer. */
38
+ export type JevDecisionFailureGate = Exclude<JevDecision["gate"], "apply">;
39
+
40
+ /**
41
+ * Transport options shared with the route resolver. `signal` is the caller's cancellation and always
42
+ * rejects with its own reason; `timeoutMs` is the separate decision deadline that yields a `timeout` gate.
43
+ */
44
+ export type JevServiceExchangeOptions = Pick<ResolveJevDecisionOptions,
45
+ "config" | "decisionProvider" | "isDestinationAllowed" | "timeoutMs" | "signal" | "post">;
46
+
47
+ /** Request shape only: question builders never receive the destination or credential. */
48
+ export interface JevDecisionEndpointShape {
49
+ model: string;
50
+ descriptiveCriteria: boolean;
51
+ }
52
+
53
+ /** The decision deadline: the configured value when in bounds, otherwise the four-second default. */
54
+ export function jevDecisionTimeoutMs(value: number | undefined): number {
55
+ return value !== undefined
56
+ && Number.isInteger(value)
57
+ && value >= JEV_DECISION_TIMEOUT_MIN_MS
58
+ && value <= JEV_DECISION_TIMEOUT_MAX_MS
59
+ ? value
60
+ : JEV_DECISION_TIMEOUT_DEFAULT_MS;
61
+ }
62
+
63
+ /** Retain a registry-matching JEV row's transport policy; ignore a retargeted row for hosted requests. */
64
+ function canonicalJevProvider(config: OcxConfig): OcxProviderConfig {
65
+ const configured = config.providers[JEV_PROVIDER_ID];
66
+ if (configured && providerMatchesRegistryTransport(JEV_PROVIDER_ID, configured)) return configured;
67
+ return {
68
+ adapter: "jev-decision",
69
+ baseUrl: JEV_API_URL,
70
+ authMode: "key",
71
+ liveModels: false,
72
+ };
73
+ }
74
+
75
+ interface JevDecisionEndpoint {
76
+ name: string;
77
+ provider: OcxProviderConfig;
78
+ url: string;
79
+ model: string;
80
+ apiKey: string | undefined;
81
+ /** Self-hosted System One services accept only string option descriptions. */
82
+ descriptiveCriteria: boolean;
83
+ }
84
+
85
+ /** Recognize the $NAME and ${NAME} credential references before applying self-hosted ownership rules. */
86
+ function envReferenceName(value: string): string | undefined {
87
+ const braced = /^\$\{(\w+)\}$/.exec(value);
88
+ if (braced) return braced[1];
89
+ return value.startsWith("$") ? value.slice(1) : undefined;
90
+ }
91
+
92
+ /**
93
+ * A self-hosted row may carry only its own secret: never a reference to the TypeSafe environment
94
+ * keys, and never a keychain entry that belongs to another provider. `null` means refused.
95
+ */
96
+ function selfHostedApiKey(name: string, apiKey: string | undefined): string | undefined | null {
97
+ if (!apiKey) return undefined;
98
+ const envName = envReferenceName(apiKey);
99
+ if (envName !== undefined && TYPESAFE_ENV_KEYS.has(envName)) return null;
100
+ if (isKeychainReference(apiKey) && !keychainReferenceBelongsToProvider(apiKey, name)) return null;
101
+ return resolveProviderApiKey(apiKey)?.trim() || undefined;
102
+ }
103
+
104
+ /**
105
+ * Resolve where one decision request goes and which credential it may carry.
106
+ *
107
+ * The `jev` id stays pinned to the canonical TypeSafe URL and `jev-latest`: its row key is used
108
+ * only while the row still matches the registry transport, and the environment fallbacks exist
109
+ * only for that URL. A retargeted `jev` row therefore keeps today's behavior instead of becoming a
110
+ * custom destination. Any other id must be an enabled `jev-decision` row whose baseUrl is a
111
+ * full HTTPS decision endpoint (or a local HTTP `/systemone` endpoint) and which names its own
112
+ * model; only its own key may accompany it, so no TypeSafe credential can reach a self-hosted
113
+ * service. The URL is `jevDecisionEndpointUrl`, shared with the management surfaces: an HTTPS
114
+ * path is sent exactly as configured and only `/systemone` normalizes trailing slashes.
115
+ * `undefined` means no usable decision service (reported through the existing `missing_key`
116
+ * gate); `null` means the request's destination scope refused it before any credential access.
117
+ */
118
+ function jevDecisionEndpoint(
119
+ config: OcxConfig,
120
+ decisionProvider: string,
121
+ isDestinationAllowed?: ResolveJevDecisionOptions["isDestinationAllowed"],
122
+ ): JevDecisionEndpoint | undefined | null {
123
+ const configured = Object.hasOwn(config.providers, decisionProvider)
124
+ ? config.providers[decisionProvider]
125
+ : undefined;
126
+ if (configured?.disabled === true) return undefined;
127
+ if (decisionProvider === JEV_PROVIDER_ID) {
128
+ if (isDestinationAllowed?.(JEV_PROVIDER_ID, JEV_MODEL) === false) return null;
129
+ const configuredOwnsJev = configured
130
+ && providerMatchesRegistryTransport(JEV_PROVIDER_ID, configured);
131
+ const apiKey = (
132
+ configuredOwnsJev ? resolveProviderApiKey(configured.apiKey)?.trim() : undefined
133
+ ) || process.env.TYPESAFE_API_KEY?.trim()
134
+ || process.env.JEV_API_KEY?.trim();
135
+ if (!apiKey) return undefined;
136
+ return {
137
+ name: JEV_PROVIDER_ID,
138
+ provider: canonicalJevProvider(config),
139
+ url: JEV_API_URL,
140
+ model: JEV_MODEL,
141
+ apiKey,
142
+ descriptiveCriteria: false,
143
+ };
144
+ }
145
+ if (configured?.adapter !== "jev-decision" || typeof configured.baseUrl !== "string") return undefined;
146
+ const url = jevDecisionEndpointUrl(configured.baseUrl);
147
+ if (!url || !isSystemOneEndpoint(url)) return undefined;
148
+ // `jev-latest` is TypeSafe's model name; a self-hosted host must name its own.
149
+ const model = configured.defaultModel?.trim() || configured.models?.[0]?.trim();
150
+ if (!model) return undefined;
151
+ if (isDestinationAllowed?.(decisionProvider, model) === false) return null;
152
+ const apiKey = selfHostedApiKey(decisionProvider, configured.apiKey);
153
+ if (apiKey === null) return undefined;
154
+ return {
155
+ name: decisionProvider,
156
+ provider: configured,
157
+ url,
158
+ model,
159
+ apiKey,
160
+ descriptiveCriteria: true,
161
+ };
162
+ }
163
+
164
+ /** Run the question builder: a refusal keeps its gate; a throw or an over-cap body is `invalid`. */
165
+ function prepareRequestBody(
166
+ prepare: (endpoint: JevDecisionEndpointShape) => { body: string } | JevDecisionFailureGate,
167
+ shape: JevDecisionEndpointShape,
168
+ ): { body: string } | { gate: JevDecisionFailureGate } {
169
+ try {
170
+ const prepared = prepare(shape);
171
+ if (typeof prepared === "string") return { gate: prepared };
172
+ const body = prepared.body;
173
+ if (new TextEncoder().encode(body).byteLength > JEV_MAX_REQUEST_BYTES) return { gate: "invalid" };
174
+ return { body };
175
+ } catch {
176
+ return { gate: "invalid" };
177
+ }
178
+ }
179
+
180
+ /**
181
+ * One bounded System One round-trip, independent of the decision question and its choice policy.
182
+ * Destination authorization precedes credential access and `prepare`; request bytes, deadline,
183
+ * redirects and UTF-8 JSON are bounded here. `parse` validates the question's answer inside the
184
+ * cancellation boundary. Local preparation/parser failures are `invalid`.
185
+ *
186
+ * This function owns caller cancellation: an aborted `options.signal` rejects with its reason by
187
+ * identity, never as a gate. It is checked before endpoint, credential or `prepare` work, after
188
+ * endpoint resolution and preparation (including failures), after POST, redirect inspection, HTTP
189
+ * error-body cleanup and response reads, and when `parse` returns or throws. Only the caller's signal counts; expiry of
190
+ * the separate decision deadline remains a `timeout` gate.
191
+ */
192
+ export async function exchangeJevDecision<T>(
193
+ options: JevServiceExchangeOptions,
194
+ prepare: (endpoint: JevDecisionEndpointShape) => { body: string } | JevDecisionFailureGate,
195
+ parse: (payload: unknown) => T,
196
+ ): Promise<{ value: T } | { gate: JevDecisionFailureGate }> {
197
+ if (options.signal?.aborted) throw options.signal.reason;
198
+
199
+ let endpoint: JevDecisionEndpoint | undefined | null;
200
+ try {
201
+ endpoint = jevDecisionEndpoint(options.config, options.decisionProvider ?? JEV_PROVIDER_ID, options.isDestinationAllowed);
202
+ } catch (error) {
203
+ // Authorization and credential callbacks may cancel the caller and then throw; cancellation wins.
204
+ if (options.signal?.aborted) throw options.signal.reason;
205
+ throw error;
206
+ }
207
+ if (options.signal?.aborted) throw options.signal.reason;
208
+ if (endpoint === null) return { gate: "invalid" };
209
+ if (!endpoint) return { gate: "missing_key" };
210
+
211
+ const request = prepareRequestBody(prepare, { model: endpoint.model, descriptiveCriteria: endpoint.descriptiveCriteria });
212
+ if (options.signal?.aborted) throw options.signal.reason;
213
+ if ("gate" in request) return request;
214
+ const requestBody = request.body;
215
+
216
+ const timeoutMs = jevDecisionTimeoutMs(options.timeoutMs);
217
+ const timeoutSignal = AbortSignal.timeout(timeoutMs);
218
+ const signal = options.signal
219
+ ? AbortSignal.any([options.signal, timeoutSignal])
220
+ : timeoutSignal;
221
+ const post = options.post ?? providerOutboundPost;
222
+
223
+ try {
224
+ const response = await post(
225
+ endpoint.name,
226
+ endpoint.provider,
227
+ endpoint.url,
228
+ {
229
+ headers: {
230
+ ...(endpoint.apiKey ? { Authorization: `Bearer ${endpoint.apiKey}` } : {}),
231
+ "Content-Type": "application/json",
232
+ },
233
+ body: requestBody,
234
+ signal,
235
+ },
236
+ JEV_OUTBOUND_DEPENDENCIES,
237
+ );
238
+ if (options.signal?.aborted) throw options.signal.reason;
239
+
240
+ const redirectError = await providerRedirectError(response, endpoint.url);
241
+ if (options.signal?.aborted) throw options.signal.reason;
242
+ if (redirectError) return { gate: "redirect" };
243
+ if (!response.ok) {
244
+ try { void response.body?.cancel().catch(() => undefined); } catch { /* best effort */ }
245
+ if (options.signal?.aborted) throw options.signal.reason;
246
+ return { gate: "http" };
247
+ }
248
+
249
+ const bounded = await readBoundedResponseBytes(response, {
250
+ maxBytes: JEV_MAX_RESPONSE_BYTES,
251
+ signal,
252
+ });
253
+ if (options.signal?.aborted) throw options.signal.reason;
254
+ if (bounded.oversized) return { gate: "malformed" };
255
+
256
+ let payload: unknown;
257
+ try {
258
+ const text = new TextDecoder("utf-8", { fatal: true }).decode(bounded.bytes);
259
+ payload = JSON.parse(text);
260
+ } catch {
261
+ return { gate: "malformed" };
262
+ }
263
+
264
+ let parsed: T;
265
+ try {
266
+ parsed = parse(payload);
267
+ } catch {
268
+ if (options.signal?.aborted) throw options.signal.reason;
269
+ return { gate: "invalid" };
270
+ }
271
+ if (options.signal?.aborted) throw options.signal.reason;
272
+ return { value: parsed };
273
+ } catch (error) {
274
+ if (options.signal?.aborted) throw options.signal.reason;
275
+ if (timeoutSignal.aborted
276
+ || (error instanceof DOMException && error.name === "TimeoutError")) {
277
+ return { gate: "timeout" };
278
+ }
279
+ return { gate: "network" };
280
+ }
281
+ }
package/src/combos/jev.ts CHANGED
@@ -1,42 +1,26 @@
1
- import { readBoundedResponseBytes } from "../lib/bounded-body";
1
+ import type { providerOutboundPost } from "../lib/provider-outbound";
2
+ import type { OcxComboDefaultEffort, OcxConfig } from "../types";
2
3
  import {
3
- providerOutboundPost,
4
- providerRedirectError,
5
- } from "../lib/provider-outbound";
6
- import {
7
- isKeychainReference,
8
- keychainReferenceBelongsToProvider,
9
- resolveProviderApiKey,
10
- } from "../providers/api-key-resolve";
11
- import { providerMatchesRegistryTransport } from "../providers/registry";
12
- import type { OcxComboDefaultEffort, OcxConfig, OcxProviderConfig } from "../types";
13
- import { jevDecisionEndpointUrl } from "./jev-decision-contract";
14
- import {
15
- isSystemOneEndpoint,
16
- JEV_DECISION_TIMEOUT_DEFAULT_MS,
17
- JEV_DECISION_TIMEOUT_MAX_MS,
18
- JEV_DECISION_TIMEOUT_MIN_MS,
19
4
  JEV_MAX_CANDIDATE_FIELD_CHARS,
20
5
  } from "./types";
21
6
 
22
- export const JEV_PROVIDER_ID = "jev";
23
- export const JEV_API_URL = "https://api.typesafe.ai/v1/systemone";
24
- export const JEV_MODEL = "jev-latest";
7
+ import {
8
+ exchangeJevDecision,
9
+ JEV_PROVIDER_ID,
10
+ } from "./jev-service-exchange";
11
+ export {
12
+ JEV_API_URL,
13
+ JEV_MODEL,
14
+ JEV_PROVIDER_ID,
15
+ JEV_MAX_REQUEST_BYTES,
16
+ JEV_MAX_RESPONSE_BYTES,
17
+ jevDecisionTimeoutMs,
18
+ } from "./jev-service-exchange";
25
19
 
26
20
  export const JEV_MAX_CANDIDATES = 64;
27
21
  /** Ollama's System One choice questions accept 2..26 options; other services share the floor. */
28
22
  const SELF_HOSTED_MIN_OPTIONS = 2;
29
23
  const SELF_HOSTED_MAX_OPTIONS = 26;
30
- /** Environment names that hold TypeSafe credentials; a self-hosted row may never reference them. */
31
- const TYPESAFE_ENV_KEYS = new Set(["TYPESAFE_API_KEY", "JEV_API_KEY"]);
32
- export const JEV_MAX_REQUEST_BYTES = 65_536;
33
- export const JEV_MAX_RESPONSE_BYTES = 65_536;
34
- const JEV_OUTBOUND_DEPENDENCIES = {
35
- isCanonicalUrl: (name: string, url: string) => name === JEV_PROVIDER_ID && url === JEV_API_URL,
36
- // Self-hosted decision models (Ollama tev1) listen on plain HTTP loopback. The outbound wrapper
37
- // still requires the row's own allowPrivateNetwork and a literal local address for that.
38
- allowLocalCleartextPost: true,
39
- };
40
24
 
41
25
  const TASK_CHARS = 500;
42
26
  const TASK_HEAD_CHARS = 320;
@@ -384,6 +368,7 @@ function hasImageContent(item: Record<string, unknown>): boolean {
384
368
  return item.content.some(part => isRecord(part) && (part.type === "input_image" || part.type === "image_url"));
385
369
  }
386
370
 
371
+ /** Extract bounded task/assistant/tool evidence, omit protected envelopes, and attach configured target notes. */
387
372
  export function buildJevState(body: unknown, candidates: readonly JevCandidate[] = []): Record<string, unknown> {
388
373
  const input = isRecord(body) ? body.input : undefined;
389
374
  let task = "";
@@ -445,6 +430,7 @@ export function buildJevState(body: unknown, candidates: readonly JevCandidate[]
445
430
  };
446
431
  }
447
432
 
433
+ /** Require task text, an image signal, or a tool-output tail; assistant context and operator notes alone do not suffice. */
448
434
  export function hasJevDecisionState(state: Record<string, unknown>): boolean {
449
435
  if (typeof state.task === "string" && state.task.trim()) return true;
450
436
  if (isRecord(state.signals) && state.signals.has_image === true) return true;
@@ -476,6 +462,7 @@ function candidateOptions(candidates: readonly JevCandidate[]): Map<string, JevR
476
462
  return options;
477
463
  }
478
464
 
465
+ /** Check candidate count and field lengths before state extraction; the exchange separately caps serialized bytes. */
479
466
  export function candidatesFitRequestBounds(candidates: readonly JevCandidate[]): boolean {
480
467
  if (candidates.length > JEV_MAX_CANDIDATES) return false;
481
468
  return candidates.every(candidate => [candidate.key, candidate.provider, candidate.model]
@@ -550,6 +537,7 @@ export function buildJevRouteQuestion(
550
537
  };
551
538
  }
552
539
 
540
+ /** Project only allowlisted, nonnegative safe-integer usage counters; omit missing or wholly invalid usage. */
553
541
  export function jevUsage(payload: Record<string, unknown>): Record<string, number> | undefined {
554
542
  if (!isRecord(payload.usage)) return undefined;
555
543
  const usage: Record<string, number> = {};
@@ -560,6 +548,7 @@ export function jevUsage(payload: Record<string, unknown>): Record<string, numbe
560
548
  return Object.keys(usage).length > 0 ? usage : undefined;
561
549
  }
562
550
 
551
+ /** Validate an allowlisted route choice and any complete probability distribution; throw on invalid answers. */
563
552
  export function parseJevDecision(
564
553
  payload: unknown,
565
554
  candidates: readonly JevCandidate[],
@@ -612,6 +601,7 @@ export function parseJevDecision(
612
601
  };
613
602
  }
614
603
 
604
+ /** Preserve the caller's eligible target/effort fallback while recording the failed gate, backend, and latency. */
615
605
  export function fallbackDecision(
616
606
  fallback: ResolveJevDecisionOptions["fallback"],
617
607
  gate: Exclude<JevDecision["gate"], "apply">,
@@ -621,113 +611,6 @@ export function fallbackDecision(
621
611
  return { backend, ...fallback, gate, latencyMs };
622
612
  }
623
613
 
624
- /** The decision deadline: the configured value when in bounds, otherwise the four-second default. */
625
- export function jevDecisionTimeoutMs(value: number | undefined): number {
626
- return value !== undefined
627
- && Number.isInteger(value)
628
- && value >= JEV_DECISION_TIMEOUT_MIN_MS
629
- && value <= JEV_DECISION_TIMEOUT_MAX_MS
630
- ? value
631
- : JEV_DECISION_TIMEOUT_DEFAULT_MS;
632
- }
633
-
634
- function canonicalJevProvider(config: OcxConfig): OcxProviderConfig {
635
- const configured = config.providers[JEV_PROVIDER_ID];
636
- if (configured && providerMatchesRegistryTransport(JEV_PROVIDER_ID, configured)) return configured;
637
- return {
638
- adapter: "jev-decision",
639
- baseUrl: JEV_API_URL,
640
- authMode: "key",
641
- liveModels: false,
642
- };
643
- }
644
-
645
- interface JevDecisionEndpoint {
646
- name: string;
647
- provider: OcxProviderConfig;
648
- url: string;
649
- model: string;
650
- apiKey: string | undefined;
651
- /** Self-hosted System One services accept only string option descriptions. */
652
- descriptiveCriteria: boolean;
653
- }
654
-
655
- function envReferenceName(value: string): string | undefined {
656
- const braced = /^\$\{(\w+)\}$/.exec(value);
657
- if (braced) return braced[1];
658
- return value.startsWith("$") ? value.slice(1) : undefined;
659
- }
660
-
661
- /**
662
- * A self-hosted row may carry only its own secret: never a reference to the TypeSafe environment
663
- * keys, and never a keychain entry that belongs to another provider. `null` means refused.
664
- */
665
- function selfHostedApiKey(name: string, apiKey: string | undefined): string | undefined | null {
666
- if (!apiKey) return undefined;
667
- const envName = envReferenceName(apiKey);
668
- if (envName !== undefined && TYPESAFE_ENV_KEYS.has(envName)) return null;
669
- if (isKeychainReference(apiKey) && !keychainReferenceBelongsToProvider(apiKey, name)) return null;
670
- return resolveProviderApiKey(apiKey)?.trim() || undefined;
671
- }
672
-
673
- /**
674
- * Resolve where one decision request goes and which credential it may carry.
675
- *
676
- * The `jev` id stays pinned to the canonical TypeSafe URL and `jev-latest`: its row key is used
677
- * only while the row still matches the registry transport, and the environment fallbacks exist
678
- * only for that URL. A retargeted `jev` row therefore keeps today's behavior instead of becoming a
679
- * custom destination. Any other id must be an enabled `jev-decision` row whose baseUrl is a
680
- * full HTTPS decision endpoint (or a local HTTP `/systemone` endpoint) and names its own model; only its own key may accompany it, so no
681
- * TypeSafe credential can reach a self-hosted service. `undefined` means no usable decision
682
- * service (reported through the existing `missing_key` gate); `null` means the request's
683
- * destination scope refused it before any credential access.
684
- */
685
- function jevDecisionEndpoint(
686
- config: OcxConfig,
687
- decisionProvider: string,
688
- isDestinationAllowed?: ResolveJevDecisionOptions["isDestinationAllowed"],
689
- ): JevDecisionEndpoint | undefined | null {
690
- const configured = Object.hasOwn(config.providers, decisionProvider)
691
- ? config.providers[decisionProvider]
692
- : undefined;
693
- if (configured?.disabled === true) return undefined;
694
- if (decisionProvider === JEV_PROVIDER_ID) {
695
- if (isDestinationAllowed?.(JEV_PROVIDER_ID, JEV_MODEL) === false) return null;
696
- const configuredOwnsJev = configured
697
- && providerMatchesRegistryTransport(JEV_PROVIDER_ID, configured);
698
- const apiKey = (
699
- configuredOwnsJev ? resolveProviderApiKey(configured.apiKey)?.trim() : undefined
700
- ) || process.env.TYPESAFE_API_KEY?.trim()
701
- || process.env.JEV_API_KEY?.trim();
702
- if (!apiKey) return undefined;
703
- return {
704
- name: JEV_PROVIDER_ID,
705
- provider: canonicalJevProvider(config),
706
- url: JEV_API_URL,
707
- model: JEV_MODEL,
708
- apiKey,
709
- descriptiveCriteria: false,
710
- };
711
- }
712
- if (configured?.adapter !== "jev-decision" || typeof configured.baseUrl !== "string") return undefined;
713
- const url = jevDecisionEndpointUrl(configured.baseUrl);
714
- if (!url || !isSystemOneEndpoint(url)) return undefined;
715
- // `jev-latest` is TypeSafe's model name; a self-hosted host must name its own.
716
- const model = configured.defaultModel?.trim() || configured.models?.[0]?.trim();
717
- if (!model) return undefined;
718
- if (isDestinationAllowed?.(decisionProvider, model) === false) return null;
719
- const apiKey = selfHostedApiKey(decisionProvider, configured.apiKey);
720
- if (apiKey === null) return undefined;
721
- return {
722
- name: decisionProvider,
723
- provider: configured,
724
- url,
725
- model,
726
- apiKey,
727
- descriptiveCriteria: true,
728
- };
729
- }
730
-
731
614
  /**
732
615
  * Ask the configured JEV decision service for one allowlisted target/effort decision.
733
616
  *
@@ -745,98 +628,29 @@ export async function resolveJevDecision(options: ResolveJevDecisionOptions): Pr
745
628
  if (options.candidates.length === 0) return failed("no_choices");
746
629
  if (!candidatesFitRequestBounds(options.candidates)) return failed("invalid");
747
630
 
748
- const endpoint = jevDecisionEndpoint(options.config, options.decisionProvider ?? JEV_PROVIDER_ID, options.isDestinationAllowed);
749
- if (endpoint === null) return failed("invalid");
750
- if (!endpoint) return failed("missing_key");
751
-
752
- let requestBody: string;
753
- try {
631
+ const exchanged = await exchangeJevDecision(options, (endpoint) => {
754
632
  if (endpoint.descriptiveCriteria) {
755
633
  // Self-hosted choice questions accept 2..26 options; decide locally instead of spending a
756
634
  // round-trip on a request the service will refuse.
757
635
  const optionCount = candidateOptions(options.candidates).size;
758
- if (optionCount < SELF_HOSTED_MIN_OPTIONS) return failed("no_choices");
759
- if (optionCount > SELF_HOSTED_MAX_OPTIONS) return failed("invalid");
636
+ if (optionCount < SELF_HOSTED_MIN_OPTIONS) return "no_choices";
637
+ if (optionCount > SELF_HOSTED_MAX_OPTIONS) return "invalid";
760
638
  }
761
639
  const state = buildJevState(options.body, options.candidates);
762
- if (!hasJevDecisionState(state)) return failed("no_state");
763
- requestBody = JSON.stringify({
640
+ if (!hasJevDecisionState(state)) return "no_state";
641
+ return { body: JSON.stringify({
764
642
  model: endpoint.model,
765
643
  state,
766
644
  questions: buildJevRouteQuestion(options.candidates, { descriptiveCriteria: endpoint.descriptiveCriteria }),
767
- });
768
- if (new TextEncoder().encode(requestBody).byteLength > JEV_MAX_REQUEST_BYTES) return failed("invalid");
769
- } catch {
770
- return failed("invalid");
771
- }
772
-
773
- const timeoutMs = jevDecisionTimeoutMs(options.timeoutMs);
774
- const timeoutSignal = AbortSignal.timeout(timeoutMs);
775
- const signal = options.signal
776
- ? AbortSignal.any([options.signal, timeoutSignal])
777
- : timeoutSignal;
778
- const post = options.post ?? providerOutboundPost;
779
-
780
- try {
781
- const response = await post(
782
- endpoint.name,
783
- endpoint.provider,
784
- endpoint.url,
785
- {
786
- headers: {
787
- ...(endpoint.apiKey ? { Authorization: `Bearer ${endpoint.apiKey}` } : {}),
788
- "Content-Type": "application/json",
789
- },
790
- body: requestBody,
791
- signal,
792
- },
793
- JEV_OUTBOUND_DEPENDENCIES,
794
- );
795
- if (options.signal?.aborted) throw options.signal.reason;
796
-
797
- const redirectError = await providerRedirectError(response, endpoint.url);
798
- if (redirectError) return failed("redirect");
799
- if (!response.ok) {
800
- try { void response.body?.cancel().catch(() => undefined); } catch { /* best effort */ }
801
- return failed("http");
802
- }
803
-
804
- const bounded = await readBoundedResponseBytes(response, {
805
- maxBytes: JEV_MAX_RESPONSE_BYTES,
806
- signal,
807
- });
808
- if (options.signal?.aborted) throw options.signal.reason;
809
- if (bounded.oversized) return failed("malformed");
810
-
811
- let payload: unknown;
812
- try {
813
- const text = new TextDecoder("utf-8", { fatal: true }).decode(bounded.bytes);
814
- payload = JSON.parse(text);
815
- } catch {
816
- return failed("malformed");
817
- }
818
-
819
- let parsed: ReturnType<typeof parseJevDecision>;
820
- try {
821
- parsed = parseJevDecision(payload, options.candidates);
822
- } catch {
823
- return failed("invalid");
824
- }
825
- if (options.signal?.aborted) throw options.signal.reason;
826
- return {
827
- backend,
828
- ...parsed,
829
- gate: "apply",
830
- latencyMs: Math.max(0, now() - startedAt),
831
- };
832
- } catch (error) {
833
- if (options.signal?.aborted) throw options.signal.reason;
834
- if (timeoutSignal.aborted
835
- || (error instanceof DOMException && error.name === "TimeoutError")) {
836
- return failed("timeout");
837
- }
838
- return failed("network");
839
- }
645
+ }) };
646
+ }, (payload) => parseJevDecision(payload, options.candidates));
647
+ if ("gate" in exchanged) return failed(exchanged.gate);
648
+ return {
649
+ backend,
650
+ ...exchanged.value,
651
+ gate: "apply",
652
+ latencyMs: Math.max(0, now() - startedAt),
653
+ };
840
654
  }
841
655
 
842
656
  export type JevDecisionProbeResult =
@@ -304,7 +304,7 @@ function atomicWriteFileToTarget(
304
304
  }
305
305
  try { chmodSync(tempPath, 0o600); } catch { /* platform may ignore chmod */ }
306
306
  },
307
- rename: renameAtomicFile,
307
+ rename: (source, destination) => renameAtomicFile(source, destination, undefined, "config", hooks),
308
308
  truncate: tempPath => truncateSync(tempPath, 0),
309
309
  unlink: unlinkSync,
310
310
  };
@@ -0,0 +1,10 @@
1
+ /** Process-local opt-in for cross-turn Codex WebSocket reuse. Default off. */
2
+ let reuseAcrossTurns = false;
3
+
4
+ export function setCodexWsReuseAcrossTurns(enabled: boolean): void {
5
+ reuseAcrossTurns = enabled;
6
+ }
7
+
8
+ export function codexWsReuseAcrossTurnsEnabled(): boolean {
9
+ return reuseAcrossTurns;
10
+ }
@@ -8,6 +8,7 @@ import {
8
8
  clearPendingConfigObjectChildDeletions,
9
9
  prepareConfigObjectChildDeletionRebase,
10
10
  clearPendingConfigTopLevelDeletions,
11
+ clearConfigTopLevelDeletionIntent,
11
12
  configHasRebaseProvenance,
12
13
  configRebaseDeletionKeys,
13
14
  CONFIG_REBASE_PROVENANCE_KEY,
@@ -42,6 +43,22 @@ const claudeCodeBaseline = new WeakMap<OcxConfig, unknown>();
42
43
  */
43
44
  const liveConfigBaseline = new WeakMap<OcxConfig, OcxConfig>();
44
45
 
46
+ /** Adopt a committed manual selection without advancing unrelated live merge baselines. */
47
+ export function adoptPersistedCodexAccountSelection(
48
+ config: OcxConfig,
49
+ persisted: Pick<OcxConfig, "activeCodexAccountId" | "activeCodexAccountPinned">,
50
+ ): void {
51
+ const keys = ["activeCodexAccountId", "activeCodexAccountPinned"] as const;
52
+ const baseline = liveConfigBaseline.get(config);
53
+ for (const key of keys) {
54
+ config[key] = persisted[key];
55
+ // Retain known absent keys: a later disk-only selection is a new edit, not stale intent.
56
+ if (baseline) baseline[key] = persisted[key];
57
+ }
58
+ clearConfigTopLevelDeletionIntent(config, keys);
59
+ if (baseline) clearConfigTopLevelDeletionIntent(baseline, keys);
60
+ }
61
+
45
62
  /**
46
63
  * Adopt a committed discovery decision and its merge baseline as one synchronous step.
47
64
  * Visibility writers assign and save synchronously; any future writer yielding between