Recovery semantics: owned-stranded vs handed-out (clawback) — encode the class in code, give clawback its own verb and policy
Maintainers usually reply within 1 day
Nobody has claimed this yet.
Assessment
- Difficulty
- 5/5
- Estimated time
- Over a week
- Newbie friendliness
- 25/100
Research direction
Start by reading src/tollwallet/port.go and src/cli/server.go for the existing recovery contract, then compare the manual clawback flow in scripts/token-recovery/main.go with src/upstream_session_manager/token_recovery.go. The issue depends on #549, #500, #502, and #423 and calls for new policy, ownership coordination, dispute handling, and crash consistency. Done means the new clawback path is distinct from wallet recover and passes the specified unit, integration, and failure-injection tests.
Written by the indexing model from the issue text.
Description
Priority: P2 — recovery semantics: owned-stranded vs handed-out (clawback). Encode the distinction in code; give clawback its own verb and policy. Companion to #500, downstream of #549. Explicitly post-0.6.0.
Problem
NUT-07 liveness checks serve two kinds of recovery with opposite verdict semantics, and the codebase already implements both without naming the split:
- Owned-stranded recovery (type 1) — tokens that are ours, stuck mid-operation by a failure: drain-journal entries (#375/#433, recovered by #549's
wallet recover), proofs reserved in the gonuts pending bucket (#500'sReclaimUnspentProofs). - Handed-out clawback (type 2) — tokens we gave to someone else whose payment then failed:
scripts/token-recoveryovertokens-to-recover.txt(written byupstream_session_manager/token_recovery.goon autopay failures), clientd rejections (#423), reseller interrupted hand-offs.
The mint cannot tell these apart — same checkstate call — but the meaning of each verdict inverts:
| Verdict | Owned-stranded (type 1) | Handed-out (type 2) |
|---|---|---|
UNSPENT |
stranded value; re-import, races nobody | recipient has not redeemed — clawback window open; re-spending is a race against their redemption |
SPENT |
already secured; nothing to do | the counterparty took the value while we got nothing — a dispute, not a closed case |
PENDING |
re-check later (in-flight op) | their redemption is literally in flight — cannot and should not act |
| unknown/error | re-run later | must not sweep; after a partial sweep, ambiguity is monetary (ErrSidecarAmbiguous class), not an incomplete report |
#549 documents this taxonomy on the WalletPort.CheckTokenSpendable seam and keeps wallet recover strictly type-1. But only type 1 has an in-tree, policy-governed path. Type 2 today is an out-of-tree script run by hand, with no grace window, no ownership coordination with #500's sweeper, no dispute record, and no crash story of its own — even though Receive()-back-to-wallet is itself an irreversible operation.
Why it matters
Conflating the two classes produces one of two failures: treating a clawback verdict as simple recovery (double-pay — voiding a slow-but-honest recipient's payment), or treating "spent" as "case closed" when it means the counterparty kept the money. Type-2 actions are spends; running them under a read-only report's exit-code contract ("unknown exits non-zero") is category confusion — a partial sweep must surface as monetary ambiguity, not as a re-runnable report.
Current behavior (source refs)
src/tollwallet/port.go—CheckTokenSpendabledoc comment states the two-semantics contract (#549, 0cbe21d).src/cli/server.gohandleWalletRecover— type-1 only, ownership assumption explicit.scripts/token-recovery/main.go— manual type-2 tool: bounded checkstate,Wallet.Receive()on UNSPENT, dry-run mode.src/upstream_session_manager/token_recovery.go— writestokens-to-recover.txton autopay failure.- gonuts
ReclaimUnspentProofs/GetPendingMeltQuotes— type-1 machinery, never invoked by the daemon (#500). - #423 — clientd burns tokens on rejected payments; no reclaim path at all.
Desired invariant
Every recovery action declares its class. Owned recovery may report (and, per #500, reclaim automatically within our own wallet). Handed-out clawback is a separate verb — a spend that races the recipient — governed by an explicit policy: grace window, ownership markers, dispute surfacing, and its own crash-consistency (the clawback swap itself needs durable intent before the irreversible step).
Proposed scope
- Name the class at the seams:
RecoveryClass(owned vs handed-out) or distinct verbs —wallet recover(report, type 1) vswallet reclaim(sweep, type 2).recovermust never sweep handed-out tokens. - In-process type-2 path replacing the ad-hoc script for
tokens-to-recover.txt: checkstate → still unspent past the grace window → swap-to-self → record; spent → mark dispute/owed (feeds the business-transaction record #502). - Policy state: per-token grace window; ownership markers so the #500 sweeper and an in-flight reseller hand-off never fight over the same proofs (extends #500 scope item 4).
- clientd alignment (#423): same class distinction client-side.
- Exit-code/output contract for sweeps: any unknown after a partial sweep is reported as monetary ambiguity with per-token state, not a generic non-zero.
Acceptance criteria
- Fault test: token handed out, recipient never redeems → after grace, reclaim succeeds and the recipient's later redemption fails cleanly with a surfaced dispute record.
- Fault test: recipient redeems during the grace window → no double effect; state becomes dispute.
- Unknown at sweep time → no sweep attempted, retried next interval, no crash-loop.
Required tests
- Unit: class routing (recover never sweeps); grace-window expiry; ownership-marker skip shared with #500.
- Integration (cloud-lab): interrupted autopay lane → automated reclaim; dispute state when upstream did redeem.
Failure-injection tests
- Wedged mint during clawback check (bounded, #525/#532 idiom) — no sweep on unknown.
- Recipient redemption racing the reclaim swap (
-race). - SIGKILL between clawback intent persistence and the swap (the clawback needs its own crash story — journal or pending-bucket entry before the irreversible step, per the AGENTS.md fund-safety rules).
Compatibility
New verbs/state only; wallet recover behavior unchanged.
Dependencies
#549 (seam + taxonomy docs, merged first), #500 (sweeper interplay), #502 (dispute records), #423 (clientd).
Out of scope
Changing when tokens enter tokens-to-recover.txt (each flow's own fix); NUT-07 protocol changes; the #497 swap-crash-window saga work (separate track).
- Dominant language
- Go
- Stars
- 12
- Forks
- 14
- Avg merge
- 1d 6h
- Merged PRs (30d)
- 211
Getting set up
- No Dockerfile or Docker Compose file
- No pull request template
- Read the contributing guide
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
More from OpenTollGate/tollgate-module-basic-go
-
go-battery needs an ndsctl on PATH: TestPurchaseSessionGuardHoldsThroughTheOutcomeUnknownWindow fails on bare hosts (passes with stub)Possibly taken A pull request linked to this issue is open or already merged. Open
Difficulty 2/5 1-3 hours Newbie friendliness 78/100
OpenTollGate/tollgate-module-basic-go#726 ·
Maintainers usually reply within 1 day
-
rebrand-literal-gutter: uhttpd section-vocabulary check trips on a COMMENT (uhttpd.luci in 92-tollgate-admin-setup:178)Possibly taken A pull request linked to this issue is open or already merged. Open
Difficulty 2/5 1-3 hours Newbie friendliness 78/100
OpenTollGate/tollgate-module-basic-go#723 ·
Maintainers usually reply within 1 day
-
Discovery endpoint serves text/plain content-type on / — r2r clients warnPossibly taken A pull request linked to this issue is open or already merged. Open
Difficulty 2/5 1-3 hours Newbie friendliness 68/100
OpenTollGate/tollgate-module-basic-go#628 ·
Maintainers usually reply within 1 day
-
Difficulty 4/5 3-5 days Newbie friendliness 48/100
OpenTollGate/tollgate-module-basic-go#725 ·
Maintainers usually reply within 1 day
-
cloud-lab Dockerfile.client: mid-file ARG invisible to FROM — client and killer images unbuildable on docker/buildkit 29 (golang:-bookworm)Possibly taken @Amperstrand claimed this today. Open
Difficulty 1/5 Under an hour Newbie friendliness 25/100
OpenTollGate/tollgate-module-basic-go#724 ·
Maintainers usually reply within 1 day
All issues in OpenTollGate/tollgate-module-basic-go
Similar issues
-
agent-research-recommend agent-review-finding chore
Difficulty 2/5 1-3 hours Newbie friendliness 78/100
jordansmall/spindrift#4821 · 1 comment ·
Maintainers usually reply within 1 day
-
area:web
Difficulty 2/5 1-3 hours Newbie friendliness 82/100
praetorianer777/GoTome#178 ·
Maintainers usually reply within 1 day
-
Difficulty 2/5 1-3 hours Newbie friendliness 70/100
oracle/go-oracledb#105 ·
Maintainers usually reply within 1 day
-
Difficulty 2/5 1-3 hours Newbie friendliness 84/100
-
Difficulty 2/5 1-3 hours Newbie friendliness 70/100
Maintainers usually reply within 1 day