@bitkyc08/opencodex 2.80.0 → 2.82.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/bin/ocx.mjs +166 -73
- package/gui/dist/assets/App-Ds_qL7o8.js +54 -0
- package/gui/dist/assets/Tray-BqHF5Trd.js +1 -0
- package/gui/dist/assets/{index-Brk6F3fn.css → index-BAbcj51J.css} +1 -1
- package/gui/dist/assets/index-D3FBCNiP.js +94 -0
- package/gui/dist/assets/quota-summary-DTBC8n7-.js +1 -0
- package/gui/dist/index.html +2 -2
- 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 +8 -3
- 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/devin/live-models.ts +2 -0
- package/src/adapters/kiro-retry.ts +9 -4
- package/src/adapters/openai-chat/messages.ts +7 -6
- package/src/adapters/openai-responses/compaction-search-history.ts +109 -0
- package/src/adapters/openai-responses/passthrough.ts +9 -4
- package/src/adapters/openai-responses/tool-output-recovery.ts +20 -0
- 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/agents-inject.ts +14 -3
- package/src/claude/context-windows.ts +19 -17
- package/src/claude/desktop-3p.ts +88 -12
- package/src/claude/desktop-profile.ts +2 -1
- package/src/claude/gateway-cache.ts +18 -1
- package/src/claude/inbound-cache-stabilize.ts +4 -9
- package/src/claude/inbound.ts +78 -3
- package/src/claude/intercept/cli-catalog.ts +3 -0
- package/src/claude/intercept/model-bindings.ts +1 -0
- package/src/claude/intercept/picker-ca-persistence.ts +7 -6
- package/src/claude/intercept/picker-listener.ts +71 -4
- package/src/claude/intercept/picker-models.ts +14 -6
- package/src/claude/intercept/settings.ts +3 -0
- package/src/claude/long-context.ts +66 -0
- package/src/claude/model-info.ts +19 -16
- package/src/claude/outbound.ts +94 -9
- package/src/claude/subagent-model.ts +40 -11
- 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/agent.ts +9 -5
- package/src/cli/aside-profile-recovery.ts +33 -0
- package/src/cli/capabilities-accounts.ts +2 -2
- package/src/cli/capabilities-agents-routing.ts +24 -0
- package/src/cli/capabilities-base.ts +5 -5
- package/src/cli/capabilities-integrations.ts +18 -2
- package/src/cli/cli-path-diagnostics.ts +132 -0
- package/src/cli/codex-shim-autorestore.ts +3 -0
- package/src/cli/desktop-runtime-guidance.ts +65 -0
- package/src/cli/dispatch.ts +15 -1
- package/src/cli/doctor.ts +16 -3
- package/src/cli/help.ts +4 -1
- package/src/cli/index.ts +25 -14
- package/src/cli/integration-aside-sync.ts +2 -1
- package/src/cli/integration-plan-dto.ts +1 -0
- package/src/cli/integration-preview.ts +8 -1
- package/src/cli/integrations.ts +48 -6
- 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/cli/resolve.ts +25 -2
- package/src/cli/root.ts +4 -2
- package/src/cli/status.ts +64 -12
- package/src/cli/update-restart-child.ts +4 -2
- package/src/cli/update-restart-home.ts +28 -10
- package/src/cli/update-restart-service-record.ts +116 -0
- package/src/cli/update-restart-supervision.ts +96 -0
- package/src/cli/update-restart.ts +59 -13
- package/src/cli/v2.ts +27 -20
- package/src/cli/version-skew-notice.ts +77 -0
- 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/clients/lazycodex.ts +1 -1
- package/src/clients/omo-role-models.ts +32 -7
- package/src/codex/auth-api/account-list.ts +11 -3
- package/src/codex/auth-api/account-selection.ts +107 -0
- 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 +11 -49
- package/src/codex/auth-context.ts +20 -1
- package/src/codex/autostart-health.ts +12 -3
- package/src/codex/catalog/auto-review.ts +13 -6
- package/src/codex/catalog/build-entries.ts +33 -7
- package/src/codex/catalog/bundled.ts +155 -26
- package/src/codex/catalog/combo-member.ts +3 -2
- package/src/codex/catalog/control-plane.ts +45 -0
- package/src/codex/catalog/effort.ts +37 -6
- package/src/codex/catalog/gather-capture.ts +10 -3
- package/src/codex/catalog/model-hints.ts +15 -8
- package/src/codex/catalog/model-visibility.ts +3 -1
- package/src/codex/catalog/operator-tiers.ts +38 -0
- package/src/codex/catalog/parsing.ts +2 -1
- package/src/codex/catalog/provider-models.ts +58 -44
- package/src/codex/catalog/retained-sync.ts +7 -5
- package/src/codex/catalog/routed-gather.ts +15 -1
- package/src/codex/catalog-auto-refresh-sources.ts +3 -4
- package/src/codex/catalog-auto-refresh.ts +19 -0
- package/src/codex/catalog.ts +1 -1
- package/src/codex/codex-home-owner.ts +3 -0
- package/src/codex/config-write-lock.ts +333 -0
- package/src/codex/control-plane-models.ts +6 -0
- package/src/codex/convergence.ts +2 -1
- package/src/codex/features.ts +257 -200
- package/src/codex/inject/config-write-section.ts +34 -0
- package/src/codex/inject/multi-agent-v2.ts +14 -6
- package/src/codex/inject/remove.ts +36 -11
- package/src/codex/inject/restore.ts +85 -20
- package/src/codex/inject-coordination.ts +15 -6
- package/src/codex/inject.ts +62 -20
- package/src/codex/journal.ts +62 -17
- package/src/codex/main-account-cache.ts +43 -0
- package/src/codex/paths.ts +2 -0
- package/src/codex/prepared-home.ts +18 -0
- package/src/codex/prompt-journal.ts +25 -12
- package/src/codex/prompt-layers.ts +49 -26
- package/src/codex/prompt-lock-claim.ts +134 -0
- package/src/codex/prompt-lock-evidence.ts +26 -0
- package/src/codex/prompt-lock-io.ts +16 -0
- package/src/codex/prompt-lock-owner.ts +130 -0
- package/src/codex/prompt-lock.ts +84 -53
- package/src/codex/routing/failure-window.ts +146 -0
- package/src/codex/routing/selection.ts +4 -1
- package/src/codex/routing.ts +13 -17
- package/src/codex/runtime.ts +106 -18
- package/src/codex/shim.ts +45 -19
- package/src/combos/jev-decision-contract.ts +25 -2
- package/src/combos/jev-service-exchange.ts +281 -0
- package/src/combos/jev.ts +34 -219
- package/src/combos/types.ts +1 -1
- package/src/config/atomic-write.ts +1 -1
- package/src/config/codex-ws-reuse-setting.ts +10 -0
- package/src/config/diagnostics.ts +27 -16
- package/src/config/live-reconcile.ts +17 -0
- package/src/config/load-degrade.ts +42 -0
- package/src/config/rebase-provenance.ts +11 -0
- package/src/config/schema/anthropic-account-pool.ts +143 -0
- package/src/config/schema/config-schema.ts +11 -1
- package/src/config/schema/leaf-validators.ts +4 -0
- package/src/config.ts +2 -0
- package/src/generated/compatibility-version.json +566 -298
- package/src/generated/model-metadata.ts +7 -6
- package/src/grok/inject.ts +50 -4
- package/src/grok/sync.ts +2 -1
- package/src/images/loop.ts +30 -11
- package/src/integrations/aside-profiles.ts +10 -3
- package/src/integrations/catalog-refresh.ts +19 -3
- 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 +17 -5
- package/src/lib/bun-path-runtime.d.mts +32 -0
- package/src/lib/bun-path-runtime.mjs +95 -0
- package/src/lib/bun-runtime-preflight.ts +140 -0
- package/src/lib/desktop-cli-handoff.d.mts +20 -0
- package/src/lib/desktop-cli-handoff.mjs +92 -0
- package/src/lib/desktop-cli-record.d.mts +29 -0
- package/src/lib/desktop-cli-record.mjs +102 -0
- package/src/lib/lab-live-route-production.ts +9 -1
- package/src/lib/request-execution-budget.ts +244 -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/synced-state-location.ts +46 -6
- package/src/lib/test-home-guard.ts +87 -1
- package/src/lib/translator-budget.ts +17 -4
- package/src/lib/upstream-retry.ts +17 -5
- package/src/lib/windows-atomic-replace.ts +38 -16
- package/src/lib/windows-elevation.ts +6 -0
- package/src/lib/windows-secret-acl.ts +92 -25
- package/src/lib/winsw.ts +2 -3
- 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 +90 -18
- 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/chatgpt.ts +113 -41
- package/src/oauth/generic-account-failover.ts +3 -1
- package/src/oauth/health.ts +8 -6
- package/src/oauth/index.ts +96 -175
- 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 +68 -10
- 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/azure-model-metadata.ts +115 -0
- package/src/providers/derive.ts +23 -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-key.ts +127 -17
- package/src/providers/quota/vendor-probes-oauth.ts +46 -14
- package/src/providers/quota.ts +23 -33
- 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/reasoning-envelope.ts +29 -1
- package/src/responses/reasoning-replay-cache.ts +17 -1
- package/src/responses/spill-store.ts +20 -3
- package/src/responses/state/metrics.ts +95 -0
- package/src/responses/state/snapshot-codec.ts +6 -0
- package/src/responses/state/spill-failure.ts +12 -0
- package/src/responses/state/spill-queue.ts +13 -5
- package/src/responses/state/unforced-store-false.ts +55 -0
- package/src/responses/state.ts +68 -76
- package/src/responses/task-input.ts +17 -0
- 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/background-lifecycle.ts +27 -0
- package/src/server/chat-completions.ts +7 -1
- package/src/server/chat-native.ts +64 -28
- package/src/server/claude-messages.ts +59 -8
- package/src/server/gui-static.ts +38 -4
- package/src/server/inbound-body-admission.ts +1 -0
- package/src/server/index/claude-intercept-lifecycle.ts +3 -1
- package/src/server/index/serve-options.ts +10 -2
- package/src/server/index/spend-ledger-lifecycle.ts +12 -5
- package/src/server/index/startup-warnings.ts +19 -0
- package/src/server/index.ts +5 -2
- package/src/server/inference/context.ts +1 -1
- package/src/server/lifecycle.ts +1 -1
- package/src/server/management/agent-settings-routes.ts +123 -38
- 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/codex-agent-role-routes.ts +4 -2
- package/src/server/management/codex-prompt-routes.ts +1 -0
- package/src/server/management/codex-role-auto-assign.ts +13 -2
- package/src/server/management/config-routes.ts +85 -14
- package/src/server/management/context.ts +2 -2
- 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/shared.ts +3 -3
- 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 +91 -45
- package/src/server/messages-native-selector.ts +39 -0
- package/src/server/messages-native.ts +133 -38
- package/src/server/messages-response-headers.ts +14 -0
- package/src/server/request-log-conversation.ts +48 -5
- package/src/server/request-log.ts +36 -2
- package/src/server/responses/adapter-continuation.ts +31 -19
- package/src/server/responses/adapter-dispatch.ts +75 -24
- package/src/server/responses/antigravity-validation-refusal.ts +20 -6
- package/src/server/responses/codex-ws-exchange.ts +17 -2
- package/src/server/responses/codex-ws-pool.ts +100 -13
- package/src/server/responses/codex-ws-wire.ts +9 -0
- package/src/server/responses/combo-requested-effort.ts +3 -1
- package/src/server/responses/compact.ts +25 -8
- package/src/server/responses/compaction-recovery.ts +1 -1
- package/src/server/responses/core-codex-account.ts +22 -1
- package/src/server/responses/core-combo-native.ts +24 -4
- package/src/server/responses/core-combo.ts +38 -25
- package/src/server/responses/core-lifetime.ts +3 -36
- package/src/server/responses/core-normalize.ts +1 -0
- package/src/server/responses/core-options.ts +8 -1
- package/src/server/responses/core-replay.ts +78 -0
- package/src/server/responses/core.ts +3 -3
- package/src/server/responses/fetch-helpers.ts +26 -3
- package/src/server/responses/passthrough-delivery.ts +30 -4
- package/src/server/responses/passthrough-dispatch.ts +39 -9
- package/src/server/responses/request-prepare.ts +41 -5
- package/src/server/responses/request-send-budget.ts +150 -50
- package/src/server/responses/request-spend.ts +325 -33
- package/src/server/responses/request-transport.ts +132 -52
- package/src/server/responses/response-effects.ts +1 -0
- package/src/server/responses/run-turn-execution.ts +13 -3
- package/src/server/responses/sidecar-execution.ts +113 -22
- package/src/server/responses/sidecar-send-budget.ts +58 -0
- package/src/server/responses/ws-upstream.ts +19 -6
- package/src/server/responses-undeclared-tool-guard.ts +5 -1
- package/src/server/startup-health-cache.ts +2 -1
- package/src/server/workflow-refusal.ts +33 -0
- package/src/service/cli.ts +20 -4
- package/src/service/desktop-command-guard.ts +32 -0
- package/src/service/desktop-startup.ts +66 -40
- package/src/service/desktop-supervision.d.mts +26 -0
- package/src/service/desktop-supervision.mjs +140 -0
- package/src/service/managing-cli.ts +1 -0
- package/src/service/orchestration.ts +46 -20
- package/src/service/ownership-mutation-lease.d.mts +1 -1
- package/src/service/ownership-mutation-lease.mjs +5 -7
- package/src/service/repair.ts +25 -9
- package/src/service/state.ts +4 -3
- package/src/service/systemd.ts +17 -8
- package/src/service/windows-ops.ts +61 -22
- package/src/sidecar/anthropic-binding.ts +79 -0
- package/src/sidecar/auth.ts +76 -2
- package/src/storage/policy-job.ts +5 -1
- package/src/storage/policy-worker.ts +3 -1
- package/src/storage/policy.ts +9 -0
- package/src/types/anthropic-account-pool.ts +33 -0
- package/src/types/config.ts +33 -28
- package/src/types/provider.ts +5 -0
- package/src/types/request.ts +5 -0
- package/src/types/tools.ts +7 -0
- package/src/types/wire.ts +1 -0
- package/src/types.ts +4 -0
- package/src/update/index.ts +57 -9
- package/src/update/restart-ownership.ts +10 -3
- package/src/update/runtime-ownership.d.mts +6 -0
- package/src/update/runtime-ownership.mjs +23 -7
- 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/index.ts +7 -3
- package/src/web-search/loop.ts +32 -7
- package/src/web-search/passthrough-bridge.ts +4 -1
- package/src/web-search/sidecar-providers.ts +4 -3
- package/gui/dist/assets/App-MbDVfUeC.js +0 -54
- package/gui/dist/assets/Tray-CzkxK82A.js +0 -1
- package/gui/dist/assets/index-UJt6npXk.js +0 -94
- package/gui/dist/assets/quota-summary-DU3sD6Z-.js +0 -1
|
@@ -54,7 +54,7 @@
|
|
|
54
54
|
*/
|
|
55
55
|
|
|
56
56
|
import { appendFileSync, chmodSync, closeSync, lstatSync, mkdirSync, openSync, readFileSync, renameSync, statSync, unlinkSync, writeFileSync } from "node:fs";
|
|
57
|
-
import {
|
|
57
|
+
import { randomBytes } from "node:crypto";
|
|
58
58
|
import { dirname, join } from "node:path";
|
|
59
59
|
// Definition-site import, not the ../config barrel -- same reasoning as
|
|
60
60
|
// src/quota/reset-seen-store.ts: the barrel pulls ~154 modules into a hot path.
|
|
@@ -62,6 +62,8 @@ import { getConfigDir } from "../config/paths";
|
|
|
62
62
|
// Type-only, so it is erased before this module has a runtime import graph at all. The
|
|
63
63
|
// config SHAPE is what this file needs; the config loader is what the note above keeps out.
|
|
64
64
|
import type { OcxSpendConfig, OcxSpendScopeConfig } from "../types/config";
|
|
65
|
+
import { resolvePoolAliases } from "./spend-pool-continuity";
|
|
66
|
+
import { hashSpendAlias, validatePoolAliasOwners } from "./spend-pool-alias-validation";
|
|
65
67
|
import { assertNotRealHomeUnderTest } from "./test-home-guard";
|
|
66
68
|
// Windows chmod does not remove inherited ACEs; this is the repository's icacls path.
|
|
67
69
|
import { hardenSecretPath } from "./windows-secret-acl";
|
|
@@ -93,6 +95,9 @@ export interface SpendScopeLimit {
|
|
|
93
95
|
}
|
|
94
96
|
|
|
95
97
|
export interface SpendReservationPolicy {
|
|
98
|
+
/** Exact historical salted pool alias -> canonical provider. Malformed input fails closed. */
|
|
99
|
+
readonly poolAliases?: unknown;
|
|
100
|
+
readonly canonicalProviderIds?: readonly string[];
|
|
96
101
|
readonly root: SpendScopeLimit;
|
|
97
102
|
readonly identity: SpendScopeLimit;
|
|
98
103
|
readonly pool: SpendScopeLimit;
|
|
@@ -147,7 +152,11 @@ export interface SpendUsage {
|
|
|
147
152
|
readonly outputTokens: number;
|
|
148
153
|
}
|
|
149
154
|
|
|
155
|
+
/** Immutable numeric ceilings captured by one request; totals and capacity remain shared. */
|
|
156
|
+
export type SpendAdmissionPolicy = Readonly<Pick<SpendReservationPolicy, "root" | "identity" | "pool">>;
|
|
157
|
+
|
|
150
158
|
export interface SpendReservationRequest {
|
|
159
|
+
readonly admissionPolicy?: SpendAdmissionPolicy;
|
|
151
160
|
/** Stable id of the physical send. Settlement is idempotent on this key. */
|
|
152
161
|
readonly sendId: string;
|
|
153
162
|
readonly scopes: SpendScopes;
|
|
@@ -181,12 +190,15 @@ export interface SpendReservationRequest {
|
|
|
181
190
|
* to buy an unlimited number of physical sends while the scope totals never moved.
|
|
182
191
|
*/
|
|
183
192
|
export type SpendDenial =
|
|
193
|
+
| { readonly reason: "pool-history-unresolved" }
|
|
184
194
|
| {
|
|
185
195
|
readonly reason: "spend-limit-exceeded";
|
|
186
196
|
readonly scope: SpendScope;
|
|
187
197
|
readonly scopeId: string;
|
|
188
198
|
readonly limit: number;
|
|
189
199
|
readonly projected: number;
|
|
200
|
+
/** Pool checks include all unbound positive history until an operator verifies its owner. */
|
|
201
|
+
readonly includesUnboundPoolHistory?: boolean;
|
|
190
202
|
}
|
|
191
203
|
/** This send id is already known -- open, settled, lost or abandoned. */
|
|
192
204
|
| { readonly reason: "duplicate-send-id"; readonly sendId: string }
|
|
@@ -606,8 +618,30 @@ export interface ScopeSpendSnapshot {
|
|
|
606
618
|
readonly exhausted: boolean;
|
|
607
619
|
}
|
|
608
620
|
|
|
621
|
+
/** In-memory proof of one booked send; never journaled or exposed in request logs. */
|
|
622
|
+
export interface SpendReservationProof {
|
|
623
|
+
readonly ledger: SpendReservationLedger;
|
|
624
|
+
readonly sendId: string;
|
|
625
|
+
}
|
|
626
|
+
|
|
627
|
+
export interface SpendSeed { readonly sendId: string }
|
|
628
|
+
export type SpendSeedDecision = { reserved: true; seed: SpendSeed } | { reserved: false; denial: SpendDenial };
|
|
629
|
+
|
|
609
630
|
export interface SpendReservationLedger {
|
|
631
|
+
/** Tracker owners isolate cleanup barriers; omitted owners retain the installation barrier. */
|
|
632
|
+
reserveSeed(request: SpendReservationRequest, owner?: object): SpendSeedDecision;
|
|
633
|
+
reserveReportedFromSeed(seed: SpendSeed, request: { sendId: string; inputTokens: number; outputCeilingTokens: number }): boolean;
|
|
634
|
+
forgetResolved(sendIds: readonly string[], owner?: object): boolean;
|
|
635
|
+
finishSeed(seed: SpendSeed, owner?: object): boolean;
|
|
636
|
+
registerReporter(owner?: object): { close(): void };
|
|
637
|
+
/** Retry completed tracker cleanup on later admission or maintenance after storage recovers. */
|
|
638
|
+
deferCleanup(owner: object, cleanup: () => boolean): void;
|
|
639
|
+
waitForReporterDrain(): Promise<void>;
|
|
610
640
|
reserve(request: SpendReservationRequest): SpendReservationDecision;
|
|
641
|
+
/** Pre-dispatch guard, including transports which report their sends after dispatch. */
|
|
642
|
+
checkPoolContinuity(): SpendDenial | undefined;
|
|
643
|
+
/** Whether any positive pool balance is still unbound and therefore charged to each candidate pool. */
|
|
644
|
+
hasUnboundPositivePoolHistory(): boolean;
|
|
611
645
|
/**
|
|
612
646
|
* The send left for upstream. Until this is called the reservation may be abandoned for
|
|
613
647
|
* free; after it, a missing usage frame becomes unresolved spend. Returns false when the
|
|
@@ -624,6 +658,7 @@ export interface SpendReservationLedger {
|
|
|
624
658
|
/**
|
|
625
659
|
* Settle with real usage. Returns false when the send is unknown or already resolved --
|
|
626
660
|
* double settlement is as wrong as none, so a repeat call changes nothing.
|
|
661
|
+
* Seeded terminal debt applies immediately; failed writes queue before durable forgetting.
|
|
627
662
|
*/
|
|
628
663
|
settle(sendId: string, usage: SpendUsage): boolean;
|
|
629
664
|
/**
|
|
@@ -632,7 +667,8 @@ export interface SpendReservationLedger {
|
|
|
632
667
|
*/
|
|
633
668
|
markLost(sendId: string): boolean;
|
|
634
669
|
snapshot(scope: SpendScope, scopeId: string): ScopeSpendSnapshot | undefined;
|
|
635
|
-
|
|
670
|
+
/** Exclude only a proven current dispatch's still-open reservation in this scope. */
|
|
671
|
+
exhausted(scope: SpendScope, scopeId: string, excludingSendId?: string, admissionPolicy?: SpendAdmissionPolicy): boolean;
|
|
636
672
|
/**
|
|
637
673
|
* Drop dormant scopes per the retention rule in SpendReservationPolicy. Cleanup also runs
|
|
638
674
|
* automatically on every reservation, so nothing depends on a caller remembering this.
|
|
@@ -701,6 +737,31 @@ export function createSpendReservationLedger(options: {
|
|
|
701
737
|
const compactAfterRecords = (): number => policy.compactAfterRecords ?? DEFAULT_COMPACT_AFTER_RECORDS;
|
|
702
738
|
const scopes = new Map<string, ScopeState>();
|
|
703
739
|
const reservations = new Map<string, Reservation>();
|
|
740
|
+
// Object identity is the capability: a copied sendId or another ledger's seed is not proof.
|
|
741
|
+
type SeedState = { targets: readonly ScopeRef[]; sends: Set<string>; abandoned: boolean; owner?: object };
|
|
742
|
+
const seeds = new Map<SpendSeed, SeedState>();
|
|
743
|
+
const seedBySend = new Map<string, SeedState>();
|
|
744
|
+
const pinnedKeys = new Map<string, number>();
|
|
745
|
+
const seedForSend = (send: string) => seedBySend.get(send);
|
|
746
|
+
const pinnedScope = (key: string): boolean => pinnedKeys.has(key);
|
|
747
|
+
const pendingRecords: JournalRecord[] = [];
|
|
748
|
+
// Bounded by live ordinary reservations; never persisted as new metadata.
|
|
749
|
+
const nondurableReserves = new Map<string, JournalRecord>();
|
|
750
|
+
const retainedOrdinary = new Set<string>();
|
|
751
|
+
let reporters = 0;
|
|
752
|
+
const ownerReporters = new WeakMap<object, number>();
|
|
753
|
+
const activeReporters = (owner?: object): number => owner === undefined ? reporters : ownerReporters.get(owner) ?? 0;
|
|
754
|
+
const drainWaiters = new Set<() => void>();
|
|
755
|
+
const deferredCleanups = new Map<object, () => boolean>();
|
|
756
|
+
let retryingCleanup = false;
|
|
757
|
+
const retryCleanup = (): void => {
|
|
758
|
+
if (retryingCleanup) return;
|
|
759
|
+
retryingCleanup = true;
|
|
760
|
+
try {
|
|
761
|
+
for (const [owner, cleanup] of deferredCleanups) if (cleanup()) deferredCleanups.delete(owner);
|
|
762
|
+
} finally { retryingCleanup = false; }
|
|
763
|
+
};
|
|
764
|
+
let poolContinuity = resolvePoolAliases(policy.poolAliases, salt, policy.canonicalProviderIds);
|
|
704
765
|
let persistFailures = 0;
|
|
705
766
|
let corruptRecords = 0;
|
|
706
767
|
let recordsOnDisk = 0;
|
|
@@ -710,9 +771,8 @@ export function createSpendReservationLedger(options: {
|
|
|
710
771
|
* credential id, a pool name -- never leaves this function, so nothing identifying is
|
|
711
772
|
* written to disk or held in a map key.
|
|
712
773
|
*/
|
|
713
|
-
const aliasFor = (kind: SpendScope | "send", id: string): string =>
|
|
714
|
-
|
|
715
|
-
.digest("hex").slice(0, 32);
|
|
774
|
+
const aliasFor = (kind: SpendScope | "send", id: string): string => hashSpendAlias(salt, kind, id);
|
|
775
|
+
const poolAliasFor = (provider: string): string => aliasFor("pool", provider);
|
|
716
776
|
|
|
717
777
|
const scopeState = (scope: SpendScope, alias: string): ScopeState => {
|
|
718
778
|
const key = scopeKey(scope, alias);
|
|
@@ -724,6 +784,37 @@ export function createSpendReservationLedger(options: {
|
|
|
724
784
|
return state;
|
|
725
785
|
};
|
|
726
786
|
|
|
787
|
+
const historicalPools = (): Set<string> => new Set([...scopes].filter(([key, state]) =>
|
|
788
|
+
key.startsWith("pool\0") && state.settled + state.reserved + state.unresolved > 0,
|
|
789
|
+
).map(([key]) => key.slice(5)));
|
|
790
|
+
const hasUnboundPositivePoolHistory = (): boolean => [...historicalPools()].some(alias => !poolContinuity.known(alias));
|
|
791
|
+
const poolState = (alias: string): { totals: ScopeState; includesUnboundHistory: boolean } | undefined => {
|
|
792
|
+
const group = poolContinuity.resolve(alias);
|
|
793
|
+
let total: ScopeState | undefined;
|
|
794
|
+
let includesUnboundHistory = false;
|
|
795
|
+
for (const [key, state] of scopes) {
|
|
796
|
+
if (!key.startsWith("pool\0")) continue;
|
|
797
|
+
const member = key.slice(5);
|
|
798
|
+
const unbound = !poolContinuity.known(member);
|
|
799
|
+
if (unbound) {
|
|
800
|
+
// Configured provider buckets bind at read time. Only unmapped historical labels
|
|
801
|
+
// absent from that roster remain unbound and overlay every candidate provider.
|
|
802
|
+
if (state.settled + state.reserved + state.unresolved === 0) continue;
|
|
803
|
+
includesUnboundHistory = true;
|
|
804
|
+
} else if (poolContinuity.resolve(member) !== group) {
|
|
805
|
+
continue;
|
|
806
|
+
}
|
|
807
|
+
total ??= { settled: 0, reserved: 0, unresolved: 0, lastSeenAt: 0 };
|
|
808
|
+
total.settled += state.settled;
|
|
809
|
+
total.reserved += state.reserved;
|
|
810
|
+
total.unresolved += state.unresolved;
|
|
811
|
+
total.lastSeenAt = Math.max(total.lastSeenAt, state.lastSeenAt);
|
|
812
|
+
}
|
|
813
|
+
return total ? { totals: total, includesUnboundHistory } : undefined;
|
|
814
|
+
};
|
|
815
|
+
const stateFor = (scope: SpendScope, alias: string): ScopeState | undefined =>
|
|
816
|
+
scope === "pool" ? poolState(alias)?.totals : scopes.get(scopeKey(scope, alias));
|
|
817
|
+
|
|
727
818
|
const limitFor = (scope: SpendScope): number | undefined => policy[scope].maxTokens;
|
|
728
819
|
|
|
729
820
|
const isExhausted = (scope: SpendScope, state: ScopeState): boolean => {
|
|
@@ -736,7 +827,7 @@ export function createSpendReservationLedger(options: {
|
|
|
736
827
|
const refs: ScopeRef[] = [];
|
|
737
828
|
if (targets.rootId !== undefined) refs.push({ scope: "root", alias: aliasFor("root", targets.rootId) });
|
|
738
829
|
if (targets.identityId !== undefined) refs.push({ scope: "identity", alias: aliasFor("identity", targets.identityId) });
|
|
739
|
-
if (targets.poolId !== undefined) refs.push({ scope: "pool", alias:
|
|
830
|
+
if (targets.poolId !== undefined) refs.push({ scope: "pool", alias: poolAliasFor(targets.poolId) });
|
|
740
831
|
return refs;
|
|
741
832
|
};
|
|
742
833
|
|
|
@@ -759,6 +850,29 @@ export function createSpendReservationLedger(options: {
|
|
|
759
850
|
}
|
|
760
851
|
};
|
|
761
852
|
|
|
853
|
+
// Already-sent debt owns its record before I/O, including an owner error that propagates.
|
|
854
|
+
const appendReported = (record: JournalRecord): void => {
|
|
855
|
+
pendingRecords.push(record);
|
|
856
|
+
if (pendingRecords.length === 1 && append(record)) pendingRecords.shift();
|
|
857
|
+
};
|
|
858
|
+
|
|
859
|
+
const flushPending = (): boolean => {
|
|
860
|
+
while (pendingRecords.length > 0) {
|
|
861
|
+
if (!append(pendingRecords[0]!)) return false;
|
|
862
|
+
pendingRecords.shift();
|
|
863
|
+
}
|
|
864
|
+
return true;
|
|
865
|
+
};
|
|
866
|
+
|
|
867
|
+
const repairReservePrefix = (send: string, reservation: Reservation): void => {
|
|
868
|
+
const reserve = nondurableReserves.get(send);
|
|
869
|
+
if (!reserve) return;
|
|
870
|
+
pendingRecords.push(reserve);
|
|
871
|
+
if (reservation.status === "dispatched") pendingRecords.push({ v: 1, kind: "dispatch", send, at: reservation.resolvedAt });
|
|
872
|
+
nondurableReserves.delete(send);
|
|
873
|
+
retainedOrdinary.add(send);
|
|
874
|
+
};
|
|
875
|
+
|
|
762
876
|
const applyReserve = (send: string, targets: readonly ScopeRef[], tokens: number, at: number): void => {
|
|
763
877
|
if (reservations.has(send)) return;
|
|
764
878
|
reservations.set(send, { targets, tokens, status: "open", at, resolvedAt: at });
|
|
@@ -779,6 +893,8 @@ export function createSpendReservationLedger(options: {
|
|
|
779
893
|
const applyResolve = (send: string, outcome: "settled" | "lost" | "abandoned", tokens: number, at: number): void => {
|
|
780
894
|
const reservation = reservations.get(send);
|
|
781
895
|
if (!reservation || !isLive(reservation.status)) return;
|
|
896
|
+
nondurableReserves.delete(send);
|
|
897
|
+
retainedOrdinary.delete(send);
|
|
782
898
|
reservation.status = outcome;
|
|
783
899
|
reservation.resolvedAt = at;
|
|
784
900
|
for (const ref of reservation.targets) {
|
|
@@ -802,6 +918,8 @@ export function createSpendReservationLedger(options: {
|
|
|
802
918
|
const reservation = reservations.get(send);
|
|
803
919
|
if (!reservation || isLive(reservation.status)) return;
|
|
804
920
|
reservations.delete(send);
|
|
921
|
+
nondurableReserves.delete(send);
|
|
922
|
+
retainedOrdinary.delete(send);
|
|
805
923
|
};
|
|
806
924
|
|
|
807
925
|
const applyDrop = (scope: SpendScope, alias: string): void => {
|
|
@@ -848,12 +966,13 @@ export function createSpendReservationLedger(options: {
|
|
|
848
966
|
const line = lines[index] as string;
|
|
849
967
|
const record = parseSpendJournalRecord(line);
|
|
850
968
|
if (!record) {
|
|
851
|
-
// A
|
|
852
|
-
//
|
|
853
|
-
//
|
|
854
|
-
//
|
|
855
|
-
|
|
856
|
-
|
|
969
|
+
// A malformed final JSON line can be an incomplete tail write. Rejected records
|
|
970
|
+
// elsewhere cannot be skipped: later complete writes may have authorized spend.
|
|
971
|
+
// Only malformed JSON can be a torn tail. A complete unknown/invalid final
|
|
972
|
+
// record is evidence of an unsupported format, not permission to forget accounting.
|
|
973
|
+
let completeJson = false;
|
|
974
|
+
try { JSON.parse(line); completeJson = true; } catch { /* torn tail */ }
|
|
975
|
+
if (completeJson || index < lines.length - 1) corruptRecords += 1;
|
|
857
976
|
continue;
|
|
858
977
|
}
|
|
859
978
|
switch (record.kind) {
|
|
@@ -880,10 +999,16 @@ export function createSpendReservationLedger(options: {
|
|
|
880
999
|
// had already fired. An exhausted scope staying exhausted across a restart is the whole
|
|
881
1000
|
// reason this store is on disk.
|
|
882
1001
|
const reconciledAt = now();
|
|
1002
|
+
const enforcedReplay = spendCeilingsConfigured(policy);
|
|
883
1003
|
for (const [send, reservation] of reservations) {
|
|
884
|
-
if (
|
|
885
|
-
|
|
886
|
-
|
|
1004
|
+
if (isLive(reservation.status)) {
|
|
1005
|
+
const record: JournalRecord = { v: 1, kind: "lost", send, at: reconciledAt };
|
|
1006
|
+
const durable = append(record);
|
|
1007
|
+
applyResolve(send, "lost", 0, reconciledAt);
|
|
1008
|
+
if (!durable && enforcedReplay) pendingRecords.push(record);
|
|
1009
|
+
}
|
|
1010
|
+
// No live producer or capability survives restart. Preserve totals, reclaim only IDs.
|
|
1011
|
+
if (enforcedReplay && flushPending() && append({ v: 1, kind: "forget", send, at: reconciledAt })) applyForget(send);
|
|
887
1012
|
}
|
|
888
1013
|
}
|
|
889
1014
|
|
|
@@ -900,24 +1025,76 @@ export function createSpendReservationLedger(options: {
|
|
|
900
1025
|
*/
|
|
901
1026
|
const evictScopes = (at: number, force: boolean): number => {
|
|
902
1027
|
const cutoff = at - policy.retentionMs;
|
|
1028
|
+
const activeKeys = new Set<string>();
|
|
1029
|
+
for (const reservation of reservations.values()) if (isLive(reservation.status)) {
|
|
1030
|
+
for (const ref of reservation.targets) activeKeys.add(scopeKey(ref.scope, ref.alias));
|
|
1031
|
+
}
|
|
1032
|
+
const idle = (key: string, state: ScopeState): boolean => state.reserved === 0
|
|
1033
|
+
&& !activeKeys.has(key) && !pinnedScope(key);
|
|
1034
|
+
let removed = 0;
|
|
1035
|
+
// Unknown original buckets expire at their own cutoff, even when the overlay is exhausted.
|
|
1036
|
+
// This pass precedes group selection so a removed overlay cannot keep unrelated groups alive.
|
|
1037
|
+
for (const [key, state] of scopes) {
|
|
1038
|
+
if (!key.startsWith("pool\0") || poolContinuity.known(key.slice(5))
|
|
1039
|
+
|| !idle(key, state) || state.lastSeenAt >= cutoff) continue;
|
|
1040
|
+
if (!flushPending() || !append({ v: 1, kind: "drop", scope: "pool", alias: key.slice(5), at })) continue;
|
|
1041
|
+
scopes.delete(key);
|
|
1042
|
+
removed += 1;
|
|
1043
|
+
}
|
|
1044
|
+
// Aggregate once per pass; resolving each candidate by scanning every scope is quadratic.
|
|
1045
|
+
const pinnedPoolGroups = new Set<string>();
|
|
1046
|
+
for (const key of activeKeys) if (key.startsWith("pool\0")) pinnedPoolGroups.add(poolContinuity.resolve(key.slice(5)));
|
|
1047
|
+
for (const seed of seeds.values()) for (const ref of seed.targets) if (ref.scope === "pool") {
|
|
1048
|
+
pinnedPoolGroups.add(poolContinuity.resolve(ref.alias));
|
|
1049
|
+
}
|
|
1050
|
+
const groups = new Map<string, ScopeState>();
|
|
1051
|
+
const overlay: ScopeState = { settled: 0, reserved: 0, unresolved: 0, lastSeenAt: 0 };
|
|
1052
|
+
for (const [key, state] of scopes) {
|
|
1053
|
+
if (!key.startsWith("pool\0")) continue;
|
|
1054
|
+
const alias = key.slice(5);
|
|
1055
|
+
const known = poolContinuity.known(alias);
|
|
1056
|
+
let total = overlay;
|
|
1057
|
+
if (known) {
|
|
1058
|
+
const group = poolContinuity.resolve(alias);
|
|
1059
|
+
total = groups.get(group) ?? { settled: 0, reserved: 0, unresolved: 0, lastSeenAt: 0 };
|
|
1060
|
+
groups.set(group, total);
|
|
1061
|
+
}
|
|
1062
|
+
total.settled += state.settled;
|
|
1063
|
+
total.reserved += state.reserved;
|
|
1064
|
+
total.unresolved += state.unresolved;
|
|
1065
|
+
total.lastSeenAt = Math.max(total.lastSeenAt, state.lastSeenAt);
|
|
1066
|
+
}
|
|
903
1067
|
const candidates: { key: string; scope: SpendScope; alias: string; seenAt: number }[] = [];
|
|
904
1068
|
for (const [key, state] of scopes) {
|
|
1069
|
+
if (!idle(key, state)) continue;
|
|
905
1070
|
const separator = key.indexOf("\0");
|
|
906
1071
|
const scope = key.slice(0, separator) as SpendScope;
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
1072
|
+
const alias = key.slice(separator + 1);
|
|
1073
|
+
const unbound = scope === "pool" && !poolContinuity.known(alias);
|
|
1074
|
+
// Pressure cannot shorten retention for unknown history, including zero-token keys.
|
|
1075
|
+
if (unbound && state.settled + state.reserved + state.unresolved > 0) continue;
|
|
1076
|
+
const group = scope === "pool" ? groups.get(poolContinuity.resolve(alias)) : undefined;
|
|
1077
|
+
const effective = scope === "pool" && !unbound ? {
|
|
1078
|
+
settled: (group?.settled ?? 0) + overlay.settled,
|
|
1079
|
+
reserved: (group?.reserved ?? 0) + overlay.reserved,
|
|
1080
|
+
unresolved: (group?.unresolved ?? 0) + overlay.unresolved,
|
|
1081
|
+
lastSeenAt: group?.lastSeenAt ?? state.lastSeenAt,
|
|
1082
|
+
} : state;
|
|
1083
|
+
if (effective.reserved > 0 || (scope === "pool" && pinnedPoolGroups.has(poolContinuity.resolve(alias)))
|
|
1084
|
+
|| isExhausted(scope, effective)) continue;
|
|
1085
|
+
if (!force && effective.lastSeenAt >= cutoff) continue;
|
|
1086
|
+
candidates.push({ key, scope, alias, seenAt: state.lastSeenAt });
|
|
911
1087
|
}
|
|
912
1088
|
if (force) {
|
|
913
1089
|
candidates.sort((a, b) => a.seenAt - b.seenAt);
|
|
914
1090
|
candidates.length = Math.min(candidates.length, 1);
|
|
915
1091
|
}
|
|
916
1092
|
for (const candidate of candidates) {
|
|
1093
|
+
if (!flushPending() || !append({ v: 1, kind: "drop", scope: candidate.scope, alias: candidate.alias, at })) continue;
|
|
917
1094
|
scopes.delete(candidate.key);
|
|
918
|
-
|
|
1095
|
+
removed += 1;
|
|
919
1096
|
}
|
|
920
|
-
return
|
|
1097
|
+
return removed;
|
|
921
1098
|
};
|
|
922
1099
|
|
|
923
1100
|
/**
|
|
@@ -929,7 +1106,7 @@ export function createSpendReservationLedger(options: {
|
|
|
929
1106
|
const cutoff = at - policy.retentionMs;
|
|
930
1107
|
const candidates: { send: string; resolvedAt: number }[] = [];
|
|
931
1108
|
for (const [send, reservation] of reservations) {
|
|
932
|
-
if (isLive(reservation.status)) continue;
|
|
1109
|
+
if (isLive(reservation.status) || seedForSend(send)) continue;
|
|
933
1110
|
if (!force && reservation.resolvedAt >= cutoff) continue;
|
|
934
1111
|
candidates.push({ send, resolvedAt: reservation.resolvedAt });
|
|
935
1112
|
}
|
|
@@ -937,11 +1114,15 @@ export function createSpendReservationLedger(options: {
|
|
|
937
1114
|
candidates.sort((a, b) => a.resolvedAt - b.resolvedAt);
|
|
938
1115
|
candidates.length = Math.min(candidates.length, 1);
|
|
939
1116
|
}
|
|
1117
|
+
let removed = 0;
|
|
940
1118
|
for (const candidate of candidates) {
|
|
1119
|
+
if (!flushPending() || !append({ v: 1, kind: "forget", send: candidate.send, at })) continue;
|
|
941
1120
|
reservations.delete(candidate.send);
|
|
942
|
-
|
|
1121
|
+
nondurableReserves.delete(candidate.send);
|
|
1122
|
+
retainedOrdinary.delete(candidate.send);
|
|
1123
|
+
removed += 1;
|
|
943
1124
|
}
|
|
944
|
-
return
|
|
1125
|
+
return removed;
|
|
945
1126
|
};
|
|
946
1127
|
|
|
947
1128
|
/**
|
|
@@ -949,10 +1130,7 @@ export function createSpendReservationLedger(options: {
|
|
|
949
1130
|
* Bounded maps are not enough on their own: the file behind them is what replay reads, and
|
|
950
1131
|
* an uncompacted file grows forever on unique root and send ids.
|
|
951
1132
|
*/
|
|
952
|
-
const
|
|
953
|
-
const rewrite = journal?.rewrite;
|
|
954
|
-
if (!journal || !rewrite || recordsOnDisk < compactAfterRecords()) return;
|
|
955
|
-
const checkpoint: JournalRecord = {
|
|
1133
|
+
const checkpointRecord = (at: number): Extract<JournalRecord, { kind: "checkpoint" }> => ({
|
|
956
1134
|
v: 1,
|
|
957
1135
|
kind: "checkpoint",
|
|
958
1136
|
at,
|
|
@@ -974,9 +1152,14 @@ export function createSpendReservationLedger(options: {
|
|
|
974
1152
|
at: reservation.at,
|
|
975
1153
|
resolvedAt: reservation.resolvedAt,
|
|
976
1154
|
})),
|
|
977
|
-
|
|
1155
|
+
});
|
|
1156
|
+
|
|
1157
|
+
const compact = (at: number): void => {
|
|
1158
|
+
const rewrite = journal?.rewrite;
|
|
1159
|
+
// Restore shipped compaction semantics; corruption refusal persists until clean replay.
|
|
1160
|
+
if (!journal || !rewrite || pendingRecords.length > 0 || recordsOnDisk < compactAfterRecords()) return;
|
|
978
1161
|
try {
|
|
979
|
-
rewrite.call(journal, [JSON.stringify(
|
|
1162
|
+
rewrite.call(journal, [JSON.stringify(checkpointRecord(at))]);
|
|
980
1163
|
recordsOnDisk = 1;
|
|
981
1164
|
} catch (error) {
|
|
982
1165
|
if (error instanceof SpendLedgerOwnerError) throw error;
|
|
@@ -986,6 +1169,14 @@ export function createSpendReservationLedger(options: {
|
|
|
986
1169
|
}
|
|
987
1170
|
};
|
|
988
1171
|
|
|
1172
|
+
const preparePoolContinuity = (requestedProvider?: string): SpendDenial | undefined => {
|
|
1173
|
+
const providers = [...(policy.canonicalProviderIds ?? []), ...(requestedProvider === undefined ? [] : [requestedProvider])];
|
|
1174
|
+
if (!poolContinuity.valid || validatePoolAliasOwners(policy.poolAliases, salt, providers)) {
|
|
1175
|
+
return { reason: "pool-history-unresolved" };
|
|
1176
|
+
}
|
|
1177
|
+
return undefined;
|
|
1178
|
+
};
|
|
1179
|
+
|
|
989
1180
|
/** The denial when tracking cannot fit this request, or undefined when it can. */
|
|
990
1181
|
const makeRoom = (refs: readonly ScopeRef[], at: number): SpendDenial | undefined => {
|
|
991
1182
|
evictSends(at, false);
|
|
@@ -993,9 +1184,8 @@ export function createSpendReservationLedger(options: {
|
|
|
993
1184
|
while (reservations.size >= maxTrackedSends()) {
|
|
994
1185
|
if (evictSends(at, true) === 0) return { reason: "tracking-capacity-exhausted" };
|
|
995
1186
|
}
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
while (scopes.size + fresh > maxTrackedScopes()) {
|
|
1187
|
+
const freshCount = (): number => refs.filter(ref => !scopes.has(scopeKey(ref.scope, ref.alias))).length;
|
|
1188
|
+
while (scopes.size + freshCount() > maxTrackedScopes()) {
|
|
999
1189
|
if (evictScopes(at, true) === 0) {
|
|
1000
1190
|
return { reason: "tracking-capacity-exhausted", scope: refs[0]?.scope };
|
|
1001
1191
|
}
|
|
@@ -1003,7 +1193,106 @@ export function createSpendReservationLedger(options: {
|
|
|
1003
1193
|
return undefined;
|
|
1004
1194
|
};
|
|
1005
1195
|
|
|
1006
|
-
|
|
1196
|
+
const ledger: SpendReservationLedger = {
|
|
1197
|
+
reserveSeed(request, owner): SpendSeedDecision {
|
|
1198
|
+
assertOwnedAccounting?.();
|
|
1199
|
+
// Initial anchors never borrow already-sent capacity or bypass token/durability checks.
|
|
1200
|
+
const decision = ledger.reserve({ ...request, alreadySent: false });
|
|
1201
|
+
if (!decision.reserved) return decision;
|
|
1202
|
+
const seed: SpendSeed = Object.freeze({ sendId: request.sendId });
|
|
1203
|
+
const send = aliasFor("send", seed.sendId);
|
|
1204
|
+
const state: SeedState = { targets: reservations.get(send)!.targets, sends: new Set([send]), abandoned: false, owner };
|
|
1205
|
+
seeds.set(seed, state);
|
|
1206
|
+
seedBySend.set(send, state);
|
|
1207
|
+
for (const ref of state.targets) {
|
|
1208
|
+
const key = scopeKey(ref.scope, ref.alias);
|
|
1209
|
+
pinnedKeys.set(key, (pinnedKeys.get(key) ?? 0) + 1);
|
|
1210
|
+
}
|
|
1211
|
+
return { reserved: true, seed };
|
|
1212
|
+
},
|
|
1213
|
+
reserveReportedFromSeed(seed, request): boolean {
|
|
1214
|
+
assertOwnedAccounting?.();
|
|
1215
|
+
const owned = seeds.get(seed);
|
|
1216
|
+
const send = aliasFor("send", request.sendId);
|
|
1217
|
+
if (!owned || owned.abandoned || owned.targets.some(ref => !scopes.has(scopeKey(ref.scope, ref.alias)))) return false;
|
|
1218
|
+
const tokens = sanitizeTokens(request.inputTokens) + sanitizeTokens(request.outputCeilingTokens);
|
|
1219
|
+
const existing = reservations.get(send);
|
|
1220
|
+
if (existing || seedBySend.has(send)) {
|
|
1221
|
+
return seedBySend.get(send) === owned && existing !== undefined && isLive(existing.status) && existing.tokens === tokens;
|
|
1222
|
+
}
|
|
1223
|
+
const at = now();
|
|
1224
|
+
const record: JournalRecord = { v: 1, kind: "reserve", send, targets: [...owned.targets], tokens, at };
|
|
1225
|
+
// Enrollment is stable before an owner error can escape the journal write.
|
|
1226
|
+
applyReserve(send, owned.targets, tokens, at);
|
|
1227
|
+
owned.sends.add(send);
|
|
1228
|
+
seedBySend.set(send, owned);
|
|
1229
|
+
appendReported(record);
|
|
1230
|
+
compact(at);
|
|
1231
|
+
return true;
|
|
1232
|
+
},
|
|
1233
|
+
forgetResolved(sendIds, owner): boolean {
|
|
1234
|
+
assertOwnedAccounting?.();
|
|
1235
|
+
if (activeReporters(owner) > 0) return false;
|
|
1236
|
+
const sends = [...new Set(sendIds.map(id => aliasFor("send", id)))];
|
|
1237
|
+
if (sends.some(send => { const seed = seedForSend(send); return seed && seed.owner !== owner; }) || !flushPending()) return false;
|
|
1238
|
+
if (sends.some(send => { const entry = reservations.get(send); return entry && isLive(entry.status); })) return false;
|
|
1239
|
+
for (const send of sends) {
|
|
1240
|
+
if (!reservations.has(send)) continue;
|
|
1241
|
+
if (!append({ v: 1, kind: "forget", send, at: now() })) return false;
|
|
1242
|
+
applyForget(send);
|
|
1243
|
+
}
|
|
1244
|
+
return true;
|
|
1245
|
+
},
|
|
1246
|
+
finishSeed(seed, owner): boolean {
|
|
1247
|
+
assertOwnedAccounting?.();
|
|
1248
|
+
const owned = seeds.get(seed);
|
|
1249
|
+
if (!owned || owned.owner !== owner || !flushPending()) return false;
|
|
1250
|
+
if (owned.abandoned) {
|
|
1251
|
+
// A never-sent provisional anchor cannot have delayed physical reports. Rebinding
|
|
1252
|
+
// during another live producer is safe only for this explicitly abandoned capability.
|
|
1253
|
+
const send = aliasFor("send", seed.sendId);
|
|
1254
|
+
if (reservations.has(send)) {
|
|
1255
|
+
if (!append({ v: 1, kind: "forget", send, at: now() })) return false;
|
|
1256
|
+
applyForget(send);
|
|
1257
|
+
}
|
|
1258
|
+
} else if (activeReporters(owner) > 0) return false;
|
|
1259
|
+
if ([...owned.sends].some(send => reservations.has(send))) return false;
|
|
1260
|
+
seeds.delete(seed);
|
|
1261
|
+
for (const send of owned.sends) seedBySend.delete(send);
|
|
1262
|
+
for (const ref of owned.targets) {
|
|
1263
|
+
const key = scopeKey(ref.scope, ref.alias);
|
|
1264
|
+
const count = pinnedKeys.get(key)! - 1;
|
|
1265
|
+
if (count === 0) pinnedKeys.delete(key); else pinnedKeys.set(key, count);
|
|
1266
|
+
}
|
|
1267
|
+
return true;
|
|
1268
|
+
},
|
|
1269
|
+
registerReporter(owner) {
|
|
1270
|
+
assertOwnedAccounting?.();
|
|
1271
|
+
reporters += 1;
|
|
1272
|
+
if (owner) ownerReporters.set(owner, activeReporters(owner) + 1);
|
|
1273
|
+
let closed = false;
|
|
1274
|
+
return { close() {
|
|
1275
|
+
if (closed) return;
|
|
1276
|
+
closed = true;
|
|
1277
|
+
reporters -= 1;
|
|
1278
|
+
if (owner) {
|
|
1279
|
+
const remaining = activeReporters(owner) - 1;
|
|
1280
|
+
if (remaining === 0) ownerReporters.delete(owner); else ownerReporters.set(owner, remaining);
|
|
1281
|
+
}
|
|
1282
|
+
try { assertOwnedAccounting?.(); retryCleanup(); }
|
|
1283
|
+
finally {
|
|
1284
|
+
if (reporters === 0) { for (const resolve of drainWaiters) resolve(); drainWaiters.clear(); }
|
|
1285
|
+
}
|
|
1286
|
+
} };
|
|
1287
|
+
},
|
|
1288
|
+
deferCleanup(owner, cleanup) {
|
|
1289
|
+
assertOwnedAccounting?.();
|
|
1290
|
+
deferredCleanups.set(owner, cleanup);
|
|
1291
|
+
},
|
|
1292
|
+
waitForReporterDrain() {
|
|
1293
|
+
assertOwnedAccounting?.();
|
|
1294
|
+
return reporters === 0 ? Promise.resolve() : new Promise<void>(resolve => { drainWaiters.add(resolve); });
|
|
1295
|
+
},
|
|
1007
1296
|
// Every figure this ledger reports describes a journal it must still own. Reporting one
|
|
1008
1297
|
// after ownership ended is the same error as writing then, with a quieter symptom.
|
|
1009
1298
|
get persistFailures() { assertOwnedAccounting?.(); return persistFailures; },
|
|
@@ -1011,18 +1300,33 @@ export function createSpendReservationLedger(options: {
|
|
|
1011
1300
|
get degraded() { assertOwnedAccounting?.(); return persistFailures > 0 || corruptRecords > 0; },
|
|
1012
1301
|
get policy() { assertOwnedAccounting?.(); return policy; },
|
|
1013
1302
|
|
|
1303
|
+
checkPoolContinuity(): SpendDenial | undefined {
|
|
1304
|
+
assertOwnedAccounting?.();
|
|
1305
|
+
if (policy.pool.maxTokens === undefined) return undefined;
|
|
1306
|
+
if (corruptRecords > 0) return { reason: "journal-corrupt", corruptRecords };
|
|
1307
|
+
return preparePoolContinuity();
|
|
1308
|
+
},
|
|
1309
|
+
|
|
1310
|
+
hasUnboundPositivePoolHistory(): boolean {
|
|
1311
|
+
assertOwnedAccounting?.();
|
|
1312
|
+
return hasUnboundPositivePoolHistory();
|
|
1313
|
+
},
|
|
1314
|
+
|
|
1014
1315
|
reserve(request: SpendReservationRequest): SpendReservationDecision {
|
|
1015
1316
|
assertOwnedAccounting?.();
|
|
1317
|
+
retryCleanup();
|
|
1318
|
+
const pendingDurable = flushPending(); // Observe-only admissions also retry ordinary repair debt.
|
|
1016
1319
|
const tokens = sanitizeTokens(request.inputTokens) + sanitizeTokens(request.outputCeilingTokens);
|
|
1017
1320
|
const at = request.at ?? now();
|
|
1018
1321
|
const send = aliasFor("send", request.sendId);
|
|
1019
1322
|
const refs = refsFor(request.scopes);
|
|
1020
|
-
const
|
|
1323
|
+
const admissionLimit = (scope: SpendScope) => (request.admissionPolicy ?? policy)[scope].maxTokens;
|
|
1324
|
+
const enforced = refs.some((ref) => admissionLimit(ref.scope) !== undefined);
|
|
1021
1325
|
|
|
1022
1326
|
// A send id this ledger already knows is REFUSED. Returning success while booking
|
|
1023
1327
|
// nothing -- the old behaviour -- let one id authorise an unlimited number of physical
|
|
1024
1328
|
// sends with the scope totals never moving.
|
|
1025
|
-
if (reservations.has(send)) {
|
|
1329
|
+
if (reservations.has(send) || seedBySend.has(send)) {
|
|
1026
1330
|
return { reserved: false, denial: { reason: "duplicate-send-id", sendId: request.sendId } };
|
|
1027
1331
|
}
|
|
1028
1332
|
// Replay could not prove these totals are complete, so a configured ceiling cannot be
|
|
@@ -1030,6 +1334,13 @@ export function createSpendReservationLedger(options: {
|
|
|
1030
1334
|
if (enforced && corruptRecords > 0) {
|
|
1031
1335
|
return { reserved: false, denial: { reason: "journal-corrupt", corruptRecords } };
|
|
1032
1336
|
}
|
|
1337
|
+
if (enforced && request.alreadySent !== true && !pendingDurable) {
|
|
1338
|
+
return { reserved: false, denial: { reason: "reserve-not-durable", sendId: request.sendId } };
|
|
1339
|
+
}
|
|
1340
|
+
const continuityDenial = preparePoolContinuity(request.scopes.poolId);
|
|
1341
|
+
if (continuityDenial && enforced && request.alreadySent !== true) {
|
|
1342
|
+
return { reserved: false, denial: continuityDenial };
|
|
1343
|
+
}
|
|
1033
1344
|
const capacity = makeRoom(refs, at);
|
|
1034
1345
|
if (capacity) return { reserved: false, denial: capacity };
|
|
1035
1346
|
|
|
@@ -1039,9 +1350,10 @@ export function createSpendReservationLedger(options: {
|
|
|
1039
1350
|
for (const ref of refs) {
|
|
1040
1351
|
// A recorded send has no limit to fail: it already happened, and the point of booking
|
|
1041
1352
|
// it is to let the total go OVER the ceiling so the next request can be refused.
|
|
1042
|
-
const limit = request.alreadySent === true ? undefined :
|
|
1353
|
+
const limit = request.alreadySent === true ? undefined : admissionLimit(ref.scope);
|
|
1043
1354
|
if (limit === undefined) continue;
|
|
1044
|
-
const
|
|
1355
|
+
const poolView = ref.scope === "pool" ? poolState(ref.alias) : undefined;
|
|
1356
|
+
const state = ref.scope === "pool" ? poolView?.totals : scopes.get(scopeKey(ref.scope, ref.alias));
|
|
1045
1357
|
const projected = (state ? state.settled + state.reserved + state.unresolved : 0) + tokens;
|
|
1046
1358
|
if (projected > limit) {
|
|
1047
1359
|
const scopeId = ref.scope === "root"
|
|
@@ -1049,7 +1361,10 @@ export function createSpendReservationLedger(options: {
|
|
|
1049
1361
|
: ref.scope === "identity" ? request.scopes.identityId : request.scopes.poolId;
|
|
1050
1362
|
return {
|
|
1051
1363
|
reserved: false,
|
|
1052
|
-
denial: {
|
|
1364
|
+
denial: {
|
|
1365
|
+
reason: "spend-limit-exceeded", scope: ref.scope, scopeId: scopeId ?? "", limit, projected,
|
|
1366
|
+
...(poolView?.includesUnboundHistory ? { includesUnboundPoolHistory: true } : {}),
|
|
1367
|
+
},
|
|
1053
1368
|
};
|
|
1054
1369
|
}
|
|
1055
1370
|
}
|
|
@@ -1057,11 +1372,13 @@ export function createSpendReservationLedger(options: {
|
|
|
1057
1372
|
// Durability BEFORE admission. The record goes to disk first, and under a configured
|
|
1058
1373
|
// limit a failed write refuses the request rather than admitting one that a restart
|
|
1059
1374
|
// would forget -- which is exactly the disk-full and permission case durability is for.
|
|
1060
|
-
const
|
|
1375
|
+
const record: JournalRecord = { v: 1, kind: "reserve", send, targets: refs, tokens, at };
|
|
1376
|
+
const durable = append(record);
|
|
1061
1377
|
if (!durable && enforced && request.alreadySent !== true) {
|
|
1062
1378
|
return { reserved: false, denial: { reason: "reserve-not-durable", sendId: request.sendId } };
|
|
1063
1379
|
}
|
|
1064
1380
|
applyReserve(send, refs, tokens, at);
|
|
1381
|
+
if (!durable) nondurableReserves.set(send, record);
|
|
1065
1382
|
compact(at);
|
|
1066
1383
|
return { reserved: true, sendId: request.sendId, tokens, durable };
|
|
1067
1384
|
},
|
|
@@ -1070,10 +1387,15 @@ export function createSpendReservationLedger(options: {
|
|
|
1070
1387
|
assertOwnedAccounting?.();
|
|
1071
1388
|
const send = aliasFor("send", sendId);
|
|
1072
1389
|
const reservation = reservations.get(send);
|
|
1390
|
+
if (reservation?.status === "dispatched" && seedForSend(send)) return true;
|
|
1073
1391
|
if (!reservation || reservation.status !== "open") return false;
|
|
1392
|
+
repairReservePrefix(send, reservation);
|
|
1074
1393
|
const at = now();
|
|
1075
1394
|
applyDispatch(send, at);
|
|
1076
|
-
|
|
1395
|
+
const record: JournalRecord = { v: 1, kind: "dispatch", send, at };
|
|
1396
|
+
const retained = seedForSend(send) || retainedOrdinary.has(send);
|
|
1397
|
+
if (retained) appendReported(record);
|
|
1398
|
+
else append(record);
|
|
1077
1399
|
return true;
|
|
1078
1400
|
},
|
|
1079
1401
|
|
|
@@ -1085,8 +1407,12 @@ export function createSpendReservationLedger(options: {
|
|
|
1085
1407
|
// upstream the tokens may already be billed, so the caller owes settle or markLost.
|
|
1086
1408
|
if (!reservation || reservation.status !== "open") return false;
|
|
1087
1409
|
const at = now();
|
|
1410
|
+
const record: JournalRecord = { v: 1, kind: "abandon", send, at };
|
|
1411
|
+
const seeded = seedForSend(send);
|
|
1412
|
+
if (seeded && (!flushPending() || !append(record))) return false;
|
|
1088
1413
|
applyResolve(send, "abandoned", 0, at);
|
|
1089
|
-
|
|
1414
|
+
if (seeded) seeded.abandoned = true;
|
|
1415
|
+
else append(record);
|
|
1090
1416
|
return true;
|
|
1091
1417
|
},
|
|
1092
1418
|
|
|
@@ -1095,10 +1421,16 @@ export function createSpendReservationLedger(options: {
|
|
|
1095
1421
|
const send = aliasFor("send", sendId);
|
|
1096
1422
|
const reservation = reservations.get(send);
|
|
1097
1423
|
if (!reservation || !isLive(reservation.status)) return false;
|
|
1424
|
+
repairReservePrefix(send, reservation);
|
|
1098
1425
|
const tokens = sanitizeTokens(usage.inputTokens) + sanitizeTokens(usage.outputTokens);
|
|
1099
1426
|
const at = now();
|
|
1427
|
+
const record: JournalRecord = { v: 1, kind: "settle", send, tokens, at };
|
|
1428
|
+
const seeded = seedForSend(send) || retainedOrdinary.has(send);
|
|
1429
|
+
// Terminal usage cannot be refused: the tracker may never report it again, and
|
|
1430
|
+
// the estimate can be smaller than the actual bill. Preserve ordered retryable debt.
|
|
1431
|
+
if (seeded && (pendingRecords.length > 0 || !append(record))) pendingRecords.push(record);
|
|
1100
1432
|
applyResolve(send, "settled", tokens, at);
|
|
1101
|
-
append(
|
|
1433
|
+
if (!seeded) append(record);
|
|
1102
1434
|
return true;
|
|
1103
1435
|
},
|
|
1104
1436
|
|
|
@@ -1107,22 +1439,28 @@ export function createSpendReservationLedger(options: {
|
|
|
1107
1439
|
const send = aliasFor("send", sendId);
|
|
1108
1440
|
const reservation = reservations.get(send);
|
|
1109
1441
|
if (!reservation || !isLive(reservation.status)) return false;
|
|
1442
|
+
repairReservePrefix(send, reservation);
|
|
1110
1443
|
const at = now();
|
|
1444
|
+
const record: JournalRecord = { v: 1, kind: "lost", send, at };
|
|
1445
|
+
const seeded = seedForSend(send) || retainedOrdinary.has(send);
|
|
1446
|
+
if (seeded && (pendingRecords.length > 0 || !append(record))) pendingRecords.push(record);
|
|
1111
1447
|
applyResolve(send, "lost", 0, at);
|
|
1112
|
-
append(
|
|
1448
|
+
if (!seeded) append(record);
|
|
1113
1449
|
return true;
|
|
1114
1450
|
},
|
|
1115
1451
|
|
|
1116
1452
|
knows(sendId: string): boolean {
|
|
1117
1453
|
assertOwnedAccounting?.();
|
|
1118
|
-
|
|
1454
|
+
const send = aliasFor("send", sendId);
|
|
1455
|
+
return reservations.has(send) || seedBySend.has(send);
|
|
1119
1456
|
},
|
|
1120
1457
|
|
|
1121
1458
|
snapshot(scope: SpendScope, scopeId: string): ScopeSpendSnapshot | undefined {
|
|
1122
1459
|
// Reading accounting from a handle whose ownership has ended is as wrong as writing it:
|
|
1123
1460
|
// the figures describe a journal this process no longer owns.
|
|
1124
1461
|
assertOwnedAccounting?.();
|
|
1125
|
-
const
|
|
1462
|
+
const alias = scope === "pool" ? poolAliasFor(scopeId) : aliasFor(scope, scopeId);
|
|
1463
|
+
const state = stateFor(scope, alias);
|
|
1126
1464
|
if (!state) return undefined;
|
|
1127
1465
|
return {
|
|
1128
1466
|
settled: state.settled,
|
|
@@ -1132,14 +1470,23 @@ export function createSpendReservationLedger(options: {
|
|
|
1132
1470
|
};
|
|
1133
1471
|
},
|
|
1134
1472
|
|
|
1135
|
-
exhausted(scope: SpendScope, scopeId: string): boolean {
|
|
1473
|
+
exhausted(scope: SpendScope, scopeId: string, excludingSendId?: string, admissionPolicy?: SpendAdmissionPolicy): boolean {
|
|
1136
1474
|
assertOwnedAccounting?.();
|
|
1137
|
-
const
|
|
1138
|
-
|
|
1475
|
+
const alias = scope === "pool" ? poolAliasFor(scopeId) : aliasFor(scope, scopeId);
|
|
1476
|
+
const state = stateFor(scope, alias);
|
|
1477
|
+
if (!state) return false;
|
|
1478
|
+
const own = excludingSendId === undefined ? undefined : reservations.get(aliasFor("send", excludingSendId));
|
|
1479
|
+
const matches = own?.status === "open" && own.targets.some(target => target.scope === scope
|
|
1480
|
+
&& (scope === "pool" ? (!poolContinuity.known(target.alias) || poolContinuity.resolve(target.alias) === poolContinuity.resolve(alias)) : target.alias === alias));
|
|
1481
|
+
const totals = matches ? { ...state, reserved: Math.max(0, state.reserved - own.tokens) } : state;
|
|
1482
|
+
const limit = (admissionPolicy ?? policy)[scope].maxTokens;
|
|
1483
|
+
return limit !== undefined && totals.settled + totals.reserved + totals.unresolved >= limit;
|
|
1139
1484
|
},
|
|
1140
1485
|
|
|
1141
1486
|
prune(at: number = now()): void {
|
|
1142
1487
|
assertOwnedAccounting?.();
|
|
1488
|
+
flushPending();
|
|
1489
|
+
retryCleanup();
|
|
1143
1490
|
// Removal requires BOTH inactive and not exhausted inside the window. An
|
|
1144
1491
|
// exhausted-but-idle scope that was dropped would be recreated fresh under the
|
|
1145
1492
|
// same id -- the exact laundering the ceiling exists to stop.
|
|
@@ -1149,9 +1496,22 @@ export function createSpendReservationLedger(options: {
|
|
|
1149
1496
|
|
|
1150
1497
|
reconfigure(next: SpendReservationPolicy): void {
|
|
1151
1498
|
assertOwnedAccounting?.();
|
|
1499
|
+
retryCleanup();
|
|
1152
1500
|
policy = next;
|
|
1501
|
+
poolContinuity = resolvePoolAliases(policy.poolAliases, salt, policy.canonicalProviderIds);
|
|
1153
1502
|
},
|
|
1154
1503
|
};
|
|
1504
|
+
return ledger;
|
|
1505
|
+
}
|
|
1506
|
+
|
|
1507
|
+
/** Drain only an already-constructed ledger; shutdown must not open an unused journal. */
|
|
1508
|
+
export function waitForSharedSpendReporterDrain(): Promise<void> {
|
|
1509
|
+
return sharedLedger?.waitForReporterDrain() ?? Promise.resolve();
|
|
1510
|
+
}
|
|
1511
|
+
|
|
1512
|
+
/** Does no I/O for installations without a pool ceiling. */
|
|
1513
|
+
export function sharedPoolContinuityDenial(): SpendDenial | undefined {
|
|
1514
|
+
return sharedPolicy.pool.maxTokens === undefined ? undefined : sharedSpendLedger().checkPoolContinuity();
|
|
1155
1515
|
}
|
|
1156
1516
|
|
|
1157
1517
|
let sharedLedger: SpendReservationLedger | undefined;
|
|
@@ -1189,9 +1549,11 @@ const spendScopeLimitFromConfig = (scope: OcxSpendScopeConfig | undefined): Spen
|
|
|
1189
1549
|
* ceiling would start refusing real traffic on the first upgrade that ran this code, against
|
|
1190
1550
|
* a number nobody chose. There is deliberately no default figure here at all.
|
|
1191
1551
|
*/
|
|
1192
|
-
export function spendPolicyFromConfig(spend: OcxSpendConfig | undefined): SpendReservationPolicy {
|
|
1552
|
+
export function spendPolicyFromConfig(spend: OcxSpendConfig | undefined, poolAliases?: unknown, canonicalProviderIds?: readonly string[]): SpendReservationPolicy {
|
|
1193
1553
|
const retentionDays = spend?.retentionDays;
|
|
1194
1554
|
return {
|
|
1555
|
+
...(poolAliases !== undefined ? { poolAliases } : {}),
|
|
1556
|
+
...(canonicalProviderIds !== undefined ? { canonicalProviderIds } : {}),
|
|
1195
1557
|
root: spendScopeLimitFromConfig(spend?.root),
|
|
1196
1558
|
identity: spendScopeLimitFromConfig(spend?.identity),
|
|
1197
1559
|
pool: spendScopeLimitFromConfig(spend?.pool),
|