Skip to content

Fix AI credit overspend and uncharged generations in ai-bot#5235

Open
jurgenwerk wants to merge 2 commits into
mainfrom
cs-11504-a-user-can-run-unlimited-paid-ai-generations-beyond-their
Open

Fix AI credit overspend and uncharged generations in ai-bot#5235
jurgenwerk wants to merge 2 commits into
mainfrom
cs-11504-a-user-can-run-unlimited-paid-ai-generations-beyond-their

Conversation

@jurgenwerk

@jurgenwerk jurgenwerk commented Jun 15, 2026

Copy link
Copy Markdown
Contributor

(Written by Claude on Matic's behalf.)

Fixes CS-11504 and CS-11505 — both HIGH severity.

What was broken

The ai-bot locked credit checking per room, not per user, and recorded costs after generating.

  • Overspend (CS-11504): one user firing requests across N rooms passed the balance check N times against the same stale balance → balance goes arbitrarily negative.
  • Uncharged usage (CS-11505): cost tracking dropped the charge if a prior one was still in flight (if (trackAiUsageCostPromises.has(user)) return) → a steady stream of messages = free generations.

The fix

Wrap check credits → generate → charge in the per-user withUserCostLock that the realm-server proxy paths already use. The charge is awaited inside the lock, so a concurrent same-user request blocks until the prior charge lands and then validates against the real balance. Every generation's cost is recorded (no more drop-if-busy).

Where to look

File Change
ai-bot/main.ts wrap the credit/generate/charge block in withUserCostLock (most of the diff is just re-indentation from nesting)
ai-bot/lib/credit-tracking.ts fallback cost helper for the rare no-inline-cost case (never coalesces)
ai-bot/tests/credit-tracking-test.ts tests for no-dropped-charges + fallback bookkeeping

🤖 Generated with Claude Code

The ai-bot's room lock serializes only per room, so a user firing
concurrent requests across N rooms passed validateAICredits N times
against the same stale balance and overspent (CS-11504). The in-memory
barrier didn't help: waitForPendingCreditTracking only waits on an
already-in-flight debit, and trackAiUsageCost early-returned when a
same-user debit was pending, silently dropping all but one concurrent
cost.

Wrap the credit gate -> generate -> debit section in
PgAdapter.withUserCostLock, the per-user cross-replica barrier CS-11128
already applies to the realm-server proxy paths. The inline-cost debit
is awaited inside the lock so the next same-user request validates
against the post-deduction balance. The rare no-inline-cost fallback is
registered in the tracking map inside the lock (so the next request
observes it) but its up-to-10-minute fetch+debit runs detached, since we
can't pin a DB connection that long.

Split the spend logic into spendInlineCost and scheduleFallbackCostTracking
in lib/credit-tracking.ts and add tests covering no-coalescing of debits
and the fallback map bookkeeping.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@jurgenwerk jurgenwerk force-pushed the cs-11504-a-user-can-run-unlimited-paid-ai-generations-beyond-their branch from 5246f03 to b30d8ac Compare June 15, 2026 11:22

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot encountered an error and was unable to review this pull request. You can try again by re-requesting a review.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 3 out of 3 changed files in this pull request and generated 3 comments.

Comment thread packages/ai-bot/lib/credit-tracking.ts
Comment thread packages/ai-bot/main.ts Outdated
Comment thread packages/ai-bot/tests/credit-tracking-test.ts
- Guard the in-lock cost debit with try/catch so a billing-write failure
  can't skip the activeGenerations cleanup and leave a stale entry that
  would abort a later, already-finished run.
- Chain each fallback debit onto any prior pending one for the same user
  so the tracking map represents all in-flight debits, and only delete
  the entry while it still points at the current chain — an earlier debit
  settling can no longer unlink a newer fallback that overwrote it.
- Add a regression test for that older-settles-first race.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@jurgenwerk jurgenwerk changed the title Serialize ai-bot AI generations per user to prevent credit overspend Fix AI credit overspend and uncharged generations in ai-bot (CS-11504, CS-11505) Jun 16, 2026
@jurgenwerk jurgenwerk marked this pull request as ready for review June 16, 2026 08:19
@jurgenwerk jurgenwerk requested a review from a team June 16, 2026 08:20
@jurgenwerk jurgenwerk changed the title Fix AI credit overspend and uncharged generations in ai-bot (CS-11504, CS-11505) Fix AI credit overspend and uncharged generations in ai-bot Jun 16, 2026

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: db3bb89b56

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread packages/ai-bot/main.ts
Comment on lines +433 to +435
await assistant.pgAdapter.withUserCostLock(
senderMatrixUserId,
async () => {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Avoid holding the room lock while waiting on user cost lock

When the same user already has a generation running in another room, this awaits withUserCostLock after the handler has acquired this room's lock and built eventList/promptParts, but before activeGenerations.set is reached below. A later message or stop event in this same room during that wait will not find an active generation to abort, then acquireRoomLock returns false because the room lock is still held, so that event is dropped; once the other room finishes, this request generates from stale history. Consider moving the per-user wait before taking the room lock/history or registering an abortable queued generation before waiting.

Useful? React with 👍 / 👎.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants