@tangle-network/agent-app 0.45.33 → 0.45.35

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.
@@ -252,6 +252,98 @@ interface SpendOwnershipSummary {
252
252
  */
253
253
  readonly foreignSandboxIds: readonly string[];
254
254
  }
255
+ /**
256
+ * The stretch of time a reconciliation pass covers — the same window the
257
+ * product's own ledger fetch was scoped to.
258
+ *
259
+ * Declared by the caller rather than derived from the rows, because deriving it
260
+ * from the rows is circular: a feed that returns nothing would produce a
261
+ * zero-width window that expects nothing and certifies itself.
262
+ */
263
+ interface SpendWindow {
264
+ /** Inclusive start, epoch ms. */
265
+ readonly startAt: number;
266
+ /** Inclusive end, epoch ms. */
267
+ readonly endAt: number;
268
+ }
269
+ /**
270
+ * One box's life, measured against a reconciliation window.
271
+ *
272
+ * The live interval is `[createdAt, horizon]`, where the horizon is the SAME one
273
+ * `computeExpectedCeiling` derives — so a box cannot count as live for the
274
+ * expectation check and dead for the ceiling check.
275
+ */
276
+ interface SpendBoxLiveness {
277
+ readonly sandboxId: string;
278
+ readonly workspaceId: string;
279
+ /** First instant the box could have been billable. */
280
+ readonly liveFrom: number;
281
+ /** Last instant it could have been, evaluated at the window's end. */
282
+ readonly liveUntil: number;
283
+ /** Which fact closed the interval — the ceiling's own vocabulary. */
284
+ readonly basis: CeilingBasis;
285
+ /** True when any of that life fell inside the window. */
286
+ readonly overlaps: boolean;
287
+ /** How much of it did, in ms. Zero for a box that did not overlap. */
288
+ readonly liveMsInWindow: number;
289
+ /**
290
+ * True when the overlap is long enough that a settlement should have landed.
291
+ * A box that came up moments before the window closed is live but not yet
292
+ * expected — settlement lags provisioning, and demanding a row inside that lag
293
+ * would report the platform's ordinary queue as a defect.
294
+ */
295
+ readonly expectSettlement: boolean;
296
+ }
297
+ /**
298
+ * What this pass EXPECTED to be billed for — present on every report, including
299
+ * a clean one, for the same reason {@link SpendOwnershipSummary} is: "nothing
300
+ * fired" and "nothing was expected" and "nothing could be expected" are three
301
+ * different answers and a report that cannot tell them apart is the failure this
302
+ * closes.
303
+ */
304
+ interface SpendExpectationSummary {
305
+ /**
306
+ * False when the caller declared no window, or the store cannot list its
307
+ * boxes. The pass then cannot say what it expected — which is reported, never
308
+ * rounded down to a clean bill.
309
+ */
310
+ readonly declared: boolean;
311
+ readonly window: SpendWindow | null;
312
+ /** Live ms inside the window before a settlement is expected of a box. */
313
+ readonly graceMs: number;
314
+ /** Boxes whose life overlapped the window at all. */
315
+ readonly liveBoxes: number;
316
+ /** Of those, the ones live long enough that a settlement should have landed. */
317
+ readonly expectedBoxes: number;
318
+ /** Of those, the ones at least one settlement this pass claimed did land against. */
319
+ readonly settledBoxes: number;
320
+ /**
321
+ * The expected boxes nothing settled against, in full — the exhibit list for
322
+ * "the check stopped checking", and an expectation a reader cannot audit is
323
+ * one they have to take on trust.
324
+ */
325
+ readonly unsettledSandboxIds: readonly string[];
326
+ }
327
+ /**
328
+ * How much this pass is entitled to claim about the bill.
329
+ *
330
+ * `ok` is gated on this, which is what makes an examined-nobody pass
331
+ * structurally incapable of rendering as a clean bill.
332
+ */
333
+ type SpendCoverage =
334
+ /** The pass examined this product's settlements, or knows what it expected. */
335
+ 'verified'
336
+ /**
337
+ * Expectation was declared and no box was live long enough to expect a bill.
338
+ * Zero settlements is the RIGHT answer — an idle product, not a defect, and
339
+ * the one case in which a pass that examined nobody is still clean.
340
+ */
341
+ | 'nothing-expected'
342
+ /**
343
+ * The pass examined none of this product's settlements and cannot say what it
344
+ * expected. The CHECK is suspect, not the bill.
345
+ */
346
+ | 'unverified';
255
347
  /** The parts of a settlement reference id, once parsed. */
256
348
  interface SettlementReference {
257
349
  /** `stop` | `compute` | `egress` | `gpu-lease` | anything the platform adds. */
@@ -284,7 +376,18 @@ type SpendCheckId =
284
376
  /** A spend window far above the trailing median — the burst shape of a defect. */
285
377
  | 'velocity'
286
378
  /** The balance the product observes has gone below its floor. */
287
- | 'negative-balance';
379
+ | 'negative-balance'
380
+ /**
381
+ * The product expected settlements and saw none — the only check whose
382
+ * subject is the CHECK rather than the bill.
383
+ *
384
+ * Every other rule is driven by a settlement row, so all of them go quiet
385
+ * together when the rows stop arriving: an empty ledger fetch, a stale
386
+ * ownership rule that excludes every box, an expectation ledger naming
387
+ * nobody. This is the rule that fires when the others cannot, and it reads as
388
+ * "do not trust this report" rather than "dispute this charge".
389
+ */
390
+ | 'silent-ledger';
288
391
  declare const SPEND_CHECKS: readonly SpendCheckId[];
289
392
  /**
290
393
  * One discrepancy, with every number the rule compared.
@@ -316,15 +419,34 @@ interface SpendFinding {
316
419
  readonly windowNanoUsd: number | null;
317
420
  readonly trailingMedianNanoUsd: number | null;
318
421
  readonly velocityRatio: number | null;
422
+ /** `velocity` and `silent-ledger` — the window the finding is about. */
319
423
  readonly windowStartAt: number | null;
424
+ readonly windowEndAt: number | null;
320
425
  /** `negative-balance` — the observed balance and the floor it broke. */
321
426
  readonly balanceNanoUsd: number | null;
322
427
  readonly balanceFloorNanoUsd: number | null;
428
+ /** `silent-ledger` — how many boxes were expected to settle, and how many did. */
429
+ readonly expectedBoxes: number | null;
430
+ readonly settledBoxes: number | null;
431
+ /** `silent-ledger`, per-box form — how long that box was live inside the window. */
432
+ readonly liveMsInWindow: number | null;
323
433
  }
324
434
  /** What one reconciliation pass concluded. */
325
435
  interface SpendReport {
326
- /** True when nothing fired. `ok === findings.length === 0`. */
436
+ /**
437
+ * True when nothing fired AND the pass earned the right to say so:
438
+ * `findings.length === 0 && coverage !== 'unverified'`.
439
+ *
440
+ * The second half is not decoration. Every rule but `silent-ledger` is driven
441
+ * by a settlement row, so a pass that read no rows — an empty ledger fetch, a
442
+ * stale ownership rule excluding every box, an expectation ledger naming
443
+ * nobody — fires nothing and used to report a clean bill. `coverage` is what
444
+ * makes that shape unrepresentable, and it holds even when a caller skips the
445
+ * `silent-ledger` check: the skip removes the finding, never the verdict.
446
+ */
327
447
  readonly ok: boolean;
448
+ /** How much this pass is entitled to claim. See {@link SpendCoverage}. */
449
+ readonly coverage: SpendCoverage;
328
450
  readonly findings: readonly SpendFinding[];
329
451
  readonly checksRun: readonly SpendCheckId[];
330
452
  /** Rows the pass read, including the ones no rule looked at. */
@@ -337,6 +459,8 @@ interface SpendReport {
337
459
  readonly creditedNanoUsd: number;
338
460
  /** What this pass claimed as its own, and what it excluded as another product's. */
339
461
  readonly ownership: SpendOwnershipSummary;
462
+ /** What this pass expected to be billed for, and what did not arrive. */
463
+ readonly expectation: SpendExpectationSummary;
340
464
  /** The instant the pass treated as "now". */
341
465
  readonly asOf: number;
342
466
  }
@@ -428,6 +552,137 @@ declare function ownedByBillingKeys(keyIds: readonly string[]): SpendOwnershipRu
428
552
  */
429
553
  declare function decideBoxOwnership(rule: SpendOwnershipRule, sandboxId: string, rows: readonly SettlementRow[]): SpendOwnershipVerdict;
430
554
 
555
+ /**
556
+ * Expectation liveness: what the product BELIEVES it should have been billed for.
557
+ *
558
+ * ## The hole this closes
559
+ *
560
+ * `reconcileSpend` compares settlements against expectations, and every one of
561
+ * its rules is driven by a settlement row. That makes it able to answer exactly
562
+ * one direction of the question:
563
+ *
564
+ * > were we billed for something we did not ask for, or for more than allowed?
565
+ *
566
+ * It cannot answer the other direction — *were we NOT billed for something we
567
+ * DID ask for* — because with no row there is nothing to iterate. Three real
568
+ * shapes fall straight through that hole and every one of them renders as a
569
+ * clean bill:
570
+ *
571
+ * - the expectation ledger names no box, so the pass examines nobody → `ok:true`;
572
+ * - the billing endpoint quietly starts returning zero rows → `ok:true`,
573
+ * `rowsExamined:0`;
574
+ * - a stale or rotated key list excludes every box → `ok:true` with everything
575
+ * `foreign`.
576
+ *
577
+ * In all three the check stopped checking while looking green. That is the exact
578
+ * failure class the module exists to prevent, reproduced inside the module.
579
+ *
580
+ * ## The discriminator, and why it is not a new store
581
+ *
582
+ * The expectation ledger already records the only fact needed: when the product
583
+ * first saw a box (`createdAt`), the last work it observed (`lastActivityAt`),
584
+ * and any stop or delete it knows about. Those are precisely the inputs
585
+ * {@link computeExpectedCeiling} already folds into a horizon — the latest
586
+ * instant a box could still have been billable. So a box's LIVE INTERVAL is
587
+ * `[createdAt, horizon]`, derived from the same fold the ceiling uses rather
588
+ * than from a second, drift-prone definition of "running".
589
+ *
590
+ * A box whose live interval overlaps the reconciliation window is a box the
591
+ * product expected to be billed for. The absence of a settlement against it is
592
+ * then a first-class outcome, not silence.
593
+ *
594
+ * ## Why the grace period is load-bearing
595
+ *
596
+ * Settlement lags provisioning. A box that came up ninety seconds before the
597
+ * window closed has no settlement yet and never should have — expecting one
598
+ * would manufacture a finding out of the platform's ordinary queue behaviour.
599
+ * So expectation is asserted only for boxes with at least
600
+ * {@link DEFAULT_EXPECTATION_GRACE_MS} of live time INSIDE the window, which is
601
+ * the platform's own declared-normal settlement lag (see the constant).
602
+ *
603
+ * The asymmetry the rest of the module runs on holds here too, but it points the
604
+ * other way and that is deliberate: an over-tight liveness derivation produces a
605
+ * false ALARM about the check, never a false clean bill. Nothing in this file
606
+ * can make a report cleaner than it would otherwise be.
607
+ */
608
+ /**
609
+ * How much live time a box needs inside the window before a settlement is
610
+ * EXPECTED for it. 15 minutes.
611
+ *
612
+ * Same number and same justification as {@link DEFAULT_CEILING_TOLERANCE_MS},
613
+ * arrived at from the other side: the platform's runbook clears a compute
614
+ * settlement incident when `/health computeSettlement.oldestAgeSeconds` is "back
615
+ * under 900", so 900 s is lag the platform has already declared normal. A box
616
+ * live for less than that inside the window may legitimately have settled
617
+ * nothing yet, and demanding a row for it would report the platform's own queue
618
+ * as a defect.
619
+ *
620
+ * A caller parameter, because a product reconciling a very short window must
621
+ * shrink it or the window expects nothing at all.
622
+ */
623
+ declare const DEFAULT_EXPECTATION_GRACE_MS = 900000;
624
+ /** Options for {@link boxLivenessInWindow}. */
625
+ interface BoxLivenessOptions {
626
+ /** Live ms inside the window before a settlement is expected. Default {@link DEFAULT_EXPECTATION_GRACE_MS}. */
627
+ readonly graceMs?: number;
628
+ }
629
+ /** Reject a window that cannot be reconciled, rather than examining nobody inside it. */
630
+ declare function assertSpendWindow(window: SpendWindow): void;
631
+ /**
632
+ * One box's live interval, and whether the product should have been billed for
633
+ * it inside this window.
634
+ *
635
+ * The live interval's end is {@link computeExpectedCeiling}'s horizon evaluated
636
+ * at the window's end — the SAME derivation the ceiling check uses, so a box
637
+ * cannot be considered live here and dead there. `toleranceMs` is zero because
638
+ * tolerance is slack allowed to the PLATFORM's clock, and widening a box's life
639
+ * by it would expect settlements for boxes that were already gone.
640
+ */
641
+ declare function boxLivenessInWindow(record: SpendBoxRecord, window: SpendWindow, options?: BoxLivenessOptions): SpendBoxLiveness;
642
+ /** Why {@link assessAllExcluded} answered the way it did. */
643
+ type AllExcludedBasis =
644
+ /** Not every box was excluded — the pass examined some of this product's own. */
645
+ 'not-all-excluded'
646
+ /** Boxes of this product's were live in the window, so an all-foreign pass is wrong. */
647
+ | 'expected-boxes-live'
648
+ /** Expectation was declared and nothing was live: an idle product, not a defect. */
649
+ | 'nothing-expected'
650
+ /** No expectation was declared, so the question cannot be answered. Fails closed. */
651
+ | 'not-declared';
652
+ /** {@link assessAllExcluded}'s verdict, with the reason a pager message needs. */
653
+ interface AllExcludedAssessment {
654
+ /** True when the caller should raise. */
655
+ readonly pathological: boolean;
656
+ readonly basis: AllExcludedBasis;
657
+ /** One sentence naming the numbers behind the verdict. */
658
+ readonly reason: string;
659
+ }
660
+ /**
661
+ * Should "we saw settlements but none of them were ours" page a human?
662
+ *
663
+ * Consumers raise this pathology on `boxesExamined > 0 && ownedBoxes === 0`.
664
+ * That shape has two completely different causes and the naive test cannot tell
665
+ * them apart:
666
+ *
667
+ * - an ownership rule that has gone stale — a rotated key, a new deployment key
668
+ * nobody added — so every one of the product's OWN boxes now reads as a
669
+ * sibling's. The check has stopped checking.
670
+ * - a product that was legitimately idle while a sibling settled on the same
671
+ * wallet. Nothing is wrong, and paging on it pages EVERY day the product is
672
+ * quiet, which is how a real alert gets muted.
673
+ *
674
+ * The discriminator is expectation liveness: did this product have a box alive
675
+ * during the window? Only then is an all-excluded pass pathological.
676
+ *
677
+ * Fails closed when no expectation was declared — that is today's behaviour, and
678
+ * a pass that cannot answer the question must not answer it optimistically. The
679
+ * `reason` says so, and the fix (declare `window`, implement `listLiveBetween`)
680
+ * is in the string a human reads.
681
+ */
682
+ declare function assessAllExcluded(report: SpendReport): AllExcludedAssessment;
683
+ /** The expectation summary a pass that declared none reports. Loud, never absent. */
684
+ declare function undeclaredExpectation(graceMs: number): SpendExpectationSummary;
685
+
431
686
  /**
432
687
  * Persistence seam for the expectation ledger — the product implements it over
433
688
  * its own tables.
@@ -449,6 +704,30 @@ interface SpendLedgerStorePort {
449
704
  * statement as the record, or ignore them if the table has no extra columns. */
450
705
  insert(record: SpendBoxRecord, extras?: Record<string, unknown>): Promise<SpendBoxRecord>;
451
706
  update(sandboxId: string, patch: SpendBoxPatch): Promise<SpendBoxRecord | null>;
707
+ /**
708
+ * OPTIONAL — every box that could have been live during the window.
709
+ *
710
+ * This is the one capability that lets a reconciliation answer "were we NOT
711
+ * billed for something we DID ask for". Without it the pass is driven entirely
712
+ * by settlement rows, so a feed that returns nothing fires nothing and reads
713
+ * as a clean bill. Omitting it is safe and additive: the pass reports
714
+ * `expectation.declared: false` and refuses to certify a bill it could not
715
+ * check (see {@link SpendReport.coverage}).
716
+ *
717
+ * **It may over-return.** The reconciler re-derives liveness itself with
718
+ * `boxLivenessInWindow`, so a store that returns every row it has is correct,
719
+ * merely slower. That is deliberate: the definition of "live" must live in one
720
+ * place, not once per product's SQL. The intended predicate is the coarse one
721
+ * a WHERE clause can express —
722
+ *
723
+ * ```sql
724
+ * WHERE created_at <= :endAt AND (deleted_at IS NULL OR deleted_at >= :startAt)
725
+ * ```
726
+ *
727
+ * — and the exact answer (idle timeout, max lifetime, stop, open detached run)
728
+ * is the reconciler's.
729
+ */
730
+ listLiveBetween?(window: SpendWindow): Promise<readonly SpendBoxRecord[]>;
452
731
  }
453
732
  /**
454
733
  * Apply one fold step. Exported so a SQL implementation and an in-memory one
@@ -697,6 +976,27 @@ interface ReconcileSpendOptions {
697
976
  readonly workspaceId?: string;
698
977
  /** Checks to leave out of this pass. */
699
978
  readonly skip?: readonly SpendCheckId[];
979
+ /**
980
+ * The stretch of time these `rows` were fetched for.
981
+ *
982
+ * Declaring it — together with a store that implements `listLiveBetween` — is
983
+ * what lets the pass answer the direction every settlement-driven rule is
984
+ * blind to: *were we NOT billed for something we DID ask for*. The expectation
985
+ * ledger already holds the answer; nothing new is stored for it.
986
+ *
987
+ * Omitting it is additive and changes no existing finding. It is not silent:
988
+ * `report.expectation.declared` is `false`, `formatSpendReport` prints
989
+ * `expectation: NOT DECLARED` above the findings, and a pass that also
990
+ * examined none of this product's settlements reports `coverage: 'unverified'`
991
+ * and cannot render as a clean bill.
992
+ */
993
+ readonly window?: SpendWindow;
994
+ /**
995
+ * Live ms a box needs inside the window before a settlement is EXPECTED of it.
996
+ * Default {@link DEFAULT_EXPECTATION_GRACE_MS} (15 min — the platform's own
997
+ * declared-normal settlement lag). Shrink it for a short window.
998
+ */
999
+ readonly expectationGraceMs?: number;
700
1000
  }
701
1001
  /**
702
1002
  * Diff what the platform charged against what the product believes it asked for.
@@ -842,4 +1142,4 @@ declare function formatSpendReport(report: SpendReport): string;
842
1142
  /** The report as a plain JSON value, for an alerting pipeline. */
843
1143
  declare function spendReportToJson(report: SpendReport): string;
844
1144
 
845
- export { type BilledDurationBasis, type BoxRateResolver, type CeilingBasis, type ComputeBudget, ComputeBudgetExceededError, type ComputeBudgetRefusal, type ComputeExpectedCeilingOptions, DEFAULT_CEILING_TOLERANCE_MS, type ExpectedCeiling, type InMemorySpendLedgerStore, type ObserveSandboxInput, type ObservedBalance, type ReconcileSpendOptions, SPEND_CHECKS, type SandboxSpendHooksOptions, type SandboxSpendSeam, type SettlementReference, type SettlementRow, type SpendBoxPatch, type SpendBoxRecord, type SpendCheckId, type SpendFinding, type SpendLedger, type SpendLedgerOptions, type SpendLedgerStorePort, type SpendOwnershipCandidate, type SpendOwnershipRule, type SpendOwnershipSummary, type SpendOwnershipVerdict, type SpendProvisionObservation, type SpendReport, type VelocityOptions, assertComputeBudget, chargeNanoUsd, computeExpectedCeiling, createInMemorySpendLedgerStore, createSandboxSpendHooks, createSpendLedger, decideBoxOwnership, foldSpendBoxRecord, formatSpendReport, isCharge, ownedByBillingKeys, parseSandboxGroupKey, parseSettlementReference, reconcileSpend, settlementSandboxId, spendReportToJson };
1145
+ export { type AllExcludedAssessment, type AllExcludedBasis, type BilledDurationBasis, type BoxLivenessOptions, type BoxRateResolver, type CeilingBasis, type ComputeBudget, ComputeBudgetExceededError, type ComputeBudgetRefusal, type ComputeExpectedCeilingOptions, DEFAULT_CEILING_TOLERANCE_MS, DEFAULT_EXPECTATION_GRACE_MS, type ExpectedCeiling, type InMemorySpendLedgerStore, type ObserveSandboxInput, type ObservedBalance, type ReconcileSpendOptions, SPEND_CHECKS, type SandboxSpendHooksOptions, type SandboxSpendSeam, type SettlementReference, type SettlementRow, type SpendBoxLiveness, type SpendBoxPatch, type SpendBoxRecord, type SpendCheckId, type SpendCoverage, type SpendExpectationSummary, type SpendFinding, type SpendLedger, type SpendLedgerOptions, type SpendLedgerStorePort, type SpendOwnershipCandidate, type SpendOwnershipRule, type SpendOwnershipSummary, type SpendOwnershipVerdict, type SpendProvisionObservation, type SpendReport, type SpendWindow, type VelocityOptions, assertComputeBudget, assertSpendWindow, assessAllExcluded, boxLivenessInWindow, chargeNanoUsd, computeExpectedCeiling, createInMemorySpendLedgerStore, createSandboxSpendHooks, createSpendLedger, decideBoxOwnership, foldSpendBoxRecord, formatSpendReport, isCharge, ownedByBillingKeys, parseSandboxGroupKey, parseSettlementReference, reconcileSpend, settlementSandboxId, spendReportToJson, undeclaredExpectation };
@@ -1,6 +1,10 @@
1
1
  import {
2
2
  DEFAULT_CEILING_TOLERANCE_MS,
3
+ DEFAULT_EXPECTATION_GRACE_MS,
3
4
  SPEND_CHECKS,
5
+ assertSpendWindow,
6
+ assessAllExcluded,
7
+ boxLivenessInWindow,
4
8
  chargeNanoUsd,
5
9
  computeExpectedCeiling,
6
10
  decideBoxOwnership,
@@ -11,8 +15,9 @@ import {
11
15
  parseSettlementReference,
12
16
  reconcileSpend,
13
17
  settlementSandboxId,
14
- spendReportToJson
15
- } from "../chunk-5LC2VXH5.js";
18
+ spendReportToJson,
19
+ undeclaredExpectation
20
+ } from "../chunk-TRWJ5CRT.js";
16
21
 
17
22
  // src/spend/store.ts
18
23
  function foldSpendBoxRecord(record, patch) {
@@ -61,6 +66,11 @@ function createInMemorySpendLedgerStore() {
61
66
  rows.set(sandboxId, next);
62
67
  return structuredClone(next);
63
68
  },
69
+ async listLiveBetween(window) {
70
+ return [...rows.values()].filter(
71
+ (row) => row.createdAt <= window.endAt && (row.deletedAt === null || row.deletedAt >= window.startAt)
72
+ ).map((row) => structuredClone(row));
73
+ },
64
74
  records() {
65
75
  return [...rows.values()].map((row) => structuredClone(row));
66
76
  },
@@ -179,8 +189,12 @@ function createSandboxSpendHooks(options) {
179
189
  export {
180
190
  ComputeBudgetExceededError,
181
191
  DEFAULT_CEILING_TOLERANCE_MS,
192
+ DEFAULT_EXPECTATION_GRACE_MS,
182
193
  SPEND_CHECKS,
183
194
  assertComputeBudget,
195
+ assertSpendWindow,
196
+ assessAllExcluded,
197
+ boxLivenessInWindow,
184
198
  chargeNanoUsd,
185
199
  computeExpectedCeiling,
186
200
  createInMemorySpendLedgerStore,
@@ -195,6 +209,7 @@ export {
195
209
  parseSettlementReference,
196
210
  reconcileSpend,
197
211
  settlementSandboxId,
198
- spendReportToJson
212
+ spendReportToJson,
213
+ undeclaredExpectation
199
214
  };
200
215
  //# sourceMappingURL=index.js.map
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/spend/store.ts","../../src/spend/budget.ts"],"sourcesContent":["import type { SpendBoxPatch, SpendBoxRecord } from './types'\n\n/**\n * Persistence seam for the expectation ledger — the product implements it over\n * its own tables.\n *\n * Deliberately NOT compare-and-set, unlike `MissionStorePort`. A mission has one\n * serialized owner and a lost write corrupts a state machine; a box record is a\n * MONOTONIC FOLD (activity takes a max, a detached-run id joins or leaves a set,\n * delete is set-once) so concurrent writers converge no matter what order they\n * land in. The worst a lost race can do here is leave `lastActivityAt` behind\n * the truth — which makes the derived ceiling TIGHTER, so the failure mode is a\n * false alarm a human dismisses, never a missed charge. That asymmetry is the\n * whole reason the fold is shaped this way.\n *\n * `update` returns null when the row does not exist, never a throw.\n */\nexport interface SpendLedgerStorePort {\n load(sandboxId: string): Promise<SpendBoxRecord | null>\n /** `extras` are the opaque product-column values — write them in the SAME\n * statement as the record, or ignore them if the table has no extra columns. */\n insert(record: SpendBoxRecord, extras?: Record<string, unknown>): Promise<SpendBoxRecord>\n update(sandboxId: string, patch: SpendBoxPatch): Promise<SpendBoxRecord | null>\n}\n\n/**\n * Apply one fold step. Exported so a SQL implementation and an in-memory one\n * reach the same record, and so a product can unit-test its own store against\n * the canonical answer.\n *\n * The two rules worth stating out loud:\n *\n * - `observedActivityAt` only ever moves `lastActivityAt` FORWARD. A replayed\n * or out-of-order event cannot rewind the ceiling.\n * - activity later than a recorded `stoppedAt` CLEARS the stop. A box that\n * worked after the product thought it stopped is running again, and keeping\n * the stale stop would make the ceiling too tight — inventing an over-ceiling\n * finding out of the product's own bookkeeping rather than the platform's.\n */\nexport function foldSpendBoxRecord(record: SpendBoxRecord, patch: SpendBoxPatch): SpendBoxRecord {\n let lastActivityAt = record.lastActivityAt\n let stoppedAt = record.stoppedAt\n let openDetachedRunIds = record.openDetachedRunIds\n\n if (patch.observedActivityAt !== undefined) {\n lastActivityAt = Math.max(lastActivityAt, patch.observedActivityAt)\n if (stoppedAt !== null && patch.observedActivityAt > stoppedAt) stoppedAt = null\n }\n if (patch.openDetachedRunAdd !== undefined && !openDetachedRunIds.includes(patch.openDetachedRunAdd)) {\n openDetachedRunIds = [...openDetachedRunIds, patch.openDetachedRunAdd]\n }\n if (patch.openDetachedRunRemove !== undefined) {\n openDetachedRunIds = openDetachedRunIds.filter((id) => id !== patch.openDetachedRunRemove)\n }\n if (patch.stoppedAt !== undefined) {\n // Latest-wins, but never behind observed activity: a stop we are told about\n // that predates work we watched is not the stop that closed this box.\n stoppedAt = patch.stoppedAt >= lastActivityAt ? patch.stoppedAt : stoppedAt\n }\n\n return {\n ...record,\n lastActivityAt,\n stoppedAt,\n openDetachedRunIds,\n // Set-once: a deleted sandbox id never comes back, so a second observation\n // is a duplicate delivery, not a second deletion.\n deletedAt: record.deletedAt ?? patch.deletedAt ?? null,\n }\n}\n\n/** An in-memory store that also lets a test inspect and force state. */\nexport interface InMemorySpendLedgerStore extends SpendLedgerStorePort {\n /** Every record, insertion order. */\n records(): SpendBoxRecord[]\n /** Unguarded direct write — simulates a crash-shaped or platform-seeded row. */\n put(record: SpendBoxRecord): void\n}\n\n/** Create an in-memory expectation ledger. Production writers use the same port. */\nexport function createInMemorySpendLedgerStore(): InMemorySpendLedgerStore {\n const rows = new Map<string, SpendBoxRecord>()\n return {\n async load(sandboxId) {\n const row = rows.get(sandboxId)\n return row ? structuredClone(row) : null\n },\n async insert(record) {\n const stored = structuredClone(record)\n rows.set(record.sandboxId, stored)\n return structuredClone(stored)\n },\n async update(sandboxId, patch) {\n const current = rows.get(sandboxId)\n if (!current) return null\n const next = foldSpendBoxRecord(current, patch)\n rows.set(sandboxId, next)\n return structuredClone(next)\n },\n records() {\n return [...rows.values()].map((row) => structuredClone(row))\n },\n put(record) {\n rows.set(record.sandboxId, structuredClone(record))\n },\n }\n}\n\n/** What the product tells the ledger when it first sees a box. */\nexport interface ObserveSandboxInput {\n readonly sandboxId: string\n readonly workspaceId: string\n /** The idle timeout the product asked the platform for, seconds. */\n readonly idleTimeoutSeconds: number\n /** The max lifetime the product asked for, seconds, when it asked for one. */\n readonly maxLifetimeSeconds?: number | null\n /** Defaults to the ledger's clock. */\n readonly at?: number\n}\n\nexport interface SpendLedgerOptions {\n readonly store: SpendLedgerStorePort\n /** Injectable clock (epoch ms). Default `Date.now`. */\n readonly now?: () => number\n /** Product columns written verbatim on every insert. */\n readonly extras?: Record<string, unknown>\n}\n\n/**\n * The recording half of spend verification: the product's own account of what\n * it asked the platform for.\n *\n * Every method is best-effort from the caller's point of view — a product wires\n * these into paths that must not fail because bookkeeping failed. They still\n * reject on a store error rather than swallowing it, so a caller that wants\n * fire-and-forget says so at the call site (`/sandbox`'s hook does).\n */\nexport interface SpendLedger {\n /**\n * Record that a box exists and is billable from now. Inserts on first sight,\n * and otherwise records activity — reuse and resume are both \"the platform is\n * charging for this box again\", and the record's own existence is what\n * distinguishes them, so no caller has to know which happened.\n */\n observeSandbox(input: ObserveSandboxInput): Promise<SpendBoxRecord>\n /** Record that the product saw this box do work. */\n recordActivity(sandboxId: string, at?: number): Promise<SpendBoxRecord | null>\n /**\n * Record that the product handed the platform work it will NOT watch finish.\n * Until the matching end is recorded, this box's ceiling cannot rest on\n * observed activity — see `computeExpectedCeiling`.\n */\n recordDetachedRunStarted(sandboxId: string, runId: string, at?: number): Promise<SpendBoxRecord | null>\n /** Record that a detached run was confirmed finished. */\n recordDetachedRunEnded(sandboxId: string, runId: string, at?: number): Promise<SpendBoxRecord | null>\n /** Record that the product knows this box stopped. */\n recordStopped(sandboxId: string, at?: number): Promise<SpendBoxRecord | null>\n /** Record that the product knows this box was deleted. */\n recordDeleted(sandboxId: string, at?: number): Promise<SpendBoxRecord | null>\n}\n\n/** Create the recording half over a product-supplied store. */\nexport function createSpendLedger(options: SpendLedgerOptions): SpendLedger {\n const { store } = options\n const clock = options.now ?? Date.now\n\n return {\n async observeSandbox(input) {\n const at = input.at ?? clock()\n const existing = await store.load(input.sandboxId)\n if (existing) {\n const updated = await store.update(input.sandboxId, { observedActivityAt: at })\n return updated ?? existing\n }\n return await store.insert(\n {\n sandboxId: input.sandboxId,\n workspaceId: input.workspaceId,\n createdAt: at,\n idleTimeoutSeconds: input.idleTimeoutSeconds,\n maxLifetimeSeconds: input.maxLifetimeSeconds ?? null,\n lastActivityAt: at,\n openDetachedRunIds: [],\n stoppedAt: null,\n deletedAt: null,\n },\n options.extras,\n )\n },\n async recordActivity(sandboxId, at) {\n return await store.update(sandboxId, { observedActivityAt: at ?? clock() })\n },\n async recordDetachedRunStarted(sandboxId, runId, at) {\n return await store.update(sandboxId, {\n observedActivityAt: at ?? clock(),\n openDetachedRunAdd: runId,\n })\n },\n async recordDetachedRunEnded(sandboxId, runId, at) {\n return await store.update(sandboxId, {\n observedActivityAt: at ?? clock(),\n openDetachedRunRemove: runId,\n })\n },\n async recordStopped(sandboxId, at) {\n return await store.update(sandboxId, { stoppedAt: at ?? clock() })\n },\n async recordDeleted(sandboxId, at) {\n return await store.update(sandboxId, { deletedAt: at ?? clock() })\n },\n }\n}\n","import type { SpendLedger } from './store'\n\n/** Why provisioning was refused, with every number the decision used. */\nexport interface ComputeBudgetRefusal {\n readonly workspaceId: string\n /** The cap, unsigned nanodollars. */\n readonly limitNanoUsd: number\n /** Cumulative settled compute spend for this workspace, unsigned nanodollars. */\n readonly settledNanoUsd: number\n /** How far past the cap it already is. */\n readonly overageNanoUsd: number\n readonly at: number\n}\n\n/**\n * Provisioning refused because the workspace is already past its compute cap.\n *\n * Correctable by design: every number the decision used is on the error, so a\n * product can render \"this workspace has spent $X of its $Y compute budget\" and\n * an operator can raise the cap or investigate without reading logs.\n *\n * This is the failure mode the module exists to produce. A platform billing\n * defect that used to end in a silent negative balance now ends in provisioning\n * stopping and something loud happening instead.\n */\nexport class ComputeBudgetExceededError extends Error {\n readonly workspaceId: string\n readonly limitNanoUsd: number\n readonly settledNanoUsd: number\n readonly overageNanoUsd: number\n\n constructor(refusal: ComputeBudgetRefusal) {\n super(\n `Compute budget exceeded for workspace ${refusal.workspaceId}: ` +\n `$${(refusal.settledNanoUsd / 1_000_000_000).toFixed(2)} settled against a cap of ` +\n `$${(refusal.limitNanoUsd / 1_000_000_000).toFixed(2)} ` +\n `(over by $${(refusal.overageNanoUsd / 1_000_000_000).toFixed(2)}). ` +\n 'No sandbox was provisioned. Raise the cap or reconcile the spend before retrying.',\n )\n this.name = 'ComputeBudgetExceededError'\n this.workspaceId = refusal.workspaceId\n this.limitNanoUsd = refusal.limitNanoUsd\n this.settledNanoUsd = refusal.settledNanoUsd\n this.overageNanoUsd = refusal.overageNanoUsd\n }\n}\n\n/**\n * A per-workspace cap on sandbox compute.\n *\n * `/billing`'s budget primitive caps MODEL keys, and it works because the\n * platform enforces the cap at the key it minted. Sandbox compute has no such\n * key: a box bills the shared company wallet, so nothing upstream refuses. This\n * carries the same shape to the one place a consumer can still act — the moment\n * before it asks for another box.\n *\n * `settledNanoUsd` is a callback rather than a number because the authority is\n * the platform ledger, not this package: the product reads the same rows it\n * hands the reconciler. Cache it if the read is expensive; a cap is a\n * coarse-grained control and a slightly stale total still refuses.\n */\nexport interface ComputeBudget {\n /** The cap, unsigned nanodollars. */\n readonly limitNanoUsd: number\n /** Cumulative settled compute spend for the workspace, unsigned nanodollars. */\n readonly settledNanoUsd: (workspaceId: string) => Promise<number> | number\n /**\n * Called on every refusal, before the error is thrown. This is the alert\n * seam: a refusal nobody hears is a product that silently stopped working.\n */\n readonly onRefusal?: (refusal: ComputeBudgetRefusal) => void\n /** Injectable clock (epoch ms). Default `Date.now`. */\n readonly now?: () => number\n}\n\n/**\n * Throw {@link ComputeBudgetExceededError} when the workspace is already past\n * its cap. Returns normally — and reads nothing — when no budget is configured.\n *\n * Deliberately a pre-check against spend ALREADY SETTLED, not a reservation\n * against spend about to happen: settlement lags provisioning by design (the\n * platform's durable settlement queue), so there is no instant at which a\n * consumer could hold an accurate running total. The cap therefore overshoots by\n * at most the unsettled tail, which is bounded by the box's own idle timeout.\n * A cap that refuses one box late is worth far more than one that cannot be\n * implemented honestly.\n */\nexport async function assertComputeBudget(\n budget: ComputeBudget | undefined,\n workspaceId: string,\n): Promise<void> {\n if (!budget) return\n const settledNanoUsd = await budget.settledNanoUsd(workspaceId)\n if (settledNanoUsd < budget.limitNanoUsd) return\n\n const refusal: ComputeBudgetRefusal = {\n workspaceId,\n limitNanoUsd: budget.limitNanoUsd,\n settledNanoUsd,\n overageNanoUsd: settledNanoUsd - budget.limitNanoUsd,\n at: (budget.now ?? Date.now)(),\n }\n budget.onRefusal?.(refusal)\n throw new ComputeBudgetExceededError(refusal)\n}\n\n// ── the /sandbox seam ─────────────────────────────────────────────────────────\n\n/**\n * What `/sandbox` reports once a box is provisioned, reused or resumed.\n *\n * Structurally identical to `SandboxProvisionedObservation` in `/sandbox`, and\n * deliberately re-declared rather than imported: `/spend` composes `/sandbox`,\n * so a type import in the other direction would invert the dependency. The two\n * are pinned together by a compile-time assignment in this module's tests.\n */\nexport interface SpendProvisionObservation {\n readonly workspaceId: string\n readonly userId?: string\n readonly sandboxId: string\n readonly boxKey?: string | undefined\n readonly idleTimeoutSeconds: number\n readonly maxLifetimeSeconds?: number | undefined\n readonly at: number\n}\n\n/**\n * The optional seam `EnsureWorkspaceSandboxOptions.spend` and the turn\n * primitives' `spend` option both accept. One object, wired in both places.\n */\nexport interface SandboxSpendSeam {\n beforeProvision?(input: { workspaceId: string; userId?: string }): Promise<void> | void\n onProvisioned?(observation: SpendProvisionObservation): Promise<void> | void\n /** Synchronous by contract — it sits on the turn path. See `createSandboxSpendHooks`. */\n onActivity?(input: { sandboxId: string; at: number }): void\n}\n\nexport interface SandboxSpendHooksOptions {\n /** Records box lifecycle. Omit to run the budget guard alone. */\n readonly ledger?: SpendLedger\n /** Refuses provisioning past a cap. Omit to record alone. */\n readonly budget?: ComputeBudget\n /**\n * Called when RECORDING fails. Recording is best-effort — a bookkeeping\n * failure must never take down the provisioning it is bookkeeping — so this\n * is the only place such a failure is visible. A refusal is NOT routed here;\n * refusals throw, by design.\n */\n readonly onError?: (error: unknown) => void\n}\n\n/**\n * Build the object to hand `ensureWorkspaceSandbox`'s `spend` option.\n *\n * Wiring it is the entire adoption cost: one field, and the product's boxes are\n * both budget-capped and recorded.\n */\nexport function createSandboxSpendHooks(options: SandboxSpendHooksOptions): SandboxSpendSeam {\n const { ledger, budget, onError } = options\n return {\n async beforeProvision(input) {\n await assertComputeBudget(budget, input.workspaceId)\n },\n async onProvisioned(observation) {\n if (!ledger) return\n try {\n await ledger.observeSandbox({\n sandboxId: observation.sandboxId,\n workspaceId: observation.workspaceId,\n idleTimeoutSeconds: observation.idleTimeoutSeconds,\n maxLifetimeSeconds: observation.maxLifetimeSeconds ?? null,\n at: observation.at,\n })\n } catch (err) {\n onError?.(err)\n }\n },\n onActivity(input) {\n if (!ledger) return\n // The turn path calls this synchronously and does not await it, so the\n // promise is settled here rather than escaping as an unhandled rejection.\n // Recording activity is a monotonic max, so a write that lands late — or\n // out of order against another turn's — still converges.\n void ledger.recordActivity(input.sandboxId, input.at).catch((err: unknown) => onError?.(err))\n },\n }\n}\n"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../../src/spend/store.ts","../../src/spend/budget.ts"],"sourcesContent":["import type { SpendBoxPatch, SpendBoxRecord, SpendWindow } from './types'\n\n/**\n * Persistence seam for the expectation ledger — the product implements it over\n * its own tables.\n *\n * Deliberately NOT compare-and-set, unlike `MissionStorePort`. A mission has one\n * serialized owner and a lost write corrupts a state machine; a box record is a\n * MONOTONIC FOLD (activity takes a max, a detached-run id joins or leaves a set,\n * delete is set-once) so concurrent writers converge no matter what order they\n * land in. The worst a lost race can do here is leave `lastActivityAt` behind\n * the truth — which makes the derived ceiling TIGHTER, so the failure mode is a\n * false alarm a human dismisses, never a missed charge. That asymmetry is the\n * whole reason the fold is shaped this way.\n *\n * `update` returns null when the row does not exist, never a throw.\n */\nexport interface SpendLedgerStorePort {\n load(sandboxId: string): Promise<SpendBoxRecord | null>\n /** `extras` are the opaque product-column values — write them in the SAME\n * statement as the record, or ignore them if the table has no extra columns. */\n insert(record: SpendBoxRecord, extras?: Record<string, unknown>): Promise<SpendBoxRecord>\n update(sandboxId: string, patch: SpendBoxPatch): Promise<SpendBoxRecord | null>\n /**\n * OPTIONAL — every box that could have been live during the window.\n *\n * This is the one capability that lets a reconciliation answer \"were we NOT\n * billed for something we DID ask for\". Without it the pass is driven entirely\n * by settlement rows, so a feed that returns nothing fires nothing and reads\n * as a clean bill. Omitting it is safe and additive: the pass reports\n * `expectation.declared: false` and refuses to certify a bill it could not\n * check (see {@link SpendReport.coverage}).\n *\n * **It may over-return.** The reconciler re-derives liveness itself with\n * `boxLivenessInWindow`, so a store that returns every row it has is correct,\n * merely slower. That is deliberate: the definition of \"live\" must live in one\n * place, not once per product's SQL. The intended predicate is the coarse one\n * a WHERE clause can express —\n *\n * ```sql\n * WHERE created_at <= :endAt AND (deleted_at IS NULL OR deleted_at >= :startAt)\n * ```\n *\n * — and the exact answer (idle timeout, max lifetime, stop, open detached run)\n * is the reconciler's.\n */\n listLiveBetween?(window: SpendWindow): Promise<readonly SpendBoxRecord[]>\n}\n\n/**\n * Apply one fold step. Exported so a SQL implementation and an in-memory one\n * reach the same record, and so a product can unit-test its own store against\n * the canonical answer.\n *\n * The two rules worth stating out loud:\n *\n * - `observedActivityAt` only ever moves `lastActivityAt` FORWARD. A replayed\n * or out-of-order event cannot rewind the ceiling.\n * - activity later than a recorded `stoppedAt` CLEARS the stop. A box that\n * worked after the product thought it stopped is running again, and keeping\n * the stale stop would make the ceiling too tight — inventing an over-ceiling\n * finding out of the product's own bookkeeping rather than the platform's.\n */\nexport function foldSpendBoxRecord(record: SpendBoxRecord, patch: SpendBoxPatch): SpendBoxRecord {\n let lastActivityAt = record.lastActivityAt\n let stoppedAt = record.stoppedAt\n let openDetachedRunIds = record.openDetachedRunIds\n\n if (patch.observedActivityAt !== undefined) {\n lastActivityAt = Math.max(lastActivityAt, patch.observedActivityAt)\n if (stoppedAt !== null && patch.observedActivityAt > stoppedAt) stoppedAt = null\n }\n if (patch.openDetachedRunAdd !== undefined && !openDetachedRunIds.includes(patch.openDetachedRunAdd)) {\n openDetachedRunIds = [...openDetachedRunIds, patch.openDetachedRunAdd]\n }\n if (patch.openDetachedRunRemove !== undefined) {\n openDetachedRunIds = openDetachedRunIds.filter((id) => id !== patch.openDetachedRunRemove)\n }\n if (patch.stoppedAt !== undefined) {\n // Latest-wins, but never behind observed activity: a stop we are told about\n // that predates work we watched is not the stop that closed this box.\n stoppedAt = patch.stoppedAt >= lastActivityAt ? patch.stoppedAt : stoppedAt\n }\n\n return {\n ...record,\n lastActivityAt,\n stoppedAt,\n openDetachedRunIds,\n // Set-once: a deleted sandbox id never comes back, so a second observation\n // is a duplicate delivery, not a second deletion.\n deletedAt: record.deletedAt ?? patch.deletedAt ?? null,\n }\n}\n\n/** An in-memory store that also lets a test inspect and force state. */\nexport interface InMemorySpendLedgerStore extends SpendLedgerStorePort {\n /** Every record, insertion order. */\n records(): SpendBoxRecord[]\n /** Unguarded direct write — simulates a crash-shaped or platform-seeded row. */\n put(record: SpendBoxRecord): void\n}\n\n/** Create an in-memory expectation ledger. Production writers use the same port. */\nexport function createInMemorySpendLedgerStore(): InMemorySpendLedgerStore {\n const rows = new Map<string, SpendBoxRecord>()\n return {\n async load(sandboxId) {\n const row = rows.get(sandboxId)\n return row ? structuredClone(row) : null\n },\n async insert(record) {\n const stored = structuredClone(record)\n rows.set(record.sandboxId, stored)\n return structuredClone(stored)\n },\n async update(sandboxId, patch) {\n const current = rows.get(sandboxId)\n if (!current) return null\n const next = foldSpendBoxRecord(current, patch)\n rows.set(sandboxId, next)\n return structuredClone(next)\n },\n async listLiveBetween(window) {\n // Deliberately the COARSE predicate a product's SQL can express, not the\n // exact one: the idle-timeout / max-lifetime / detached-run derivation is\n // the reconciler's, and this over-returning is what proves the reconciler\n // re-filters rather than trusting whatever a store hands it.\n return [...rows.values()]\n .filter(\n (row) =>\n row.createdAt <= window.endAt && (row.deletedAt === null || row.deletedAt >= window.startAt),\n )\n .map((row) => structuredClone(row))\n },\n records() {\n return [...rows.values()].map((row) => structuredClone(row))\n },\n put(record) {\n rows.set(record.sandboxId, structuredClone(record))\n },\n }\n}\n\n/** What the product tells the ledger when it first sees a box. */\nexport interface ObserveSandboxInput {\n readonly sandboxId: string\n readonly workspaceId: string\n /** The idle timeout the product asked the platform for, seconds. */\n readonly idleTimeoutSeconds: number\n /** The max lifetime the product asked for, seconds, when it asked for one. */\n readonly maxLifetimeSeconds?: number | null\n /** Defaults to the ledger's clock. */\n readonly at?: number\n}\n\nexport interface SpendLedgerOptions {\n readonly store: SpendLedgerStorePort\n /** Injectable clock (epoch ms). Default `Date.now`. */\n readonly now?: () => number\n /** Product columns written verbatim on every insert. */\n readonly extras?: Record<string, unknown>\n}\n\n/**\n * The recording half of spend verification: the product's own account of what\n * it asked the platform for.\n *\n * Every method is best-effort from the caller's point of view — a product wires\n * these into paths that must not fail because bookkeeping failed. They still\n * reject on a store error rather than swallowing it, so a caller that wants\n * fire-and-forget says so at the call site (`/sandbox`'s hook does).\n */\nexport interface SpendLedger {\n /**\n * Record that a box exists and is billable from now. Inserts on first sight,\n * and otherwise records activity — reuse and resume are both \"the platform is\n * charging for this box again\", and the record's own existence is what\n * distinguishes them, so no caller has to know which happened.\n */\n observeSandbox(input: ObserveSandboxInput): Promise<SpendBoxRecord>\n /** Record that the product saw this box do work. */\n recordActivity(sandboxId: string, at?: number): Promise<SpendBoxRecord | null>\n /**\n * Record that the product handed the platform work it will NOT watch finish.\n * Until the matching end is recorded, this box's ceiling cannot rest on\n * observed activity — see `computeExpectedCeiling`.\n */\n recordDetachedRunStarted(sandboxId: string, runId: string, at?: number): Promise<SpendBoxRecord | null>\n /** Record that a detached run was confirmed finished. */\n recordDetachedRunEnded(sandboxId: string, runId: string, at?: number): Promise<SpendBoxRecord | null>\n /** Record that the product knows this box stopped. */\n recordStopped(sandboxId: string, at?: number): Promise<SpendBoxRecord | null>\n /** Record that the product knows this box was deleted. */\n recordDeleted(sandboxId: string, at?: number): Promise<SpendBoxRecord | null>\n}\n\n/** Create the recording half over a product-supplied store. */\nexport function createSpendLedger(options: SpendLedgerOptions): SpendLedger {\n const { store } = options\n const clock = options.now ?? Date.now\n\n return {\n async observeSandbox(input) {\n const at = input.at ?? clock()\n const existing = await store.load(input.sandboxId)\n if (existing) {\n const updated = await store.update(input.sandboxId, { observedActivityAt: at })\n return updated ?? existing\n }\n return await store.insert(\n {\n sandboxId: input.sandboxId,\n workspaceId: input.workspaceId,\n createdAt: at,\n idleTimeoutSeconds: input.idleTimeoutSeconds,\n maxLifetimeSeconds: input.maxLifetimeSeconds ?? null,\n lastActivityAt: at,\n openDetachedRunIds: [],\n stoppedAt: null,\n deletedAt: null,\n },\n options.extras,\n )\n },\n async recordActivity(sandboxId, at) {\n return await store.update(sandboxId, { observedActivityAt: at ?? clock() })\n },\n async recordDetachedRunStarted(sandboxId, runId, at) {\n return await store.update(sandboxId, {\n observedActivityAt: at ?? clock(),\n openDetachedRunAdd: runId,\n })\n },\n async recordDetachedRunEnded(sandboxId, runId, at) {\n return await store.update(sandboxId, {\n observedActivityAt: at ?? clock(),\n openDetachedRunRemove: runId,\n })\n },\n async recordStopped(sandboxId, at) {\n return await store.update(sandboxId, { stoppedAt: at ?? clock() })\n },\n async recordDeleted(sandboxId, at) {\n return await store.update(sandboxId, { deletedAt: at ?? clock() })\n },\n }\n}\n","import type { SpendLedger } from './store'\n\n/** Why provisioning was refused, with every number the decision used. */\nexport interface ComputeBudgetRefusal {\n readonly workspaceId: string\n /** The cap, unsigned nanodollars. */\n readonly limitNanoUsd: number\n /** Cumulative settled compute spend for this workspace, unsigned nanodollars. */\n readonly settledNanoUsd: number\n /** How far past the cap it already is. */\n readonly overageNanoUsd: number\n readonly at: number\n}\n\n/**\n * Provisioning refused because the workspace is already past its compute cap.\n *\n * Correctable by design: every number the decision used is on the error, so a\n * product can render \"this workspace has spent $X of its $Y compute budget\" and\n * an operator can raise the cap or investigate without reading logs.\n *\n * This is the failure mode the module exists to produce. A platform billing\n * defect that used to end in a silent negative balance now ends in provisioning\n * stopping and something loud happening instead.\n */\nexport class ComputeBudgetExceededError extends Error {\n readonly workspaceId: string\n readonly limitNanoUsd: number\n readonly settledNanoUsd: number\n readonly overageNanoUsd: number\n\n constructor(refusal: ComputeBudgetRefusal) {\n super(\n `Compute budget exceeded for workspace ${refusal.workspaceId}: ` +\n `$${(refusal.settledNanoUsd / 1_000_000_000).toFixed(2)} settled against a cap of ` +\n `$${(refusal.limitNanoUsd / 1_000_000_000).toFixed(2)} ` +\n `(over by $${(refusal.overageNanoUsd / 1_000_000_000).toFixed(2)}). ` +\n 'No sandbox was provisioned. Raise the cap or reconcile the spend before retrying.',\n )\n this.name = 'ComputeBudgetExceededError'\n this.workspaceId = refusal.workspaceId\n this.limitNanoUsd = refusal.limitNanoUsd\n this.settledNanoUsd = refusal.settledNanoUsd\n this.overageNanoUsd = refusal.overageNanoUsd\n }\n}\n\n/**\n * A per-workspace cap on sandbox compute.\n *\n * `/billing`'s budget primitive caps MODEL keys, and it works because the\n * platform enforces the cap at the key it minted. Sandbox compute has no such\n * key: a box bills the shared company wallet, so nothing upstream refuses. This\n * carries the same shape to the one place a consumer can still act — the moment\n * before it asks for another box.\n *\n * `settledNanoUsd` is a callback rather than a number because the authority is\n * the platform ledger, not this package: the product reads the same rows it\n * hands the reconciler. Cache it if the read is expensive; a cap is a\n * coarse-grained control and a slightly stale total still refuses.\n */\nexport interface ComputeBudget {\n /** The cap, unsigned nanodollars. */\n readonly limitNanoUsd: number\n /** Cumulative settled compute spend for the workspace, unsigned nanodollars. */\n readonly settledNanoUsd: (workspaceId: string) => Promise<number> | number\n /**\n * Called on every refusal, before the error is thrown. This is the alert\n * seam: a refusal nobody hears is a product that silently stopped working.\n */\n readonly onRefusal?: (refusal: ComputeBudgetRefusal) => void\n /** Injectable clock (epoch ms). Default `Date.now`. */\n readonly now?: () => number\n}\n\n/**\n * Throw {@link ComputeBudgetExceededError} when the workspace is already past\n * its cap. Returns normally — and reads nothing — when no budget is configured.\n *\n * Deliberately a pre-check against spend ALREADY SETTLED, not a reservation\n * against spend about to happen: settlement lags provisioning by design (the\n * platform's durable settlement queue), so there is no instant at which a\n * consumer could hold an accurate running total. The cap therefore overshoots by\n * at most the unsettled tail, which is bounded by the box's own idle timeout.\n * A cap that refuses one box late is worth far more than one that cannot be\n * implemented honestly.\n */\nexport async function assertComputeBudget(\n budget: ComputeBudget | undefined,\n workspaceId: string,\n): Promise<void> {\n if (!budget) return\n const settledNanoUsd = await budget.settledNanoUsd(workspaceId)\n if (settledNanoUsd < budget.limitNanoUsd) return\n\n const refusal: ComputeBudgetRefusal = {\n workspaceId,\n limitNanoUsd: budget.limitNanoUsd,\n settledNanoUsd,\n overageNanoUsd: settledNanoUsd - budget.limitNanoUsd,\n at: (budget.now ?? Date.now)(),\n }\n budget.onRefusal?.(refusal)\n throw new ComputeBudgetExceededError(refusal)\n}\n\n// ── the /sandbox seam ─────────────────────────────────────────────────────────\n\n/**\n * What `/sandbox` reports once a box is provisioned, reused or resumed.\n *\n * Structurally identical to `SandboxProvisionedObservation` in `/sandbox`, and\n * deliberately re-declared rather than imported: `/spend` composes `/sandbox`,\n * so a type import in the other direction would invert the dependency. The two\n * are pinned together by a compile-time assignment in this module's tests.\n */\nexport interface SpendProvisionObservation {\n readonly workspaceId: string\n readonly userId?: string\n readonly sandboxId: string\n readonly boxKey?: string | undefined\n readonly idleTimeoutSeconds: number\n readonly maxLifetimeSeconds?: number | undefined\n readonly at: number\n}\n\n/**\n * The optional seam `EnsureWorkspaceSandboxOptions.spend` and the turn\n * primitives' `spend` option both accept. One object, wired in both places.\n */\nexport interface SandboxSpendSeam {\n beforeProvision?(input: { workspaceId: string; userId?: string }): Promise<void> | void\n onProvisioned?(observation: SpendProvisionObservation): Promise<void> | void\n /** Synchronous by contract — it sits on the turn path. See `createSandboxSpendHooks`. */\n onActivity?(input: { sandboxId: string; at: number }): void\n}\n\nexport interface SandboxSpendHooksOptions {\n /** Records box lifecycle. Omit to run the budget guard alone. */\n readonly ledger?: SpendLedger\n /** Refuses provisioning past a cap. Omit to record alone. */\n readonly budget?: ComputeBudget\n /**\n * Called when RECORDING fails. Recording is best-effort — a bookkeeping\n * failure must never take down the provisioning it is bookkeeping — so this\n * is the only place such a failure is visible. A refusal is NOT routed here;\n * refusals throw, by design.\n */\n readonly onError?: (error: unknown) => void\n}\n\n/**\n * Build the object to hand `ensureWorkspaceSandbox`'s `spend` option.\n *\n * Wiring it is the entire adoption cost: one field, and the product's boxes are\n * both budget-capped and recorded.\n */\nexport function createSandboxSpendHooks(options: SandboxSpendHooksOptions): SandboxSpendSeam {\n const { ledger, budget, onError } = options\n return {\n async beforeProvision(input) {\n await assertComputeBudget(budget, input.workspaceId)\n },\n async onProvisioned(observation) {\n if (!ledger) return\n try {\n await ledger.observeSandbox({\n sandboxId: observation.sandboxId,\n workspaceId: observation.workspaceId,\n idleTimeoutSeconds: observation.idleTimeoutSeconds,\n maxLifetimeSeconds: observation.maxLifetimeSeconds ?? null,\n at: observation.at,\n })\n } catch (err) {\n onError?.(err)\n }\n },\n onActivity(input) {\n if (!ledger) return\n // The turn path calls this synchronously and does not await it, so the\n // promise is settled here rather than escaping as an unhandled rejection.\n // Recording activity is a monotonic max, so a write that lands late — or\n // out of order against another turn's — still converges.\n void ledger.recordActivity(input.sandboxId, input.at).catch((err: unknown) => onError?.(err))\n },\n }\n}\n"],"mappings":"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","names":[]}