Found during the #166 (lazystart) money review. #166 closes the trade-loop spend path — MCP and mobee collect are thin clients over the daemon socket, the daemon is the only trade-path wallet owner, serialized by buyer.lock + its internal money lock.
The residual surface: the wallet-management CLI — mobee wallet send, mobee wallet melt (and wallet reconcile) — still opens the CDK wallet IN-PROCESS for a spend (wallet_cli.rs → wallet_ops::send_blocking/melt_blocking → open_wallet) and acquires neither buyer.lock nor the daemon money lock. Running one of these while the daemon is live opens a second concurrent handle to the same CDK sqlite — the cross-process window the daemon architecture exists to close, surviving on the manual-management surface. Pre-existing (not introduced by #166, out of its diff and its stated scope).
Fix options, in rough preference order:
- Refuse-with-message when a daemon holds the home's
buyer.lock ("daemon owns this wallet — use the daemon path or stop it first") — smallest, fail-closed.
- Route send/melt through the daemon socket like collect (daemon gains two management RPCs).
- Have the CLI take the same locks the daemon takes (turns the daemon into just-another-contender; weakest).
Until one lands, the operator contract is: don't run manual wallet spends while the buyer daemon is up.
Found during the #166 (lazystart) money review. #166 closes the trade-loop spend path — MCP and
mobee collectare thin clients over the daemon socket, the daemon is the only trade-path wallet owner, serialized bybuyer.lock+ its internal money lock.The residual surface: the wallet-management CLI —
mobee wallet send,mobee wallet melt(andwallet reconcile) — still opens the CDK wallet IN-PROCESS for a spend (wallet_cli.rs→wallet_ops::send_blocking/melt_blocking→open_wallet) and acquires neitherbuyer.locknor the daemon money lock. Running one of these while the daemon is live opens a second concurrent handle to the same CDK sqlite — the cross-process window the daemon architecture exists to close, surviving on the manual-management surface. Pre-existing (not introduced by #166, out of its diff and its stated scope).Fix options, in rough preference order:
buyer.lock("daemon owns this wallet — use the daemon path or stop it first") — smallest, fail-closed.Until one lands, the operator contract is: don't run manual wallet spends while the buyer daemon is up.