@bitkyc08/opencodex 2.80.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 (227) 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/devin/live-models.ts +2 -0
  20. package/src/adapters/kiro-retry.ts +9 -4
  21. package/src/adapters/openai-chat/messages.ts +7 -6
  22. package/src/adapters/physical-send.ts +8 -5
  23. package/src/adapters/qoder/adapter.ts +41 -6
  24. package/src/adapters/qoder/scaffold-guard.ts +2 -3
  25. package/src/adapters/zed.ts +16 -1
  26. package/src/claude/inbound-cache-stabilize.ts +4 -9
  27. package/src/claude/inbound.ts +1 -0
  28. package/src/claude/intercept/model-bindings.ts +1 -0
  29. package/src/claude/intercept/picker-listener.ts +71 -4
  30. package/src/cli/account-anthropic-threshold.ts +14 -5
  31. package/src/cli/account-api.ts +3 -1
  32. package/src/cli/account-extended.ts +13 -9
  33. package/src/cli/account-policy-dto.ts +1 -1
  34. package/src/cli/account-policy.ts +13 -4
  35. package/src/cli/account.ts +3 -3
  36. package/src/cli/capabilities-accounts.ts +2 -2
  37. package/src/cli/capabilities-agents-routing.ts +24 -0
  38. package/src/cli/capabilities-integrations.ts +16 -0
  39. package/src/cli/codex-shim-autorestore.ts +3 -0
  40. package/src/cli/dispatch.ts +10 -1
  41. package/src/cli/help.ts +4 -1
  42. package/src/cli/index.ts +11 -5
  43. package/src/cli/integration-plan-dto.ts +1 -0
  44. package/src/cli/integration-preview.ts +8 -1
  45. package/src/cli/integrations.ts +32 -3
  46. package/src/cli/message-args.ts +34 -0
  47. package/src/cli/message-command.ts +43 -0
  48. package/src/cli/message-runtime.ts +6 -0
  49. package/src/cli/observe.ts +3 -1
  50. package/src/cli/provider.ts +29 -6
  51. package/src/cli/registry.ts +24 -2
  52. package/src/clients/config-export/commandcode.ts +131 -0
  53. package/src/clients/config-export/contracts.ts +12 -0
  54. package/src/clients/config-export.ts +64 -1
  55. package/src/codex/auth-api/account-list.ts +11 -3
  56. package/src/codex/auth-api/main-account-probe.ts +25 -8
  57. package/src/codex/auth-api/pool-mode-gate.ts +33 -9
  58. package/src/codex/auth-api/pool-quota-probe.ts +15 -1
  59. package/src/codex/auth-api/routes.ts +1 -0
  60. package/src/codex/catalog/auto-review.ts +13 -6
  61. package/src/codex/catalog/build-entries.ts +15 -7
  62. package/src/codex/catalog/control-plane.ts +45 -0
  63. package/src/codex/catalog/effort.ts +11 -1
  64. package/src/codex/catalog/gather-capture.ts +5 -3
  65. package/src/codex/catalog/parsing.ts +2 -1
  66. package/src/codex/catalog/provider-models.ts +15 -9
  67. package/src/codex/control-plane-models.ts +6 -0
  68. package/src/combos/jev-decision-contract.ts +25 -2
  69. package/src/combos/jev.ts +3 -2
  70. package/src/combos/types.ts +1 -1
  71. package/src/config/diagnostics.ts +27 -16
  72. package/src/config/load-degrade.ts +34 -0
  73. package/src/config/schema/anthropic-account-pool.ts +143 -0
  74. package/src/config/schema/config-schema.ts +7 -1
  75. package/src/config/schema/leaf-validators.ts +4 -0
  76. package/src/config.ts +2 -0
  77. package/src/generated/compatibility-version.json +320 -184
  78. package/src/generated/model-metadata.ts +5 -4
  79. package/src/images/loop.ts +2 -1
  80. package/src/integrations/catalog-refresh.ts +1 -1
  81. package/src/integrations/mutation-plan.ts +7 -0
  82. package/src/integrations/registry.ts +8 -0
  83. package/src/integrations/state.ts +9 -2
  84. package/src/integrations/writer.ts +8 -2
  85. package/src/lib/lab-live-route-production.ts +9 -1
  86. package/src/lib/request-execution-budget.ts +236 -58
  87. package/src/lib/spend-pool-alias-validation.ts +79 -0
  88. package/src/lib/spend-pool-continuity.ts +34 -0
  89. package/src/lib/spend-reservation-ledger.ts +412 -50
  90. package/src/lib/state-store-registrations.ts +20 -3
  91. package/src/lib/upstream-retry.ts +17 -5
  92. package/src/lib/workflow-budget.ts +53 -11
  93. package/src/messaging/budget.ts +45 -0
  94. package/src/messaging/discovery.ts +57 -0
  95. package/src/messaging/envelope.ts +49 -0
  96. package/src/messaging/input.ts +37 -0
  97. package/src/messaging/rpc.ts +196 -0
  98. package/src/messaging/send.ts +58 -0
  99. package/src/messaging/socket.ts +72 -0
  100. package/src/messaging/types.ts +29 -0
  101. package/src/oauth/anthropic-account-refusal.ts +51 -17
  102. package/src/oauth/anthropic-account-threshold.ts +8 -2
  103. package/src/oauth/anthropic-continuity.ts +15 -1
  104. package/src/oauth/anthropic-model-quota.ts +120 -48
  105. package/src/oauth/anthropic-model-routes.ts +11 -3
  106. package/src/oauth/anthropic-oauth-definitions.ts +98 -0
  107. package/src/oauth/anthropic-pool-config.ts +30 -0
  108. package/src/oauth/anthropic-rate-limit-policy.ts +64 -9
  109. package/src/oauth/anthropic-routing.ts +1202 -866
  110. package/src/oauth/anthropic-send-ownership.ts +41 -0
  111. package/src/oauth/anthropic.ts +4 -1
  112. package/src/oauth/generic-account-failover.ts +3 -1
  113. package/src/oauth/health.ts +8 -6
  114. package/src/oauth/index.ts +90 -173
  115. package/src/oauth/kiro.ts +22 -9
  116. package/src/oauth/login-cli.ts +2 -0
  117. package/src/oauth/model-discovery-auth.ts +43 -0
  118. package/src/oauth/pool-kernel.ts +11 -5
  119. package/src/oauth/pool-settings-capability.ts +5 -3
  120. package/src/oauth/store-anthropic-instance.ts +87 -0
  121. package/src/oauth/store.ts +67 -9
  122. package/src/oauth/token-guardian.ts +2 -0
  123. package/src/oauth/types.ts +2 -0
  124. package/src/protocols/plan-snapshot.ts +5 -0
  125. package/src/protocols/settings.ts +20 -5
  126. package/src/providers/anthropic-instance-id.ts +47 -0
  127. package/src/providers/anthropic-instance.ts +48 -0
  128. package/src/providers/anthropic-reset-grant-ledger.ts +11 -1
  129. package/src/providers/derive.ts +2 -0
  130. package/src/providers/kiro-models.ts +3 -0
  131. package/src/providers/label.ts +1 -1
  132. package/src/providers/model-rename-fields.ts +2 -0
  133. package/src/providers/quota/account-cache.ts +37 -11
  134. package/src/providers/quota/anthropic-account-quota.ts +93 -0
  135. package/src/providers/quota/anthropic-cooldown-recovery.ts +222 -111
  136. package/src/providers/quota/report-cache.ts +24 -1
  137. package/src/providers/quota/vendor-probes-oauth.ts +46 -14
  138. package/src/providers/quota.ts +22 -32
  139. package/src/providers/registry/entries-core.ts +35 -30
  140. package/src/providers/registry/model-ids.ts +1 -0
  141. package/src/providers/registry/model-seeds.ts +7 -4
  142. package/src/providers/registry/types.ts +3 -0
  143. package/src/providers/registry.ts +3 -3
  144. package/src/responses/spill-store.ts +5 -1
  145. package/src/responses/state/metrics.ts +87 -0
  146. package/src/responses/state/snapshot-codec.ts +6 -0
  147. package/src/responses/state/spill-queue.ts +1 -0
  148. package/src/responses/state/unforced-store-false.ts +55 -0
  149. package/src/responses/state.ts +50 -66
  150. package/src/router.ts +28 -4
  151. package/src/routing/compatibility/assemble.ts +1 -1
  152. package/src/routing/quota.ts +7 -3
  153. package/src/server/auth-cors.ts +5 -0
  154. package/src/server/chat-completions.ts +6 -1
  155. package/src/server/chat-native.ts +45 -25
  156. package/src/server/claude-messages.ts +16 -2
  157. package/src/server/gui-static.ts +38 -4
  158. package/src/server/index/spend-ledger-lifecycle.ts +10 -4
  159. package/src/server/index.ts +4 -2
  160. package/src/server/inference/context.ts +1 -1
  161. package/src/server/lifecycle.ts +1 -1
  162. package/src/server/management/agent-settings-routes.ts +30 -6
  163. package/src/server/management/anthropic-account-threshold.ts +12 -9
  164. package/src/server/management/anthropic-pool-settings.ts +72 -0
  165. package/src/server/management/anthropic-reset-grant-routes.ts +98 -39
  166. package/src/server/management/config-routes.ts +27 -7
  167. package/src/server/management/decision-routes.ts +2 -1
  168. package/src/server/management/integration-routes.ts +25 -2
  169. package/src/server/management/oauth-account-routes.ts +79 -98
  170. package/src/server/management/provider-instance-ownership.ts +44 -0
  171. package/src/server/management/provider-routes.ts +21 -28
  172. package/src/server/management/route-registry.ts +3 -1
  173. package/src/server/management/routing-profile-routes.ts +4 -4
  174. package/src/server/management/system-restart.ts +44 -14
  175. package/src/server/management/system-routes.ts +24 -2
  176. package/src/server/management/vision-sidecar-options.ts +9 -6
  177. package/src/server/management/web-search-sidecar-options.ts +51 -2
  178. package/src/server/management-auth.ts +6 -0
  179. package/src/server/messages-native-eligibility.ts +3 -2
  180. package/src/server/messages-native-oauth.ts +86 -41
  181. package/src/server/messages-native-selector.ts +39 -0
  182. package/src/server/messages-native.ts +84 -25
  183. package/src/server/request-log.ts +5 -0
  184. package/src/server/responses/adapter-continuation.ts +29 -17
  185. package/src/server/responses/adapter-dispatch.ts +47 -17
  186. package/src/server/responses/antigravity-validation-refusal.ts +20 -6
  187. package/src/server/responses/compact.ts +25 -8
  188. package/src/server/responses/compaction-recovery.ts +1 -1
  189. package/src/server/responses/core-combo-native.ts +4 -1
  190. package/src/server/responses/core-combo.ts +16 -11
  191. package/src/server/responses/core-normalize.ts +1 -0
  192. package/src/server/responses/core-options.ts +3 -1
  193. package/src/server/responses/core-replay.ts +5 -0
  194. package/src/server/responses/passthrough-delivery.ts +7 -3
  195. package/src/server/responses/passthrough-dispatch.ts +21 -8
  196. package/src/server/responses/request-prepare.ts +37 -5
  197. package/src/server/responses/request-send-budget.ts +74 -37
  198. package/src/server/responses/request-spend.ts +325 -33
  199. package/src/server/responses/request-transport.ts +132 -52
  200. package/src/server/responses/run-turn-execution.ts +11 -3
  201. package/src/server/responses/sidecar-execution.ts +96 -14
  202. package/src/server/responses-undeclared-tool-guard.ts +5 -1
  203. package/src/server/workflow-refusal.ts +30 -0
  204. package/src/service/ownership-mutation-lease.d.mts +1 -1
  205. package/src/service/ownership-mutation-lease.mjs +5 -7
  206. package/src/sidecar/anthropic-binding.ts +79 -0
  207. package/src/sidecar/auth.ts +76 -2
  208. package/src/types/anthropic-account-pool.ts +33 -0
  209. package/src/types/config.ts +12 -28
  210. package/src/types/provider.ts +5 -0
  211. package/src/types/tools.ts +7 -0
  212. package/src/types.ts +4 -0
  213. package/src/usage/cost.ts +3 -2
  214. package/src/usage/expected-prices.ts +66 -14
  215. package/src/vision/anthropic-describe.ts +16 -3
  216. package/src/vision/index.ts +3 -0
  217. package/src/vision/plan.ts +19 -6
  218. package/src/web-search/alpha-search.ts +7 -1
  219. package/src/web-search/anthropic-executor.ts +16 -4
  220. package/src/web-search/index.ts +7 -3
  221. package/src/web-search/loop.ts +27 -5
  222. package/src/web-search/passthrough-bridge.ts +4 -1
  223. package/src/web-search/sidecar-providers.ts +4 -3
  224. package/gui/dist/assets/App-MbDVfUeC.js +0 -54
  225. package/gui/dist/assets/Tray-CzkxK82A.js +0 -1
  226. package/gui/dist/assets/index-UJt6npXk.js +0 -94
  227. package/gui/dist/assets/quota-summary-DU3sD6Z-.js +0 -1
@@ -1,3 +1,4 @@
1
+ import { AsyncLocalStorage } from "node:async_hooks";
1
2
  /**
2
3
  * One logical request, one execution budget (#4546).
3
4
  *
@@ -14,6 +15,7 @@
14
15
  * sends plus one alternate -- by funding the alternate from a reserve that a validated
15
16
  * sanitized repair can spend instead, but never both.
16
17
  */
18
+ import type { SpendSeed, SpendScopes, SpendReservationProof, SpendAdmissionPolicy } from "./spend-reservation-ledger";
17
19
  import type { TransientSendBudget } from "./upstream-retry";
18
20
 
19
21
  export type SendClass =
@@ -97,6 +99,8 @@ export interface SingleUseDispatchPermit {
97
99
  * false, which is what keeps a retry thunk from sending twice on one permit.
98
100
  */
99
101
  use(): boolean;
102
+ /** Keep the selected start rebindable until its executor finishes. */
103
+ execute?<T>(run: () => Promise<T>): Promise<T>;
100
104
  /**
101
105
  * Hand back a reservation that never dispatched -- a credential move that found no alternate,
102
106
  * a rebuild abandoned before the send. Idempotent, and a no-op once the permit was used or
@@ -145,9 +149,18 @@ export interface RequestSendObserver {
145
149
  * would cross a ceiling never joins the total, the total stays just under, and the ceiling
146
150
  * never fires for any later request either.
147
151
  */
148
- charge(options?: { alreadySent?: boolean }): boolean;
152
+ charge(options?: { alreadySent?: boolean; deferDispatch?: boolean; onReserved?: (proof: SpendReservationProof) => void; targetKey?: string }): boolean;
153
+ /** Confirm the exact reservation once its dispatch is known. */
154
+ readonly enforced?: boolean;
155
+ readonly policyStarted?: boolean;
156
+ readonly spendAdmissionPolicy?: SpendAdmissionPolicy;
157
+ /** Capture request policy at its first admission or actual physical start. */
158
+ startRequest?(target?: SpendScopes & { targetKey?: string }): void;
159
+ ensureSeed?(target: SpendScopes & { targetKey?: string }, proof?: SpendReservationProof): SpendSeed | undefined;
160
+ beginReporter?(): { start(seed: SpendSeed, ordinal: number): void; rebindTarget(target: SpendScopes & { targetKey?: string }, ordinal: number): boolean; report(sends: number): void; close(): void };
161
+ dispatch?(proof: SpendReservationProof): void;
149
162
  /** Give back a booking whose send never happened. */
150
- refund(): void;
163
+ refund(proof?: SpendReservationProof | null): void;
151
164
  }
152
165
 
153
166
  /**
@@ -156,6 +169,14 @@ export interface RequestSendObserver {
156
169
  * model send; the reserve is what the fourth send draws on once the base allowance is gone.
157
170
  */
158
171
  export interface RequestExecutionBudget extends TransientSendBudget {
172
+ readonly physicalStarted?: number;
173
+ readonly physicalLimit?: number;
174
+ readonly spendEnforced?: boolean;
175
+ readonly spendPolicyStarted?: boolean;
176
+ readonly spendAdmissionPolicy?: SpendAdmissionPolicy;
177
+ startRequest?(target?: SpendScopes & { targetKey?: string }): void;
178
+ claimPhysicalSend?(): number | undefined;
179
+ beginSpendProducer?(): { close(): void } | undefined;
159
180
  readonly logicalRequestId: string;
160
181
  readonly policyVersion: string;
161
182
  readonly policy: RequestExecutionBudgetPolicy;
@@ -223,7 +244,9 @@ let logicalRequestSeq = 0;
223
244
  */
224
245
  interface SharedSendLedger {
225
246
  spent: number;
226
- pendingExternalSends: number;
247
+ physicalStarted: number;
248
+ physicalLimit?: number;
249
+ pendingExternalSends: Set<object>;
227
250
  /**
228
251
  * Spend one of this logical request's replacements for an ambiguous failure. Beside `spent`
229
252
  * for the same reason `pendingExternalSends` is: a derived scope that shared one without the
@@ -239,6 +262,102 @@ interface SharedSendLedger {
239
262
  }
240
263
 
241
264
  const sharedSendLedgers = new WeakMap<RequestExecutionBudget, SharedSendLedger>();
265
+ const dispatchSpendProofs = new WeakMap<SingleUseDispatchPermit, {
266
+ owner: SharedSendLedger;
267
+ claim(): SpendReservationProof | undefined;
268
+ report(): boolean;
269
+ }>();
270
+
271
+ /** One preflight for the exact prepaid dispatch, never another budget or a replayed permit. */
272
+ export function claimDispatchSpendProof(budget: RequestExecutionBudget, permit?: SingleUseDispatchPermit): SpendReservationProof | undefined {
273
+ const proof = permit && dispatchSpendProofs.get(permit);
274
+ return proof && proof.owner === sharedSendLedgers.get(budget) ? proof.claim() : undefined;
275
+ }
276
+
277
+ /** Reports actual sends against only the named permit on this shared ledger. */
278
+ export function reportDispatchSends(
279
+ budget: TransientSendBudget,
280
+ sends: number,
281
+ permit?: SingleUseDispatchPermit,
282
+ ): void {
283
+ const count = Number.isFinite(sends) ? Math.max(0, Math.trunc(sends)) : 0;
284
+ if (count === 0) return;
285
+ const receipt = permit && dispatchSpendProofs.get(permit);
286
+ const prepaid = receipt && receipt.owner === sharedSendLedgers.get(budget as RequestExecutionBudget)
287
+ && receipt.report() ? 1 : 0;
288
+ // Unnamed, foreign, released or already-reported permits cannot consume another receipt.
289
+ budget.used += count - prepaid;
290
+ }
291
+
292
+ /** A helper owns its starts and can never consume another helper's receipts. */
293
+ export type PhysicalSendReporter = ((sends: number) => void) & {
294
+ beforeSend?: () => boolean;
295
+ close?: () => void;
296
+ execute?: <T>(run: () => Promise<T>) => Promise<T>;
297
+ bind?: (target: SpendScopes & { targetKey?: string }) => boolean;
298
+ };
299
+ const activePhysicalReporters = new AsyncLocalStorage<PhysicalSendReporter>();
300
+ /** Selection owners rebind this helper's current start before invoking the executor. */
301
+ export function rebindPhysicalSend(budget: TransientSendBudget | undefined, target: SpendScopes & { targetKey?: string }): boolean {
302
+ if (!(budget as RequestExecutionBudget | undefined)?.spendEnforced) return true;
303
+ return activePhysicalReporters.getStore()?.bind?.(target) ?? true;
304
+ }
305
+ export function createPhysicalSendReporter(
306
+ budget: TransientSendBudget,
307
+ target: () => SpendScopes & { targetKey?: string },
308
+ permit?: SingleUseDispatchPermit,
309
+ telemetry?: (sends: number) => void,
310
+ ): PhysicalSendReporter {
311
+ const counter = sharedSendLedgers.get(budget as RequestExecutionBudget);
312
+ const observer = counter?.observer;
313
+ let producer: ReturnType<NonNullable<RequestSendObserver["beginReporter"]>> | undefined;
314
+ let closed = false;
315
+ let started = 0;
316
+ let reported = 0;
317
+ let currentOrdinal: number | undefined;
318
+ const report: PhysicalSendReporter = (sends) => {
319
+ if (closed) return;
320
+ if (!producer) { if (observer?.enforced !== true) { reportDispatchSends(budget, sends, permit); telemetry?.(sends); } return; }
321
+ const count = Math.min(Math.max(0, Math.trunc(sends)), started - reported);
322
+ try { producer.report(count); }
323
+ finally {
324
+ reported += count;
325
+ const receipt = permit && dispatchSpendProofs.get(permit);
326
+ const prepaid = count > 0 && receipt && receipt.owner === counter && receipt.report() ? 1 : 0;
327
+ if (counter) counter.spent += count - prepaid;
328
+ telemetry?.(count);
329
+ }
330
+ };
331
+ if (observer?.startRequest || observer?.enforced === true) report.beforeSend = () => {
332
+ if (closed) return false;
333
+ const selected = target();
334
+ if ((budget as RequestExecutionBudget).startRequest) (budget as RequestExecutionBudget).startRequest!(selected);
335
+ else observer?.startRequest?.(selected);
336
+ if (observer?.enforced !== true) return true;
337
+ producer ??= observer.beginReporter?.();
338
+ const receipt = permit && dispatchSpendProofs.get(permit);
339
+ const proof = receipt?.owner === counter ? receipt?.claim() : undefined;
340
+ const seed = observer.ensureSeed?.(selected, proof);
341
+ if (!seed) return false;
342
+ const ordinal = (budget as RequestExecutionBudget).claimPhysicalSend?.();
343
+ if (ordinal === undefined) return false;
344
+ producer?.start(seed, ordinal);
345
+ currentOrdinal = ordinal;
346
+ started++;
347
+ return true;
348
+ };
349
+ report.execute = run => activePhysicalReporters.run(report, run);
350
+ report.bind = (selected: SpendScopes & { targetKey?: string }) => {
351
+ if (!producer || currentOrdinal === undefined || observer?.enforced !== true) return true;
352
+ return producer.rebindTarget(selected, currentOrdinal);
353
+ };
354
+ report.close = () => {
355
+ if (closed) return;
356
+ try { if (producer) report(started - reported); }
357
+ finally { closed = true; producer?.close(); }
358
+ };
359
+ return report;
360
+ }
242
361
 
243
362
  /**
244
363
  * One logical request's replacement grant: how many it has spent, and the ceiling it is held
@@ -281,22 +400,35 @@ function createRequestExecutionBudgetWithLedger(
281
400
  let alternateTargetSends = 0;
282
401
  let targetTransitions = 0;
283
402
  let lastTargetKey: string | undefined;
403
+ const targetReservations: Array<{ targetKey: string }> = [];
284
404
 
285
405
  const budget: RequestExecutionBudget = {
406
+ get physicalStarted(): number { return counter.physicalStarted; },
407
+ get physicalLimit(): number | undefined { return counter.physicalLimit; },
408
+ get spendEnforced(): boolean { return observer?.enforced === true; },
409
+ get spendPolicyStarted(): boolean { return observer?.policyStarted === true; },
410
+ get spendAdmissionPolicy(): SpendAdmissionPolicy | undefined { return observer?.spendAdmissionPolicy; },
411
+ startRequest(target) {
412
+ observer?.startRequest?.(target);
413
+ if (observer?.enforced && counter.physicalLimit === undefined) {
414
+ counter.physicalLimit = Number.isSafeInteger(policy.maxTotalModelSends) && policy.maxTotalModelSends > 0 ? policy.maxTotalModelSends : 0;
415
+ }
416
+ },
417
+ beginSpendProducer() { return observer?.enforced ? observer.beginReporter?.() : undefined; },
418
+ claimPhysicalSend(): number | undefined {
419
+ budget.startRequest?.();
420
+ if (observer?.enforced !== true || counter.physicalLimit === undefined) return undefined;
421
+ if (counter.physicalStarted >= counter.physicalLimit) return undefined;
422
+ return ++counter.physicalStarted;
423
+ },
286
424
  get used(): number { return counter.spent; },
287
425
  set used(next: number) {
288
- // The retry helpers report their real send count by assigning through this field. A
289
- // reservation taken with `countedExternally` has already booked one of those sends, so
290
- // the report settles the pending booking first and only the surplus is charged.
291
- const delta = next - counter.spent;
292
- if (delta <= 0) {
293
- counter.spent = Math.max(0, next);
294
- return;
295
- }
296
- const settled = Math.min(delta, counter.pendingExternalSends);
297
- counter.pendingExternalSends -= settled;
298
- const charged = delta - settled;
299
- counter.spent += charged;
426
+ // A numeric report has no receipt identity. Only reportDispatchSends may settle a
427
+ // prepaid permit; guessing by reservation order can refund another leg's actual send.
428
+ // Enforced sends can only be reconciled through their producer's claimed starts.
429
+ if (observer?.enforced === true) return;
430
+ const charged = next - counter.spent;
431
+ counter.spent = Math.max(0, next);
300
432
  // These sends have already left. The ledger records them even past a ceiling it would
301
433
  // have refused, because refusing after the fact only hides spend that was really
302
434
  // incurred -- the refusal has to happen at the reservation below, or not at all.
@@ -349,59 +481,104 @@ function createRequestExecutionBudgetWithLedger(
349
481
  // Consulted last, because it is the only bound here that WRITES. A ledger entry booked
350
482
  // for a dispatch a cheaper check above would have refused is spend this request never
351
483
  // makes, and it would hold those tokens against the scope until retention expired.
352
- if (observer && !observer.charge()) return { allowed: false, reason: "spend-exhausted" };
484
+ budget.startRequest?.({ targetKey: intent.targetKey });
485
+ let spendProof: SpendReservationProof | undefined;
486
+ if (observer && !observer.charge({ targetKey: intent.targetKey, deferDispatch: true, onReserved: proof => { spendProof = proof; } })) {
487
+ return { allowed: false, reason: "spend-exhausted" };
488
+ }
353
489
 
354
490
  // THE RESERVATION IS THE CHARGE. Deciding here and charging in `use()` left a window in
355
491
  // which two legs read the same remainder, both received a permit, and both dispatched:
356
492
  // one remaining send admitted two physical sends, which is the per-request multiplication
357
493
  // this budget exists to stop. Everything is booked now; `release()` is the way back.
358
- const previousTargetKey = lastTargetKey;
359
494
  counter.spent += 1;
360
- if (intent.countedExternally === true) counter.pendingExternalSends += 1;
495
+ const receipt = { targetKey: intent.targetKey };
496
+ targetReservations.push(receipt);
497
+ if (intent.countedExternally === true) counter.pendingExternalSends.add(receipt);
361
498
  if (drawsReserve) reserveSpent = true;
362
499
  if (chargesAlternateTarget) alternateTargetSends += 1;
363
500
  if (chargesTransition) targetTransitions += 1;
364
501
  lastTargetKey = intent.targetKey;
365
502
 
503
+ let producer: ReturnType<NonNullable<RequestSendObserver["beginReporter"]>> | undefined;
504
+ let executing = false;
505
+ let physicalOrdinal: number | undefined;
506
+ const startPhysical = (): boolean => {
507
+ if (observer?.enforced !== true) return true;
508
+ const seed = observer.ensureSeed?.({ targetKey: intent.targetKey }, spendProof);
509
+ if (!seed) return false;
510
+ const ordinal = budget.claimPhysicalSend?.();
511
+ if (ordinal === undefined) return false;
512
+ producer = observer.beginReporter?.();
513
+ producer?.start(seed, ordinal);
514
+ physicalOrdinal = ordinal;
515
+ if (!executing) { try { producer?.report(1); } finally { producer?.close(); } }
516
+ return true;
517
+ };
366
518
  let settled: "open" | "used" | "released" = "open";
367
- return {
368
- allowed: true,
369
- permit: {
370
- sendClass: intent.sendClass,
371
- use(): boolean {
372
- if (settled !== "open") return false;
373
- settled = "used";
374
- return true;
375
- },
376
- assumeCharge(): boolean {
377
- if (settled !== "open") return false;
378
- settled = "used";
379
- // The booking this reservation made for an external reporter is now owned by the
380
- // caller. Leaving it pending is not harmless: the next `used` report of this request
381
- // would settle against it and one real send would go uncharged.
382
- if (intent.countedExternally === true && counter.pendingExternalSends > 0) {
383
- counter.pendingExternalSends -= 1;
384
- }
385
- return true;
386
- },
387
- release(): void {
388
- if (settled !== "open") return;
389
- settled = "released";
390
- // An externally counted reservation the reporter already settled paid for a send
391
- // that physically happened. Refunding it would hand the request a free send back.
392
- if (intent.countedExternally === true) {
393
- if (counter.pendingExternalSends === 0) return;
394
- counter.pendingExternalSends -= 1;
395
- }
396
- counter.spent -= 1;
397
- observer?.refund();
398
- if (drawsReserve) reserveSpent = false;
399
- if (chargesAlternateTarget) alternateTargetSends -= 1;
400
- if (chargesTransition) targetTransitions -= 1;
401
- lastTargetKey = previousTargetKey;
402
- },
519
+ const permit: SingleUseDispatchPermit = {
520
+ sendClass: intent.sendClass,
521
+ async execute<T>(run: () => Promise<T>): Promise<T> {
522
+ if (executing) throw new Error("Dispatch permit executor already active");
523
+ executing = true;
524
+ const binding: PhysicalSendReporter = () => {};
525
+ binding.bind = (target: SpendScopes & { targetKey?: string }) => physicalOrdinal === undefined || !producer
526
+ ? true : producer.rebindTarget(target, physicalOrdinal);
527
+ try { return await activePhysicalReporters.run(binding, run); }
528
+ finally {
529
+ executing = false;
530
+ try { producer?.report(1); } finally { producer?.close(); }
531
+ }
532
+ },
533
+ use(): boolean {
534
+ if (settled !== "open") return false;
535
+ if (intent.countedExternally !== true && !startPhysical()) return false;
536
+ settled = "used";
537
+ if (intent.countedExternally !== true && spendProof) observer?.dispatch?.(spendProof);
538
+ return true;
539
+ },
540
+ assumeCharge(): boolean {
541
+ if (settled !== "open" || (intent.countedExternally === true && !counter.pendingExternalSends.has(receipt))) return false;
542
+ if (!startPhysical()) return false;
543
+ settled = "used";
544
+ // The booking this reservation made for an external reporter is now owned by the
545
+ // caller. Close only its own receipt so a later reporter cannot spend it again.
546
+ if (intent.countedExternally === true) counter.pendingExternalSends.delete(receipt);
547
+ if (spendProof) observer?.dispatch?.(spendProof);
548
+ return true;
549
+ },
550
+ release(): void {
551
+ if (settled !== "open") return;
552
+ settled = "released";
553
+ // An externally counted reservation the reporter already settled paid for a send
554
+ // that physically happened. Refunding it would hand the request a free send back.
555
+ if (intent.countedExternally === true) {
556
+ if (!counter.pendingExternalSends.delete(receipt)) return;
557
+ }
558
+ counter.spent -= 1;
559
+ observer?.refund(spendProof ?? null);
560
+ if (drawsReserve) reserveSpent = false;
561
+ if (chargesAlternateTarget) alternateTargetSends -= 1;
562
+ if (chargesTransition) targetTransitions -= 1;
563
+ targetReservations.splice(targetReservations.indexOf(receipt), 1);
564
+ lastTargetKey = targetReservations.at(-1)?.targetKey;
403
565
  },
404
566
  };
567
+ let preflightClaimed = false;
568
+ dispatchSpendProofs.set(permit, { owner: counter, report: () => {
569
+ if (!counter.pendingExternalSends.has(receipt)) return false;
570
+ if (spendProof) observer?.dispatch?.(spendProof);
571
+ counter.pendingExternalSends.delete(receipt);
572
+ // Reset-only helpers report just BEFORE calling the dispatch thunk. Its one use()
573
+ // remains available, but release/proof cannot refund or reuse this reported receipt.
574
+ return true;
575
+ }, claim: () => {
576
+ if (preflightClaimed || settled === "released"
577
+ || (intent.countedExternally === true ? !counter.pendingExternalSends.has(receipt) : settled !== "open")) return undefined;
578
+ preflightClaimed = true;
579
+ return spendProof;
580
+ } });
581
+ return { allowed: true, permit };
405
582
  },
406
583
  };
407
584
  sharedSendLedgers.set(budget, counter);
@@ -416,7 +593,8 @@ export function createRequestExecutionBudget(
416
593
  const grant = createAmbiguousResendGrant();
417
594
  return createRequestExecutionBudgetWithLedger(policy, logicalRequestId, {
418
595
  spent: 0,
419
- pendingExternalSends: 0,
596
+ physicalStarted: 0,
597
+ pendingExternalSends: new Set(),
420
598
  claimAmbiguousResend: grant.claimAmbiguousResend,
421
599
  get ambiguousResendSpent(): boolean { return grant.ambiguousResendSpent; },
422
600
  ...(observer ? { observer } : {}),
@@ -460,7 +638,7 @@ const bridgedGrantClaims = new WeakMap<RequestExecutionBudget, { claimed: boolea
460
638
  function ledgerFor(parent: RequestExecutionBudget): SharedSendLedger {
461
639
  const existing = sharedSendLedgers.get(parent);
462
640
  if (existing) return existing;
463
- let pendingExternalSends = 0;
641
+ const pendingExternalSends = new Set<object>();
464
642
  let bridged = bridgedGrantClaims.get(parent);
465
643
  if (!bridged) {
466
644
  bridged = { claimed: false };
@@ -470,8 +648,8 @@ function ledgerFor(parent: RequestExecutionBudget): SharedSendLedger {
470
648
  return {
471
649
  get spent(): number { return parent.used; },
472
650
  set spent(next: number) { parent.used = next; },
473
- get pendingExternalSends(): number { return pendingExternalSends; },
474
- set pendingExternalSends(next: number) { pendingExternalSends = next; },
651
+ pendingExternalSends,
652
+ physicalStarted: 0,
475
653
  // Asked of the parent rather than counted here. A local counter is a SECOND grant: two
476
654
  // scopes derived from one bridged parent, or one scope beside the parent it was derived
477
655
  // from, each replaced an unknown-state send once. Pending bookings and the durable-spend
@@ -0,0 +1,79 @@
1
+ import { createHash } from "node:crypto";
2
+ import { constants, lstatSync, openSync, fstatSync, readFileSync, closeSync, type Stats } from "node:fs";
3
+ import { join } from "node:path";
4
+
5
+ /** Read an existing single-owner salt without creating or hardening any file. */
6
+ export function readExistingSpendSalt(home: string): string | undefined {
7
+ let fd: number | undefined;
8
+ let salt: string | undefined;
9
+ try {
10
+ const path = join(home, "spend-ledger.salt");
11
+ const before = lstatSync(path);
12
+ if (!before.isFile() || before.nlink !== 1) return undefined;
13
+ const matches = (stat: Stats): boolean => stat.isFile() && stat.nlink === 1
14
+ && stat.dev === before.dev && stat.ino === before.ino;
15
+ fd = openSync(path, constants.O_RDONLY | (constants.O_NOFOLLOW ?? 0));
16
+ if (!matches(fstatSync(fd))) return undefined;
17
+ const contents = readFileSync(fd, "utf8").trim();
18
+ if (matches(fstatSync(fd)) && matches(lstatSync(path)) && /^[0-9a-f]{32,}$/.test(contents)) salt = contents;
19
+ } catch { salt = undefined; }
20
+ finally {
21
+ if (fd !== undefined) {
22
+ try { closeSync(fd); } catch { salt = undefined; }
23
+ }
24
+ }
25
+ return salt;
26
+ }
27
+
28
+ /**
29
+ * The one owner of the spend ledger's salted alias hash. The ledger, config diagnostics and
30
+ * runtime policy validation must all compute the same alias for the same (salt, domain, id),
31
+ * so the hash lives here and nowhere else. The raw id never leaves this function.
32
+ */
33
+ export function hashSpendAlias(salt: string, kind: string, id: string): string {
34
+ return createHash("sha256").update(salt).update("\u0000").update(kind).update("\u0000").update(id)
35
+ .digest("hex").slice(0, 32);
36
+ }
37
+
38
+ const isAlias = (value: unknown): value is string =>
39
+ typeof value === "string" && /^[0-9a-f]{32}$/.test(value);
40
+
41
+ /** Shape and optional roster check, outside `spend` so old schemas keep configured ceilings. */
42
+ export function spendPoolAliasesError(value: unknown, providerIds?: Iterable<string>): string | undefined {
43
+ if (value === undefined) return undefined;
44
+ if (!value || typeof value !== "object" || Array.isArray(value)) return "spendPoolAliases must be an object";
45
+ if (Object.keys(value).length > 4_096) return "spendPoolAliases has too many entries";
46
+ const providers = providerIds === undefined ? undefined : new Set(providerIds);
47
+ for (const [alias, provider] of Object.entries(value)) {
48
+ if (!isAlias(alias) || typeof provider !== "string" || !provider.trim() || provider.trim() !== provider || provider.length > 256) {
49
+ return "spendPoolAliases requires exact 32-character salted pool aliases and nonempty provider IDs";
50
+ }
51
+ if (providers && !providers.has(provider)) return "spendPoolAliases targets a provider that is not configured";
52
+ }
53
+ return undefined;
54
+ }
55
+
56
+ /**
57
+ * Semantic owner check: an alias that is the salted pool alias of a configured provider P
58
+ * belongs to P. Mapping it to any other provider Q would move P's own history into Q's pool,
59
+ * so `aliases[h("pool", P)] = Q` with Q !== P is rejected. Re-run whenever the provider set
60
+ * changes: adding P later turns an existing mapping into a conflict.
61
+ */
62
+ export function validatePoolAliasOwners(
63
+ aliases: unknown,
64
+ salt: string,
65
+ providerIds: Iterable<string>,
66
+ ): string | undefined {
67
+ const shape = spendPoolAliasesError(aliases);
68
+ if (shape) return shape;
69
+ if (aliases === undefined) return undefined;
70
+ const mapping = aliases as Record<string, string>;
71
+ for (const provider of new Set(providerIds)) {
72
+ const owned = hashSpendAlias(salt, "pool", provider);
73
+ const target = Object.hasOwn(mapping, owned) ? mapping[owned] : undefined;
74
+ if (target !== undefined && target !== provider) {
75
+ return "spendPoolAliases maps a configured provider's own pool alias to a different provider";
76
+ }
77
+ }
78
+ return undefined;
79
+ }
@@ -0,0 +1,34 @@
1
+ import { hashSpendAlias, spendPoolAliasesError, validatePoolAliasOwners } from "./spend-pool-alias-validation";
2
+ export { spendPoolAliasesError } from "./spend-pool-alias-validation";
3
+
4
+ /** Current config supplies a read-time graph; original counters never move between buckets. */
5
+ export function resolvePoolAliases(config: unknown, salt: string, providerIds?: Iterable<string>) {
6
+ const providers = providerIds === undefined ? undefined : [...providerIds];
7
+ const error = spendPoolAliasesError(config, providers) ?? validatePoolAliasOwners(config, salt, providers ?? []);
8
+ const bindings = new Map<string, string>();
9
+ if (!error) for (const [alias, provider] of Object.entries(config ?? {})) {
10
+ bindings.set(alias, hashSpendAlias(salt, "pool", provider as string));
11
+ }
12
+ // Current provider IDs own their ordinary v1 pool buckets; no identity record is written.
13
+ if (!error) for (const provider of providers ?? []) {
14
+ const alias = hashSpendAlias(salt, "pool", provider);
15
+ bindings.set(alias, alias);
16
+ }
17
+ const resolve = (original: string): string => {
18
+ let alias = original;
19
+ const seen = new Set<string>();
20
+ while (bindings.has(alias) && bindings.get(alias) !== alias) {
21
+ if (seen.has(alias)) throw new Error("cyclic pool alias mapping");
22
+ seen.add(alias);
23
+ alias = bindings.get(alias)!;
24
+ }
25
+ return alias;
26
+ };
27
+ let invalid = error !== undefined;
28
+ if (!invalid) {
29
+ try { for (const alias of bindings.keys()) resolve(alias); }
30
+ catch { invalid = true; }
31
+ }
32
+ if (invalid) bindings.clear();
33
+ return { valid: !invalid, resolve, known: (alias: string): boolean => bindings.has(alias) };
34
+ }