@bitkyc08/opencodex 2.79.0 → 2.81.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 (245) hide show
  1. package/README.md +3 -3
  2. package/gui/dist/assets/App-7yhoys8a.js +54 -0
  3. package/gui/dist/assets/Tray-EwAOsP0S.js +1 -0
  4. package/gui/dist/assets/index-RsoaAxXY.js +94 -0
  5. package/gui/dist/assets/quota-summary-D2lN8tDm.js +1 -0
  6. package/gui/dist/index.html +1 -1
  7. package/gui/dist/provider-icons/claude-green.svg +1 -0
  8. package/package.json +3 -2
  9. package/src/adapters/anthropic-model-contract.ts +8 -2
  10. package/src/adapters/anthropic.ts +7 -2
  11. package/src/adapters/codebuddy/mcp-server.ts +2 -25
  12. package/src/adapters/codebuddy/tool-bridge.ts +1 -7
  13. package/src/adapters/coding-agent/mcp-server.ts +33 -0
  14. package/src/adapters/coding-agent/protocol.ts +122 -17
  15. package/src/adapters/coding-agent/tool-catalog.ts +75 -0
  16. package/src/adapters/coding-agent/turn.ts +101 -44
  17. package/src/adapters/cursor/catalog.ts +8 -0
  18. package/src/adapters/cursor/effort-map.ts +2 -0
  19. package/src/adapters/cursor/live-transport.ts +5 -3
  20. package/src/adapters/cursor/native-exec-fs.ts +12 -9
  21. package/src/adapters/cursor/native-exec.ts +13 -5
  22. package/src/adapters/cursor/protobuf-request.ts +13 -7
  23. package/src/adapters/cursor/request-builder.ts +23 -10
  24. package/src/adapters/cursor/tool-definitions.ts +9 -6
  25. package/src/adapters/cursor/tool-guidance.ts +29 -8
  26. package/src/adapters/cursor/tool-naming.ts +40 -7
  27. package/src/adapters/cursor/tool-wording.ts +46 -0
  28. package/src/adapters/cursor.ts +3 -0
  29. package/src/adapters/devin/live-models.ts +2 -0
  30. package/src/adapters/devin.ts +25 -11
  31. package/src/adapters/kiro-retry.ts +9 -4
  32. package/src/adapters/openai-chat/messages.ts +7 -4
  33. package/src/adapters/physical-send.ts +8 -5
  34. package/src/adapters/qoder/adapter.ts +41 -6
  35. package/src/adapters/qoder/scaffold-guard.ts +2 -3
  36. package/src/adapters/zed.ts +16 -1
  37. package/src/claude/inbound-cache-stabilize.ts +4 -9
  38. package/src/claude/inbound.ts +10 -0
  39. package/src/claude/intercept/model-bindings.ts +1 -0
  40. package/src/claude/intercept/picker-listener.ts +71 -4
  41. package/src/cli/account-anthropic-threshold.ts +14 -5
  42. package/src/cli/account-api.ts +3 -1
  43. package/src/cli/account-extended.ts +13 -9
  44. package/src/cli/account-policy-dto.ts +1 -1
  45. package/src/cli/account-policy.ts +13 -4
  46. package/src/cli/account.ts +3 -3
  47. package/src/cli/capabilities-accounts.ts +2 -2
  48. package/src/cli/capabilities-agents-routing.ts +24 -0
  49. package/src/cli/capabilities-integrations.ts +16 -0
  50. package/src/cli/codex-shim-autorestore.ts +3 -0
  51. package/src/cli/dispatch.ts +10 -1
  52. package/src/cli/help.ts +4 -1
  53. package/src/cli/index.ts +11 -5
  54. package/src/cli/integration-plan-dto.ts +1 -0
  55. package/src/cli/integration-preview.ts +8 -1
  56. package/src/cli/integrations.ts +32 -3
  57. package/src/cli/message-args.ts +34 -0
  58. package/src/cli/message-command.ts +43 -0
  59. package/src/cli/message-runtime.ts +6 -0
  60. package/src/cli/observe.ts +3 -1
  61. package/src/cli/provider.ts +29 -6
  62. package/src/cli/registry.ts +24 -2
  63. package/src/clients/config-export/commandcode.ts +131 -0
  64. package/src/clients/config-export/contracts.ts +12 -0
  65. package/src/clients/config-export.ts +64 -1
  66. package/src/codex/auth-api/account-list.ts +11 -3
  67. package/src/codex/auth-api/main-account-probe.ts +25 -8
  68. package/src/codex/auth-api/pool-mode-gate.ts +33 -9
  69. package/src/codex/auth-api/pool-quota-probe.ts +15 -1
  70. package/src/codex/auth-api/routes.ts +1 -0
  71. package/src/codex/autostart-health.ts +3 -3
  72. package/src/codex/catalog/auto-review.ts +13 -6
  73. package/src/codex/catalog/build-entries.ts +15 -7
  74. package/src/codex/catalog/control-plane.ts +45 -0
  75. package/src/codex/catalog/effort.ts +11 -1
  76. package/src/codex/catalog/gather-capture.ts +5 -3
  77. package/src/codex/catalog/parsing.ts +2 -1
  78. package/src/codex/catalog/provider-models.ts +15 -9
  79. package/src/codex/control-plane-models.ts +6 -0
  80. package/src/codex/journal.ts +36 -5
  81. package/src/codex/shim-overlay.ts +9 -1
  82. package/src/combos/jev-decision-contract.ts +25 -2
  83. package/src/combos/jev.ts +6 -3
  84. package/src/combos/types.ts +1 -1
  85. package/src/config/diagnostics.ts +27 -16
  86. package/src/config/load-degrade.ts +34 -0
  87. package/src/config/schema/anthropic-account-pool.ts +143 -0
  88. package/src/config/schema/config-schema.ts +7 -1
  89. package/src/config/schema/leaf-validators.ts +4 -0
  90. package/src/config.ts +2 -0
  91. package/src/generated/compatibility-version.json +347 -199
  92. package/src/generated/model-metadata.ts +5 -4
  93. package/src/images/loop.ts +2 -1
  94. package/src/integrations/catalog-refresh.ts +1 -1
  95. package/src/integrations/mutation-plan.ts +7 -0
  96. package/src/integrations/registry.ts +8 -0
  97. package/src/integrations/state.ts +9 -2
  98. package/src/integrations/writer.ts +8 -2
  99. package/src/lib/lab-live-route-production.ts +9 -1
  100. package/src/lib/request-execution-budget.ts +236 -58
  101. package/src/lib/spend-pool-alias-validation.ts +79 -0
  102. package/src/lib/spend-pool-continuity.ts +34 -0
  103. package/src/lib/spend-reservation-ledger.ts +412 -50
  104. package/src/lib/state-store-registrations.ts +20 -3
  105. package/src/lib/upstream-retry.ts +17 -5
  106. package/src/lib/workflow-budget.ts +53 -11
  107. package/src/messaging/budget.ts +45 -0
  108. package/src/messaging/discovery.ts +57 -0
  109. package/src/messaging/envelope.ts +49 -0
  110. package/src/messaging/input.ts +37 -0
  111. package/src/messaging/rpc.ts +196 -0
  112. package/src/messaging/send.ts +58 -0
  113. package/src/messaging/socket.ts +72 -0
  114. package/src/messaging/types.ts +29 -0
  115. package/src/oauth/anthropic-account-refusal.ts +51 -17
  116. package/src/oauth/anthropic-account-threshold.ts +8 -2
  117. package/src/oauth/anthropic-continuity.ts +15 -1
  118. package/src/oauth/anthropic-model-quota.ts +120 -48
  119. package/src/oauth/anthropic-model-routes.ts +11 -3
  120. package/src/oauth/anthropic-oauth-definitions.ts +98 -0
  121. package/src/oauth/anthropic-pool-config.ts +30 -0
  122. package/src/oauth/anthropic-rate-limit-policy.ts +64 -9
  123. package/src/oauth/anthropic-routing.ts +1202 -866
  124. package/src/oauth/anthropic-send-ownership.ts +41 -0
  125. package/src/oauth/anthropic.ts +4 -1
  126. package/src/oauth/generic-account-failover.ts +3 -1
  127. package/src/oauth/health.ts +8 -6
  128. package/src/oauth/index.ts +90 -173
  129. package/src/oauth/kiro.ts +22 -9
  130. package/src/oauth/login-cli.ts +2 -0
  131. package/src/oauth/model-discovery-auth.ts +43 -0
  132. package/src/oauth/pool-kernel.ts +11 -5
  133. package/src/oauth/pool-settings-capability.ts +5 -3
  134. package/src/oauth/store-anthropic-instance.ts +87 -0
  135. package/src/oauth/store.ts +67 -9
  136. package/src/oauth/token-guardian.ts +2 -0
  137. package/src/oauth/types.ts +2 -0
  138. package/src/protocols/plan-snapshot.ts +5 -0
  139. package/src/protocols/settings.ts +20 -5
  140. package/src/providers/anthropic-instance-id.ts +47 -0
  141. package/src/providers/anthropic-instance.ts +48 -0
  142. package/src/providers/anthropic-reset-grant-ledger.ts +11 -1
  143. package/src/providers/derive.ts +2 -0
  144. package/src/providers/kiro-models.ts +3 -0
  145. package/src/providers/label.ts +1 -1
  146. package/src/providers/model-rename-fields.ts +2 -0
  147. package/src/providers/quota/account-cache.ts +37 -11
  148. package/src/providers/quota/anthropic-account-quota.ts +93 -0
  149. package/src/providers/quota/anthropic-cooldown-recovery.ts +222 -111
  150. package/src/providers/quota/report-cache.ts +24 -1
  151. package/src/providers/quota/vendor-probes-oauth.ts +46 -14
  152. package/src/providers/quota.ts +22 -32
  153. package/src/providers/registry/entries-core.ts +35 -30
  154. package/src/providers/registry/model-ids.ts +1 -0
  155. package/src/providers/registry/model-seeds.ts +7 -4
  156. package/src/providers/registry/types.ts +3 -0
  157. package/src/providers/registry.ts +3 -3
  158. package/src/responses/spill-store.ts +5 -1
  159. package/src/responses/state/metrics.ts +87 -0
  160. package/src/responses/state/snapshot-codec.ts +6 -0
  161. package/src/responses/state/spill-queue.ts +1 -0
  162. package/src/responses/state/unforced-store-false.ts +55 -0
  163. package/src/responses/state.ts +50 -66
  164. package/src/router.ts +28 -4
  165. package/src/routing/compatibility/assemble.ts +1 -1
  166. package/src/routing/quota.ts +7 -3
  167. package/src/server/auth-cors.ts +5 -0
  168. package/src/server/chat-completions.ts +6 -1
  169. package/src/server/chat-native.ts +45 -25
  170. package/src/server/claude-messages.ts +16 -2
  171. package/src/server/gui-static.ts +38 -4
  172. package/src/server/index/spend-ledger-lifecycle.ts +10 -4
  173. package/src/server/index.ts +16 -16
  174. package/src/server/inference/context.ts +1 -1
  175. package/src/server/lifecycle.ts +1 -1
  176. package/src/server/management/agent-settings-routes.ts +30 -6
  177. package/src/server/management/anthropic-account-threshold.ts +12 -9
  178. package/src/server/management/anthropic-pool-settings.ts +72 -0
  179. package/src/server/management/anthropic-reset-grant-routes.ts +98 -39
  180. package/src/server/management/config-routes.ts +27 -7
  181. package/src/server/management/decision-routes.ts +2 -1
  182. package/src/server/management/integration-routes.ts +25 -2
  183. package/src/server/management/oauth-account-routes.ts +79 -98
  184. package/src/server/management/provider-instance-ownership.ts +44 -0
  185. package/src/server/management/provider-routes.ts +21 -28
  186. package/src/server/management/route-registry.ts +3 -1
  187. package/src/server/management/routing-profile-routes.ts +4 -4
  188. package/src/server/management/system-restart.ts +44 -14
  189. package/src/server/management/system-routes.ts +24 -2
  190. package/src/server/management/vision-sidecar-options.ts +9 -6
  191. package/src/server/management/web-search-sidecar-options.ts +51 -2
  192. package/src/server/management-auth.ts +6 -0
  193. package/src/server/messages-native-eligibility.ts +3 -2
  194. package/src/server/messages-native-oauth.ts +86 -41
  195. package/src/server/messages-native-scope.ts +4 -1
  196. package/src/server/messages-native-selector.ts +39 -0
  197. package/src/server/messages-native.ts +84 -25
  198. package/src/server/request-log.ts +5 -0
  199. package/src/server/responses/adapter-continuation.ts +29 -17
  200. package/src/server/responses/adapter-dispatch.ts +47 -17
  201. package/src/server/responses/antigravity-validation-refusal.ts +20 -6
  202. package/src/server/responses/combo-requested-effort.ts +14 -0
  203. package/src/server/responses/compact.ts +25 -8
  204. package/src/server/responses/compaction-recovery.ts +1 -1
  205. package/src/server/responses/core-combo-native.ts +4 -1
  206. package/src/server/responses/core-combo.ts +21 -18
  207. package/src/server/responses/core-normalize.ts +1 -0
  208. package/src/server/responses/core-options.ts +3 -1
  209. package/src/server/responses/core-replay.ts +5 -0
  210. package/src/server/responses/passthrough-delivery.ts +7 -3
  211. package/src/server/responses/passthrough-dispatch.ts +21 -8
  212. package/src/server/responses/request-prepare.ts +37 -5
  213. package/src/server/responses/request-send-budget.ts +74 -37
  214. package/src/server/responses/request-spend.ts +325 -33
  215. package/src/server/responses/request-transport.ts +132 -52
  216. package/src/server/responses/run-turn-execution.ts +11 -3
  217. package/src/server/responses/sidecar-execution.ts +96 -14
  218. package/src/server/responses-undeclared-tool-guard.ts +5 -1
  219. package/src/server/workflow-refusal.ts +30 -0
  220. package/src/service/desktop-startup.ts +100 -3
  221. package/src/service/ownership-mutation-lease.d.mts +1 -1
  222. package/src/service/ownership-mutation-lease.mjs +5 -7
  223. package/src/sidecar/anthropic-binding.ts +79 -0
  224. package/src/sidecar/auth.ts +76 -2
  225. package/src/types/anthropic-account-pool.ts +33 -0
  226. package/src/types/config.ts +12 -28
  227. package/src/types/provider.ts +5 -0
  228. package/src/types/tools.ts +7 -0
  229. package/src/types.ts +4 -0
  230. package/src/usage/cost.ts +3 -2
  231. package/src/usage/expected-prices.ts +66 -14
  232. package/src/vision/anthropic-describe.ts +16 -3
  233. package/src/vision/index.ts +3 -0
  234. package/src/vision/plan.ts +19 -6
  235. package/src/web-search/alpha-search.ts +7 -1
  236. package/src/web-search/anthropic-executor.ts +16 -4
  237. package/src/web-search/header-deadline.ts +67 -0
  238. package/src/web-search/index.ts +7 -3
  239. package/src/web-search/loop.ts +32 -10
  240. package/src/web-search/passthrough-bridge.ts +4 -1
  241. package/src/web-search/sidecar-providers.ts +4 -3
  242. package/gui/dist/assets/App-BOUa-ek3.js +0 -54
  243. package/gui/dist/assets/Tray-DUtzB4zF.js +0 -1
  244. package/gui/dist/assets/index-DwHNDF4s.js +0 -94
  245. package/gui/dist/assets/quota-summary-BuL4WIBo.js +0 -1
@@ -1,4 +1,6 @@
1
1
  import { listCodexAuthAccountsSnapshot } from "../codex/auth-api";
2
+ import { isAnthropicInstanceId, configuredAnthropicInstance } from "./anthropic-instance";
3
+ import { fetchAnthropicAccountQuota } from "./quota/anthropic-account-quota";
2
4
  import { resolveEnvValue } from "../config";
3
5
  import { getAccountCredential, getAccountSet } from "../oauth/store";
4
6
  import { apiKeyPoolEntryId } from "./api-keys";
@@ -18,6 +20,7 @@ import {
18
20
  accountReportCurrent,
19
21
  AUTHORITATIVE_EMPTY_QUOTA,
20
22
  bumpProviderQuotaInvalidationEpoch,
23
+ captureProviderReportEpochs, providerReportEpochChanged,
21
24
  cacheKeyWithAggregationState,
22
25
  getProviderQuotaReportCache,
23
26
  hasCodexPoolProvider,
@@ -56,7 +59,6 @@ import {
56
59
  } from "./quota/account-cache";
57
60
  import {
58
61
  fetchAnthropicQuota,
59
- fetchAnthropicUsageQuota,
60
62
  fetchChatGptForwardQuota,
61
63
  fetchCursorQuota,
62
64
  fetchKiroQuota,
@@ -68,11 +70,10 @@ import { fetchCommandCodeQuota, fetchKimiQuota, keyQuotaReaderForProvider } from
68
70
  import { antigravityQuotaDiagnosticIdentity, fetchAntigravityQuota, probeAntigravityUsageQuota } from "./quota/antigravity";
69
71
  import { persistKiroAccountState } from "./kiro-account-state-disk";
70
72
  import { kiroProbeCurrent, kiroProbeIdentity } from "./quota/kiro-account-probe";
71
- import { AnthropicQuotaProbeOwnershipError, anthropicCooldownFlightKey, assertAnthropicQuotaSendAllowed, probeAnthropicQuotaWithRecovery } from "./quota/anthropic-cooldown-recovery";
72
73
  export type { ProviderQuota, ProviderQuotaCreditsUsd, ProviderQuotaWindow } from "./quota-types";
73
74
  export { QUOTA_RESPONSE_MAX_BYTES } from "./quota-wire";
74
75
  export {
75
- clearProviderQuotaCache,
76
+ clearProviderQuotaCache, clearProviderQuotaCacheFor,
76
77
  publishKeyReportForTests,
77
78
  readProviderQuotaJsonForTests,
78
79
  setProviderQuotaBeforePublishForTests,
@@ -86,7 +87,7 @@ export {
86
87
  parseAnthropicRateLimitHeaders,
87
88
  providerOAuthAccountQuotaMode,
88
89
  readPassiveProviderAccountQuotas,
89
- recordAnthropicAccountQuotaFromHeaders,
90
+ recordAnthropicAccountQuotaFromHeaders, recordAnthropicAccountQuotaFromHeadersForInstance,
90
91
  recordPassiveAccountQuota,
91
92
  reconcileProviderAccountQuotaRows,
92
93
  resetProviderQuotaReconcileStateForTests,
@@ -121,7 +122,7 @@ async function maybeFetchProviderQuota(
121
122
  return fetchChatGptForwardQuota(config, name, provider, forceRefresh, prefetchedCodexSnapshot);
122
123
  }
123
124
  if (provider.authMode === "oauth" && explicitAccountReader(name)) return await fetchExplicitCurrentQuota(name, provider, config);
124
- if (provider.authMode === "oauth" && name === "anthropic") return fetchAnthropicQuota(name);
125
+ if (provider.authMode === "oauth" && configuredAnthropicInstance(config, name)) return fetchAnthropicQuota(name, config);
125
126
  if (provider.authMode === "oauth" && name === "google-antigravity") return await fetchAntigravityQuota(name);
126
127
  if (provider.authMode === "oauth" && name === "kiro") return fetchKiroQuota(name);
127
128
  // meta-muse: a device-logged-in account can be probed at the key endpoint; an
@@ -236,6 +237,7 @@ export async function fetchProviderQuotaReports(config: OcxConfig, forceRefresh
236
237
  const keyCandidate = cacheKeyWithAggregationState(config, prefetchedCodexSnapshot);
237
238
  const key = typeof keyCandidate === "string" ? keyCandidate : await keyCandidate;
238
239
  const writerGeneration = captureConfigGeneration();
240
+ const providerEpochs = captureProviderReportEpochs();
239
241
  const now = Date.now();
240
242
  // The cache fast path must not extend a preserved last-good row past its 30-minute bound:
241
243
  // a row preserved at age 29:59 plus a full 5-minute TTL would otherwise serve until ~35min.
@@ -312,6 +314,8 @@ export async function fetchProviderQuotaReports(config: OcxConfig, forceRefresh
312
314
  byProvider.delete(provider);
313
315
  generationMismatchedProviders.delete(provider);
314
316
  }
317
+ // A provider-scoped clear during this probe retires only that provider's row.
318
+ for (const provider of [...byProvider.keys()]) if (providerReportEpochChanged(providerEpochs, provider)) byProvider.delete(provider);
315
319
 
316
320
  const response = { generatedAt: Date.now(), reports: [...byProvider.values()] };
317
321
  // Commit only when this probe still holds authority (no clear/force superseded it).
@@ -415,27 +419,27 @@ async function fetchAccountQuota(
415
419
  accountId: string,
416
420
  forceRefresh: boolean,
417
421
  providerConfig?: OcxProviderConfig,
422
+ liveConfig?: OcxConfig,
418
423
  ): Promise<AccountQuotaCacheEntry> {
419
424
  if (accountQuotaProbeSkip(provider, accountId)) return accountQuotaProbeSkip(provider, accountId)!;
425
+ if (isAnthropicInstanceId(provider)) return fetchAnthropicAccountQuota(provider, accountId, forceRefresh, liveConfig);
420
426
  if (explicitAccountReader(provider)) return fetchExplicitAccountQuota(provider, accountId, forceRefresh, providerConfig);
421
- if (provider === "anthropic" || provider === "kiro") hydrateAccountQuotaCache();
427
+ if (provider === "kiro") hydrateAccountQuotaCache();
422
428
  const key = accountCacheKey(provider, accountId);
423
429
  const writerGeneration = captureConfigGeneration();
424
430
  const kiroIdentity = provider === "kiro" ? kiroProbeIdentity(accountId) : undefined;
425
431
  const cachedCandidate = accountQuotaCache.get(key);
426
- const cached = (provider !== "kiro" || cachedCandidate?.identity === kiroIdentity)
427
- && (provider !== "anthropic" || cachedCandidate?.isCurrent?.() !== false) ? cachedCandidate : undefined;
432
+ const cached = provider !== "kiro" || cachedCandidate?.identity === kiroIdentity ? cachedCandidate : undefined;
428
433
  if (!forceRefresh && cached && Date.now() - cached.ts < ACCOUNT_QUOTA_TTL_MS) {
429
434
  if (provider === "google-antigravity" && cached.quotaFailure && cached.quotaFailureIsCurrent?.() !== true) return { ...cached, quotaFailure: undefined };
430
- return provider === "anthropic" ? { ...cached, quota: normalizeAnthropicQuota(cached.quota, Date.now()) } : cached;
435
+ return cached;
431
436
  }
432
- const flightKey = provider === "anthropic" ? anthropicCooldownFlightKey(key, accountId) : key;
433
- const flightCurrent = () => provider !== "anthropic" || anthropicCooldownFlightKey(key, accountId) === flightKey;
437
+ const flightKey = key;
434
438
  const joinable = accountQuotaInflight.get(flightKey);
435
439
  if (joinable) {
436
440
  const joined = await joinable;
437
441
  const joinedCurrent = joined.isCurrent?.();
438
- if (joinedCurrent === false || (!flightCurrent() && joinedCurrent !== true)) return { ts: Date.now(), quota: null, unavailable: true };
442
+ if (joinedCurrent === false) return { ts: Date.now(), quota: null, unavailable: true };
439
443
  return provider !== "kiro" || joined.identity === kiroIdentity
440
444
  ? joined : fetchAccountQuota(provider, accountId, true, providerConfig);
441
445
  }
@@ -449,14 +453,10 @@ async function fetchAccountQuota(
449
453
  };
450
454
  const diagnosticFields = () => quotaFailure && quotaFailureIsCurrent() ? { quotaFailure, quotaFailureIsCurrent } : {};
451
455
  const unavailable = (): AccountQuotaCacheEntry => {
452
- if (!flightCurrent()) return { ts: Date.now(), quota: null, unavailable: true };
453
- const previous = accountQuotaCache.get(key);
454
- const retained = provider === "anthropic" && previous?.isCurrent?.() === false ? undefined : previous;
455
456
  const entry: AccountQuotaCacheEntry = {
456
457
  ts: Date.now(),
457
- quota: provider === "anthropic" ? normalizeAnthropicQuota(retained?.quota, Date.now()) : cached?.quota ?? null,
458
+ quota: cached?.quota ?? null,
458
459
  unavailable: true,
459
- ...(provider === "anthropic" && retained?.isCurrent ? { isCurrent: retained.isCurrent } : {}),
460
460
  ...(provider === "kiro" ? { identity: kiroIdentity } : {}),
461
461
  ...diagnosticFields(),
462
462
  };
@@ -470,14 +470,12 @@ async function fetchAccountQuota(
470
470
  try {
471
471
  if (provider === "google-antigravity") diagnosticIdentity = antigravityQuotaDiagnosticIdentity(accountId);
472
472
  let quota: ProviderQuota | null;
473
- let anthropicCurrent: (() => boolean) | undefined;
474
473
  let kiroSnapshot: KiroUsageSnapshot | null = null;
475
474
  if (provider === "kiro") {
476
475
  kiroSnapshot = await fetchKiroUsageSnapshot(await kiroUsageContextForAccount(accountId));
477
476
  quota = kiroSnapshot?.quota ?? null;
478
477
  } else {
479
478
  const token = await getTokenForAccountQuotaProbe(provider, accountId);
480
- if (!flightCurrent()) throw new AnthropicQuotaProbeOwnershipError("anthropic quota flight is stale");
481
479
  if (provider === "google-antigravity") {
482
480
  // Per-account Gem/Cla windows (#1082). The project id is part of the stored
483
481
  // credential; without it the probe cannot be made, and that is "unavailable",
@@ -488,22 +486,14 @@ async function fetchAccountQuota(
488
486
  const result = await probeAntigravityUsageQuota(token, credential.projectId);
489
487
  quota = result.kind === "available" ? result.quota : null;
490
488
  if (result.kind === "unavailable") quotaFailure = result.failure;
491
- } else if (provider === "anthropic") {
492
- const result = await probeAnthropicQuotaWithRecovery(accountId, token,
493
- fresh => { assertAnthropicQuotaSendAllowed(accountId, token); return fetchAnthropicUsageQuota(token, fresh); },
494
- () => mayCommitAccountQuotaKey(key, writerGeneration));
495
- if (result && !result.isCurrent()) throw new AnthropicQuotaProbeOwnershipError("anthropic quota probe lost publication ownership");
496
- quota = result?.quota ?? null;
497
- anthropicCurrent = result?.isCurrent;
498
489
  } else {
499
490
  return { ts: Date.now(), quota: null, unavailable: true };
500
491
  }
501
492
  }
502
493
  if (!quota) return unavailable();
503
494
  const entry: AccountQuotaCacheEntry = {
504
- ts: Date.now(), quota: provider === "anthropic" ? normalizeAnthropicQuota(quota, Date.now()) : quota,
495
+ ts: Date.now(), quota,
505
496
  ...(provider === "kiro" ? { identity: kiroIdentity } : {}),
506
- ...(anthropicCurrent ? { isCurrent: anthropicCurrent } : {}),
507
497
  };
508
498
  if (mayCommitAccountQuotaKey(key, writerGeneration)
509
499
  && (provider !== "kiro" || kiroProbeCurrent(accountId, kiroIdentity))) {
@@ -513,7 +503,6 @@ async function fetchAccountQuota(
513
503
  }
514
504
  return entry;
515
505
  } catch (error) {
516
- if (provider === "anthropic" && error instanceof AnthropicQuotaProbeOwnershipError) return { ts: Date.now(), quota: null, unavailable: true };
517
506
  if (provider === "google-antigravity") quotaFailure = "account_unavailable";
518
507
  return unavailable();
519
508
  }
@@ -531,20 +520,21 @@ export async function fetchProviderAccountQuotas(
531
520
  provider: string,
532
521
  forceRefresh = false,
533
522
  providerConfig?: OcxProviderConfig,
523
+ liveConfig?: OcxConfig,
534
524
  ): Promise<ProviderAccountQuota[]> {
535
525
  if (!supportsPerAccountQuota(provider)) return [];
536
526
  const set = getAccountSet(provider);
537
527
  if (!set) return [];
538
528
  return mapQuotaRoster(set.accounts, async account => {
539
- const entry = await fetchAccountQuota(provider, account.id, forceRefresh, providerConfig);
529
+ const entry = await fetchAccountQuota(provider, account.id, forceRefresh, providerConfig, liveConfig);
540
530
  const result: ProviderAccountQuota = {
541
531
  accountId: account.id,
542
- quota: provider === "anthropic" ? normalizeAnthropicQuota(entry.quota, Date.now()) : entry.quota,
532
+ quota: isAnthropicInstanceId(provider) ? normalizeAnthropicQuota(entry.quota, Date.now()) : entry.quota,
543
533
  ...(entry.unavailable ? { unavailable: true as const } : {}),
544
534
  ...(entry.unavailable && entry.quotaFailure && entry.quotaFailureIsCurrent?.() === true ? { quotaFailure: entry.quotaFailure } : {}),
545
535
  };
546
536
  if (entry.quotaFailureIsCurrent) Object.defineProperty(result, "quotaFailureIsCurrent", { value: entry.quotaFailureIsCurrent });
547
- if (provider === "anthropic" && entry.isCurrent) Object.defineProperty(result, "isCurrent", { value: entry.isCurrent });
537
+ if (isAnthropicInstanceId(provider) && entry.isCurrent) Object.defineProperty(result, "isCurrent", { value: entry.isCurrent });
548
538
  if (!explicitAccountReader(provider)) return result;
549
539
  const identity = entry.identity;
550
540
  Object.defineProperty(result, "isCurrent", { value: () => {
@@ -14,6 +14,7 @@ import { cursorFastCapableBases } from "../../adapters/cursor/catalog";
14
14
  import { COMMAND_CODE_MODEL_REASONING_EFFORTS } from "../command-code-efforts";
15
15
  import { isCanonicalOpenRouterTarget } from "../openrouter-routing";
16
16
  import type { ProviderRegistryEntry } from "./types";
17
+ import type { AnthropicInstanceId } from "../anthropic-instance-id";
17
18
  import { ANTHROPIC_FAST_MODE_BETA } from "../anthropic-fast";
18
19
  import {
19
20
  ANTHROPIC_MODELS,
@@ -109,6 +110,37 @@ const ANTHROPIC_FAST_MODELS: Readonly<Record<string, boolean>> = Object.freeze({
109
110
  const ANTHROPIC_FAST_TIER_DESCRIPTION =
110
111
  "Claude fast mode: faster output at 2x price; needs usage credits (subscription) or fast-mode access (API)";
111
112
 
113
+ /** Each instance owns its entire seed, including nested model lists and Fast wire maps. */
114
+ function anthropicOAuthEntry(id: AnthropicInstanceId, label: string): ProviderRegistryEntry {
115
+ return structuredClone<ProviderRegistryEntry>({
116
+ id,
117
+ label,
118
+ adapter: "anthropic",
119
+ baseUrl: "https://api.anthropic.com",
120
+ authKind: "oauth",
121
+ allowBaseUrlOverride: true,
122
+ featured: true,
123
+ oauthId: id,
124
+ oauthFamily: "anthropic",
125
+ jawcodeBundle: "anthropic",
126
+ note: id === "anthropic" ? "Log in with your Claude account"
127
+ : "Independent Claude account pool — log in with a separate Claude account",
128
+ models: [...ANTHROPIC_MODELS],
129
+ modelContextWindows: { ...ANTHROPIC_MODEL_CONTEXT_WINDOWS },
130
+ modelInputModalities: { ...ANTHROPIC_MODEL_INPUT_MODALITIES },
131
+ modelReasoningEfforts: { ...ANTHROPIC_MODEL_REASONING_EFFORTS },
132
+ // Codex omits max_output_tokens; the fallback 8192 truncates long answers.
133
+ defaultMaxOutputTokens: ANTHROPIC_DEFAULT_MAX_OUTPUT_TOKENS,
134
+ modelMaxOutputTokens: { ...ANTHROPIC_MODEL_MAX_OUTPUT_TOKENS },
135
+ defaultModel: "claude-sonnet-5",
136
+ // Subscription /fast remains opt-in: it draws usage credits at 2x price.
137
+ fastWire: ANTHROPIC_FAST_WIRE,
138
+ modelSupportsServiceTier: { ...ANTHROPIC_FAST_MODELS },
139
+ fastTierDescription: ANTHROPIC_FAST_TIER_DESCRIPTION,
140
+ fastOptIn: true,
141
+ });
142
+ }
143
+
112
144
  export const PROVIDER_REGISTRY_CORE: readonly ProviderRegistryEntry[] = [
113
145
  {
114
146
  id: "openai",
@@ -209,7 +241,7 @@ export const PROVIDER_REGISTRY_CORE: readonly ProviderRegistryEntry[] = [
209
241
  // (it is the current catalog, so its default ordering wins), then the ids
210
242
  // only the old devin entry carried. Degraded-mode seed only either way —
211
243
  // `liveModels` discovers the account's real roster.
212
- models: ["swe-2", "swe-1-7", "gpt-5-6-sol", "gpt-6-astra", "gpt-6-1-sol", "gpt-6-sol", "gpt-6-luna", "claude-opus-5-5", "claude-opus-5", "claude-fable-5-1", "claude-sonnet-5-5", "claude-sonnet-5", "glm-5-3", "kimi-k3", "gemini-3-8-flash", "grok-4-6", "grok-4-7", "swe-1-7-lightning", "gpt-5-6-luna", "gpt-5-6-terra", "claude-opus-4-8", "glm-5-2", "kimi-k2-7", "grok-4-5"],
244
+ models: ["swe-2", "swe-1-7", "gpt-5-6-sol", "gpt-6-astra", "gpt-6-1-sol", "gpt-6-sol", "gpt-6-luna", "claude-opus-5-5", "claude-opus-5", "claude-fable-5-1", "claude-sonnet-5-5", "claude-haiku-5-5", "claude-sonnet-5", "glm-5-3", "kimi-k3", "gemini-3-8-flash", "grok-4-6", "grok-4-7", "swe-1-7-lightning", "gpt-5-6-luna", "gpt-5-6-terra", "claude-opus-4-8", "glm-5-2", "kimi-k2-7", "grok-4-5"],
213
245
  liveModels: true,
214
246
  defaultModel: "swe-2",
215
247
  modelContextWindows: DEVIN_MODEL_CONTEXT_WINDOWS,
@@ -490,35 +522,8 @@ export const PROVIDER_REGISTRY_CORE: readonly ProviderRegistryEntry[] = [
490
522
  modelReasoningEfforts: ORCAROUTER_MODEL_REASONING_EFFORTS,
491
523
  note: "Connect your OrcaRouter account with OAuth 2.0 + PKCE; the issued API key is stored in OpenCodex's existing credential store.",
492
524
  },
493
- {
494
- id: "anthropic",
495
- label: "Anthropic Claude",
496
- adapter: "anthropic",
497
- baseUrl: "https://api.anthropic.com",
498
- authKind: "oauth",
499
- allowBaseUrlOverride: true,
500
- featured: true,
501
- oauthId: "anthropic",
502
- jawcodeBundle: "anthropic",
503
- note: "Log in with your Claude account",
504
- models: [...ANTHROPIC_MODELS],
505
- modelContextWindows: { ...ANTHROPIC_MODEL_CONTEXT_WINDOWS },
506
- modelInputModalities: { ...ANTHROPIC_MODEL_INPUT_MODALITIES },
507
- modelReasoningEfforts: { ...ANTHROPIC_MODEL_REASONING_EFFORTS },
508
- // Codex omits max_output_tokens; without a provider budget the Anthropic adapter
509
- // falls back to 8192, which truncates long answers with stop_reason=max_tokens.
510
- defaultMaxOutputTokens: ANTHROPIC_DEFAULT_MAX_OUTPUT_TOKENS,
511
- modelMaxOutputTokens: { ...ANTHROPIC_MODEL_MAX_OUTPUT_TOKENS },
512
- defaultModel: "claude-sonnet-5",
513
- // Claude fast mode on the subscription lane (Claude Code `/fast`): the OAuth route accepts
514
- // `speed` and gates it on account entitlement (usage credits / org enablement), probed live
515
- // 2026-09-23 (devlog/_plan/260923_anthropic_fast_speed/020_probe-evidence.md).
516
- // Off until the operator opts in: fast mode draws usage credits at 2x price.
517
- fastWire: ANTHROPIC_FAST_WIRE,
518
- modelSupportsServiceTier: { ...ANTHROPIC_FAST_MODELS },
519
- fastTierDescription: ANTHROPIC_FAST_TIER_DESCRIPTION,
520
- fastOptIn: true,
521
- },
525
+ anthropicOAuthEntry("anthropic", "Anthropic Claude"),
526
+ anthropicOAuthEntry("anthropic2", "Anthropic · Pool 2"),
522
527
  {
523
528
  id: "anthropic-apikey",
524
529
  label: "Anthropic (API key)",
@@ -129,6 +129,7 @@ export const REGISTRY_FIELD_MODEL_ID_ROLES = {
129
129
  thinkingBudgetModels: NONE,
130
130
  escapeBuiltinToolNames: NONE,
131
131
  oauthId: NONE,
132
+ oauthFamily: NONE,
132
133
  virtualModels: RECORD_KEYS,
133
134
  modelMaxInputTokens: RECORD_KEYS,
134
135
  jawcodeBundle: NONE,
@@ -12,8 +12,10 @@ import type { ProviderModelDiscoverySpec } from "./types";
12
12
  // 260929 Claude Sonnet 5.5 (`claude-sonnet-5-5`, released 2026-09-28): 1M context / 128K output /
13
13
  // adaptive thinking / effort low..max with a high default, same price as Sonnet 5, per the Sonnet 5.5
14
14
  // overview, migration guide, effort and pricing pages (platform.claude.com).
15
- export const ANTHROPIC_MODELS = ["claude-fable-5-1", "claude-fable-5", "claude-sonnet-5-5", "claude-sonnet-5", "claude-opus-5-5", "claude-opus-5", "claude-opus-4-8", "claude-opus-4-7", "claude-opus-4-6", "claude-sonnet-4-6", "claude-haiku-4-5"];
16
- export const ANTHROPIC_MODEL_CONTEXT_WINDOWS: Record<string, number> = { "claude-fable-5-1": 1_000_000, "claude-sonnet-5-5": 1_000_000, "claude-sonnet-5": 1_000_000, "claude-fable-5": 1_000_000, "claude-opus-5-5": 1_000_000, "claude-opus-5": 1_000_000, "claude-opus-4-8": 1_000_000, "claude-opus-4-7": 1_000_000, "claude-opus-4-6": 1_000_000, "claude-sonnet-4-6": 1_000_000, "claude-haiku-4-5": 200_000 };
15
+ // 261008 Claude Haiku 5.5: 1M / 128K, adaptive thinking, effort low..max (default medium),
16
+ // per platform.claude.com Haiku 5.5 overview, migration guide and pricing page.
17
+ export const ANTHROPIC_MODELS = ["claude-fable-5-1", "claude-fable-5", "claude-sonnet-5-5", "claude-sonnet-5", "claude-opus-5-5", "claude-opus-5", "claude-opus-4-8", "claude-opus-4-7", "claude-opus-4-6", "claude-sonnet-4-6", "claude-haiku-5-5", "claude-haiku-4-5"];
18
+ export const ANTHROPIC_MODEL_CONTEXT_WINDOWS: Record<string, number> = { "claude-fable-5-1": 1_000_000, "claude-sonnet-5-5": 1_000_000, "claude-sonnet-5": 1_000_000, "claude-fable-5": 1_000_000, "claude-opus-5-5": 1_000_000, "claude-opus-5": 1_000_000, "claude-opus-4-8": 1_000_000, "claude-opus-4-7": 1_000_000, "claude-opus-4-6": 1_000_000, "claude-sonnet-4-6": 1_000_000, "claude-haiku-5-5": 1_000_000, "claude-haiku-4-5": 200_000 };
17
19
  // All seeded Claude models support vision: https://platform.claude.com/docs/en/models/overview
18
20
  export const ANTHROPIC_MODEL_INPUT_MODALITIES: Record<string, string[]> = Object.fromEntries(
19
21
  ANTHROPIC_MODELS.map(id => [id, ["text", "image"]]),
@@ -40,6 +42,7 @@ export const ANTHROPIC_DEFAULT_MAX_OUTPUT_TOKENS = 64_000;
40
42
  * headroom arithmetic wrong.
41
43
  */
42
44
  export const ANTHROPIC_MODEL_MAX_OUTPUT_TOKENS: Record<string, number> = {
45
+ "claude-haiku-5-5": 128_000,
43
46
  "claude-fable-5-1": 128_000,
44
47
  "claude-fable-5": 128_000,
45
48
  "claude-sonnet-5-5": 128_000,
@@ -70,8 +73,8 @@ export const ANTHROPIC_MODEL_MAX_OUTPUT_TOKENS: Record<string, number> = {
70
73
  * do what it says:
71
74
  * - `minimal`: `adaptiveEffort` rewrites it to `low` (the adaptive wire 400s on it), so
72
75
  * it is not a distinct setting.
73
- * - `none`: only sonnet >= 5 accepts an explicit thinking disable
74
- * (`EXPLICIT_THINKING_DISABLE_FAMILY_MINIMUMS`); Fable rejects one outright.
76
+ * - `none`: thinking-off support differs by family (`anthropic-model-contract.ts`);
77
+ * Haiku 5.5 accepts it without effort, while Fable rejects one outright.
75
78
  * - `ultra`: not an Anthropic concept, and it is degraded to `max` at the request
76
79
  * boundary anyway (src/responses/parser.ts).
77
80
  */
@@ -361,6 +361,8 @@ export interface ProviderRegistryEntry {
361
361
  thinkingBudgetModels?: string[];
362
362
  escapeBuiltinToolNames?: boolean;
363
363
  oauthId?: string;
364
+ /** OAuth implementation family; instance identity remains the exact id/oauthId. */
365
+ oauthFamily?: "anthropic";
364
366
  virtualModels?: Record<string, { wireModelId: string; reasoningMode: "pro" }>;
365
367
  modelMaxInputTokens?: Record<string, number>;
366
368
  jawcodeBundle?: string;
@@ -373,6 +375,7 @@ export interface ProviderRegistryEntry {
373
375
 
374
376
  export type ProviderConfigSeed = Pick<
375
377
  OcxProviderConfig,
378
+ "anthropicOAuthInstance" |
376
379
  "adapter" | "baseUrl" | "apiKeyTransport" | "responsesPath" | "chatCompletionsPath" | "authMode" | "keyOptional" | "freeTier" | "modelSuffixBracketStrip" | "defaultModel" | "models"
377
380
  | "liveModels" | "contextWindow" | "modelContextWindows" | "modelInputModalities"
378
381
  | "modelDisplayNames"
@@ -7,7 +7,7 @@ import type {
7
7
  } from "./registry/types";
8
8
  import { PROVIDER_REGISTRY_CORE } from "./registry/entries-core";
9
9
  import { PROVIDER_REGISTRY_EXTENDED } from "./registry/entries-extended";
10
-
10
+ import { anthropicInstanceRowShapeMatches } from "./anthropic-instance-id";
11
11
  export type {
12
12
  ProviderAuthKind,
13
13
  MetadataModelIdNormalize,
@@ -102,10 +102,11 @@ export function normalizedProviderEndpoint(value: string): string {
102
102
  */
103
103
  export function providerMatchesRegistryTransport(
104
104
  id: string,
105
- provider: Pick<OcxProviderConfig, "baseUrl" | "adapter"> & Partial<Pick<OcxProviderConfig, "authMode">>,
105
+ provider: Pick<OcxProviderConfig, "baseUrl" | "adapter"> & Partial<Pick<OcxProviderConfig, "authMode" | "anthropicOAuthInstance">>,
106
106
  ): boolean {
107
107
  const entry = getProviderRegistryEntry(id);
108
108
  if (!entry) return false;
109
+ if (id === "anthropic2") return entry.authKind === "oauth" && entry.oauthId === id && entry.oauthFamily === "anthropic" && anthropicInstanceRowShapeMatches(id, provider);
109
110
  if (entry.authKind !== "key" || entry.preserveCustomDestination !== true) return true;
110
111
  // The opt-in is intentionally limited to fixed key destinations. Fail closed if a future
111
112
  // registry edit combines it with an override/template despite the registry parity tests.
@@ -115,7 +116,6 @@ export function providerMatchesRegistryTransport(
115
116
  if (provider.authMode !== undefined && provider.authMode !== "key") return false;
116
117
  return normalizedProviderEndpoint(provider.baseUrl) === normalizedProviderEndpoint(entry.baseUrl);
117
118
  }
118
-
119
119
  /**
120
120
  * Resolve the registry entry a configured provider actually points at, by TRANSPORT
121
121
  * rather than by name.
@@ -72,6 +72,7 @@ export interface ResponseSpillPayload {
72
72
  */
73
73
  providerOutputStart?: number;
74
74
  providers?: OcxProviderContinuationState;
75
+ unforcedStoreFalse?: boolean;
75
76
  }
76
77
 
77
78
  export interface ResponseSpillRef {
@@ -496,6 +497,7 @@ function serializedSpill(
496
497
  items: state.items,
497
498
  ...(state.providerOutputStart !== undefined ? { providerOutputStart: state.providerOutputStart } : {}),
498
499
  ...(state.providers ? { providers: state.providers } : {}),
500
+ ...(state.unforcedStoreFalse ? { unforcedStoreFalse: state.unforcedStoreFalse } : {}),
499
501
  };
500
502
  const serialized = JSON.stringify(payload);
501
503
  if (serialized === undefined) throw new Error("Response spill serialization failed");
@@ -549,12 +551,14 @@ function validPayload(value: unknown, responseId: string): value is ResponseSpil
549
551
  if (!value || typeof value !== "object" || Array.isArray(value)) return false;
550
552
  const payload = value as Record<string, unknown>;
551
553
  const keys = Object.keys(payload);
552
- if (keys.some(key => !["version", "responseId", "createdAt", "clientThreadId", "items", "providerOutputStart", "providers"].includes(key))) return false;
554
+ if (keys.some(key => !["version", "responseId", "createdAt", "clientThreadId", "items", "providerOutputStart", "providers", "unforcedStoreFalse"].includes(key))) return false;
553
555
  if (payload.version !== 1 || payload.responseId !== responseId) return false;
554
556
  if (typeof payload.createdAt !== "number" || !Number.isFinite(payload.createdAt)) return false;
555
557
  if (payload.clientThreadId !== undefined
556
558
  && (typeof payload.clientThreadId !== "string" || payload.clientThreadId.trim().length === 0)) return false;
559
+ if (payload.unforcedStoreFalse !== undefined && typeof payload.unforcedStoreFalse !== "boolean") return false;
557
560
  if (!Array.isArray(payload.items)) return false;
561
+ if (payload.unforcedStoreFalse === true && payload.providerOutputStart === undefined) return false;
558
562
  // A malformed boundary must degrade to "never skip", never to a bad index: reject the
559
563
  // payload outright so materialization treats it as corrupt rather than trusting it.
560
564
  if (payload.providerOutputStart !== undefined) {
@@ -0,0 +1,87 @@
1
+ import type { StoredResponseState } from "../state";
2
+ import type { ResponseSpillWriteStatus, ResponseSpillWriteFailureCode, ResponseSpillWriteFailureOrigin } from "./spill-failure";
3
+
4
+ export interface ResponseStateMetrics {
5
+ count: number;
6
+ residentCount: number;
7
+ spillStubCount: number;
8
+ tombstoneCount: number;
9
+ totalBytes: number;
10
+ spillPayloadBytes: number;
11
+ largestBytes: number;
12
+ oldestAgeMs: number;
13
+ spillWrites: number;
14
+ spillWriteFailures: number;
15
+ spillWriteStatus: ResponseSpillWriteStatus;
16
+ spillWriteConsecutiveFailures: number;
17
+ spillLastWriteFailureCode: ResponseSpillWriteFailureCode | null;
18
+ spillLastWriteFailureOrigin: ResponseSpillWriteFailureOrigin | null;
19
+ spillAclRetryReturnedTimeouts: number;
20
+ spillAclTimeoutMemoRefusals: number;
21
+ spillLastWriteFailureAt: number | null;
22
+ spillLastWriteSuccessAt: number | null;
23
+ spillReadFailures: number;
24
+ replayScopeMismatchDrops: number;
25
+ }
26
+
27
+ export function computeResponseStateMetrics(params: {
28
+ states: Iterable<StoredResponseState>;
29
+ stateCount: number;
30
+ now: number;
31
+ totalBytes: number;
32
+ spillWrites: number;
33
+ spillWriteFailures: number;
34
+ spillWriteConsecutiveFailures: number;
35
+ spillLastFailureCode: ResponseSpillWriteFailureCode | null;
36
+ spillLastFailureOrigin: ResponseSpillWriteFailureOrigin | null;
37
+ spillLastSuccessAt: number | null;
38
+ spillAclRetryReturnedTimeouts: number;
39
+ spillAclTimeoutMemoRefusals: number;
40
+ spillLastFailureAt: number | null;
41
+ spillReadFailures: number;
42
+ replayScopeMismatchDrops: number;
43
+ }): ResponseStateMetrics {
44
+ let largestBytes = 0;
45
+ let oldestCreatedAt = params.now;
46
+ let residentCount = 0;
47
+ let spillStubCount = 0;
48
+ let tombstoneCount = 0;
49
+ let spillPayloadBytes = 0;
50
+ for (const state of params.states) {
51
+ const bytes = state.sizeBytes;
52
+ if (bytes > largestBytes) largestBytes = bytes;
53
+ if (state.createdAt < oldestCreatedAt) oldestCreatedAt = state.createdAt;
54
+ if (state.kind === "resident") {
55
+ residentCount += 1;
56
+ } else if (state.kind === "spill") {
57
+ spillStubCount += 1;
58
+ spillPayloadBytes += state.spill.payloadBytes;
59
+ } else tombstoneCount += 1;
60
+ }
61
+ return {
62
+ count: params.stateCount,
63
+ residentCount,
64
+ spillStubCount,
65
+ tombstoneCount,
66
+ totalBytes: params.totalBytes,
67
+ spillPayloadBytes,
68
+ largestBytes,
69
+ oldestAgeMs: params.stateCount > 0 ? params.now - oldestCreatedAt : 0,
70
+ spillWrites: params.spillWrites,
71
+ spillWriteFailures: params.spillWriteFailures,
72
+ spillWriteStatus: params.spillWriteConsecutiveFailures > 0
73
+ ? "degraded"
74
+ : params.spillLastSuccessAt !== null
75
+ ? "healthy"
76
+ : "initial",
77
+ spillWriteConsecutiveFailures: params.spillWriteConsecutiveFailures,
78
+ spillLastWriteFailureCode: params.spillLastFailureCode,
79
+ spillLastWriteFailureOrigin: params.spillLastFailureOrigin,
80
+ spillAclRetryReturnedTimeouts: params.spillAclRetryReturnedTimeouts,
81
+ spillAclTimeoutMemoRefusals: params.spillAclTimeoutMemoRefusals,
82
+ spillLastWriteFailureAt: params.spillLastFailureAt,
83
+ spillLastWriteSuccessAt: params.spillLastSuccessAt,
84
+ spillReadFailures: params.spillReadFailures,
85
+ replayScopeMismatchDrops: params.replayScopeMismatchDrops,
86
+ };
87
+ }
@@ -24,6 +24,7 @@ interface LegacySnapshotState {
24
24
  providers?: OcxProviderContinuationState;
25
25
  conversationId?: unknown;
26
26
  cursorCheckpointUsable?: unknown;
27
+ unforcedStoreFalse?: unknown;
27
28
  }
28
29
 
29
30
  function isSpillRef(value: unknown): value is ResponseSpillRef {
@@ -51,6 +52,9 @@ export function loadSnapshotEntry(id: string, value: unknown, store: SnapshotLoa
51
52
  ? raw as number
52
53
  : undefined;
53
54
  };
55
+ // Restricted retention requires proven provider output; legacy unrestricted rows stay readable.
56
+ if (rec.unforcedStoreFalse === true
57
+ && anchorFor(rec.kind === "spill" ? Number.MAX_SAFE_INTEGER : Array.isArray(rec.items) ? rec.items.length : 0) === undefined) return;
54
58
  if (rec.kind === "spill") {
55
59
  if (!isSpillRef(rec.spill)) return;
56
60
  const base: Omit<SpilledResponseState, "sizeBytes"> = {
@@ -61,6 +65,7 @@ export function loadSnapshotEntry(id: string, value: unknown, store: SnapshotLoa
61
65
  // here; the spill payload validator re-checks it against the real array.
62
66
  ...(anchorFor(Number.MAX_SAFE_INTEGER) !== undefined ? { providerOutputStart: anchorFor(Number.MAX_SAFE_INTEGER) } : {}),
63
67
  ...(rec.providers ? { providers: rec.providers } : {}),
68
+ ...(rec.unforcedStoreFalse === true ? { unforcedStoreFalse: true } : {}),
64
69
  spill: rec.spill,
65
70
  };
66
71
  store.replaceMapEntry(id, { ...base, sizeBytes: store.stubSize(id, base) });
@@ -88,6 +93,7 @@ export function loadSnapshotEntry(id: string, value: unknown, store: SnapshotLoa
88
93
  items: rec.items,
89
94
  ...(anchorFor(rec.items.length) !== undefined ? { providerOutputStart: anchorFor(rec.items.length) } : {}),
90
95
  ...(providers ? { providers } : {}),
96
+ ...(rec.unforcedStoreFalse === true ? { unforcedStoreFalse: true } : {}),
91
97
  });
92
98
  if (!resident) {
93
99
  store.replaceMapEntry(id, store.tombstone(id, rec.createdAt));
@@ -191,6 +191,7 @@ function spillPayloadForResident(candidate: ResidentResponseState): Parameters<t
191
191
  items: candidate.items,
192
192
  ...(candidate.providerOutputStart !== undefined ? { providerOutputStart: candidate.providerOutputStart } : {}),
193
193
  ...(candidate.providers ? { providers: candidate.providers } : {}),
194
+ ...(candidate.unforcedStoreFalse ? { unforcedStoreFalse: candidate.unforcedStoreFalse } : {}),
194
195
  };
195
196
  }
196
197
 
@@ -0,0 +1,55 @@
1
+ /**
2
+ * Closed provenance and replay gating for unforced store:false client tool call responses.
3
+ *
4
+ * An unforced store:false response is cached exclusively to allow the client to send a matching
5
+ * function/custom tool output continuation on the next turn. Normal replay, text continuation, or
6
+ * mismatched output without a matching pending call fails replay and requires the client to
7
+ * supply the full conversation.
8
+ */
9
+
10
+ export function hasPendingClientToolCall(output: readonly unknown[]): boolean {
11
+ return output.some(item =>
12
+ !!item && typeof item === "object" && clientToolOutputType((item as { type?: unknown }).type) !== undefined,
13
+ );
14
+ }
15
+
16
+ export function allowsUnforcedStoreFalseReplay(
17
+ stored: readonly unknown[],
18
+ providerOutputStart: number | undefined,
19
+ clientInput: readonly unknown[],
20
+ carried: number,
21
+ ): boolean {
22
+ if (typeof providerOutputStart !== "number" || !Number.isSafeInteger(providerOutputStart)
23
+ || providerOutputStart < 0 || providerOutputStart > stored.length) return false;
24
+ const anchor = providerOutputStart;
25
+ const pendingCallIds = new Map<string, string>();
26
+ for (const item of stored.slice(anchor)) {
27
+ if (item && typeof item === "object") {
28
+ const outputType = clientToolOutputType((item as { type?: unknown }).type);
29
+ const callId = (item as { call_id?: unknown }).call_id;
30
+ if (outputType && typeof callId === "string" && callId) pendingCallIds.set(callId, outputType);
31
+ }
32
+ }
33
+ for (let i = 0; i < carried; i++) {
34
+ const item = clientInput[i];
35
+ if (item && typeof item === "object") {
36
+ const callId = (item as { call_id?: unknown }).call_id;
37
+ if (typeof callId === "string" && pendingCallIds.get(callId) === (item as { type?: unknown }).type) pendingCallIds.delete(callId);
38
+ }
39
+ }
40
+ return clientInput.slice(carried).some(item => {
41
+ if (item && typeof item === "object") {
42
+ const callId = (item as { call_id?: unknown }).call_id;
43
+ return typeof callId === "string" && pendingCallIds.has(callId)
44
+ && pendingCallIds.get(callId) === (item as { type?: unknown }).type;
45
+ }
46
+ return false;
47
+ });
48
+ }
49
+
50
+ /** Closed call/output pairs supported by the client-owned continuation path. */
51
+ function clientToolOutputType(type: unknown): string | undefined {
52
+ if (type === "function_call") return "function_call_output";
53
+ if (type === "custom_tool_call") return "custom_tool_call_output";
54
+ return undefined;
55
+ }