Skip to content

fix(cashu): stop transient outputs-pending (11004) from crashing swap tasks#1115

Draft
orveth wants to merge 1 commit into
masterfrom
fix/send-swap-11004-outputs-pending
Draft

fix(cashu): stop transient outputs-pending (11004) from crashing swap tasks#1115
orveth wants to merge 1 commit into
masterfrom
fix/send-swap-11004-outputs-pending

Conversation

@orveth

@orveth orveth commented Jun 1, 2026

Copy link
Copy Markdown
Contributor

Problem

Sentry 7512228812MintOperationError: outputs are pending (cashu error code 11004 OUTPUTS_ARE_PENDING), recurring in production. It crashes the task-processing lead client to the root error boundary ("Oops, something went wrong / Reload Page").

Chain: the background TaskProcessor (rendered only on the lead client) runs the cashu send-swap task → swapProofs (cashu-send-swap-service.ts). Its catch recovers only OUTPUT_ALREADY_SIGNED (11003) and TOKEN_ALREADY_SPENT (11001) via wallet.restore(); 11004 is not in the allowlist, so it re-throws. The mutation runs retry: 3 + throwOnError: true, so once retries exhaust the error is thrown during render → root boundary + Sentry.captureException. (These send/receive flows have no explicit capture, so the Sentry Issue is the boundary firing.)

Fix

OUTPUTS_ARE_PENDING (11004) and PROOFS_ARE_PENDING (11002) are transient — the mint is still processing the outputs/inputs in a parallel operation and resolves them on its own. They need retry-until-resolved, not wallet.restore() (the outputs aren't signed yet — distinct from the 11003 path).

Two layers, both leaning on the existing ConcurrencyError ("always retry") convention — not a new operation-level classifier (that stays out of scope):

  1. Classify at the swap boundary. New isTransientCashuSwapError predicate (app/lib/cashu/error-codes.ts). Both swap services' swapProofs catch now map it → throw new ConcurrencyError(error.message), after (and distinct from) the existing 11003/11001 wallet.restore() recovery.
  2. Stop the boundary escalation. The send swapForProofsToSend and receive completeSwap task mutations go from retry: 3 / throwOnError: true to a type-aware retry (ConcurrencyError → retry, mirroring useCreateCashuSendSwap in the same file) plus throwOnError: (e) => !(e instanceof ConcurrencyError). Transient errors retry until the mint settles and never reach the boundary; the swap row stays DRAFT and self-heals on the next trigger cycle. Unexpected errors keep their existing loud-fail behavior.

Reviewer notes

  • Scope: send AND receive. cashu-receive-swap-service has the identical 11003/11001-only allowlist and its completeSwap hook the identical retry: 3 / throwOnError: true → the same latent 11004 crash. Both are fixed for consistency.
  • Observability tradeoff. Retrying ConcurrencyError indefinitely (matching useCreateCashuSendSwap) means a persistently-stuck pending state retries silently — the mutation never settles, so onError never fires and nothing escalates, leaving no alert (the very signal that surfaced this bug). If preferred, a follow-up can bound the retry and log on give-up. Flagging for a decision.
  • Test is predicate-level. error-codes.test.ts locks the classification (11004/11002 transient; 11003/11001/others not). A full service/hook render test isn't feasible here: importing CashuSendSwapService under bun test throws window is not defined (the Supabase client + realtime manager touch window at module top-level), which is why every existing repo test exercises pure modules only. The throw-conversion and retry config are covered by tsc + the conventions they mirror.

Verification

  • bun run fix:all — clean
  • bun run typecheck — clean
  • bun test — 139 pass / 0 fail (incl. the new 6-case predicate test)

Draft for review (gudnuf approved a draft fix).

🤖 Generated with Claude Code

@vercel

vercel Bot commented Jun 1, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
agicash Ready Ready Preview, Comment Jun 5, 2026 10:17pm

Request Review

@supabase

supabase Bot commented Jun 1, 2026

Copy link
Copy Markdown

This pull request has been ignored for the connected project hrebgkfhjpkbxpztqqke because there are no changes detected in supabase directory. You can change this behaviour in Project Integrations Settings ↗︎.


Preview Branches by Supabase.
Learn more about Supabase Branching ↗︎.

@orveth

orveth commented Jun 1, 2026

Copy link
Copy Markdown
Contributor Author

Cross-lane review note (keeper coordination):

Domain owner for cashu error-codes reviewed the classification — no conflict. The operation-level classifier is a separate future file (error-classify.ts, not yet built) that will absorb isTransientCashuSwapError rather than collide with it. They also confirmed 11004/11002 ARE transient, which corrects the 2026-05-20 error-code audit that had them as 'already-resolved' — wallet.restore is only valid post-signing, so these need retry-until-settled and then resolve via the existing 11003→restore path.

On the open retry question: both the domain owner and I recommend bounded retry + log over the current silent-infinite retry, so a never-settling swap (inputs spent elsewhere / keyset-counter desync) stays observable rather than spinning quietly forever. Happy to switch it if you agree — small change.

… tasks

Rebased onto post-monorepo master; paths relocated app/ -> apps/web-wallet/app/. Original commits:
 - fix(cashu): stop transient outputs-pending (11004) from crashing swap tasks
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.

1 participant