@bitkyc08/opencodex 2.67.0 → 2.68.0-preview.20260927

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 (257) hide show
  1. package/bin/ocx.mjs +3 -0
  2. package/gui/dist/assets/App-B5-cAEd2.js +51 -0
  3. package/gui/dist/assets/App-DCBismRi.css +1 -0
  4. package/gui/dist/assets/Tray-DFnMiyD0.js +1 -0
  5. package/gui/dist/assets/{index-DtNmX7hW.css → index-BDUBS8PW.css} +1 -1
  6. package/gui/dist/assets/index-BcXblvem.js +86 -0
  7. package/gui/dist/assets/quota-summary-i84eOU3o.js +1 -0
  8. package/gui/dist/index.html +21 -2
  9. package/package.json +1 -1
  10. package/src/adapters/anthropic-image-guard.ts +13 -1
  11. package/src/adapters/anthropic-image-normalize.ts +3 -2
  12. package/src/adapters/anthropic-output-schema.ts +36 -0
  13. package/src/adapters/anthropic.ts +10 -2
  14. package/src/adapters/base.ts +7 -0
  15. package/src/adapters/codebuddy/live-models.ts +180 -0
  16. package/src/adapters/coding-agent/protocol.ts +91 -12
  17. package/src/adapters/coding-agent/turn.ts +32 -27
  18. package/src/adapters/devin/cloud-direct/index.ts +1 -0
  19. package/src/adapters/devin/cloud-direct/stated-reset-retry.ts +18 -4
  20. package/src/adapters/devin.ts +9 -6
  21. package/src/adapters/google-http.ts +4 -4
  22. package/src/adapters/google.ts +93 -3
  23. package/src/adapters/kiro/adapter.ts +6 -2
  24. package/src/adapters/kiro/stream.ts +12 -2
  25. package/src/adapters/kiro/usage.ts +3 -1
  26. package/src/adapters/kiro-errors.ts +11 -1
  27. package/src/adapters/kiro-events.ts +27 -1
  28. package/src/adapters/kiro-refusal.ts +30 -0
  29. package/src/adapters/kiro-retry.ts +71 -31
  30. package/src/adapters/openai-chat/deepseek-artifact-schema.ts +45 -0
  31. package/src/adapters/openai-chat/messages.ts +31 -4
  32. package/src/adapters/openai-chat/serialized-tool-call-content.ts +52 -7
  33. package/src/adapters/openai-chat/tool-call-id-remint.ts +65 -0
  34. package/src/adapters/openai-chat/tool-schema.ts +6 -2
  35. package/src/adapters/openai-chat.ts +2 -2
  36. package/src/adapters/openai-responses/muse-tool-choice.ts +31 -0
  37. package/src/adapters/openai-responses/passthrough.ts +9 -3
  38. package/src/adapters/opencode-go-additional-tools.ts +19 -10
  39. package/src/adapters/physical-send.ts +10 -3
  40. package/src/adapters/registry.ts +2 -1
  41. package/src/adapters/responses-tool-schema.ts +31 -3
  42. package/src/adapters/run-turn-queue.ts +63 -23
  43. package/src/adapters/unique-tool-call-ids.ts +63 -0
  44. package/src/adapters/xai-web-search.ts +32 -2
  45. package/src/bridge/sse.ts +14 -2
  46. package/src/chat/inbound.ts +20 -6
  47. package/src/claude/agents-inject.ts +4 -0
  48. package/src/claude/desktop-3p.ts +53 -13
  49. package/src/claude/desktop-profile.ts +41 -4
  50. package/src/claude/inbound-model-options.ts +14 -2
  51. package/src/claude/inbound.ts +1 -1
  52. package/src/claude/intercept/connect-proxy.ts +17 -1
  53. package/src/claude/intercept/local-ca.ts +7 -1
  54. package/src/claude/message-threads.ts +28 -0
  55. package/src/cli/account-api.ts +13 -0
  56. package/src/cli/account-auth.ts +49 -2
  57. package/src/cli/account-extended.ts +4 -2
  58. package/src/cli/account.ts +4 -2
  59. package/src/cli/capabilities.ts +17 -0
  60. package/src/cli/claude.ts +3 -1
  61. package/src/cli/dispatch.ts +62 -5
  62. package/src/cli/index.ts +84 -79
  63. package/src/cli/minimax.ts +4 -2
  64. package/src/cli/opencode.ts +6 -1
  65. package/src/cli/restart-handoff.ts +110 -0
  66. package/src/cli/status.ts +51 -0
  67. package/src/client/connect.ts +34 -15
  68. package/src/client/link-ingress.ts +102 -0
  69. package/src/client/link-join.ts +37 -16
  70. package/src/client/link-relay.ts +228 -45
  71. package/src/client/link-state.ts +54 -0
  72. package/src/client/link-status.ts +39 -0
  73. package/src/client/link-teardown.ts +2 -2
  74. package/src/client/link-tunnel.ts +615 -86
  75. package/src/client/machine-api.ts +10 -4
  76. package/src/client/machine-listener.ts +57 -10
  77. package/src/client/runtime.ts +210 -35
  78. package/src/clients/config-export/constants.ts +1 -12
  79. package/src/clients/config-export/contracts.ts +2 -0
  80. package/src/clients/config-export/model-metadata.ts +9 -3
  81. package/src/clients/config-export/omp.ts +1 -1
  82. package/src/clients/config-export/zcode-store.ts +2 -2
  83. package/src/clients/config-export.ts +36 -3
  84. package/src/codex/app-server-processes.ts +32 -0
  85. package/src/codex/app-server-restart-service.ts +20 -3
  86. package/src/codex/catalog/access-programs.ts +50 -0
  87. package/src/codex/catalog/build-entries.ts +13 -0
  88. package/src/codex/catalog/derive-entry.ts +1 -0
  89. package/src/codex/catalog/model-hints.ts +11 -0
  90. package/src/codex/catalog/parsing.ts +6 -2
  91. package/src/codex/catalog/provider-models.ts +81 -0
  92. package/src/codex/catalog/retained-sync.ts +3 -0
  93. package/src/codex/codex-write-lock.ts +2 -2
  94. package/src/codex/convergence.ts +2 -0
  95. package/src/codex/desired-state.ts +14 -4
  96. package/src/codex/home.ts +21 -3
  97. package/src/codex/inject/restore.ts +17 -0
  98. package/src/codex/inject/routing-classify.ts +3 -2
  99. package/src/codex/inject-coordination.ts +4 -1
  100. package/src/codex/inject.ts +8 -1
  101. package/src/codex/journal.ts +6 -2
  102. package/src/codex/management-convergence.ts +9 -0
  103. package/src/codex/model-entitlements.ts +28 -5
  104. package/src/codex/routing/idle-window.ts +58 -0
  105. package/src/codex/routing-drift.ts +134 -0
  106. package/src/codex/routing-healer.ts +419 -0
  107. package/src/codex/routing.ts +10 -0
  108. package/src/codex/runtime.ts +188 -57
  109. package/src/codex/sibling-handoff.ts +71 -0
  110. package/src/codex/sibling-start.ts +139 -0
  111. package/src/codex/sync.ts +5 -3
  112. package/src/combos/failover.ts +32 -5
  113. package/src/combos/request.ts +11 -3
  114. package/src/combos/reset-window.ts +10 -5
  115. package/src/combos/resolve.ts +36 -6
  116. package/src/config/diagnostics.ts +2 -1
  117. package/src/config/load-degrade.ts +2 -0
  118. package/src/config/paths.ts +7 -1
  119. package/src/config/process-state.ts +10 -1
  120. package/src/config/schema/compaction-recovery.ts +15 -0
  121. package/src/config/schema/config-schema.ts +2 -0
  122. package/src/config/schema/leaf-validators.ts +9 -3
  123. package/src/generated/compatibility-version.json +360 -204
  124. package/src/images/loop.ts +47 -32
  125. package/src/integrations/catalog-refresh.ts +4 -0
  126. package/src/integrations/omp-yaml-source.ts +1 -0
  127. package/src/lib/claude-request-projection.ts +101 -0
  128. package/src/lib/local-account-switch-capability.ts +48 -0
  129. package/src/lib/local-upstream.ts +146 -0
  130. package/src/lib/package-tree-integrity.ts +25 -3
  131. package/src/lib/package-tree-retarget.ts +130 -0
  132. package/src/lib/process-control.ts +4 -1
  133. package/src/lib/system-restart-contract.ts +106 -0
  134. package/src/lib/upstream-retry.ts +57 -1
  135. package/src/link/ports.ts +9 -0
  136. package/src/link/ssh-argv.ts +16 -0
  137. package/src/link/ssh-runner.ts +70 -2
  138. package/src/link/tunnel-state.ts +58 -9
  139. package/src/oauth/account-quota-rank.ts +29 -18
  140. package/src/oauth/generic-account-failover.ts +179 -22
  141. package/src/oauth/index.ts +17 -4
  142. package/src/oauth/kiro-account-load.ts +107 -0
  143. package/src/oauth/kiro-device-login.ts +312 -0
  144. package/src/oauth/kiro-terminal-failover.ts +28 -0
  145. package/src/oauth/login-flow-state.ts +7 -1
  146. package/src/oauth/pool-settings-capability.ts +23 -14
  147. package/src/oauth/store.ts +56 -3
  148. package/src/oauth/types.ts +4 -0
  149. package/src/plugins/loader.ts +350 -0
  150. package/src/plugins/upstream-hooks.ts +119 -0
  151. package/src/protocols/encoders/adapter-events.ts +9 -2
  152. package/src/providers/account-quota-disk.ts +42 -4
  153. package/src/providers/codebuddy-models.ts +6 -2
  154. package/src/providers/command-code-efforts.ts +33 -10
  155. package/src/providers/kiro-account-state-disk.ts +44 -0
  156. package/src/providers/kiro-model-catalog.ts +162 -0
  157. package/src/providers/kiro-models.ts +5 -4
  158. package/src/providers/kiro-quota-metrics.ts +35 -0
  159. package/src/providers/kiro-usage.ts +118 -14
  160. package/src/providers/quota/account-cache.ts +43 -7
  161. package/src/providers/quota/antigravity.ts +8 -3
  162. package/src/providers/quota/kiro-account-probe.ts +12 -0
  163. package/src/providers/quota/vendor-probes-key.ts +31 -26
  164. package/src/providers/quota/vendor-probes-oauth.ts +16 -7
  165. package/src/providers/quota-types.ts +14 -0
  166. package/src/providers/quota.ts +21 -21
  167. package/src/providers/registry/entries-extended.ts +5 -3
  168. package/src/providers/request-pacing.ts +410 -12
  169. package/src/remote-control/workspace-codex-runtime.ts +4 -3
  170. package/src/responses/citation-markers.ts +132 -65
  171. package/src/responses/hosted-tool-policy.ts +14 -3
  172. package/src/responses/parser-content.ts +3 -2
  173. package/src/responses/parser.ts +4 -1
  174. package/src/responses/schema.ts +4 -0
  175. package/src/responses/visualization-directives.ts +182 -0
  176. package/src/server/chat-native-sse.ts +24 -12
  177. package/src/server/chat-native.ts +137 -68
  178. package/src/server/claude-messages.ts +147 -12
  179. package/src/server/index/link-listener.ts +7 -0
  180. package/src/server/index/optional-listeners.ts +4 -1
  181. package/src/server/index/package-tree-guard.ts +35 -16
  182. package/src/server/index/serve-options.ts +4 -2
  183. package/src/server/index/startup-warnings.ts +6 -0
  184. package/src/server/index.ts +7 -7
  185. package/src/server/inference/client-encoder-delivery.ts +3 -0
  186. package/src/server/inference/context.ts +30 -2
  187. package/src/server/local-account-switch-auth.ts +79 -0
  188. package/src/server/management/agent-settings-routes.ts +11 -3
  189. package/src/server/management/config-routes.ts +28 -11
  190. package/src/server/management/context.ts +2 -0
  191. package/src/server/management/link-routes.ts +123 -39
  192. package/src/server/management/logs-usage-routes.ts +9 -42
  193. package/src/server/management/model-rows.ts +1 -0
  194. package/src/server/management/oauth-account-routes.ts +67 -12
  195. package/src/server/management/route-registry.ts +5 -5
  196. package/src/server/management/sibling-guard.ts +60 -0
  197. package/src/server/management/storage-log-guard-routes.ts +72 -6
  198. package/src/server/management/system-restart.ts +92 -135
  199. package/src/server/management/system-routes.ts +1 -1
  200. package/src/server/management-api.ts +31 -3
  201. package/src/server/management-auth.ts +3 -0
  202. package/src/server/port-reclaim.ts +111 -1
  203. package/src/server/proxy-liveness.ts +16 -0
  204. package/src/server/relay.ts +16 -15
  205. package/src/server/request-log-filter.ts +28 -27
  206. package/src/server/request-log.ts +7 -3
  207. package/src/server/request-metrics.ts +28 -0
  208. package/src/server/responses/adapter-continuation.ts +151 -21
  209. package/src/server/responses/adapter-delivery.ts +21 -7
  210. package/src/server/responses/adapter-dispatch.ts +239 -51
  211. package/src/server/responses/codex-ws-pool.ts +5 -4
  212. package/src/server/responses/codex-ws-request.ts +4 -1
  213. package/src/server/responses/compact.ts +8 -4
  214. package/src/server/responses/compaction-recovery-policy.ts +116 -0
  215. package/src/server/responses/compaction-recovery.ts +330 -0
  216. package/src/server/responses/core-combo-failure.ts +1 -0
  217. package/src/server/responses/core-combo.ts +5 -1
  218. package/src/server/responses/core-lifetime.ts +22 -0
  219. package/src/server/responses/core-options.ts +16 -1
  220. package/src/server/responses/core.ts +14 -4
  221. package/src/server/responses/empty-completion-guard.ts +2 -0
  222. package/src/server/responses/fetch-helpers.ts +82 -31
  223. package/src/server/responses/passthrough-delivery.ts +3 -2
  224. package/src/server/responses/passthrough-dispatch.ts +12 -2
  225. package/src/server/responses/passthrough-execution.ts +2 -2
  226. package/src/server/responses/request-send-budget.ts +21 -5
  227. package/src/server/responses/request-transport.ts +116 -18
  228. package/src/server/responses/reset-replay.ts +32 -0
  229. package/src/server/responses/run-turn-execution.ts +136 -19
  230. package/src/server/responses/sidecar-execution.ts +41 -8
  231. package/src/server/responses/terminal-guard.ts +2 -0
  232. package/src/server/responses/ws-upstream.ts +44 -4
  233. package/src/server/restart-replacement.ts +382 -0
  234. package/src/server/startup-health-cache.ts +23 -4
  235. package/src/server/stop-teardown.ts +35 -1
  236. package/src/server/system-env.ts +3 -0
  237. package/src/service/windows-taskxml.ts +11 -7
  238. package/src/service/windows-wrapper-exit.ts +8 -0
  239. package/src/stall-timeout.ts +35 -10
  240. package/src/storage/policy-job.ts +4 -0
  241. package/src/storage/scanner.ts +177 -29
  242. package/src/storage/storage-mutation-coordinator.ts +17 -0
  243. package/src/tray/windows-tray.ps1 +50 -23
  244. package/src/types/config.ts +4 -0
  245. package/src/types/provider.ts +5 -1
  246. package/src/types/request.ts +13 -2
  247. package/src/update/index.ts +3 -1
  248. package/src/update/job.ts +6 -2
  249. package/src/update/mise-launcher-target.ts +100 -0
  250. package/src/update/worker-launch.ts +46 -0
  251. package/src/usage/log.ts +2 -0
  252. package/src/web-search/loop.ts +5 -2
  253. package/gui/dist/assets/App-BJsT8Icc.css +0 -1
  254. package/gui/dist/assets/App-D3pNGiN4.js +0 -51
  255. package/gui/dist/assets/Tray-yb05wCjb.js +0 -1
  256. package/gui/dist/assets/index-CouvvtMV.js +0 -86
  257. package/gui/dist/assets/tray-data-CYGdjCJ7.js +0 -1
@@ -1,6 +1,9 @@
1
- import type { ProviderQuota } from "../providers/quota";
1
+ import type { ProviderQuota, ProviderQuotaWindow } from "../providers/quota";
2
2
 
3
- function collectResetCandidates(quota: ProviderQuota): number[] {
3
+ /** Which custom windows count for a ranking; provider-wide windows always count. */
4
+ export type QuotaWindowFilter = (window: ProviderQuotaWindow) => boolean;
5
+
6
+ function collectResetCandidates(quota: ProviderQuota, includeWindow?: QuotaWindowFilter): number[] {
4
7
  const candidates: number[] = [];
5
8
  const push = (value: number | undefined): void => {
6
9
  if (value !== undefined && Number.isFinite(value)) candidates.push(value);
@@ -10,7 +13,7 @@ function collectResetCandidates(quota: ProviderQuota): number[] {
10
13
  push(quota.monthlyResetAt);
11
14
  if (quota.customWindows) {
12
15
  for (const w of quota.customWindows) {
13
- push(w.resetAt);
16
+ if (!includeWindow || includeWindow(w)) push(w.resetAt);
14
17
  }
15
18
  }
16
19
  return candidates;
@@ -23,9 +26,10 @@ function collectResetCandidates(quota: ProviderQuota): number[] {
23
26
  export function earliestQuotaResetAt(
24
27
  quota: ProviderQuota | null,
25
28
  now: number,
29
+ includeWindow?: QuotaWindowFilter,
26
30
  ): number | null {
27
31
  if (!quota) return null;
28
- const future = collectResetCandidates(quota).filter(ts => ts > now);
32
+ const future = collectResetCandidates(quota, includeWindow).filter(ts => ts > now);
29
33
  if (future.length > 0) return Math.min(...future);
30
34
  return null;
31
35
  }
@@ -39,8 +43,9 @@ export function earliestQuotaResetAt(
39
43
  export function quotaResetRemainingMs(
40
44
  quota: ProviderQuota | null,
41
45
  now: number,
46
+ includeWindow?: QuotaWindowFilter,
42
47
  ): number {
43
- const nearest = earliestQuotaResetAt(quota, now);
48
+ const nearest = earliestQuotaResetAt(quota, now, includeWindow);
44
49
  if (nearest === null) return Number.POSITIVE_INFINITY;
45
50
  return nearest - now;
46
51
  }
@@ -1,6 +1,6 @@
1
1
  import type { OcxComboTarget, OcxConfig } from "../types";
2
2
  import { getCachedProviderRoutingQuota } from "../providers/quota-routing-cache";
3
- import type { ProviderQuota } from "../providers/quota-types";
3
+ import type { ProviderQuota, ProviderQuotaWindow } from "../providers/quota-types";
4
4
  import { sleepWithAbort } from "../lib/upstream-retry";
5
5
  import {
6
6
  coolComboTarget,
@@ -64,7 +64,7 @@ function targetProviderIsUsable(config: OcxConfig, target: OcxComboTarget, now:
64
64
  if (!Object.hasOwn(config.providers, target.provider)) return false;
65
65
  const provider = config.providers[target.provider];
66
66
  if (!provider || provider.disabled === true) return false;
67
- return !cachedProviderQuotaIsExhausted(getCachedProviderRoutingQuota(target.provider, provider, now), now);
67
+ return !cachedProviderQuotaIsExhausted(getCachedProviderRoutingQuota(target.provider, provider, now), now, target.model);
68
68
  }
69
69
 
70
70
  function quotaWindowExhausted(percent: number | undefined, resetAt: number | undefined, now: number): boolean {
@@ -72,15 +72,39 @@ function quotaWindowExhausted(percent: number | undefined, resetAt: number | und
72
72
  return typeof resetAt !== "number" || !Number.isFinite(resetAt) || resetAt > now;
73
73
  }
74
74
 
75
+ // `customWindows` is a generic carrier. Most labels are provider-wide counters ("Prepaid
76
+ // credits", "Free trial", "burst", "Spark") and must gate every model. Anthropic also rides it
77
+ // for per-model family counters, where a spent Opus week says nothing about a Sonnet request.
78
+ // `routing/quota.ts` already scopes those per model when it ranks accounts
79
+ // (`anthropicFamilyWindow`); this is the same fact applied to combo target selection.
80
+ //
81
+ // The scoping keys on `window.scope`, which the PRODUCER sets only where it proved model scope
82
+ // structurally, NEVER on the label text: `quota/antigravity.ts` forwards an upstream
83
+ // `group.displayName` unchanged, so a provider-wide group named "Opus" read as per-model would
84
+ // leave a spent window unenforced and send a doomed request. Every unproven direction keeps
85
+ // gating everything, which is the behaviour before this change: no scope on the window, no
86
+ // model on the request, or a family this gateway cannot match against a model id. Failing
87
+ // closed there costs at most a diverted target.
88
+ const MODEL_FAMILY_WINDOW_LABELS = new Set(["fable", "opus", "sonnet"]);
89
+
90
+ function customWindowAppliesToModel(window: ProviderQuotaWindow, model: string | undefined): boolean {
91
+ if (window.scope !== "model" || model === undefined) return true;
92
+ const family = window.label.trim().toLowerCase();
93
+ if (!MODEL_FAMILY_WINDOW_LABELS.has(family)) return true;
94
+ return model.toLowerCase().includes(family);
95
+ }
96
+
75
97
  export function cachedProviderQuotaIsExhausted(
76
98
  quota: ProviderQuota | null,
77
99
  now = Date.now(),
100
+ model?: string,
78
101
  ): boolean {
79
102
  if (!quota) return false;
80
103
  if (quotaWindowExhausted(quota.fiveHourPercent, quota.fiveHourResetAt, now)) return true;
81
104
  if (quotaWindowExhausted(quota.weeklyPercent, quota.weeklyResetAt, now)) return true;
82
105
  if (quotaWindowExhausted(quota.monthlyPercent, quota.monthlyResetAt, now)) return true;
83
- if (quota.customWindows?.some(window => quotaWindowExhausted(window.percent, window.resetAt, now))) return true;
106
+ if (quota.customWindows?.some(window => customWindowAppliesToModel(window, model)
107
+ && quotaWindowExhausted(window.percent, window.resetAt, now))) return true;
84
108
  if (quota.creditsUsd?.unlimited !== true
85
109
  && typeof quota.creditsUsd?.percent === "number"
86
110
  && Number.isFinite(quota.creditsUsd.percent)
@@ -120,7 +144,9 @@ export type QuotaInactiveReason = "no_credit";
120
144
  */
121
145
  export function quotaInactiveReason(
122
146
  config: OcxConfig,
123
- targets: readonly { provider: string }[],
147
+ // A combo votes over its own targets, which carry a model; the single-provider case has none,
148
+ // and an absent model keeps gating on every window exactly as before.
149
+ targets: readonly { provider: string; model?: string }[],
124
150
  now = Date.now(),
125
151
  ): QuotaInactiveReason | undefined {
126
152
  const usable = targets.filter(target => {
@@ -132,7 +158,7 @@ export function quotaInactiveReason(
132
158
  for (const target of usable) {
133
159
  const provider = config.providers[target.provider]!;
134
160
  const quota = getCachedProviderRoutingQuota(target.provider, provider, now);
135
- if (!quota || !cachedProviderQuotaIsExhausted(quota, now)) return undefined;
161
+ if (!quota || !cachedProviderQuotaIsExhausted(quota, now, target.model)) return undefined;
136
162
  }
137
163
  return "no_credit";
138
164
  }
@@ -185,7 +211,11 @@ function resetWindowIndex(
185
211
  const target = targets[index]!;
186
212
  if (!eligible(target)) continue;
187
213
  const remaining = quotaResetRemainingMs(
188
- getCachedProviderRoutingQuota(target.provider, config.providers[target.provider], now), now,
214
+ getCachedProviderRoutingQuota(target.provider, config.providers[target.provider], now),
215
+ now,
216
+ // Rank by the windows that gate this target: a model-scoped window of another family
217
+ // says nothing about when this model regains capacity.
218
+ window => customWindowAppliesToModel(window, target.model),
189
219
  );
190
220
  // Strict comparison deliberately retains configured order for ties,
191
221
  // including the no-snapshot fallback where every value is Infinity.
@@ -2,6 +2,7 @@ import { createHash } from "node:crypto";
2
2
  import { lstatSync, readFileSync } from "node:fs";
3
3
  import { join } from "node:path";
4
4
  import * as z from "zod/v4";
5
+ import { compactionRecoveryConfigError } from "./schema/compaction-recovery";
5
6
  import type { OcxConfig } from "../types";
6
7
  import { configReasoningPinsConfigError } from "./provider-validation";
7
8
  import { loopbackCompanionAllowed } from "../codex/loopback-target";
@@ -598,7 +599,7 @@ export function validateConfigCandidate(value: unknown): { ok: true; config: Ocx
598
599
  if (compactionRouting !== undefined && !compactionRoutingSchema.safeParse(compactionRouting).success) {
599
600
  return { ok: false, error: "schema_invalid: compactionRouting: requires a nonblank model, an optional valid reasoningEffort, and optional non-repeating triggers drawn from \"manual\" and \"auto\"" };
600
601
  }
601
- const boundaryError = configReasoningPinsConfigError(value)
602
+ const boundaryError = compactionRecoveryConfigError(value) ?? configReasoningPinsConfigError(value)
602
603
  ?? blankHostnameError(value)
603
604
  ?? claudeSubagentEffortError(value)
604
605
  ?? appOwnedMemoryBudgetError(value)
@@ -1,5 +1,6 @@
1
1
  import { chmodSync, existsSync } from "node:fs";
2
2
  import { join } from "node:path";
3
+ import { compactionRecoveryConfigError } from "./schema/compaction-recovery";
3
4
  import {
4
5
  modelPinnedEffortsConfigError,
5
6
  pinnedReasoningEffortConfigError,
@@ -118,6 +119,7 @@ export function warnDegradedCompactionRouting(rawParsed: unknown, validated: Ocx
118
119
  * the ratchet only ever moves down: a per-block call there costs a line the file does not have.
119
120
  */
120
121
  export function warnDegradedTopLevelOptIns(rawParsed: unknown, validated: OcxConfig): void {
122
+ if (compactionRecoveryConfigError(rawParsed)) console.warn("⚠️ invalid compactionRecovery disabled; the original compaction failure is preserved");
121
123
  warnDegradedStreamMode(rawParsed, validated);
122
124
  warnDegradedCompactionRouting(rawParsed, validated);
123
125
  }
@@ -1,7 +1,7 @@
1
1
  import { chmodSync, existsSync } from "node:fs";
2
2
  import { homedir } from "node:os";
3
3
  import { join, resolve } from "node:path";
4
- import { hardenSecretDirAsync, windowsSecretAclApplies } from "../lib/windows-secret-acl";
4
+ import { flushWindowsSecretAclReapsBeforeRemoval, hardenSecretDirAsync, windowsSecretAclApplies } from "../lib/windows-secret-acl";
5
5
  import { assertNotRealHomeUnderTest } from "../lib/test-home-guard";
6
6
 
7
7
  /**
@@ -63,6 +63,12 @@ export async function flushConfigDirHardening(dir: string = getConfigDir()): Pro
63
63
  if (flight) await flight;
64
64
  }
65
65
 
66
+ /** A settled ACL deadline is not proof its child has exited; stop must drain both before removal. */
67
+ export async function flushConfigDirHardeningAndReaps(dir: string): Promise<void> {
68
+ try { await flushConfigDirHardening(dir); }
69
+ finally { await flushWindowsSecretAclReapsBeforeRemoval(dir); }
70
+ }
71
+
66
72
  /** Test-only: settle every in-flight config-directory harden regardless of directory. */
67
73
  export async function flushConfigDirHardeningForTests(): Promise<void> {
68
74
  await Promise.all([...configDirHardeningFlights.values()]);
@@ -36,6 +36,12 @@ export type RuntimePortState = {
36
36
  hostname?: string;
37
37
  /** Per-process proof key; protected by the config directory and never served. */
38
38
  attestationSecret?: string;
39
+ /**
40
+ * The live proxy's port when this runtime is a sibling instance started beside it
41
+ * (`src/codex/sibling-start.ts`). `ocx stop` reads it to leave shared client routing alone.
42
+ * Absent for every other runtime, so those records keep their bytes.
43
+ */
44
+ siblingOfPort?: number;
39
45
  };
40
46
 
41
47
  function isValidRuntimePortState(value: unknown): value is RuntimePortState {
@@ -43,13 +49,16 @@ function isValidRuntimePortState(value: unknown): value is RuntimePortState {
43
49
  const state = value as Record<string, unknown>;
44
50
  const hostnameOk = state.hostname === undefined || typeof state.hostname === "string";
45
51
  const attestationOk = state.attestationSecret === undefined || isLocalAttestationSecret(state.attestationSecret);
52
+ const siblingOk = state.siblingOfPort === undefined
53
+ || (Number.isInteger(state.siblingOfPort) && Number(state.siblingOfPort) > 0 && Number(state.siblingOfPort) <= 65535);
46
54
  return Number.isSafeInteger(state.pid)
47
55
  && Number(state.pid) > 0
48
56
  && Number.isInteger(state.port)
49
57
  && Number(state.port) > 0
50
58
  && Number(state.port) <= 65535
51
59
  && hostnameOk
52
- && attestationOk;
60
+ && attestationOk
61
+ && siblingOk;
53
62
  }
54
63
 
55
64
  export function writeRuntimePort(state: RuntimePortState): void {
@@ -0,0 +1,15 @@
1
+ import * as z from "zod/v4";
2
+
3
+ /** Failure-only, opt-in recovery; it never chooses the initial compaction model. */
4
+ export const compactionRecoverySchema = z.object({
5
+ enabled: z.boolean(),
6
+ model: z.string().trim().min(1).max(512).regex(/^[^\s\u0000-\u0020\u007f-\u009f]+$/),
7
+ allowDevinInvalidArgument: z.boolean().optional(),
8
+ }).strict();
9
+
10
+ export function compactionRecoveryConfigError(value: unknown): string | null {
11
+ if (!value || typeof value !== "object" || Array.isArray(value)) return null;
12
+ const recovery = (value as Record<string, unknown>).compactionRecovery;
13
+ return recovery === undefined || compactionRecoverySchema.safeParse(recovery).success
14
+ ? null : "schema_invalid: compactionRecovery: requires enabled, a nonblank model, and optional boolean allowDevinInvalidArgument";
15
+ }
@@ -1,4 +1,5 @@
1
1
  import * as z from "zod/v4";
2
+ import { compactionRecoverySchema } from "./compaction-recovery";
2
3
  import {
3
4
  agentTaskRecoverySchema,
4
5
  catalogAutoRefreshSchema,
@@ -156,6 +157,7 @@ export const configSchema = z.object({
156
157
  providers: z.record(z.string(), providerConfigSchema),
157
158
  modelPinnedEfforts: modelPinnedEffortsSchema.optional(),
158
159
  compactionRouting: compactionRoutingSchema.optional().catch(undefined),
160
+ compactionRecovery: compactionRecoverySchema.optional().catch(undefined),
159
161
  defaultProvider: z.string().min(1).default("openai"),
160
162
  defaultModelAliases: z.boolean().optional(),
161
163
  // Malformed hand edits disable this opt-in projection without rejecting providers.
@@ -97,18 +97,23 @@ const requestPacingRuleSchema = z.object({
97
97
  // Keep the RPM-derived timer within the same one-hour bound as minIntervalMs.
98
98
  requestsPerMinute: z.number().min(1 / 60).max(60_000).optional(),
99
99
  minIntervalMs: z.number().int().min(1).max(3_600_000).optional(),
100
- }).strict().refine(value => value.requestsPerMinute !== undefined || value.minIntervalMs !== undefined, {
101
- message: "request pacing rules need requestsPerMinute or minIntervalMs",
100
+ maxConcurrentRequests: z.number().int().min(1).optional(),
101
+ }).strict().refine(value => value.requestsPerMinute !== undefined
102
+ || value.minIntervalMs !== undefined
103
+ || value.maxConcurrentRequests !== undefined, {
104
+ message: "request pacing rules need requestsPerMinute, minIntervalMs, or maxConcurrentRequests",
102
105
  });
103
106
 
104
107
  const requestPacingSchema = z.object({
105
108
  enabled: z.boolean(),
106
109
  requestsPerMinute: z.number().min(1 / 60).max(60_000).optional(),
107
110
  minIntervalMs: z.number().int().min(1).max(3_600_000).optional(),
111
+ maxConcurrentRequests: z.number().int().min(1).optional(),
108
112
  models: z.record(z.string().trim().min(1), requestPacingRuleSchema).optional(),
109
113
  }).strict().refine(value => value.enabled === false
110
114
  || value.requestsPerMinute !== undefined
111
115
  || value.minIntervalMs !== undefined
116
+ || value.maxConcurrentRequests !== undefined
112
117
  || (value.models !== undefined && Object.keys(value.models).length > 0), {
113
118
  message: "enabled request pacing needs a provider rule or model override",
114
119
  });
@@ -117,7 +122,7 @@ export function requestPacingConfigError(value: unknown): string | null {
117
122
  if (value === undefined) return null;
118
123
  const parsed = requestPacingSchema.safeParse(value);
119
124
  if (parsed.success) return null;
120
- return "requestPacing must contain enabled and a valid requestsPerMinute/minIntervalMs provider rule or model overrides";
125
+ return "requestPacing must contain enabled and a valid requestsPerMinute/minIntervalMs/maxConcurrentRequests provider rule or model overrides";
121
126
  }
122
127
 
123
128
  /**
@@ -938,6 +943,7 @@ export const clientConnectionSchema = z.object({
938
943
  */
939
944
  export const codexPoolSchema = z.object({
940
945
  excludedPlans: z.array(z.string().trim().min(1)).optional(),
946
+ startIdleWindows: z.boolean().optional(),
941
947
  }).strict();
942
948
 
943
949
  /**