Skip to content

Studio credit pause and recovery

This work builds on the save and streaming reliability fixes in PR #147. The owner approved option A, the inline conversation recovery card, on 2026-09-16. STUDIO_CREDIT_RECOVERY_ENABLED defaults to false and must remain false until the migration, backend, and recovery client are deployed together and staged verification is complete. Implementation and local validation did not modify production data.

Behavior

  • Before each official paid Studio model call, estimate the assembled prompt, tool definitions, and bounded output allowance. Reserve only that call's allowance under the wallet lock. Ordinary deductions and tips respect active holds. Holds earmark funds; they do not deduct or move wallet funding buckets.
  • Generation guards and chat regeneration/continuation use the unreserved balance as their spending budget. The displayed wallet total remains intact; BYOK and unlimited-plan exemptions are unchanged.
  • Settle the provider's observed cost against the same usage reference. Release the remainder. Concurrent or repeated settlements cannot charge the same reference twice. Bonus expiration and billing-period transitions still apply; expired funding is never restored merely because it was reserved.
  • Insufficient preflight funding pauses before another model call. A completed generation whose actual cost cannot be settled is persisted with its tool arguments and conversation context before any mutations are applied.
  • Explicit recovery claims the owned run atomically. It reuses the saved result and billing reference. Each successful write and its progress marker commit together, including held edits on published worlds. Already committed writes are skipped during recovery.
  • A changed world revision blocks replay instead of overwriting newer creator edits. Concurrent recovery attempts and active workers are fenced by claim ID. Abandoned claims require five minutes without a heartbeat before takeover.
  • Missing provider usage preserves the generated result but prevents application and automatic settlement. It requires billing reconciliation; zero is not treated as a known free call. Provider-reported cost is honored even when its token counts are zero.
  • Each completed settlement publishes the actual wallet total to the existing client balance store. Temporary reserved amounts are separate from that total.
  • A graceful server restart restores owned checkpoints instead of marking their work complete. Complete generated results with known usage are journaled and settled before recovery; missing usage still blocks automatic application.
  • The inline card shows available balance and distinguishes a next-step budget, an unpaid saved generation, and an already-settled result. Credit purchase opens the existing plans page in another tab. Returning to Studio refreshes status; a purchase never automatically resumes the task.
  • Reloading a conversation queries its own latest run. Pause events are terminal stream events, and explicit recovery uses the same run ID without adding a duplicate user message. Switching worlds or conversations invalidates older callbacks. Double clicks cannot start two recovery requests.

Limits

Budget estimation includes serialized tools, context, images, pricing tiers, markup, and cold Claude cache writes. It is not a quote for the whole task or a guarantee against provider-specific reasoning, image, or tokenization charges. The adaptive output limit can still truncate a tool call; guidance favors small complete edits, and actual settlement remains authoritative.

There is no automatic top-up or automatic resumption after a top-up. A user must explicitly continue. Recovery cannot safely replay a saved mutation after the creator has changed its source world. It does not reconstruct missing upstream usage or complete partial tool JSON from a broken network stream.

Cold-cache budgeting is conservative: a warm cached request may ultimately cost less than the required reserve. Unused reservations are released, but this may pause a small wallet earlier than a cache-hit-only estimate would. The allowance is per call, and should not be presented as a guaranteed minimum actual price.

Deployment and rollback

  1. Apply packages/server/drizzle/0056_studio_credit_recovery.sql before deploying code that reads the new checkpoint column. The SQL is additive and idempotent.
  2. Deploy the inline recovery client and backend together with STUDIO_CREDIT_RESERVATIONS_ENABLED=true and STUDIO_CREDIT_RECOVERY_ENABLED=false. The client handles credits_paused, conversation-scoped status, and explicit POST /api/studio/:worldId/agent/resume-credits.
  3. Confirm every spending instance has the new reservation guards and all old instances have exited. Recovery remains off, so this rollout creates no holds.
  4. After verifying generation, top-up, replay, stale world rejection, cancellation, and simultaneous wallet spending, enable STUDIO_CREDIT_RECOVERY_ENABLED=true. Keep reservation protection enabled. During this second rolling deployment, both old and new instances respect active holds, even though only the new instances can create them.

To disable recovery, set STUDIO_CREDIT_RECOVERY_ENABLED=false while keeping STUDIO_CREDIT_RESERVATIONS_ENABLED=true; existing holds must remain protected from ordinary deductions and tips. Before disabling reservation protection or rolling back to code without guards, drain every worker that can renew a hold and verify no active holds remain. Do not drop checkpoint or reservation rows: they retain work and settlement references for recovery/reconciliation. Leases expire automatically after five minutes without renewal.

Local validation

Use the isolated in-memory launcher; never point tests at a development or production database:

powershell
node packages/server/scripts/test-local.mjs src/lib/studio-credit-budget.test.ts src/lib/credit-reservations.test.ts src/lib/studio-credit-checkpoint.test.ts src/lib/studio-credit-loop.test.ts src/lib/studio-credit-status.test.ts
pnpm --filter @yumina/app exec tsx --tsconfig tsconfig.app.json --test src/stores/studio-credit-recovery.test.ts src/features/studio/components/credit-pause-card.test.ts src/features/studio/panels/ai-chat-credit-recovery.test.ts
pnpm build
pnpm typecheck

The tests use synthetic users and providers, real wallet transactions, and the current constrained database schema. No paid model calls are required.

The 16 end-to-end loop scenarios cover preflight rejection without a provider call, balance events, unaffordable results, top-up/replay without regeneration, zero-balance replay of already-paid work, partial-write failure and repeated recovery, explicit zero-cost calls, unknown/empty usage, stale-worker fencing, and cancellation. The migration was also executed twice in a separate fresh PGlite database; column types, constraints, indexes, cascade deletion, and transaction-reference deletion behavior matched the declarative schema.

The four existing client credit-store tests passed, including rejection of an older balance fetch after an SSE debit. Real PostgreSQL multi-connection lock contention and rendered application UI have not been exercised in this pass.

The final four-file focused run passed all 68 tests, including a real wallet with 100 total credits and an 80-credit hold yielding a 20-credit generation budget while the displayed wallet total remains 100.

The full server run passed 1,866 tests with 8 skipped and no failures. The focused run above was repeated after the final generation-budget adjustment. The final workspace build (5 tasks) and typecheck (8 tasks) both passed before client integration. The A/B preview passed its local interaction checks; the owner selected A for integration. These checks do not constitute a production rollout.

After client integration, all 77 server recovery tests passed. The full app run passed 998 tests and retained exactly the same 9 previously reproduced baseline failures (navigation, drawer geometry, credit indicator, and reading canvas); there were no additional failures. The final focused client run also covers late status and stream responses, duplicate resume clicks, chunk-split SSE events, confirmed-resume card removal, and history/undo behavior.

All 28 focused client checks passed: 18 real store/SSE scenarios, 8 React card interactions, and 2 full-panel conversation-switch races. The latter replay an immediate click on an old Continue button while the new conversation loads, and a conversation switch while Continue is waiting for an editor save. Neither case may submit a resume request for the old conversation.

The final workspace build and typecheck both passed after integration. After the last conversation-loading cleanup, the two panel races and the app build (including TypeScript compilation) passed again. The feature remains disabled by default; production enablement follows the two-stage procedure above.

After merging the latest main branch, the full app suite passed 1,002 tests. Its nine failures exactly matched the previously reproduced baseline; no new failures were introduced by the merge. Reservation-only mode also passed its real wallet regression, protecting a 100-credit wallet with an 80-credit hold from a 21-credit chat or gift while leaving the displayed balance intact.

The final loop/checkpoint/status run passed all 56 tests, including six server restart scenarios: a complete generation before settlement, a committed first write, an incomplete generation, and missing, empty, or zero-only usage. Recovery neither repeats committed writes nor treats unknown provider usage as free.

The release server suite ran 1,899 tests: 1,890 passed, eight were skipped, and one existing referral fixture failed because it omitted the plan_version column required by the latest main branch. The fixture was aligned with the declared schema without changing its assertions or runtime code; its isolated rerun passed. The final build passed all five tasks and typecheck all eight.