release_run_llm_reservations UPDATEs only status='open' rows, then SELECTs all reservations for the run:
UPDATE credit_llm_reservations SET status='released', ... WHERE run_id = %s AND status = 'open';
SELECT * FROM credit_llm_reservations WHERE run_id = %s ORDER BY call_index, attempt_index, reservation_id;
domain/credits/repository_postgres.py:1378-1379; the SQLite twin has the same shape at repository.py:1636-1649.
The service then calls _emit_released_credit_buckets on every returned row (domain/credits/service.py:413), so already-settled and already-released reservations emit a second credits_refunded event — one they already emitted at call time (service.py:388 for the unused ceiling of a settlement, :401 for a per-call release).
Observed on the timed-out run agent_20260922_202119_fc7bd724: all 94 reservations were already terminal (38 settled, 56 released) when finalize_run ran, and all 94 re-emitted, producing 94 lines of
WARNING: analytics.append_failed event=credits_refunded category=AnalyticsIdempotencyConflictError
over 35s at ~376ms apart — a DB round trip each, on the cleanup path of a run that had just burned its entire budget.
The idempotency key stops any double-count, so there is no ledger damage. What it costs is a log flood that masks real errors at exactly the moment something went wrong, plus ~35s of avoidable analytics-DB work per timed-out run.
release_run_llm_reservationsUPDATEs onlystatus='open'rows, then SELECTs all reservations for the run:domain/credits/repository_postgres.py:1378-1379; the SQLite twin has the same shape atrepository.py:1636-1649.The service then calls
_emit_released_credit_bucketson every returned row (domain/credits/service.py:413), so already-settled and already-released reservations emit a secondcredits_refundedevent — one they already emitted at call time (service.py:388for the unused ceiling of a settlement,:401for a per-call release).Observed on the timed-out run
agent_20260922_202119_fc7bd724: all 94 reservations were already terminal (38 settled, 56 released) whenfinalize_runran, and all 94 re-emitted, producing 94 lines ofover 35s at ~376ms apart — a DB round trip each, on the cleanup path of a run that had just burned its entire budget.
The idempotency key stops any double-count, so there is no ledger damage. What it costs is a log flood that masks real errors at exactly the moment something went wrong, plus ~35s of avoidable analytics-DB work per timed-out run.
RETURNING *on the UPDATE, or filter the SELECT — in both twins.