feat(engine): per-loop compute consumption ledger entries - #6126
Conversation
Rent-a-Loop has no mechanism recording what a rented loop actually consumed against what was paid or staked (JSONbored#4792). JSONbored#4796 landed the downstream half -- evaluateTenantQuota reads a tenant's computeUnitsUsed/wallClockMsUsed -- but nothing produces those numbers from a real run. Add the pure decision core for the upstream half: given one finished loop run's already-metered facts, buildLoopConsumptionEntry produces the consumption entry a rental ledger records, and totalConsumptionForTenant sums a period's entries into exactly the TenantUsage shape evaluateTenantQuota reads -- so consumption is queryable against allocation, which is what the issue asks for. Both acceptance criteria are covered: a completed run bills its real elapsed wall-clock and metered compute, and a killed-mid-run run bills identically for what it consumed, flagged incomplete rather than dropped -- it really did occupy the compute. Deliberately pure, holding the line the issue draws. It computes an entry only: no ledger write, no metering, no pricing. Persisting is the integration blocked on JSONbored#4789/JSONbored#4790, and per JSONbored#5669 must target whatever storage abstraction JSONbored#4940/JSONbored#5216 lands on -- so this core has no storage opinion and stays correct whichever that is. Invariants a billing ledger must not get wrong, all tested: an end-before-start timestamp (clock skew, or a kill against a stale start) floors at 0 rather than a negative charge; non-finite/negative/fractional readings normalize instead of producing NaN; compute is never inferred from elapsed time, since an idle loop and a saturated one of the same duration did not consume the same compute; and a tenant is never billed for another tenant's entries. LoopConsumptionOutcome is its own type rather than reusing loop-escalation.ts's LoopRunOutcome, which is a loop's health state (running/converged/abandoned/error) -- a consumption entry only exists for a run that already stopped. Closes JSONbored#4792
|
Superagent didn't find any vulnerabilities or security issues in this PR. |
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## main #6126 +/- ##
=======================================
Coverage 95.31% 95.31%
=======================================
Files 595 596 +1
Lines 47100 47111 +11
Branches 15032 15034 +2
=======================================
+ Hits 44893 44904 +11
Misses 1476 1476
Partials 731 731
Flags with carried forward coverage won't be shown. Click here to find out more.
|
|
Tip 🟩🟩🟩🟩🟩🟩🟩🟩🟩🟩🟩🟩 ✅ LoopOver review result - approve/merge recommendedReview updated: 2026-07-15 11:14:23 UTC
✅ Suggested Action - Approve/Merge
Review summary Nits — 5 non-blocking
Linked issue satisfactionPartially addressed Review context
Contributor next steps
Signal definitions
🧪 Chat with LoopOverAsk LoopOver a question about this PR directly in a comment — grounded only in the same cached, public-safe facts shown above, never a new claim.
Full command reference: https://gittensory.aethereal.dev/docs/loopover-commands 🧪 Experimental — new and may change. 🟩 Safe / merged · 🟦 Advisory · 🟨 Held for review · 🟥 Blocked / closed 💰 Earn for open-source contributions like this. Gittensor lets GitHub contributors earn for the work they already do — register to start earning →. Checked by LoopOver, a quiet PR intelligence layer for OSS maintainers.
|
Summary
Closes #4792
Rent-a-Loop has no mechanism recording what a rented loop actually consumed against what was paid or staked. #4796 landed the downstream half —
evaluateTenantQuotareads a tenant'scomputeUnitsUsed/wallClockMsUsed— but nothing produces those numbers from a real run.This adds the upstream pure decision core, mirroring
tenant-quota.ts's shape and normalization discipline exactly:buildLoopConsumptionEntry(facts)— given one finished loop run's already-metered facts, produces the consumption entry a rental ledger records: the tenant it bills to, the wall-clock it occupied, the compute it burned.totalConsumptionForTenant(entries, tenantId)— sums a period's entries into exactly theTenantUsageshapeevaluateTenantQuotareads, so consumption is queryable against allocation (the issue's deliverable). A test asserts that composition end-to-end.Both acceptance criteria:
complete: falserather than dropped. A killed run really did occupy the compute, so it is a first-class case, not an error path.Scope — what this deliberately does not do
It computes an entry only: no ledger write, no metering, no pricing. That is the line the issue draws, and it is respected:
This is the same pure-decision-core shape the sibling Rent-a-Loop issues merged as: #5801 (
tenant-quota.ts→ #4796), and #4801/#4800/#4806's own evaluators.feat(engine): …).CONTRIBUTING.md; nosite//CNAME/lovable; no changelog edit.Validation
git diff --checkclean.npm run build:miner— exit 0 (the engine package compiles + emits).npm run typecheck— exit 0.npx vitest run test/unit/loop-consumption.test.ts test/unit/tenant-quota.test.ts— 22 tests passed (the sibling suite is included to prove the composition still holds).packages/loopover-engine/src/loop-consumption.ts(in Codecov'scoverage.include): 100% statements (12/12), 100% branch (4/4), 100% functions (3/3), 100% lines (11/11).main.Invariants a billing ledger must not get wrong — each tested:
0NaNor-1 msis worse than one billing0activeLoopsis not derivedevaluateTenantQuota's concurrency dimension decide on an invented numberNaming note
LoopConsumptionOutcomeis deliberately its own type rather than reusingloop-escalation.ts'sLoopRunOutcome, which describes a loop's health state (running/converged/abandoned/error). A consumption entry only exists for a run that has already stopped, and only distinguishes finished work from work cut short — reusing the name would collide in the barrel and conflate two different domains.Safety
Date.now()), so it is deterministic and trivially testable.tenantIditself, so one tenant's entries can never leak into another's total.