@bitkyc08/opencodex 2.87.0 → 2.89.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 (246) hide show
  1. package/README.md +11 -1
  2. package/bin/ocx.mjs +15 -4
  3. package/gui/dist/assets/App-BnRSujOq.js +58 -0
  4. package/gui/dist/assets/App-dnkJ85te.css +1 -0
  5. package/gui/dist/assets/{Tray-DCd-A1az.js → Tray-7z1COnmz.js} +1 -1
  6. package/gui/dist/assets/index-uchD-ngf.js +94 -0
  7. package/gui/dist/assets/{quota-summary-ChvmKGOk.js → quota-summary-BY_10IEz.js} +1 -1
  8. package/gui/dist/index.html +1 -1
  9. package/package.json +1 -1
  10. package/src/adapters/anthropic/cache-retention.ts +117 -0
  11. package/src/adapters/anthropic/passthrough.ts +7 -4
  12. package/src/adapters/anthropic-rejection-memo.ts +71 -0
  13. package/src/adapters/anthropic-replay.ts +135 -0
  14. package/src/adapters/anthropic-tool-catalog.ts +11 -0
  15. package/src/adapters/anthropic.ts +86 -48
  16. package/src/adapters/coding-agent/protocol.ts +6 -0
  17. package/src/adapters/google-http.ts +3 -2
  18. package/src/adapters/google-progress-history.ts +53 -0
  19. package/src/adapters/google-vertex-endpoint.ts +67 -0
  20. package/src/adapters/google.ts +52 -30
  21. package/src/adapters/openai-responses/compaction-search-history.ts +27 -0
  22. package/src/adapters/openai-responses/passthrough.ts +17 -12
  23. package/src/adapters/openai-responses/reasoning.ts +68 -1
  24. package/src/adapters/registry.ts +1 -1
  25. package/src/adapters/upstream-http-error.ts +3 -2
  26. package/src/adapters/zed.ts +7 -4
  27. package/src/bridge/internal.ts +17 -3
  28. package/src/bridge/response-json.ts +4 -4
  29. package/src/bridge/sse.ts +17 -10
  30. package/src/claude/inbound-content-options.ts +66 -0
  31. package/src/claude/inbound.ts +12 -26
  32. package/src/claude/outbound.ts +18 -5
  33. package/src/cli/account-api.ts +13 -1
  34. package/src/cli/account-extended.ts +17 -5
  35. package/src/cli/account-key-quota.ts +4 -1
  36. package/src/cli/account.ts +1 -1
  37. package/src/cli/capabilities-agents-routing.ts +45 -8
  38. package/src/cli/capabilities-base.ts +5 -5
  39. package/src/cli/capabilities-provider-models.ts +5 -4
  40. package/src/cli/combo-result.ts +8 -2
  41. package/src/cli/combo.ts +17 -0
  42. package/src/cli/cross-home-owner.ts +16 -2
  43. package/src/cli/help.ts +1 -1
  44. package/src/cli/loopback-family.ts +31 -0
  45. package/src/cli/message-command.ts +4 -1
  46. package/src/cli/message-remote-args.ts +69 -0
  47. package/src/cli/message-remote-command.ts +77 -0
  48. package/src/cli/provider-runtime.ts +69 -1
  49. package/src/cli/registry.ts +3 -3
  50. package/src/codex/account-pause.ts +6 -1
  51. package/src/codex/account-runtime-state.ts +5 -0
  52. package/src/codex/account-store.ts +30 -3
  53. package/src/codex/auth-api/account-list.ts +3 -0
  54. package/src/codex/auth-api/main-account-probe.ts +3 -2
  55. package/src/codex/auth-context.ts +9 -4
  56. package/src/codex/catalog/aggregation.ts +15 -0
  57. package/src/codex/catalog/auto-review.ts +2 -1
  58. package/src/codex/catalog/build-entries.ts +17 -7
  59. package/src/codex/catalog/derive-entry.ts +3 -1
  60. package/src/codex/catalog/metadata.ts +2 -0
  61. package/src/codex/catalog/model-visibility.ts +0 -2
  62. package/src/codex/catalog/ownership.ts +71 -0
  63. package/src/codex/catalog/parsing.ts +12 -3
  64. package/src/codex/catalog/retained-sync.ts +2 -1
  65. package/src/codex/catalog/routed-gather.ts +3 -1
  66. package/src/codex/catalog/routed-removal.ts +4 -4
  67. package/src/codex/convergence.ts +2 -1
  68. package/src/codex/low-quota-desktop-notifier.ts +125 -0
  69. package/src/codex/low-quota-protection.ts +57 -31
  70. package/src/codex/main-account-cache.ts +77 -3
  71. package/src/codex/main-account.ts +6 -2
  72. package/src/codex/native-residue.ts +2 -3
  73. package/src/codex/quota.ts +26 -2
  74. package/src/codex/websocket-activity.ts +161 -0
  75. package/src/codex/websocket-registry.ts +25 -1
  76. package/src/combos/failover.ts +2 -1
  77. package/src/combos/jev-model-backend.ts +30 -7
  78. package/src/combos/jev-model-request.ts +20 -0
  79. package/src/combos/jev-quota-activation.ts +153 -0
  80. package/src/combos/jev-quota-config.ts +62 -0
  81. package/src/combos/jev-quota-route.ts +26 -0
  82. package/src/combos/jev-quota.ts +88 -0
  83. package/src/combos/jev.ts +25 -12
  84. package/src/combos/types.ts +27 -0
  85. package/src/compatibility/index.ts +3 -0
  86. package/src/compatibility/openai-responses.ts +8 -1
  87. package/src/compatibility/opengateway-responses.ts +61 -0
  88. package/src/config/account-auto-pause-revisions.ts +136 -0
  89. package/src/config/diagnostics.ts +13 -0
  90. package/src/config/live-reconcile.ts +9 -0
  91. package/src/config/opencode-zen-upgrade.ts +9 -8
  92. package/src/config/persist-unlocked.ts +10 -1
  93. package/src/config/removed-opencode-free.ts +135 -0
  94. package/src/config/schema/account-auto-pause.ts +14 -0
  95. package/src/config/schema/config-schema.ts +15 -0
  96. package/src/config/schema/leaf-validators.ts +7 -0
  97. package/src/config.ts +10 -3
  98. package/src/generated/compatibility-version.json +411 -175
  99. package/src/images/loop.ts +101 -27
  100. package/src/lib/account-auto-pause-response.ts +16 -0
  101. package/src/lib/bun-runtime.ts +18 -1
  102. package/src/lib/bun-transpiler-cache-scope.d.mts +8 -0
  103. package/src/lib/bun-transpiler-cache-scope.mjs +68 -0
  104. package/src/lib/decision-quota-sink.ts +76 -0
  105. package/src/lib/request-failure-model.ts +2 -0
  106. package/src/lib/state-store-registrations.ts +25 -0
  107. package/src/link/ssh-argv.ts +3 -0
  108. package/src/messaging/envelope.ts +6 -3
  109. package/src/messaging/remote-auth.ts +83 -0
  110. package/src/messaging/remote-bridge.ts +270 -0
  111. package/src/messaging/remote-contract.ts +108 -0
  112. package/src/messaging/remote-enrollment-recovery.ts +59 -0
  113. package/src/messaging/remote-enrollment.ts +244 -0
  114. package/src/messaging/remote-envelope.ts +40 -0
  115. package/src/messaging/remote-files.ts +117 -0
  116. package/src/messaging/remote-owner.ts +169 -0
  117. package/src/messaging/remote-ports.ts +56 -0
  118. package/src/messaging/remote-process.ts +131 -0
  119. package/src/messaging/remote-replies.ts +98 -0
  120. package/src/messaging/remote-rpc-admission.ts +32 -0
  121. package/src/messaging/remote-send.ts +72 -0
  122. package/src/messaging/remote-store.ts +111 -0
  123. package/src/messaging/remote-tunnels.ts +65 -0
  124. package/src/messaging/rpc.ts +13 -1
  125. package/src/oauth/account-auto-pause-policy.ts +66 -0
  126. package/src/oauth/account-auto-pause.ts +171 -0
  127. package/src/oauth/account-quota-rank.ts +2 -1
  128. package/src/oauth/anthropic-account-refusal.ts +6 -0
  129. package/src/oauth/anthropic-model-family.ts +8 -0
  130. package/src/oauth/anthropic-model-quota.ts +2 -6
  131. package/src/oauth/api-key-pool-preserve.ts +37 -0
  132. package/src/oauth/generic-account-failover.ts +7 -4
  133. package/src/oauth/index.ts +5 -35
  134. package/src/oauth/store.ts +35 -8
  135. package/src/oauth/types.ts +2 -0
  136. package/src/plugins/upstream-hooks.ts +14 -1
  137. package/src/protocols/encoders/adapter-events.ts +5 -5
  138. package/src/protocols/encoders/messages.ts +2 -2
  139. package/src/providers/api-key-pause.ts +32 -0
  140. package/src/providers/api-key-resolve.ts +10 -4
  141. package/src/providers/api-key-selection.ts +6 -1
  142. package/src/providers/api-keys.ts +8 -4
  143. package/src/providers/claude-cli-usage.ts +5 -0
  144. package/src/providers/key-failover.ts +11 -11
  145. package/src/providers/model-rename-fields.ts +1 -0
  146. package/src/providers/opencode-zen-rate-limit.ts +12 -73
  147. package/src/providers/quota/account-cache.ts +15 -1
  148. package/src/providers/quota/anthropic-account-quota.ts +2 -1
  149. package/src/providers/quota/anthropic-cooldown-recovery.ts +22 -4
  150. package/src/providers/quota/anthropic-decision-windows.ts +47 -0
  151. package/src/providers/quota/antigravity.ts +39 -6
  152. package/src/providers/quota/report-cache.ts +14 -0
  153. package/src/providers/quota/vendor-probes-oauth.ts +3 -3
  154. package/src/providers/quota-decision-publication.ts +77 -0
  155. package/src/providers/quota-decision-snapshot.ts +145 -0
  156. package/src/providers/quota-routing-cache.ts +3 -0
  157. package/src/providers/quota-types.ts +6 -1
  158. package/src/providers/quota.ts +3 -2
  159. package/src/providers/registry/entries-extended.ts +8 -52
  160. package/src/providers/registry/model-seeds.ts +1 -1
  161. package/src/providers/registry.ts +1 -2
  162. package/src/providers/stale-vision-classification-migration.ts +1 -1
  163. package/src/responses/parser.ts +18 -10
  164. package/src/responses/reasoning-replay-cache.ts +50 -4
  165. package/src/responses/web-search-replay.ts +138 -0
  166. package/src/routing/analytics.ts +7 -15
  167. package/src/routing/compatibility/behavior.ts +4 -1
  168. package/src/server/auth-cors.ts +7 -0
  169. package/src/server/background-lifecycle.ts +2 -1
  170. package/src/server/chat-native.ts +28 -13
  171. package/src/server/claude-messages.ts +4 -2
  172. package/src/server/index/live-sideband.ts +3 -2
  173. package/src/server/index/optional-listeners.ts +6 -0
  174. package/src/server/index/serve-options.ts +11 -5
  175. package/src/server/index/websocket-handler.ts +18 -4
  176. package/src/server/index.ts +7 -7
  177. package/src/server/lifecycle.ts +13 -1
  178. package/src/server/management/combo-routes.ts +17 -2
  179. package/src/server/management/oauth-account-routes.ts +10 -1
  180. package/src/server/management/provider-completion-test.ts +289 -0
  181. package/src/server/management/provider-routes.ts +9 -1
  182. package/src/server/management/route-registry.ts +2 -0
  183. package/src/server/messages-native.ts +34 -7
  184. package/src/server/messages-shared.ts +4 -1
  185. package/src/server/request-log.ts +7 -2
  186. package/src/server/responses/account-auto-pause-recovery.ts +26 -0
  187. package/src/server/responses/adapter-continuation.ts +34 -18
  188. package/src/server/responses/adapter-delivery.ts +53 -6
  189. package/src/server/responses/adapter-dispatch.ts +104 -29
  190. package/src/server/responses/anthropic-thinking-recovery.ts +153 -0
  191. package/src/server/responses/combo-session-recall.ts +25 -0
  192. package/src/server/responses/combo-stream-preflight.ts +6 -3
  193. package/src/server/responses/combo-tool-loop-pin.ts +70 -0
  194. package/src/server/responses/compact.ts +8 -5
  195. package/src/server/responses/compaction-recovery.ts +2 -0
  196. package/src/server/responses/context-overflow.ts +141 -2
  197. package/src/server/responses/core-codex-account.ts +9 -3
  198. package/src/server/responses/core-combo.ts +34 -7
  199. package/src/server/responses/core-lifetime.ts +3 -2
  200. package/src/server/responses/core-options.ts +2 -0
  201. package/src/server/responses/core-replay.ts +5 -1
  202. package/src/server/responses/core.ts +12 -9
  203. package/src/server/responses/empty-completion-guard.ts +2 -0
  204. package/src/server/responses/fetch-helpers.ts +35 -4
  205. package/src/server/responses/jev-model-invoke.ts +13 -14
  206. package/src/server/responses/native-steering-log.ts +2 -2
  207. package/src/server/responses/passthrough-delivery.ts +3 -2
  208. package/src/server/responses/passthrough-dispatch.ts +30 -33
  209. package/src/server/responses/passthrough-execution.ts +3 -2
  210. package/src/server/responses/passthrough-observations.ts +38 -0
  211. package/src/server/responses/request-compression.ts +98 -0
  212. package/src/server/responses/request-prepare.ts +1 -0
  213. package/src/server/responses/request-transport.ts +32 -3
  214. package/src/server/responses/response-effects.ts +1 -0
  215. package/src/server/responses/response-tool-ownership.ts +21 -0
  216. package/src/server/responses/run-turn-execution.ts +2 -0
  217. package/src/server/responses/sidecar-execution.ts +59 -10
  218. package/src/server/responses/sidecar-send-budget.ts +2 -2
  219. package/src/server/responses/terminal-guard.ts +2 -0
  220. package/src/server/responses-undeclared-tool-guard.ts +1 -1
  221. package/src/server/startup-action-control.ts +2 -2
  222. package/src/server/startup-health-cache.ts +2 -2
  223. package/src/server/windows-tray-control.ts +2 -2
  224. package/src/server/ws-bridge.ts +2 -0
  225. package/src/service/definition.ts +9 -1
  226. package/src/types/account-auto-pause.ts +15 -0
  227. package/src/types/config.ts +23 -1
  228. package/src/types/opencode-zen.ts +9 -3
  229. package/src/types/provider.ts +9 -5
  230. package/src/types/request.ts +30 -4
  231. package/src/types/wire.ts +4 -2
  232. package/src/types.ts +2 -0
  233. package/src/usage/cost.ts +17 -1
  234. package/src/usage/expected-prices.ts +3 -1
  235. package/src/usage/jev-stats.ts +4 -0
  236. package/src/usage/log.ts +2 -0
  237. package/src/usage/summary.ts +16 -3
  238. package/src/usage/telemetry-contract.ts +1 -0
  239. package/src/web-search/anthropic-history.ts +59 -0
  240. package/src/web-search/loop.ts +212 -73
  241. package/src/web-search/progress-stream.ts +14 -1
  242. package/src/web-search/run-turn-loop.ts +9 -3
  243. package/src/web-search/sidecar-search-replay.ts +96 -0
  244. package/gui/dist/assets/App-ZfYbLHkB.js +0 -58
  245. package/gui/dist/assets/App-wT3mXqFz.css +0 -1
  246. package/gui/dist/assets/index-CfYDMj34.js +0 -94
@@ -1,3 +1,5 @@
1
+ import { getConfigDir } from "../config/paths";
2
+ import { notifyMainRosterChanged, type MainDecisionRow } from "../lib/decision-quota-sink";
1
3
  import { createHash, createHmac, randomBytes, timingSafeEqual } from "node:crypto";
2
4
  import { codexCredentialMutationEpoch } from "./credential-mutation-epoch";
3
5
  import type { StoredAccountQuota } from "./quota-types";
@@ -22,11 +24,33 @@ let observedMainQuotaIdentityKey: string | undefined;
22
24
  const mainQuotaCredentialKey = randomBytes(32);
23
25
  let mainQuotaCredential: { bearerHmac: Buffer; writer: MainQuotaWriter } | undefined;
24
26
  let mainQuotaCredentialGeneration = 0;
27
+ let mainDecisionCredentialUsable = true;
28
+ // Advisory-only generation for the decision row and its writers. It follows every core credential
29
+ // generation change and additionally advances when physical presence is lost, so absence retires
30
+ // advisory evidence without touching the core credential binding that policy checks rely on.
31
+ let mainDecisionGeneration = 0;
32
+ // Advisory-only: the retained credential was re-observed since presence was last lost. Absence and
33
+ // presence-forgetting clear it, so the decision row stays unusable until a fresh owned observation.
34
+ let mainDecisionCredentialFresh = false;
35
+ // Config root that owns the observed main identity/credential for advisory decision evidence only.
36
+ // Stamped at identity/credential observations while a sink is active; other transitions never claim a root.
37
+ let mainDecisionRoot: string | undefined;
38
+ /** Existing physical credential owner supplies terminal grant/usability observations. */
39
+ export function observeMainDecisionCredentialUsable(usable: boolean): void {
40
+ mainDecisionCredentialUsable = usable;
41
+ publishMainDecisionRoster();
42
+ }
25
43
 
26
44
  /** Process-local transition fence; no credential material or persisted identity. */
27
45
  export function getMainQuotaCredentialGeneration(): number { return mainQuotaCredentialGeneration; }
28
46
 
29
47
  export type MainQuotaWriter = Readonly<{ identityKey: string; identityGeneration: number }>;
48
+ const decisionWriterGenerations = new WeakMap<MainQuotaWriter, number>();
49
+ /** Return the credential generation a main quota writer was captured under, or undefined once that writer is stale or its credential generation has moved. */
50
+ export function mainDecisionQuotaWriterGeneration(writer: MainQuotaWriter): number | undefined {
51
+ const generation = decisionWriterGenerations.get(writer);
52
+ return generation === mainDecisionGeneration && isMainQuotaWriterLive(writer) && mainDecisionRoot === getConfigDir() ? generation : undefined;
53
+ }
30
54
 
31
55
  function mainQuotaIdentityKey(accountId: string): string {
32
56
  return createHash("sha256").update("opencodex-main-quota-v1\0").update(accountId).digest("hex");
@@ -38,16 +62,22 @@ export function observeMainQuotaIdentity(accountId: string): void {
38
62
  const identityKey = mainQuotaIdentityKey(accountId);
39
63
  if (identityKey === observedMainQuotaIdentityKey) return;
40
64
  observedMainQuotaIdentityKey = identityKey;
65
+ mainDecisionCredentialUsable = true;
41
66
  mainAccountIdentityGeneration += 1;
42
67
  mainQuotaCredential = undefined;
43
68
  mainQuotaCredentialGeneration += 1;
69
+ mainDecisionGeneration += 1;
70
+ publishMainDecisionRoster(true);
44
71
  }
45
72
 
73
+ /** Capture a main quota writer for an account id only when it matches the observed physical identity, remembering the credential generation it was captured under. */
46
74
  export function captureMainQuotaWriter(accountId: string): MainQuotaWriter | undefined {
47
75
  if (!accountId) return undefined;
48
76
  const identityKey = mainQuotaIdentityKey(accountId);
49
77
  if (identityKey !== observedMainQuotaIdentityKey) return undefined;
50
- return { identityKey, identityGeneration: mainAccountIdentityGeneration };
78
+ const writer = { identityKey, identityGeneration: mainAccountIdentityGeneration };
79
+ if (mainQuotaCredential) decisionWriterGenerations.set(writer, mainDecisionGeneration);
80
+ return writer;
51
81
  }
52
82
 
53
83
  /** Credential material must come from an already-owned read, never an incoming request. */
@@ -56,9 +86,13 @@ export function observeMainQuotaCredential(accessToken: string, accountId: strin
56
86
  if (!accessToken || !writer) return undefined;
57
87
  const bearerHmac = createHmac("sha256", mainQuotaCredentialKey).update(accessToken).digest();
58
88
  if (!mainQuotaCredential || !isMainQuotaWriterLive(mainQuotaCredential.writer)
59
- || !timingSafeEqual(bearerHmac, mainQuotaCredential.bearerHmac)) mainQuotaCredentialGeneration += 1;
89
+ || !timingSafeEqual(bearerHmac, mainQuotaCredential.bearerHmac)) { mainQuotaCredentialGeneration += 1; mainDecisionGeneration += 1; }
60
90
  mainQuotaCredential = { bearerHmac, writer };
61
- return { ...writer };
91
+ mainDecisionCredentialFresh = true;
92
+ publishMainDecisionRoster(true);
93
+ const result = { ...writer };
94
+ decisionWriterGenerations.set(result, mainDecisionGeneration);
95
+ return result;
62
96
  }
63
97
 
64
98
  export function matchesMainQuotaCredential(accessToken: string, effectiveAccountId: string | undefined): boolean {
@@ -140,11 +174,14 @@ export function setMainAccountInfoCache(value: CachedMainAccountInfo): void {
140
174
  };
141
175
  }
142
176
 
177
+ /** Drop the cached main-account info and invalidate the observed identity and credential generation, then republish the main decision roster as unusable. */
143
178
  export function clearMainAccountInfoCache(): void {
144
179
  cachedMainAccountInfo = null;
145
180
  mainAccountIdentityGeneration += 1;
146
181
  mainQuotaCredential = undefined;
147
182
  mainQuotaCredentialGeneration += 1;
183
+ mainDecisionGeneration += 1;
184
+ publishMainDecisionRoster();
148
185
  }
149
186
 
150
187
  /** Last physical credential presence observed while native-main ownership was held. */
@@ -152,10 +189,47 @@ export function getMainAccountCredentialPresence(): boolean | null {
152
189
  return cachedMainCredentialPresence;
153
190
  }
154
191
 
192
+ /** Record whether the main credential is physically present. The core credential binding is left intact (a failed read must not change policy matching); losing presence only retires advisory decision evidence. */
155
193
  export function setMainAccountCredentialPresence(present: boolean): void {
194
+ if (!present && cachedMainCredentialPresence !== false) { mainDecisionGeneration += 1; mainDecisionCredentialFresh = false; }
156
195
  cachedMainCredentialPresence = present;
196
+ publishMainDecisionRoster();
157
197
  }
158
198
 
199
+ /** Forget the last observed main credential presence; retires advisory decision evidence only, leaving the core credential binding intact. */
159
200
  export function clearMainAccountCredentialPresence(): void {
160
201
  cachedMainCredentialPresence = null;
202
+ mainDecisionGeneration += 1;
203
+ mainDecisionCredentialFresh = false;
204
+ publishMainDecisionRoster();
205
+ }
206
+
207
+ /**
208
+ * Build the single `__main__` decision row from in-memory fields. With no retained credential the
209
+ * row is a known-unusable main (nothing to misattribute), as before. A retained credential counts only
210
+ * for the config root that observed it: for any other root the row is undefined (unknown) and the
211
+ * claimed root is forgotten, so returning to the old root never resurrects it without a new owned
212
+ * observation.
213
+ */
214
+ function mainDecisionRow(): MainDecisionRow | undefined {
215
+ if (mainQuotaCredential === undefined) return { generation: mainDecisionGeneration, usable: false };
216
+ if (mainDecisionRoot === undefined || mainDecisionRoot !== getConfigDir()) {
217
+ mainDecisionRoot = undefined;
218
+ return undefined;
219
+ }
220
+ return { generation: mainDecisionGeneration,
221
+ usable: mainDecisionCredentialFresh && mainDecisionCredentialUsable && cachedMainCredentialPresence !== false };
222
+ }
223
+
224
+ /** Hand the main decision row to the opt-in decision-quota slot (a no-op unless JEV quota signals are active); identity/credential observations claim the current root. */
225
+ function publishMainDecisionRoster(claimsRoot = false): void {
226
+ notifyMainRosterChanged(() => {
227
+ if (claimsRoot) mainDecisionRoot = getConfigDir();
228
+ return mainDecisionRow();
229
+ });
230
+ }
231
+
232
+ /** Activation replay for the JEV quota owner: the current row, or undefined when the observed credential belongs to another config root. */
233
+ export function peekMainDecisionRow(): MainDecisionRow | undefined {
234
+ return mainDecisionRow();
161
235
  }
@@ -1,3 +1,4 @@
1
+ import { observeMainDecisionCredentialUsable } from "./main-account-cache";
1
2
  import { classifyChatgptRefreshFailure, noteChatgptRefreshFailure } from "./chatgpt-refresh-failure";
2
3
  import { readBoundedResponseBody } from "../lib/bounded-body";
3
4
  import { createHash } from "node:crypto";
@@ -245,10 +246,12 @@ export function getMainAccountCredentialStatus(now = Date.now()): MainAccountCre
245
246
  const current = readMainAuthJsonCredential();
246
247
  const refreshGrantRejected = credentialRefreshGrantRejected(current);
247
248
  const hasRefreshGrant = !!current?.refreshToken && !refreshGrantRejected;
249
+ const usable = !refreshGrantRejected && (hasRefreshGrant || mainAccessTokenFresh(current?.accessToken, now, 0));
250
+ observeMainDecisionCredentialUsable(usable);
248
251
  return {
249
252
  refreshGrantRejected,
250
253
  hasRefreshGrant,
251
- usable: !refreshGrantRejected && (hasRefreshGrant || mainAccessTokenFresh(current?.accessToken, now, 0)),
254
+ usable,
252
255
  };
253
256
  }
254
257
 
@@ -444,6 +447,7 @@ export function beginNativeMainReauth(): {
444
447
  };
445
448
  }
446
449
 
450
+ /** Resolve the main account access token from the owned auth.json read, refreshing through the token endpoint when needed, or return null when no usable credential exists. */
447
451
  async function resolveMainAccountToken(
448
452
  dependencies: NativeMainRefreshDependencies = {},
449
453
  rejectedAccessToken?: string,
@@ -546,7 +550,7 @@ async function resolveMainAccountToken(
546
550
  if (signal.aborted) throw signal.reason;
547
551
  if (reason === "reauth") {
548
552
  assertMainAuthJsonSnapshotUnchanged(locked);
549
- markMainRefreshGrantRejected(mainRefreshGrantKey(locked)!);
553
+ markMainRefreshGrantRejected(mainRefreshGrantKey(locked)!); observeMainDecisionCredentialUsable(false);
550
554
  }
551
555
  throw new MainAccountTokenRefreshError(reason, { cause });
552
556
  }
@@ -17,6 +17,7 @@ import { Database } from "bun:sqlite";
17
17
 
18
18
  import { getConfigDir } from "../config";
19
19
  import { catalogHasRoutedEntries, parseCatalogJson } from "./catalog/parsing";
20
+ import { hasOcxRoutedAuthorship } from "./catalog/ownership";
20
21
  import { codexHistoryBackupId, legacyCodexHistoryBackupId, validateCodexHistoryBackupManifest } from "./history-manifest";
21
22
  import {
22
23
  hasInjectedCodexRouting,
@@ -79,7 +80,6 @@ const PROFILE_FILE_NAME = basename(CODEX_PROFILE_PATH);
79
80
  const CATALOG_FILE_NAME = basename(DEFAULT_CATALOG_PATH);
80
81
  const MODELS_CACHE_FILE_NAME = basename(CODEX_MODELS_CACHE_PATH);
81
82
  const JOURNAL_FILE_NAME = "opencodex-journal.json";
82
- const ROUTED_CATALOG_DESCRIPTION_PREFIX = "Routed via opencodex → ";
83
83
  const MAX_ROLLOUT_INSPECTION_BYTES = 64 * 1024 * 1024;
84
84
  const ROLLOUT_READ_CHUNK_BYTES = 64 * 1024;
85
85
  // Bound one observation across every referenced rollout, not just each file.
@@ -339,8 +339,7 @@ function classifyProfile(path: string): NativeRoutedResidueResult {
339
339
  }
340
340
 
341
341
  function isOcxRoutedCatalogEntry(entry: Record<string, unknown>): boolean {
342
- return typeof entry.description === "string"
343
- && entry.description.startsWith(ROUTED_CATALOG_DESCRIPTION_PREFIX);
342
+ return hasOcxRoutedAuthorship(entry);
344
343
  }
345
344
 
346
345
  function classifyCatalogLike(
@@ -1,3 +1,5 @@
1
+ import { bindRawCodexDecisionResets, publishCodexDecisionQuota } from "../providers/quota-decision-publication";
2
+ import { clearDecisionAccountQuotas, decisionQuotaSinkActive } from "../lib/decision-quota-sink";
1
3
  import { parseCodexCredits } from "./credits";
2
4
  import { forgetMainAccountUsage, observeMainAccountUsage, type FreshWindow } from "./main-account-external-usage";
3
5
  import { closeSync, constants as fsConstants, existsSync, fstatSync, openSync, readSync, unlinkSync } from "node:fs";
@@ -8,7 +10,7 @@ import { isThirtyDayOnlyCodexPlan } from "./plan";
8
10
  import { stampCodexQuotaUsageObservation } from "./quota-observation-freshness";
9
11
  import { observeCodexLowQuota } from "./low-quota-observer";
10
12
  import { MAIN_CODEX_ACCOUNT_ID } from "./account-id";
11
- import { getObservedMainQuotaIdentityKey, isMainQuotaWriterLive, type MainQuotaWriter } from "./main-account-cache";
13
+ import { getObservedMainQuotaIdentityKey, mainDecisionQuotaWriterGeneration, isMainQuotaWriterLive, type MainQuotaWriter } from "./main-account-cache";
12
14
 
13
15
  import { CodexQuotaHistory, QUOTA_HISTORY_LIMITS, type QuotaHistoryWindow } from "./quota-history";
14
16
  import { isPoolQuotaWriterLive, poolQuotaHistoryIdentity } from "./account-store";
@@ -301,6 +303,9 @@ export function setAccountQuotaFromParsed(
301
303
  // writer: when capture failed before dispatch, an absent writer must fail closed instead
302
304
  // of reading as a writer-free legacy/login observation.
303
305
  poolRequest = false,
306
+ decisionObservationPartial = true,
307
+ // Advisory publication is explicit opt-in evidence: display or policy data never becomes it by default.
308
+ decisionRaw: Omit<StoredAccountQuota, "updatedAt"> | null = null,
304
309
  ): void {
305
310
  quota = withoutRetiredCodexQuota(quota);
306
311
  policyQuota = withoutRetiredCodexQuota(policyQuota);
@@ -315,12 +320,16 @@ export function setAccountQuotaFromParsed(
315
320
  ? historyEvidence.writer.accountId === accountId && isPoolQuotaWriterLive(historyEvidence.writer)
316
321
  : !poolRequest;
317
322
  if (historyEvidence && livePoolEvidence) {
323
+ publishCodexDecisionQuota(accountId, historyEvidence.writer.credentialGeneration, historyEvidence.raw, historyEvidence.observedAt, "codex", historyEvidence.source === "response-header");
318
324
  quotaHistory.append(historyEvidence.writer, { observedAt: historyEvidence.observedAt, source: historyEvidence.source,
319
325
  credentialGeneration: historyEvidence.writer.credentialGeneration, windows: historyWindows(historyEvidence.raw),
320
326
  }, updatedAt);
321
327
  }
322
328
  // Rotation shares validated main-window retirement, but never inherits policy-only
323
329
  // evidence that outlived its disk TTL. Policy has a separate, identity-checked base.
330
+ const mainDecisionGeneration = mainWriter && mainDecisionQuotaWriterGeneration(mainWriter);
331
+ if (isMain && mainDecisionGeneration !== undefined && decisionRaw)
332
+ publishCodexDecisionQuota(accountId, mainDecisionGeneration, decisionRaw, updatedAt, "codex-main", decisionObservationPartial);
324
333
  const displayQuota: MainPolicyQuotaObservation = isMain && mainWriter && policyQuota?.shortWindowAbsent === true
325
334
  ? { ...quota, shortWindowAbsent: true }
326
335
  : quota;
@@ -544,6 +553,7 @@ export function withoutRetiredCodexQuota<T extends Omit<StoredAccountQuota, "upd
544
553
  return snapshotHasUsage(next) || snapshotHasCredits(next) ? next as T : null;
545
554
  }
546
555
 
556
+ /** Parse `x-codex-*` rate-limit headers into a quota observation, binding the raw reset values so decision evidence can keep invalid resets distinct from the display fields. */
547
557
  export function parseUpstreamQuotaHeaders(headers: Headers, options?: { modelId?: string }): Omit<StoredAccountQuota, "updatedAt"> | null {
548
558
  if (isRetiredCodexSparkModel(options?.modelId)) return null;
549
559
  const primaryRaw = headers.get("x-codex-primary-used-percent");
@@ -610,9 +620,15 @@ export function parseUpstreamQuotaHeaders(headers: Headers, options?: { modelId?
610
620
  if (tertiaryResetAt !== undefined) quota.monthlyResetAt = tertiaryResetAt;
611
621
  }
612
622
 
623
+ if (decisionQuotaSinkActive()) bindRawCodexDecisionResets(quota, {
624
+ short: primaryResetRaw,
625
+ weekly: primaryIsMonthly || primaryIsShort || primaryPercent === undefined ? secondaryResetRaw : primaryResetRaw,
626
+ monthly: primaryIsMonthly && primaryPercent !== undefined ? primaryResetRaw : tertiaryResetRaw,
627
+ }, Date.now());
613
628
  return hasKnownQuotaValue(quota) ? quota : null;
614
629
  }
615
630
 
631
+ /** Apply upstream rate-limit headers for an account: publish display and policy quota and, only for a valid observation, the advisory decision evidence. */
616
632
  export function applyAccountQuotaFromUpstreamHeaders(
617
633
  accountId: string,
618
634
  headers: Headers,
@@ -629,7 +645,7 @@ export function applyAccountQuotaFromUpstreamHeaders(
629
645
  .some(name => isInvalidPolicyUsagePercent(headers.get(name)));
630
646
  setAccountQuotaFromParsed(accountId, quota, writerGeneration, mainWriter, policyQuota,
631
647
  options?.poolWriter && validHistory ? { writer: options.poolWriter, observedAt: Date.now(), source: "response-header", raw: quota } : undefined,
632
- options?.poolResponse === true);
648
+ options?.poolResponse === true, true, validHistory ? quota : null);
633
649
  }
634
650
 
635
651
  export function updateAccountQuota(
@@ -822,7 +838,10 @@ function forgetCodexQuotaBaseline(accountId?: string): void {
822
838
  });
823
839
  }
824
840
 
841
+ /** Clear stored quota, advisory decision evidence and history for one account, or for all accounts when no id is given. */
825
842
  export function clearAccountQuota(accountId?: string): void {
843
+ clearDecisionAccountQuotas("codex", accountId);
844
+ if (!accountId || accountId === MAIN_CODEX_ACCOUNT_ID) clearDecisionAccountQuotas("codex-main");
826
845
  if (!accountId || accountId === MAIN_CODEX_ACCOUNT_ID) forgetMainAccountUsage();
827
846
  if (accountId) hydrateAccountQuotasFromDisk();
828
847
  quotaHistory.clear(accountId);
@@ -1009,6 +1028,11 @@ export function parseUsageQuota(data: WhamUsageResponse): Omit<StoredAccountQuot
1009
1028
 
1010
1029
  if (resetCredits !== undefined) quota.resetCredits = resetCredits;
1011
1030
 
1031
+ if (decisionQuotaSinkActive()) bindRawCodexDecisionResets(quota, {
1032
+ short: primaryWindow?.reset_at,
1033
+ weekly: primaryIsMonthly || weeklyCandidatePercent === undefined ? secondaryWindow?.reset_at : primaryWindow?.reset_at,
1034
+ monthly: primaryIsMonthly && primaryPercent !== undefined ? primaryWindow?.reset_at : tertiaryWindow?.reset_at,
1035
+ }, Date.now());
1012
1036
  return hasKnownQuotaValue(quota) || snapshotHasCredits(quota) ? quota : null;
1013
1037
  }
1014
1038
 
@@ -0,0 +1,161 @@
1
+ import { createHash } from "node:crypto";
2
+ import { isAuthorizedProviderExecutedCall, type ProviderExecutedCallType } from "../server/responses-undeclared-tool-guard";
3
+
4
+ // Logical recency avoids clock adjustments and ties between messages in one millisecond.
5
+ let activityOrder = 0;
6
+ const MAX_PENDING_RESULTS = 1024;
7
+ const MAX_RESULT_ID_LENGTH = 1024;
8
+ const resultTypes: Readonly<Record<string, string>> = {
9
+ function_call: "function_call_output",
10
+ custom_tool_call: "custom_tool_call_output",
11
+ local_shell_call: "function_call_output",
12
+ shell_call: "shell_call_output",
13
+ computer_call: "computer_call_output",
14
+ apply_patch_call: "apply_patch_call_output",
15
+ tool_search_call: "tool_search_output",
16
+ mcp_approval_request: "mcp_approval_response",
17
+ };
18
+
19
+ function record(value: unknown): value is Record<string, unknown> {
20
+ return value !== null && typeof value === "object" && !Array.isArray(value);
21
+ }
22
+
23
+ function resultKey(type: string, id: string): string {
24
+ return createHash("sha256").update(JSON.stringify([type, id])).digest("hex");
25
+ }
26
+
27
+ /** Content-free activity owned by one admitted incoming Responses socket. */
28
+ export class CodexWebSocketActivity {
29
+ lastUsed = ++activityOrder;
30
+ closing = false;
31
+ private turns = 0;
32
+ private pendingResults = new Set<string>();
33
+ private returnedResults = new Set<string>();
34
+ // Hashed identities only; null marks a route-authorized provider-owned call.
35
+ private argumentCalls = new Map<string, { type: string; result: string | null }>();
36
+ // Unknown/oversized identities cannot safely prove that client work finished.
37
+ private untrackedResults = false;
38
+
39
+ get idle(): boolean {
40
+ return !this.closing && this.turns === 0 && this.pendingResults.size === 0 && !this.untrackedResults;
41
+ }
42
+
43
+ touch(): void {
44
+ this.lastUsed = ++activityOrder;
45
+ }
46
+
47
+ /** Each turn retains its own busy reference, including superseded async cleanup. */
48
+ beginTurn(input: unknown): () => void {
49
+ this.turns += 1;
50
+ this.touch();
51
+ this.acceptResults(input);
52
+ // Ordinary creates start a new response even when its upstream omits response.created.
53
+ // Prior results settle old waits, but cannot suppress new calls reusing their IDs.
54
+ this.returnedResults.clear();
55
+ this.argumentCalls.clear();
56
+ let finished = false;
57
+ return () => {
58
+ if (finished) return;
59
+ finished = true;
60
+ this.turns -= 1;
61
+ if (this.turns === 0) this.touch();
62
+ };
63
+ }
64
+
65
+ /** Called only after admission or native control accepts the client's results. */
66
+ acceptResults(input: unknown): void {
67
+ if (this.untrackedResults) return;
68
+ if (Array.isArray(input)) {
69
+ for (const item of input) {
70
+ if (!record(item) || typeof item.type !== "string") continue;
71
+ const id = item.type === "mcp_approval_response" ? item.approval_request_id : item.call_id;
72
+ if (typeof id === "string" && id.length <= MAX_RESULT_ID_LENGTH) {
73
+ // Ordinary Codex shell results use function_call_output; native steering
74
+ // also accepts its explicit local_shell_call_output continuation stub.
75
+ const type = item.type === "local_shell_call_output" ? "function_call_output" : item.type;
76
+ const key = resultKey(type, id);
77
+ if (this.pendingResults.delete(key)) {
78
+ if (this.returnedResults.size >= MAX_PENDING_RESULTS) { this.protectUntracked(); return; }
79
+ else this.returnedResults.add(key);
80
+ }
81
+ }
82
+ }
83
+ }
84
+ }
85
+
86
+ /** Preserve tool waits even when the response terminal omits its output array. */
87
+ observePayload(payload: string, providerExecuted?: ReadonlySet<ProviderExecutedCallType>): void {
88
+ if (this.untrackedResults) return;
89
+ let frame: unknown;
90
+ try { frame = JSON.parse(payload); } catch { return; }
91
+ if (!record(frame)) return;
92
+ // Native injection can return a result before the same terminal repeats its call.
93
+ if (frame.type === "response.created") {
94
+ this.returnedResults.clear();
95
+ this.argumentCalls.clear();
96
+ }
97
+ if (frame.type === "response.output_item.added") this.rememberArgumentCall(frame.item, providerExecuted);
98
+ if (frame.type === "response.function_call_arguments.done" || frame.type === "response.custom_tool_call_input.done") {
99
+ const type = frame.type === "response.function_call_arguments.done" ? "function_call" : "custom_tool_call";
100
+ const call = typeof frame.item_id === "string" && frame.item_id.length <= MAX_RESULT_ID_LENGTH
101
+ ? this.argumentCalls.get(resultKey("item", frame.item_id)) : undefined;
102
+ if (!call || call.type !== type) this.protectUntracked();
103
+ else if (call.result !== null) this.observeResultKey(call.result);
104
+ }
105
+ if (frame.type === "response.output_item.done") this.observeOutput(frame.item, providerExecuted);
106
+ if (["response.completed", "response.incomplete", "response.failed"].includes(String(frame.type))
107
+ && record(frame.response) && Array.isArray(frame.response.output)) {
108
+ for (const item of frame.response.output) this.observeOutput(item, providerExecuted);
109
+ }
110
+ }
111
+
112
+ private rememberArgumentCall(item: unknown, providerExecuted?: ReadonlySet<ProviderExecutedCallType>): void {
113
+ if (!record(item) || (item.type !== "function_call" && item.type !== "custom_tool_call")) return;
114
+ if (typeof item.id !== "string" || !item.id.length || item.id.length > MAX_RESULT_ID_LENGTH) return;
115
+ const key = resultKey("item", item.id);
116
+ const hosted = providerExecuted && isAuthorizedProviderExecutedCall(item, providerExecuted);
117
+ if (!hosted && (typeof item.call_id !== "string" || !item.call_id.length || item.call_id.length > MAX_RESULT_ID_LENGTH)) {
118
+ if (this.argumentCalls.has(key)) this.protectUntracked();
119
+ return;
120
+ }
121
+ const result = hosted ? null : resultKey(resultTypes[item.type]!, item.call_id as string);
122
+ const previous = this.argumentCalls.get(key);
123
+ if (previous) {
124
+ if (previous.type !== item.type || previous.result !== result) this.protectUntracked();
125
+ return;
126
+ }
127
+ if (this.argumentCalls.size >= MAX_PENDING_RESULTS) { this.protectUntracked(); return; }
128
+ // An unfinished argument stream alone does not prove the client started work.
129
+ this.argumentCalls.set(key, { type: item.type, result });
130
+ }
131
+
132
+ private observeOutput(item: unknown, providerExecuted?: ReadonlySet<ProviderExecutedCallType>): void {
133
+ if (this.untrackedResults) return;
134
+ if (!record(item) || typeof item.type !== "string") return;
135
+ // Hosted execution may publish both the call and its result in the output stream.
136
+ this.acceptResults([item]);
137
+ if (!Object.hasOwn(resultTypes, item.type)) return;
138
+ if (providerExecuted && isAuthorizedProviderExecutedCall(item, providerExecuted)) return;
139
+ if (item.type === "tool_search_call" && item.execution !== "client") return;
140
+ const id = item.type === "mcp_approval_request" ? item.id
141
+ : item.type === "local_shell_call" || item.type === "tool_search_call" ? item.call_id ?? item.id : item.call_id;
142
+ if (typeof id !== "string" || !id.length || id.length > MAX_RESULT_ID_LENGTH) {
143
+ this.protectUntracked();
144
+ return;
145
+ }
146
+ this.observeResultKey(resultKey(resultTypes[item.type]!, id));
147
+ }
148
+
149
+ private observeResultKey(key: string): void {
150
+ if (this.pendingResults.has(key) || this.returnedResults.has(key)) return;
151
+ if (this.pendingResults.size >= MAX_PENDING_RESULTS) this.protectUntracked();
152
+ else this.pendingResults.add(key);
153
+ }
154
+
155
+ private protectUntracked(): void {
156
+ this.untrackedResults = true;
157
+ this.pendingResults.clear();
158
+ this.returnedResults.clear();
159
+ this.argumentCalls.clear();
160
+ }
161
+ }
@@ -1,23 +1,47 @@
1
1
  import type { ServerWebSocket } from "bun";
2
2
  import type { WsData } from "../server/ws-bridge";
3
3
  import { createAdmissionGate, type AdmissionMetrics, type AdmissionReservation } from "../lib/admission";
4
+ import { CodexWebSocketActivity } from "./websocket-activity";
4
5
 
5
6
  export const MAX_TRACKED_CODEX_WEBSOCKETS = 128;
6
7
  const websocketGate = createAdmissionGate("codex_websockets", MAX_TRACKED_CODEX_WEBSOCKETS);
8
+ const admittedSockets = new Set<ServerWebSocket<WsData>>();
9
+
10
+ function reclaimOldestIdleSocket(): void {
11
+ let oldest: ServerWebSocket<WsData> | undefined;
12
+ for (const ws of admittedSockets) {
13
+ const activity = ws.data.responsesActivity;
14
+ if (!activity?.idle || ws.data.nativeControl || ws.readyState !== 1 || ws.getBufferedAmount() !== 0) continue;
15
+ if (!oldest || activity.lastUsed < oldest.data.responsesActivity!.lastUsed) oldest = ws;
16
+ }
17
+ if (!oldest) return;
18
+ // Fence late frames before closing. Only the actual close callback releases admission.
19
+ oldest.data.responsesActivity!.closing = true;
20
+ try {
21
+ oldest.close(1001, "Idle Responses connection reclaimed");
22
+ } catch {
23
+ // A failed/delayed close never manufactures a free slot or retries another victim.
24
+ if (oldest.readyState === 1) oldest.data.responsesActivity!.closing = false;
25
+ }
26
+ }
7
27
 
8
28
  export function tryReserveCodexWebSocket(): AdmissionReservation<ServerWebSocket<WsData>> | null {
29
+ if (websocketGate.metrics().active >= MAX_TRACKED_CODEX_WEBSOCKETS) reclaimOldestIdleSocket();
9
30
  const gateLease = websocketGate.tryAcquire();
10
31
  if (!gateLease) return null;
11
32
  let socket: ServerWebSocket<WsData> | undefined;
12
33
  let active = true;
13
34
  return {
14
35
  bind(value) {
15
- if (!active) return;
36
+ if (!active || socket) return;
16
37
  socket = value;
38
+ socket.data.responsesActivity = new CodexWebSocketActivity();
39
+ admittedSockets.add(socket);
17
40
  },
18
41
  release() {
19
42
  if (!active) return;
20
43
  active = false;
44
+ if (socket) admittedSockets.delete(socket);
21
45
  socket = undefined;
22
46
  gateLease.release();
23
47
  },
@@ -666,8 +666,9 @@ function isDefiniteContextOverflowMessage(text: string): boolean {
666
666
  * arbitrary field that happens to sit in the first 500 bytes authorize a hop, so it fails closed.
667
667
  *
668
668
  * Only the exact proxy wrapper is unwrapped, within a fixed envelope budget and 16,384 characters.
669
+ * Exported for the sidecar loops, which classify their own provider failures with the same rule.
669
670
  */
670
- function isDefiniteContextOverflow(status: number, message: string): boolean {
671
+ export function isDefiniteContextOverflow(status: number, message: string): boolean {
671
672
  if (!CONTEXT_VERDICT_STATUSES.has(status) && status < 500) return false;
672
673
  if (message.length > 16_384) return false;
673
674
  let text = message.trim();
@@ -1,3 +1,6 @@
1
+ import { boundedJevQuotaPayload, jevQuotaCandidates, withJevQuotaSummary } from "./jev-quota-route";
2
+ import { JEV_QUOTA_INSTRUCTION } from "./jev-quota";
3
+ import { serializeJevModelRequest, type JevModelRequestBody } from "./jev-model-request";
1
4
  import {
2
5
  buildJevState,
3
6
  candidatesFitRequestBounds,
@@ -25,11 +28,14 @@ export interface JevModelInvokeRequest {
25
28
  instructions: string;
26
29
  input: string;
27
30
  signal: AbortSignal;
31
+ /** Budget-only fallback, selected by the detached invoker before admission or dispatch. */
32
+ withoutQuota?: Pick<JevModelRequestBody, "instructions" | "input">;
28
33
  }
29
34
 
30
35
  export interface JevModelInvokeResult {
31
36
  text: string;
32
37
  usage?: Record<string, number>;
38
+ quotaOmitted?: true;
33
39
  }
34
40
 
35
41
  export type JevModelInvoke = (request: JevModelInvokeRequest) => Promise<JevModelInvokeResult>;
@@ -69,29 +75,45 @@ export function parseJevModelChoice(text: string, allowed: ReadonlySet<string>):
69
75
  return choice;
70
76
  }
71
77
 
78
+ /** Resolve a JEV decision through the decision model, falling back with a gate reason on any failure; quota tiers are advisory input only and never widen the allowlist. */
72
79
  export async function resolveJevModelDecision(
73
80
  options: ResolveJevDecisionOptions & { decisionModel: string; invokeModel: JevModelInvoke },
74
81
  ): Promise<JevDecision> {
75
82
  const now = options.now ?? Date.now;
76
83
  const startedAt = now();
84
+ let candidates = options.candidates;
85
+ /** Build the fallback decision for a failure gate, attaching the quota summary for the candidates in scope. */
77
86
  const failed = (gate: Exclude<JevDecision["gate"], "apply">): JevDecision =>
78
- fallbackDecision(options.fallback, gate, Math.max(0, now() - startedAt), "model");
87
+ withJevQuotaSummary(fallbackDecision(options.fallback, gate, Math.max(0, now() - startedAt), "model"), candidates);
79
88
 
80
89
  if (options.signal?.aborted) throw options.signal.reason;
81
90
  if (options.candidates.length === 0) return failed("no_choices");
82
91
  if (!candidatesFitRequestBounds(options.candidates)) return failed("invalid");
83
92
 
84
93
  let routeOptions: JevRouteOptionDescriptor[];
85
- let input: string;
94
+ let input = "";
95
+ let instructions = JEV_MODEL_INSTRUCTIONS;
96
+ let withoutQuota: JevModelInvokeRequest["withoutQuota"];
86
97
  try {
87
98
  routeOptions = jevRouteOptions(options.candidates);
88
99
  if (routeOptions.length > JEV_MODEL_MAX_OPTIONS) return failed("invalid");
89
100
  const state = buildJevState(options.body, options.candidates);
90
101
  if (!hasJevDecisionState(state)) return failed("no_state");
91
- input = buildJevModelPrompt(state, options.candidates);
92
- if (new TextEncoder().encode(JEV_MODEL_INSTRUCTIONS + input).byteLength > JEV_MAX_REQUEST_BYTES) {
102
+ const payload = boundedJevQuotaPayload(jevQuotaCandidates(options), rows => {
103
+ instructions = JEV_MODEL_INSTRUCTIONS + (rows.some(row => row.quota) ? " " + JEV_QUOTA_INSTRUCTION : "");
104
+ input = buildJevModelPrompt(state, rows);
105
+ if (options.decisionQuotaSignals !== true) return instructions + input;
106
+ const request = { model: options.decisionModel, instructions, input };
107
+ return serializeJevModelRequest(request);
108
+ }, JEV_MAX_REQUEST_BYTES);
109
+ candidates = payload.candidates;
110
+ if (new TextEncoder().encode(payload.body).byteLength > JEV_MAX_REQUEST_BYTES) {
93
111
  return failed("invalid");
94
112
  }
113
+ if (candidates.some(row => row.quota)) {
114
+ withoutQuota = { instructions: JEV_MODEL_INSTRUCTIONS,
115
+ input: buildJevModelPrompt(state, candidates.map(({ quota: _quota, ...row }) => row)) };
116
+ }
95
117
  } catch {
96
118
  return failed("invalid");
97
119
  }
@@ -99,7 +121,8 @@ export async function resolveJevModelDecision(
99
121
  const timeoutSignal = AbortSignal.timeout(jevDecisionTimeoutMs(options.timeoutMs));
100
122
  const signal = options.signal ? AbortSignal.any([options.signal, timeoutSignal]) : timeoutSignal;
101
123
  try {
102
- const result = await options.invokeModel({ model: options.decisionModel, instructions: JEV_MODEL_INSTRUCTIONS, input, signal });
124
+ const result = await options.invokeModel({ model: options.decisionModel, instructions, input, signal, ...(withoutQuota ? { withoutQuota } : {}) });
125
+ if (result.quotaOmitted) candidates = candidates.map(({ quota: _quota, ...row }) => row);
103
126
  if (options.signal?.aborted) throw options.signal.reason;
104
127
  if (timeoutSignal.aborted) return failed("timeout");
105
128
  let choice: string;
@@ -111,14 +134,14 @@ export async function resolveJevModelDecision(
111
134
  const selected = routeOptions.find(option => option.key === choice)!;
112
135
  const usage = jevUsage({ usage: result.usage });
113
136
  if (options.signal?.aborted) throw options.signal.reason;
114
- return {
137
+ return withJevQuotaSummary({
115
138
  backend: "model",
116
139
  targetKey: selected.targetKey,
117
140
  effort: selected.effort,
118
141
  gate: "apply",
119
142
  latencyMs: Math.max(0, now() - startedAt),
120
143
  ...(usage ? { usage } : {}),
121
- };
144
+ }, candidates);
122
145
  } catch (error) {
123
146
  if (options.signal?.aborted) throw options.signal.reason;
124
147
  if (timeoutSignal.aborted || (error instanceof Error && error.name === "TimeoutError")) return failed("timeout");
@@ -0,0 +1,20 @@
1
+ /** The exact detached Responses envelope, shared by quota sizing and the invoker. */
2
+ export const JEV_MODEL_MAX_OUTPUT_TOKENS = 1024;
3
+ export interface JevModelRequestBody {
4
+ model: string;
5
+ instructions: string;
6
+ input: string;
7
+ }
8
+ /** Serialize the streaming decision-model request body, adding the reasoning effort only when one is given. */
9
+ export function serializeJevModelRequest(request: JevModelRequestBody, effort?: string): string {
10
+ return JSON.stringify({
11
+ model: request.model,
12
+ stream: true,
13
+ store: false,
14
+ instructions: request.instructions,
15
+ input: [{ role: "user", content: [{ type: "input_text", text: request.input }] }],
16
+ tools: [],
17
+ max_output_tokens: JEV_MODEL_MAX_OUTPUT_TOKENS,
18
+ ...(effort ? { reasoning: { effort } } : {}),
19
+ });
20
+ }