@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.
- package/README.md +3 -3
- package/gui/dist/assets/App-7yhoys8a.js +54 -0
- package/gui/dist/assets/Tray-EwAOsP0S.js +1 -0
- package/gui/dist/assets/index-RsoaAxXY.js +94 -0
- package/gui/dist/assets/quota-summary-D2lN8tDm.js +1 -0
- package/gui/dist/index.html +1 -1
- package/gui/dist/provider-icons/claude-green.svg +1 -0
- package/package.json +3 -2
- package/src/adapters/anthropic-model-contract.ts +8 -2
- package/src/adapters/anthropic.ts +7 -2
- package/src/adapters/codebuddy/mcp-server.ts +2 -25
- package/src/adapters/codebuddy/tool-bridge.ts +1 -7
- package/src/adapters/coding-agent/mcp-server.ts +33 -0
- package/src/adapters/coding-agent/protocol.ts +122 -17
- package/src/adapters/coding-agent/tool-catalog.ts +75 -0
- package/src/adapters/coding-agent/turn.ts +101 -44
- package/src/adapters/cursor/catalog.ts +8 -0
- package/src/adapters/cursor/effort-map.ts +2 -0
- package/src/adapters/cursor/live-transport.ts +5 -3
- package/src/adapters/cursor/native-exec-fs.ts +12 -9
- package/src/adapters/cursor/native-exec.ts +13 -5
- package/src/adapters/cursor/protobuf-request.ts +13 -7
- package/src/adapters/cursor/request-builder.ts +23 -10
- package/src/adapters/cursor/tool-definitions.ts +9 -6
- package/src/adapters/cursor/tool-guidance.ts +29 -8
- package/src/adapters/cursor/tool-naming.ts +40 -7
- package/src/adapters/cursor/tool-wording.ts +46 -0
- package/src/adapters/cursor.ts +3 -0
- package/src/adapters/devin/live-models.ts +2 -0
- package/src/adapters/devin.ts +25 -11
- package/src/adapters/kiro-retry.ts +9 -4
- package/src/adapters/openai-chat/messages.ts +7 -4
- package/src/adapters/physical-send.ts +8 -5
- package/src/adapters/qoder/adapter.ts +41 -6
- package/src/adapters/qoder/scaffold-guard.ts +2 -3
- package/src/adapters/zed.ts +16 -1
- package/src/claude/inbound-cache-stabilize.ts +4 -9
- package/src/claude/inbound.ts +10 -0
- package/src/claude/intercept/model-bindings.ts +1 -0
- package/src/claude/intercept/picker-listener.ts +71 -4
- package/src/cli/account-anthropic-threshold.ts +14 -5
- package/src/cli/account-api.ts +3 -1
- package/src/cli/account-extended.ts +13 -9
- package/src/cli/account-policy-dto.ts +1 -1
- package/src/cli/account-policy.ts +13 -4
- package/src/cli/account.ts +3 -3
- package/src/cli/capabilities-accounts.ts +2 -2
- package/src/cli/capabilities-agents-routing.ts +24 -0
- package/src/cli/capabilities-integrations.ts +16 -0
- package/src/cli/codex-shim-autorestore.ts +3 -0
- package/src/cli/dispatch.ts +10 -1
- package/src/cli/help.ts +4 -1
- package/src/cli/index.ts +11 -5
- package/src/cli/integration-plan-dto.ts +1 -0
- package/src/cli/integration-preview.ts +8 -1
- package/src/cli/integrations.ts +32 -3
- package/src/cli/message-args.ts +34 -0
- package/src/cli/message-command.ts +43 -0
- package/src/cli/message-runtime.ts +6 -0
- package/src/cli/observe.ts +3 -1
- package/src/cli/provider.ts +29 -6
- package/src/cli/registry.ts +24 -2
- package/src/clients/config-export/commandcode.ts +131 -0
- package/src/clients/config-export/contracts.ts +12 -0
- package/src/clients/config-export.ts +64 -1
- package/src/codex/auth-api/account-list.ts +11 -3
- package/src/codex/auth-api/main-account-probe.ts +25 -8
- package/src/codex/auth-api/pool-mode-gate.ts +33 -9
- package/src/codex/auth-api/pool-quota-probe.ts +15 -1
- package/src/codex/auth-api/routes.ts +1 -0
- package/src/codex/autostart-health.ts +3 -3
- package/src/codex/catalog/auto-review.ts +13 -6
- package/src/codex/catalog/build-entries.ts +15 -7
- package/src/codex/catalog/control-plane.ts +45 -0
- package/src/codex/catalog/effort.ts +11 -1
- package/src/codex/catalog/gather-capture.ts +5 -3
- package/src/codex/catalog/parsing.ts +2 -1
- package/src/codex/catalog/provider-models.ts +15 -9
- package/src/codex/control-plane-models.ts +6 -0
- package/src/codex/journal.ts +36 -5
- package/src/codex/shim-overlay.ts +9 -1
- package/src/combos/jev-decision-contract.ts +25 -2
- package/src/combos/jev.ts +6 -3
- package/src/combos/types.ts +1 -1
- package/src/config/diagnostics.ts +27 -16
- package/src/config/load-degrade.ts +34 -0
- package/src/config/schema/anthropic-account-pool.ts +143 -0
- package/src/config/schema/config-schema.ts +7 -1
- package/src/config/schema/leaf-validators.ts +4 -0
- package/src/config.ts +2 -0
- package/src/generated/compatibility-version.json +347 -199
- package/src/generated/model-metadata.ts +5 -4
- package/src/images/loop.ts +2 -1
- package/src/integrations/catalog-refresh.ts +1 -1
- package/src/integrations/mutation-plan.ts +7 -0
- package/src/integrations/registry.ts +8 -0
- package/src/integrations/state.ts +9 -2
- package/src/integrations/writer.ts +8 -2
- package/src/lib/lab-live-route-production.ts +9 -1
- package/src/lib/request-execution-budget.ts +236 -58
- package/src/lib/spend-pool-alias-validation.ts +79 -0
- package/src/lib/spend-pool-continuity.ts +34 -0
- package/src/lib/spend-reservation-ledger.ts +412 -50
- package/src/lib/state-store-registrations.ts +20 -3
- package/src/lib/upstream-retry.ts +17 -5
- package/src/lib/workflow-budget.ts +53 -11
- package/src/messaging/budget.ts +45 -0
- package/src/messaging/discovery.ts +57 -0
- package/src/messaging/envelope.ts +49 -0
- package/src/messaging/input.ts +37 -0
- package/src/messaging/rpc.ts +196 -0
- package/src/messaging/send.ts +58 -0
- package/src/messaging/socket.ts +72 -0
- package/src/messaging/types.ts +29 -0
- package/src/oauth/anthropic-account-refusal.ts +51 -17
- package/src/oauth/anthropic-account-threshold.ts +8 -2
- package/src/oauth/anthropic-continuity.ts +15 -1
- package/src/oauth/anthropic-model-quota.ts +120 -48
- package/src/oauth/anthropic-model-routes.ts +11 -3
- package/src/oauth/anthropic-oauth-definitions.ts +98 -0
- package/src/oauth/anthropic-pool-config.ts +30 -0
- package/src/oauth/anthropic-rate-limit-policy.ts +64 -9
- package/src/oauth/anthropic-routing.ts +1202 -866
- package/src/oauth/anthropic-send-ownership.ts +41 -0
- package/src/oauth/anthropic.ts +4 -1
- package/src/oauth/generic-account-failover.ts +3 -1
- package/src/oauth/health.ts +8 -6
- package/src/oauth/index.ts +90 -173
- package/src/oauth/kiro.ts +22 -9
- package/src/oauth/login-cli.ts +2 -0
- package/src/oauth/model-discovery-auth.ts +43 -0
- package/src/oauth/pool-kernel.ts +11 -5
- package/src/oauth/pool-settings-capability.ts +5 -3
- package/src/oauth/store-anthropic-instance.ts +87 -0
- package/src/oauth/store.ts +67 -9
- package/src/oauth/token-guardian.ts +2 -0
- package/src/oauth/types.ts +2 -0
- package/src/protocols/plan-snapshot.ts +5 -0
- package/src/protocols/settings.ts +20 -5
- package/src/providers/anthropic-instance-id.ts +47 -0
- package/src/providers/anthropic-instance.ts +48 -0
- package/src/providers/anthropic-reset-grant-ledger.ts +11 -1
- package/src/providers/derive.ts +2 -0
- package/src/providers/kiro-models.ts +3 -0
- package/src/providers/label.ts +1 -1
- package/src/providers/model-rename-fields.ts +2 -0
- package/src/providers/quota/account-cache.ts +37 -11
- package/src/providers/quota/anthropic-account-quota.ts +93 -0
- package/src/providers/quota/anthropic-cooldown-recovery.ts +222 -111
- package/src/providers/quota/report-cache.ts +24 -1
- package/src/providers/quota/vendor-probes-oauth.ts +46 -14
- package/src/providers/quota.ts +22 -32
- package/src/providers/registry/entries-core.ts +35 -30
- package/src/providers/registry/model-ids.ts +1 -0
- package/src/providers/registry/model-seeds.ts +7 -4
- package/src/providers/registry/types.ts +3 -0
- package/src/providers/registry.ts +3 -3
- package/src/responses/spill-store.ts +5 -1
- package/src/responses/state/metrics.ts +87 -0
- package/src/responses/state/snapshot-codec.ts +6 -0
- package/src/responses/state/spill-queue.ts +1 -0
- package/src/responses/state/unforced-store-false.ts +55 -0
- package/src/responses/state.ts +50 -66
- package/src/router.ts +28 -4
- package/src/routing/compatibility/assemble.ts +1 -1
- package/src/routing/quota.ts +7 -3
- package/src/server/auth-cors.ts +5 -0
- package/src/server/chat-completions.ts +6 -1
- package/src/server/chat-native.ts +45 -25
- package/src/server/claude-messages.ts +16 -2
- package/src/server/gui-static.ts +38 -4
- package/src/server/index/spend-ledger-lifecycle.ts +10 -4
- package/src/server/index.ts +16 -16
- package/src/server/inference/context.ts +1 -1
- package/src/server/lifecycle.ts +1 -1
- package/src/server/management/agent-settings-routes.ts +30 -6
- package/src/server/management/anthropic-account-threshold.ts +12 -9
- package/src/server/management/anthropic-pool-settings.ts +72 -0
- package/src/server/management/anthropic-reset-grant-routes.ts +98 -39
- package/src/server/management/config-routes.ts +27 -7
- package/src/server/management/decision-routes.ts +2 -1
- package/src/server/management/integration-routes.ts +25 -2
- package/src/server/management/oauth-account-routes.ts +79 -98
- package/src/server/management/provider-instance-ownership.ts +44 -0
- package/src/server/management/provider-routes.ts +21 -28
- package/src/server/management/route-registry.ts +3 -1
- package/src/server/management/routing-profile-routes.ts +4 -4
- package/src/server/management/system-restart.ts +44 -14
- package/src/server/management/system-routes.ts +24 -2
- package/src/server/management/vision-sidecar-options.ts +9 -6
- package/src/server/management/web-search-sidecar-options.ts +51 -2
- package/src/server/management-auth.ts +6 -0
- package/src/server/messages-native-eligibility.ts +3 -2
- package/src/server/messages-native-oauth.ts +86 -41
- package/src/server/messages-native-scope.ts +4 -1
- package/src/server/messages-native-selector.ts +39 -0
- package/src/server/messages-native.ts +84 -25
- package/src/server/request-log.ts +5 -0
- package/src/server/responses/adapter-continuation.ts +29 -17
- package/src/server/responses/adapter-dispatch.ts +47 -17
- package/src/server/responses/antigravity-validation-refusal.ts +20 -6
- package/src/server/responses/combo-requested-effort.ts +14 -0
- package/src/server/responses/compact.ts +25 -8
- package/src/server/responses/compaction-recovery.ts +1 -1
- package/src/server/responses/core-combo-native.ts +4 -1
- package/src/server/responses/core-combo.ts +21 -18
- package/src/server/responses/core-normalize.ts +1 -0
- package/src/server/responses/core-options.ts +3 -1
- package/src/server/responses/core-replay.ts +5 -0
- package/src/server/responses/passthrough-delivery.ts +7 -3
- package/src/server/responses/passthrough-dispatch.ts +21 -8
- package/src/server/responses/request-prepare.ts +37 -5
- package/src/server/responses/request-send-budget.ts +74 -37
- package/src/server/responses/request-spend.ts +325 -33
- package/src/server/responses/request-transport.ts +132 -52
- package/src/server/responses/run-turn-execution.ts +11 -3
- package/src/server/responses/sidecar-execution.ts +96 -14
- package/src/server/responses-undeclared-tool-guard.ts +5 -1
- package/src/server/workflow-refusal.ts +30 -0
- package/src/service/desktop-startup.ts +100 -3
- package/src/service/ownership-mutation-lease.d.mts +1 -1
- package/src/service/ownership-mutation-lease.mjs +5 -7
- package/src/sidecar/anthropic-binding.ts +79 -0
- package/src/sidecar/auth.ts +76 -2
- package/src/types/anthropic-account-pool.ts +33 -0
- package/src/types/config.ts +12 -28
- package/src/types/provider.ts +5 -0
- package/src/types/tools.ts +7 -0
- package/src/types.ts +4 -0
- package/src/usage/cost.ts +3 -2
- package/src/usage/expected-prices.ts +66 -14
- package/src/vision/anthropic-describe.ts +16 -3
- package/src/vision/index.ts +3 -0
- package/src/vision/plan.ts +19 -6
- package/src/web-search/alpha-search.ts +7 -1
- package/src/web-search/anthropic-executor.ts +16 -4
- package/src/web-search/header-deadline.ts +67 -0
- package/src/web-search/index.ts +7 -3
- package/src/web-search/loop.ts +32 -10
- package/src/web-search/passthrough-bridge.ts +4 -1
- package/src/web-search/sidecar-providers.ts +4 -3
- package/gui/dist/assets/App-BOUa-ek3.js +0 -54
- package/gui/dist/assets/Tray-DUtzB4zF.js +0 -1
- package/gui/dist/assets/index-DwHNDF4s.js +0 -94
- package/gui/dist/assets/quota-summary-BuL4WIBo.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
|
-
|
|
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
|
-
//
|
|
289
|
-
//
|
|
290
|
-
//
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
}
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
474
|
-
|
|
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
|
+
}
|