instar 1.3.782 → 1.3.783

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (56) hide show
  1. package/dist/core/MeteredSpendGate.d.ts +119 -0
  2. package/dist/core/MeteredSpendGate.d.ts.map +1 -0
  3. package/dist/core/MeteredSpendGate.js +228 -0
  4. package/dist/core/MeteredSpendGate.js.map +1 -0
  5. package/dist/core/MeteredSpendLedger.d.ts +154 -0
  6. package/dist/core/MeteredSpendLedger.d.ts.map +1 -0
  7. package/dist/core/MeteredSpendLedger.js +434 -0
  8. package/dist/core/MeteredSpendLedger.js.map +1 -0
  9. package/dist/core/PinAttemptStore.d.ts +35 -0
  10. package/dist/core/PinAttemptStore.d.ts.map +1 -0
  11. package/dist/core/PinAttemptStore.js +85 -0
  12. package/dist/core/PinAttemptStore.js.map +1 -0
  13. package/dist/core/PostUpdateMigrator.d.ts +7 -0
  14. package/dist/core/PostUpdateMigrator.d.ts.map +1 -1
  15. package/dist/core/PostUpdateMigrator.js +24 -0
  16. package/dist/core/PostUpdateMigrator.js.map +1 -1
  17. package/dist/core/RenderedPlanStore.d.ts +61 -0
  18. package/dist/core/RenderedPlanStore.d.ts.map +1 -0
  19. package/dist/core/RenderedPlanStore.js +92 -0
  20. package/dist/core/RenderedPlanStore.js.map +1 -0
  21. package/dist/core/RoutingSpendCapsStore.d.ts +113 -0
  22. package/dist/core/RoutingSpendCapsStore.d.ts.map +1 -0
  23. package/dist/core/RoutingSpendCapsStore.js +241 -0
  24. package/dist/core/RoutingSpendCapsStore.js.map +1 -0
  25. package/dist/core/SpendAlertResolver.d.ts +77 -0
  26. package/dist/core/SpendAlertResolver.d.ts.map +1 -0
  27. package/dist/core/SpendAlertResolver.js +141 -0
  28. package/dist/core/SpendAlertResolver.js.map +1 -0
  29. package/dist/core/WriteDomainRegistry.d.ts.map +1 -1
  30. package/dist/core/WriteDomainRegistry.js +17 -0
  31. package/dist/core/WriteDomainRegistry.js.map +1 -1
  32. package/dist/core/devGatedFeatures.d.ts.map +1 -1
  33. package/dist/core/devGatedFeatures.js +9 -0
  34. package/dist/core/devGatedFeatures.js.map +1 -1
  35. package/dist/core/routingSpendView.d.ts +10 -2
  36. package/dist/core/routingSpendView.d.ts.map +1 -1
  37. package/dist/core/routingSpendView.js +18 -10
  38. package/dist/core/routingSpendView.js.map +1 -1
  39. package/dist/core/types.d.ts +24 -2
  40. package/dist/core/types.d.ts.map +1 -1
  41. package/dist/core/types.js.map +1 -1
  42. package/dist/server/AgentServer.d.ts +9 -0
  43. package/dist/server/AgentServer.d.ts.map +1 -1
  44. package/dist/server/AgentServer.js +177 -0
  45. package/dist/server/AgentServer.js.map +1 -1
  46. package/dist/server/routes.d.ts +10 -0
  47. package/dist/server/routes.d.ts.map +1 -1
  48. package/dist/server/routes.js +196 -4
  49. package/dist/server/routes.js.map +1 -1
  50. package/dist/testing/selfActionRegistry.d.ts.map +1 -1
  51. package/dist/testing/selfActionRegistry.js +78 -0
  52. package/dist/testing/selfActionRegistry.js.map +1 -1
  53. package/package.json +1 -1
  54. package/src/data/builtin-manifest.json +64 -64
  55. package/upgrades/1.3.783.md +66 -0
  56. package/upgrades/side-effects/routing-spend-increment-b.md +44 -0
@@ -0,0 +1,119 @@
1
+ /**
2
+ * MeteredSpendGate — the O(1), never-cached, FAIL-CLOSED money gate of the
3
+ * Routing Control Room (docs/specs/routing-control-room-spend-alerts.md,
4
+ * Increment B, Layer 3).
5
+ *
6
+ * Before a metered call: read committed FRESH from the MeteredSpendLedger, read
7
+ * the door's CANONICAL VALIDATED price (never the observed cache, never the
8
+ * overlay), compute the worst-case reserve at BASE price (no subsidy/credit),
9
+ * and refuse when `committed + estCost > cap` (strict `>`). FAIL CLOSED on
10
+ * EVERY uncertainty — that is the entire safety posture:
11
+ * not-live · frozen · no-cap-slice · lease-liveness-unconfirmed ·
12
+ * unbounded-reservation · unknown-price · implausible-price ·
13
+ * stale-price-fail-closed · invalid-cap · cap-exceeded · ledger-error
14
+ *
15
+ * Authority shape (signal-vs-authority): deterministic ARITHMETIC authority at
16
+ * the metered admission point only. A refusal is a swap-tail ADVANCE for the
17
+ * router (the chain falls to a free door) — never a chain kill (LF-A2).
18
+ *
19
+ * Structural exclusions (unit-tested):
20
+ * - reads NO `feature_metrics`, NO provider-report store, NO observed price
21
+ * cache, NO subsidy/credit input — only the ledger, the canonical manifest
22
+ * meta/points, and the PIN caps store.
23
+ *
24
+ * Wiring status (FD-11, honest): the metered dispatch that CALLS this gate is
25
+ * out-of-scope in-flight S4 work; the gate is fully unit-testable against a
26
+ * stub dispatch, and go-live remains refused until the FD-11 release-gate
27
+ * preconditions are met. Nothing in Increment B routes real money.
28
+ */
29
+ import type { MeteredSpendLedger } from './MeteredSpendLedger.js';
30
+ import type { RoutingPriceAuthority } from './routingPriceAuthority.js';
31
+ import type { RoutingSpendCapsStore } from './RoutingSpendCapsStore.js';
32
+ /**
33
+ * Per-provider price plausibility floors, USD per MILLION tokens — CODE-DEFINED
34
+ * constants, deliberately not config (A-M5/S-F1: a Bearer-writable surface must
35
+ * never influence a gate-consumed price value). A canonical point BELOW its
36
+ * provider's floor is treated as a typo → `implausible-price` → fail closed.
37
+ */
38
+ export declare const PROVIDER_PRICE_FLOORS_PER_MTOK: Readonly<Record<string, {
39
+ inPerMtok: number;
40
+ outPerMtok: number;
41
+ }>>;
42
+ /** How stale the metered-lease self-confirmation may be before the holder self-fences (N-2). MUST stay strictly shorter than the mesh-death threshold. */
43
+ export declare const LEASE_LIVENESS_WINDOW_MS = 60000;
44
+ export type MoneyGateRefusalReason = 'not-live' | 'frozen' | 'no-cap-slice' | 'lease-liveness-unconfirmed' | 'unbounded-reservation' | 'unknown-price' | 'implausible-price' | 'stale-price-fail-closed' | 'invalid-cap' | 'cap-exceeded' | 'ledger-error';
45
+ /** A cap/uncertainty refusal — the router treats it as a swap-tail ADVANCE, never a chain kill. */
46
+ export declare class MoneyGateRefusal extends Error {
47
+ reason: MoneyGateRefusalReason;
48
+ detail: string;
49
+ keyRef?: string | undefined;
50
+ door?: string | undefined;
51
+ constructor(reason: MoneyGateRefusalReason, detail: string, keyRef?: string | undefined, door?: string | undefined);
52
+ }
53
+ export interface AdmitRequest {
54
+ door: string;
55
+ modelId: string;
56
+ /** Prompt tokens (estimated or counted). Cached tokens reserve as FULL input (FD-19) — no cached discount at reserve time. */
57
+ inputTokens: number;
58
+ /** REQUIRED bounded output ceiling — the metered request MUST set max_tokens (A2-4). */
59
+ maxOutputTokens: number | undefined;
60
+ }
61
+ export interface AdmitResult {
62
+ reserveId: string;
63
+ keyRef: string;
64
+ reserveUsd: number;
65
+ /** The booking price actually used (canonical base or conservative-max under stale policy). */
66
+ bookedInPerMtok: number;
67
+ bookedOutPerMtok: number;
68
+ committedLifetimeUsd: number;
69
+ committedDayUsd: number;
70
+ }
71
+ export interface MeteredSpendGateOptions {
72
+ ledger: MeteredSpendLedger;
73
+ prices: RoutingPriceAuthority;
74
+ capsStore: RoutingSpendCapsStore;
75
+ /** This machine's id — compared against the go-live designation (FD-13). */
76
+ machineId: string;
77
+ /**
78
+ * POSITIVE re-confirmation of this machine's metered-lease designation against
79
+ * the pool: ms since the last successful confirmation, or null when never/failed.
80
+ * A single-machine agent trivially self-confirms (return 0).
81
+ */
82
+ leaseConfirmedAgoMs: () => number | null;
83
+ now?: () => number;
84
+ }
85
+ export declare class MeteredSpendGate {
86
+ private readonly d;
87
+ private readonly now;
88
+ constructor(opts: MeteredSpendGateOptions);
89
+ /**
90
+ * Admit-or-refuse a metered call, booking the worst-case reserve on admit.
91
+ * Every uncertainty throws MoneyGateRefusal (fail closed).
92
+ */
93
+ admit(req: AdmitRequest): Promise<AdmitResult>;
94
+ /**
95
+ * The gate-eligible price for a (door, model): the CANONICAL as-of point,
96
+ * validated + plausibility-floored; under a stale SLA the door's staleMode
97
+ * applies (default book-conservative-max — spend continues, never under-books).
98
+ */
99
+ private gatePrice;
100
+ private plausible;
101
+ /**
102
+ * Settle helpers for the (future) metered dispatch seam: ALL no-charge outcomes
103
+ * force-settle $0 (A-B2) unless tokens were demonstrably returned; a 200 books
104
+ * actual from the per-door BILLED-token mapping (erring HIGH), or worst-case
105
+ * when the billed basis cannot be confirmed.
106
+ */
107
+ settleActual(keyRef: string, reserveId: string, actualUsd: number): Promise<void>;
108
+ settleNoCharge(keyRef: string, reserveId: string): Promise<void>;
109
+ }
110
+ /**
111
+ * Per-door BILLED-token mapping (Layer 3): which response fields mean "billed
112
+ * output" for each metered door — Gemini bills thinking tokens as output
113
+ * (candidatesTokenCount can EXCLUDE them); OpenRouter/Groq bill
114
+ * completion_tokens. A response whose billed basis cannot be confirmed from the
115
+ * mapping settles at the WORST-CASE estimate, never a lower unverified field.
116
+ * Named + exported so the metered dispatch seam and the tests share ONE truth.
117
+ */
118
+ export declare function billedOutputTokens(door: string, usage: Record<string, unknown> | undefined): number | null;
119
+ //# sourceMappingURL=MeteredSpendGate.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"MeteredSpendGate.d.ts","sourceRoot":"","sources":["../../src/core/MeteredSpendGate.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2BG;AAGH,OAAO,KAAK,EAAE,kBAAkB,EAAiB,MAAM,yBAAyB,CAAC;AACjF,OAAO,KAAK,EAAE,qBAAqB,EAAqB,MAAM,4BAA4B,CAAC;AAC3F,OAAO,KAAK,EAAE,qBAAqB,EAAiB,MAAM,4BAA4B,CAAC;AAEvF;;;;;GAKG;AACH,eAAO,MAAM,8BAA8B,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE;IAAE,SAAS,EAAE,MAAM,CAAC;IAAC,UAAU,EAAE,MAAM,CAAA;CAAE,CAAC,CAI9G,CAAC;AAEF,0JAA0J;AAC1J,eAAO,MAAM,wBAAwB,QAAS,CAAC;AAE/C,MAAM,MAAM,sBAAsB,GAC9B,UAAU,GACV,QAAQ,GACR,cAAc,GACd,4BAA4B,GAC5B,uBAAuB,GACvB,eAAe,GACf,mBAAmB,GACnB,yBAAyB,GACzB,aAAa,GACb,cAAc,GACd,cAAc,CAAC;AAEnB,mGAAmG;AACnG,qBAAa,gBAAiB,SAAQ,KAAK;IAEhC,MAAM,EAAE,sBAAsB;IAC9B,MAAM,EAAE,MAAM;IACd,MAAM,CAAC,EAAE,MAAM;IACf,IAAI,CAAC,EAAE,MAAM;gBAHb,MAAM,EAAE,sBAAsB,EAC9B,MAAM,EAAE,MAAM,EACd,MAAM,CAAC,EAAE,MAAM,YAAA,EACf,IAAI,CAAC,EAAE,MAAM,YAAA;CAKvB;AAED,MAAM,WAAW,YAAY;IAC3B,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,MAAM,CAAC;IAChB,8HAA8H;IAC9H,WAAW,EAAE,MAAM,CAAC;IACpB,wFAAwF;IACxF,eAAe,EAAE,MAAM,GAAG,SAAS,CAAC;CACrC;AAED,MAAM,WAAW,WAAW;IAC1B,SAAS,EAAE,MAAM,CAAC;IAClB,MAAM,EAAE,MAAM,CAAC;IACf,UAAU,EAAE,MAAM,CAAC;IACnB,+FAA+F;IAC/F,eAAe,EAAE,MAAM,CAAC;IACxB,gBAAgB,EAAE,MAAM,CAAC;IACzB,oBAAoB,EAAE,MAAM,CAAC;IAC7B,eAAe,EAAE,MAAM,CAAC;CACzB;AAED,MAAM,WAAW,uBAAuB;IACtC,MAAM,EAAE,kBAAkB,CAAC;IAC3B,MAAM,EAAE,qBAAqB,CAAC;IAC9B,SAAS,EAAE,qBAAqB,CAAC;IACjC,4EAA4E;IAC5E,SAAS,EAAE,MAAM,CAAC;IAClB;;;;OAIG;IACH,mBAAmB,EAAE,MAAM,MAAM,GAAG,IAAI,CAAC;IACzC,GAAG,CAAC,EAAE,MAAM,MAAM,CAAC;CACpB;AAED,qBAAa,gBAAgB;IAC3B,OAAO,CAAC,QAAQ,CAAC,CAAC,CAA0B;IAC5C,OAAO,CAAC,QAAQ,CAAC,GAAG,CAAe;gBAEvB,IAAI,EAAE,uBAAuB;IAKzC;;;OAGG;IACG,KAAK,CAAC,GAAG,EAAE,YAAY,GAAG,OAAO,CAAC,WAAW,CAAC;IA6EpD;;;;OAIG;IACH,OAAO,CAAC,SAAS;IAyBjB,OAAO,CAAC,SAAS;IAYjB;;;;;OAKG;IACG,YAAY,CAAC,MAAM,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAIjF,cAAc,CAAC,MAAM,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;CAGvE;AAED;;;;;;;GAOG;AACH,wBAAgB,kBAAkB,CAAC,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,SAAS,GAAG,MAAM,GAAG,IAAI,CAyB1G"}
@@ -0,0 +1,228 @@
1
+ /**
2
+ * MeteredSpendGate — the O(1), never-cached, FAIL-CLOSED money gate of the
3
+ * Routing Control Room (docs/specs/routing-control-room-spend-alerts.md,
4
+ * Increment B, Layer 3).
5
+ *
6
+ * Before a metered call: read committed FRESH from the MeteredSpendLedger, read
7
+ * the door's CANONICAL VALIDATED price (never the observed cache, never the
8
+ * overlay), compute the worst-case reserve at BASE price (no subsidy/credit),
9
+ * and refuse when `committed + estCost > cap` (strict `>`). FAIL CLOSED on
10
+ * EVERY uncertainty — that is the entire safety posture:
11
+ * not-live · frozen · no-cap-slice · lease-liveness-unconfirmed ·
12
+ * unbounded-reservation · unknown-price · implausible-price ·
13
+ * stale-price-fail-closed · invalid-cap · cap-exceeded · ledger-error
14
+ *
15
+ * Authority shape (signal-vs-authority): deterministic ARITHMETIC authority at
16
+ * the metered admission point only. A refusal is a swap-tail ADVANCE for the
17
+ * router (the chain falls to a free door) — never a chain kill (LF-A2).
18
+ *
19
+ * Structural exclusions (unit-tested):
20
+ * - reads NO `feature_metrics`, NO provider-report store, NO observed price
21
+ * cache, NO subsidy/credit input — only the ledger, the canonical manifest
22
+ * meta/points, and the PIN caps store.
23
+ *
24
+ * Wiring status (FD-11, honest): the metered dispatch that CALLS this gate is
25
+ * out-of-scope in-flight S4 work; the gate is fully unit-testable against a
26
+ * stub dispatch, and go-live remains refused until the FD-11 release-gate
27
+ * preconditions are met. Nothing in Increment B routes real money.
28
+ */
29
+ import { CapExceededError } from './MeteredSpendLedger.js';
30
+ /**
31
+ * Per-provider price plausibility floors, USD per MILLION tokens — CODE-DEFINED
32
+ * constants, deliberately not config (A-M5/S-F1: a Bearer-writable surface must
33
+ * never influence a gate-consumed price value). A canonical point BELOW its
34
+ * provider's floor is treated as a typo → `implausible-price` → fail closed.
35
+ */
36
+ export const PROVIDER_PRICE_FLOORS_PER_MTOK = {
37
+ google: { inPerMtok: 0.01, outPerMtok: 0.01 },
38
+ openrouter: { inPerMtok: 0.01, outPerMtok: 0.01 },
39
+ groq: { inPerMtok: 0.01, outPerMtok: 0.01 },
40
+ };
41
+ /** How stale the metered-lease self-confirmation may be before the holder self-fences (N-2). MUST stay strictly shorter than the mesh-death threshold. */
42
+ export const LEASE_LIVENESS_WINDOW_MS = 60_000;
43
+ /** A cap/uncertainty refusal — the router treats it as a swap-tail ADVANCE, never a chain kill. */
44
+ export class MoneyGateRefusal extends Error {
45
+ reason;
46
+ detail;
47
+ keyRef;
48
+ door;
49
+ constructor(reason, detail, keyRef, door) {
50
+ super(`money gate refused (${reason}): ${detail}`);
51
+ this.reason = reason;
52
+ this.detail = detail;
53
+ this.keyRef = keyRef;
54
+ this.door = door;
55
+ this.name = 'MoneyGateRefusal';
56
+ }
57
+ }
58
+ export class MeteredSpendGate {
59
+ d;
60
+ now;
61
+ constructor(opts) {
62
+ this.d = opts;
63
+ this.now = opts.now ?? (() => Date.now());
64
+ }
65
+ /**
66
+ * Admit-or-refuse a metered call, booking the worst-case reserve on admit.
67
+ * Every uncertainty throws MoneyGateRefusal (fail closed).
68
+ */
69
+ async admit(req) {
70
+ // 1) Go-live: deny-by-default (not-live), key resolution, designation + freeze.
71
+ let caps;
72
+ try {
73
+ caps = this.d.capsStore.read();
74
+ }
75
+ catch (err) {
76
+ throw new MoneyGateRefusal('invalid-cap', `caps store unreadable: ${String(err)}`, undefined, req.door);
77
+ }
78
+ const goLive = caps.goLive[req.door];
79
+ if (!goLive || !goLive.enabled) {
80
+ throw new MoneyGateRefusal('not-live', `door '${req.door}' has no live go-live record (deny-by-default)`, undefined, req.door);
81
+ }
82
+ const keyRef = goLive.keyRef;
83
+ if (goLive.designatedMachineId !== this.d.machineId) {
84
+ throw new MoneyGateRefusal('no-cap-slice', `this machine holds no cap authority for '${req.door}' (designated: ${goLive.designatedMachineId})`, keyRef, req.door);
85
+ }
86
+ // 2) Self-fence: positive lease re-confirmation within the bounded window (N-2).
87
+ const confirmedAgo = this.d.leaseConfirmedAgoMs();
88
+ if (confirmedAgo === null || confirmedAgo > LEASE_LIVENESS_WINDOW_MS) {
89
+ throw new MoneyGateRefusal('lease-liveness-unconfirmed', `metered-lease designation not positively re-confirmed within ${LEASE_LIVENESS_WINDOW_MS}ms (ago: ${confirmedAgo ?? 'never'})`, keyRef, req.door);
90
+ }
91
+ // 3) Freeze + cap validity.
92
+ const keyCaps = caps.caps[keyRef];
93
+ if (keyCaps?.frozen) {
94
+ throw new MoneyGateRefusal('frozen', `key '${keyRef}' is frozen${keyCaps.frozenBy ? ` (by ${keyCaps.frozenBy})` : ''}`, keyRef, req.door);
95
+ }
96
+ const lifetimeCap = keyCaps?.lifetimeCapUsd;
97
+ const dailyCap = keyCaps?.dailyCapUsd;
98
+ if (typeof lifetimeCap !== 'number' || !Number.isFinite(lifetimeCap) || lifetimeCap <= 0 ||
99
+ typeof dailyCap !== 'number' || !Number.isFinite(dailyCap) || dailyCap <= 0) {
100
+ throw new MoneyGateRefusal('invalid-cap', `key '${keyRef}' has no valid positive caps (a cap of 0/absent admits nothing)`, keyRef, req.door);
101
+ }
102
+ // 4) Bounded reservation (A2-4).
103
+ if (req.maxOutputTokens === undefined || !Number.isFinite(req.maxOutputTokens) || req.maxOutputTokens <= 0 ||
104
+ !Number.isFinite(req.inputTokens) || req.inputTokens < 0) {
105
+ throw new MoneyGateRefusal('unbounded-reservation', 'metered call without a bounded max_tokens output ceiling (or invalid input estimate) is refused', keyRef, req.door);
106
+ }
107
+ // 5) Canonical VALIDATED price only (never observed, never overlay), stale policy applied.
108
+ const { inPerMtok, outPerMtok } = this.gatePrice(req.door, req.modelId, keyRef);
109
+ // 6) Worst-case reserve at BASE price; cached tokens as FULL input (FD-19).
110
+ const reserveUsd = (req.inputTokens / 1e6) * inPerMtok + (req.maxOutputTokens / 1e6) * outPerMtok;
111
+ // 7+8) ATOMIC check-and-reserve: the strict-> cap comparison runs INSIDE the
112
+ // ledger's per-key booking critical section (never outside it — the only
113
+ // ordering under which two CONCURRENT reservations are guaranteed to see
114
+ // each other; the two-concurrent-reserves unit test pins this). The read is
115
+ // O(1) and fresh (high-water-checked) at the moment of booking.
116
+ let handle;
117
+ try {
118
+ handle = await this.d.ledger.reserve({
119
+ keyRef,
120
+ door: req.door,
121
+ modelId: req.modelId,
122
+ reserveUsd,
123
+ leaseEpoch: caps.leaseEpoch,
124
+ admitOnlyUnderCaps: { lifetimeCapUsd: lifetimeCap, dailyCapUsd: dailyCap },
125
+ });
126
+ }
127
+ catch (err) {
128
+ if (err instanceof CapExceededError) {
129
+ throw new MoneyGateRefusal('cap-exceeded', err.message, keyRef, req.door);
130
+ }
131
+ throw new MoneyGateRefusal('ledger-error', `reserve booking failed: ${String(err)}`, keyRef, req.door);
132
+ }
133
+ return {
134
+ reserveId: handle.reserveId,
135
+ keyRef,
136
+ reserveUsd,
137
+ bookedInPerMtok: inPerMtok,
138
+ bookedOutPerMtok: outPerMtok,
139
+ committedLifetimeUsd: handle.committedLifetimeUsd,
140
+ committedDayUsd: handle.committedDayUsd,
141
+ };
142
+ }
143
+ /**
144
+ * The gate-eligible price for a (door, model): the CANONICAL as-of point,
145
+ * validated + plausibility-floored; under a stale SLA the door's staleMode
146
+ * applies (default book-conservative-max — spend continues, never under-books).
147
+ */
148
+ gatePrice(door, modelId, keyRef) {
149
+ const res = this.d.prices.resolve(door, modelId, this.now());
150
+ // Canonical ONLY: an observed point is structurally not gate-eligible (S2-2).
151
+ if (res.priceBasis !== 'canonical' || !res.point) {
152
+ throw new MoneyGateRefusal('unknown-price', `no canonical price point for (${door}, ${modelId})`, keyRef, door);
153
+ }
154
+ const meta = this.d.prices.doorMetaFor(door);
155
+ if (res.priceStale) {
156
+ const mode = meta?.staleMode ?? 'book-conservative-max';
157
+ if (mode === 'fail-closed') {
158
+ throw new MoneyGateRefusal('stale-price-fail-closed', `canonical price for '${door}' is older than its freshness SLA and staleMode is fail-closed`, keyRef, door);
159
+ }
160
+ const cm = meta?.conservativeMax;
161
+ if (!cm || !this.plausible(door, cm.inPerMtok, cm.outPerMtok)) {
162
+ throw new MoneyGateRefusal('unknown-price', `stale price for '${door}' and no plausible conservativeMax to book against`, keyRef, door);
163
+ }
164
+ return { inPerMtok: cm.inPerMtok, outPerMtok: cm.outPerMtok };
165
+ }
166
+ const pt = res.point;
167
+ if (!this.plausible(door, pt.inPerMtok, pt.outPerMtok)) {
168
+ throw new MoneyGateRefusal('implausible-price', `canonical price for (${door}, ${modelId}) is below the code-defined provider floor — likely a typo`, keyRef, door);
169
+ }
170
+ return { inPerMtok: pt.inPerMtok, outPerMtok: pt.outPerMtok };
171
+ }
172
+ plausible(door, inPerMtok, outPerMtok) {
173
+ const provider = door.split('-')[0]; // gemini-api → gemini … door naming is provider-prefixed
174
+ const floor = PROVIDER_PRICE_FLOORS_PER_MTOK[provider] ??
175
+ PROVIDER_PRICE_FLOORS_PER_MTOK[{ gemini: 'google' }[provider] ?? ''] ??
176
+ { inPerMtok: 0.001, outPerMtok: 0.001 };
177
+ return (Number.isFinite(inPerMtok) && Number.isFinite(outPerMtok) &&
178
+ inPerMtok >= floor.inPerMtok && outPerMtok >= floor.outPerMtok);
179
+ }
180
+ /**
181
+ * Settle helpers for the (future) metered dispatch seam: ALL no-charge outcomes
182
+ * force-settle $0 (A-B2) unless tokens were demonstrably returned; a 200 books
183
+ * actual from the per-door BILLED-token mapping (erring HIGH), or worst-case
184
+ * when the billed basis cannot be confirmed.
185
+ */
186
+ async settleActual(keyRef, reserveId, actualUsd) {
187
+ await this.d.ledger.settle(keyRef, reserveId, actualUsd);
188
+ }
189
+ async settleNoCharge(keyRef, reserveId) {
190
+ await this.d.ledger.settle(keyRef, reserveId, 0);
191
+ }
192
+ }
193
+ /**
194
+ * Per-door BILLED-token mapping (Layer 3): which response fields mean "billed
195
+ * output" for each metered door — Gemini bills thinking tokens as output
196
+ * (candidatesTokenCount can EXCLUDE them); OpenRouter/Groq bill
197
+ * completion_tokens. A response whose billed basis cannot be confirmed from the
198
+ * mapping settles at the WORST-CASE estimate, never a lower unverified field.
199
+ * Named + exported so the metered dispatch seam and the tests share ONE truth.
200
+ */
201
+ export function billedOutputTokens(door, usage) {
202
+ if (!usage || typeof usage !== 'object')
203
+ return null;
204
+ const n = (v) => (typeof v === 'number' && Number.isFinite(v) && v >= 0 ? v : null);
205
+ if (door === 'gemini-api') {
206
+ // Native path: candidatesTokenCount + thoughtsTokenCount. OpenAI-compat path:
207
+ // completion_tokens (+ reasoning tokens detail when present).
208
+ const candidates = n(usage['candidatesTokenCount']);
209
+ if (candidates !== null) {
210
+ const thoughts = n(usage['thoughtsTokenCount']) ?? 0;
211
+ return candidates + thoughts;
212
+ }
213
+ const completion = n(usage['completion_tokens']);
214
+ if (completion !== null) {
215
+ const details = usage['completion_tokens_details'];
216
+ const reasoning = n(details?.['reasoning_tokens']) ?? 0;
217
+ // completion_tokens on the compat path already includes reasoning on some
218
+ // shapes; take the MAX interpretation (erring HIGH, the safe direction).
219
+ return Math.max(completion, reasoning);
220
+ }
221
+ return null;
222
+ }
223
+ if (door === 'openrouter-api' || door === 'groq-api') {
224
+ return n(usage['completion_tokens']);
225
+ }
226
+ return null;
227
+ }
228
+ //# sourceMappingURL=MeteredSpendGate.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"MeteredSpendGate.js","sourceRoot":"","sources":["../../src/core/MeteredSpendGate.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,154 @@
1
+ /**
2
+ * MeteredSpendLedger — Layer 3 of the Routing Control Room
3
+ * (docs/specs/routing-control-room-spend-alerts.md, Increment B).
4
+ *
5
+ * The AUTHORITATIVE money truth: an append-only, booking-priced ledger of every
6
+ * metered-door reservation/settlement, plus a maintained O(1) committed total the
7
+ * fail-closed gate reads. This ledger — NOT `feature_metrics`, NOT the Layer-1c
8
+ * provider-report store — is the ONLY rebuild source for committed spend
9
+ * (FD-3/FD-9): rebuilding from Layer 0 joined to current prices is FORBIDDEN at
10
+ * the gate (a downward `corrects` would re-open capped headroom).
11
+ *
12
+ * Write discipline (adopted from DriftSpendLedger, upgraded per spec C4-2):
13
+ * - Rows are APPEND-ONLY JSONL, fsync'd, written FIRST; the totals file is a
14
+ * regenerable CACHE of the fold, rewritten atomically (tmp+rename) SECOND.
15
+ * A crash between the two leaves totals STALE-LOW by at most one booking; the
16
+ * next gate read runs a cheap high-water check (file size) and re-folds.
17
+ * - Writes are FAIL-CLOSED and NON-SWALLOWING: a booking that cannot be durably
18
+ * persisted throws, and the caller must refuse the metered call.
19
+ * - Boot: fold all rows, REWRITE the totals cache from row truth (torn totals
20
+ * rename / totals-without-append are always corrected; the append-first
21
+ * ordering makes totals-without-append impossible, asserted in tests).
22
+ * - A torn trailing append (partial last line) is the malformed-row-skip case.
23
+ *
24
+ * Reserve/settle lifecycle (A-B2/A2-1):
25
+ * - reserve(): books the worst-case estimate (cached tokens as FULL input,
26
+ * FD-19; output at the REQUIRED max-tokens ceiling). Outstanding reserves are
27
+ * INSIDE the committed total (concurrent reservations see each other).
28
+ * - settle(): idempotent terminal `reserved → settled`; books ACTUAL cost.
29
+ * A settle after expiry books the actual cost as a fresh ABSOLUTE row
30
+ * (expiry-aware settle) — the late-settle race can never under-count.
31
+ * - expire(): the reserve-expiry sweep (takes the per-key mutex) expires only
32
+ * reserves older than the TTL still in `reserved` state.
33
+ * - First terminal transition wins; the loser is a no-op.
34
+ *
35
+ * Concurrency: metered calls funnel through the single server process, so the
36
+ * booking critical sections are guarded by an in-process async mutex per keyRef
37
+ * (held only for the booking, RELEASED during the LLM round-trip) plus a
38
+ * proper-lockfile advisory lock for defence against a second process.
39
+ */
40
+ /** Default reserve TTL — pinned comfortably above the metered-call latency ceiling (spec: 15 min vs a 5-min call ceiling). */
41
+ export declare const DEFAULT_RESERVE_TTL_MS: number;
42
+ export type MeteredBookingKind = 'reserve' | 'settle' | 'expire';
43
+ /** One append-only booking row. NEVER mutated; corrections are new rows. */
44
+ export interface MeteredBookingRow {
45
+ ts: string;
46
+ keyRef: string;
47
+ door: string;
48
+ modelId: string;
49
+ kind: MeteredBookingKind;
50
+ /** The per-call id (=== meteredCallId) minted at reserve time. */
51
+ reserveId: string;
52
+ /** Reserve: worst-case estimate. Settle: ACTUAL booked cost. Expire: 0. */
53
+ costUsd: number;
54
+ /** The metered-lease epoch the booking was made under (A-B4). */
55
+ leaseEpoch: number;
56
+ /** Set on a settle that arrived after its reserve was expired (books absolute). */
57
+ lateAfterExpire?: boolean;
58
+ }
59
+ /** The maintained O(1) running totals (a regenerable cache of the fold). */
60
+ export interface MeteredCommittedTotals {
61
+ keyRef: string;
62
+ committedLifetimeUsd: number;
63
+ committedDayUsd: number;
64
+ /** UTC day (YYYY-MM-DD) the day total belongs to; a new day resets it. */
65
+ dayEpoch: string;
66
+ updatedAt: string;
67
+ }
68
+ export interface ReserveRequest {
69
+ keyRef: string;
70
+ door: string;
71
+ modelId: string;
72
+ /** Worst-case reserve in USD (computed by the gate at canonical BASE price). */
73
+ reserveUsd: number;
74
+ leaseEpoch: number;
75
+ /**
76
+ * When set, the cap comparison happens ATOMICALLY inside the per-key booking
77
+ * critical section (`committed + reserve > cap` strict `>`, both caps) — the
78
+ * only ordering under which two concurrent reservations are guaranteed to see
79
+ * each other (spec Layer 3). Refusal throws CapExceededError; nothing books.
80
+ */
81
+ admitOnlyUnderCaps?: {
82
+ lifetimeCapUsd: number;
83
+ dailyCapUsd: number;
84
+ };
85
+ }
86
+ /** Thrown by reserve() when admitOnlyUnderCaps would be breached — nothing was booked. */
87
+ export declare class CapExceededError extends Error {
88
+ capKind: 'lifetime' | 'daily';
89
+ committedUsd: number;
90
+ reserveUsd: number;
91
+ capUsd: number;
92
+ constructor(capKind: 'lifetime' | 'daily', committedUsd: number, reserveUsd: number, capUsd: number);
93
+ }
94
+ export interface ReserveHandle {
95
+ reserveId: string;
96
+ /** Committed lifetime total AFTER this reservation (outstanding reserves included). */
97
+ committedLifetimeUsd: number;
98
+ committedDayUsd: number;
99
+ }
100
+ export declare class MeteredLedgerWriteError extends Error {
101
+ cause?: unknown | undefined;
102
+ constructor(msg: string, cause?: unknown | undefined);
103
+ }
104
+ export interface MeteredSpendLedgerOptions {
105
+ /** The agent's `.instar/` dir; rows/totals live under `state/`, the lock under `local/`. */
106
+ stateDir: string;
107
+ reserveTtlMs?: number;
108
+ now?: () => number;
109
+ }
110
+ export declare class MeteredSpendLedger {
111
+ private readonly rowsPath;
112
+ private readonly totalsPath;
113
+ private readonly lockTarget;
114
+ private readonly ttlMs;
115
+ private readonly now;
116
+ /** In-memory fold state, authoritative in-process. Rebuilt on construction. */
117
+ private reserves;
118
+ private totals;
119
+ /** Byte length of the rows file as of our last append/fold (the high-water mark). */
120
+ private rowsBytes;
121
+ /** In-process per-key mutex tail. */
122
+ private mutexTail;
123
+ constructor(opts: MeteredSpendLedgerOptions);
124
+ /** O(1) committed totals for a key, day-rolled to NOW. High-water-checked against external appends. */
125
+ committed(keyRef: string): MeteredCommittedTotals;
126
+ /** Reserve worst-case cost. FAIL-CLOSED: any persistence failure throws. */
127
+ reserve(req: ReserveRequest): Promise<ReserveHandle>;
128
+ /**
129
+ * Terminal settle at ACTUAL cost. Idempotent: a second settle is a no-op.
130
+ * A settle after expiry books the actual as a fresh ABSOLUTE row (late-settle).
131
+ */
132
+ settle(keyRef: string, reserveId: string, actualUsd: number): Promise<void>;
133
+ /** Reserve-expiry sweep (Layer 3 — takes the per-key mutex). Expires only TTL-stale `reserved` states. */
134
+ sweepExpired(): Promise<number>;
135
+ /** All totals (for the caps view). Day-rolled to now. */
136
+ allCommitted(): MeteredCommittedTotals[];
137
+ /** Apply one row to the in-memory fold. */
138
+ private applyRow;
139
+ /** Contribution of one reserve state to committed totals. */
140
+ private contribution;
141
+ private recomputeTotalsForKey;
142
+ /** Day-rolled view of a key's totals (a new UTC day resets the day figure). */
143
+ private currentTotals;
144
+ /** Full refold from row truth (the fold is canon; the totals file is a cache). */
145
+ private refoldFromRows;
146
+ /** Cheap external-append detection: rows-file byte length vs our high-water mark. */
147
+ private checkHighWater;
148
+ /** Append one row, fsync'd, under the advisory file lock. NON-SWALLOWING. */
149
+ private appendDurable;
150
+ /** Atomic (tmp+rename) rewrite of the totals cache. Best-effort — rows are canon. */
151
+ private rewriteTotalsCache;
152
+ private withKeyMutex;
153
+ }
154
+ //# sourceMappingURL=MeteredSpendLedger.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"MeteredSpendLedger.d.ts","sourceRoot":"","sources":["../../src/core/MeteredSpendLedger.ts"],"names":[],"mappings":"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"}