This repository has been archived on 2026-03-24 . You can view files and clone it. You cannot open issues or pull requests or push a commit.
ec5316a4dc51c768dedf48aafc51bba1f8ec32f4
Root cause: decide() was advisory but user charges were reduced from its output;
recordGrant() later might absorb less, so Timmy could absorb the gap silently.
Fix architecture (serve="free" path — fully-free jobs + sessions):
- decide() now runs _atomicPoolDebit() inside a FOR UPDATE transaction
- Pool is debited at decision time for serve="free" decisions
- Work starts immediately after, so no window for pool drain between debit and use
- If work fails → releaseReservation() returns sats to pool
Fix architecture (serve="partial" path — partial-subsidy jobs):
- decide() remains advisory for "partial" (no pool debit at decision time)
- This prevents pool drain from users who get a partial offer but never pay
- For jobs: reservePartialGrant() atomically debits pool at work-payment-confirmation
time (inside advanceJob), before work begins
- For sessions: reservePartialGrant() called after synchronous work completes,
using actual cost capped by advisory absorbSats
recordGrant() now takes (pubkey, requestHash, actualAbsorbed, reservedAbsorbed):
- Over-reservation (estimated > actual) returned to pool atomically
- Audit log and daily counter reflect actual absorbed amount
- Pool balance was already decremented by decide() or reservePartialGrant()
Result: In ALL paths, pool debit happens atomically before charges are reduced.
User charge reduction and pool debit are always consistent — Timmy never operates
at a loss due to concurrent pool depletion.
Description
Timmy — Lightning-gated AI agent (TypeScript/Express)
Languages
TypeScript
68.2%
JavaScript
14.8%
Shell
12.4%
HTML
4.1%
CSS
0.4%