Commit Graph
329 Commits
Author SHA1 Message Date
Paperclip Deployment Engineer 8009714cda feat: mint health cache, xcashu token sweep, network error fallback patterns
Three changes that harden the mint-fallback branch against upstream
failures and enable proper provider failover:

1. Mint health cache (src/daemon/wallet/index.ts)
   Track mints that are temporarily unreachable (connection refused,
   DNS failure, TLS error). Unhealthy mints are skipped by
   getActiveMintUrl() and sendToken() so we don't waste a round-trip
   on every request. Mints are automatically retried after a 60s
   cooldown. The active mint switches to a healthy one when the current
   one goes unhealthy.

   This is critical for the xcashu 402 failover fix: when the primary
   provider (localhost:8011) is down, the wallet needs to successfully
   create a token via a healthy mint BEFORE the SDK's failover logic
   can route the request to the next provider (routstr.otrta.me).
   Without the health cache, every request wasted 2-3 seconds on a
   failed mint.minibits.cash fetch before falling back to cubabitcoin.

2. xcashu token sweep (src/daemon/index.ts)
   The scheduled refund loop now also sweeps pending xcashu tokens that
   failed inline refund (e.g. 'proofs already spent' — the token stays
   in storage and needs retry). This prevents orphaned IOUs from
   accumulating and ensures the background refundXcashuTokens path
   gets a chance to reclaim sats from tokens where the inline receive
   raced with the sweep.

   This is the race condition that caused the double-refund negative
   satsSpent bug (xcashu-double-refund-negative-sats, prio 998): the
   inline receive and the background sweep both received the same token,
   producing a negative satsSpent written to usage_tracking.

3. Network error patterns (src/daemon/wallet/mint-fallback.ts)
   Extend inspectForMintUnreachable to recognize Bun-specific network
   failures: 'Failed to fetch mint <url>', 'NetworkError when attempting
   to fetch resource', and 'Load failed'. These are treated as
   mint-unreachable so the fallback to the next configured mint kicks
   in immediately instead of surfacing the raw error.

   This pairs with the SDK fix (routstr-sdk PR #32) that adds
   'Unable to connect' and 'ECONNREFUSED' to isNetworkErrorMessage so
   the SDK's failover path is triggered when the upstream provider is
   completely unreachable (not just returning an HTTP error).

Related issues:
- Nostr: xcashu-402-body-refund-no-failover (prio 990, event a4d7e1cd...)
- Nostr: xcashu-double-refund-negative-sats (prio 998, event 14982f8d...)
- SDK PR: https://github.com/Routstr/routstr-sdk/pull/32
- SDK PR: https://github.com/Routstr/routstr-sdk/pull/31 (clamp fix)
2026-07-28 13:24:14 +00:00
Paperclip Deployment Engineer d38a8120d7 fix: skip Lightning fallback in xcashu mode, only fallback to next mint
In xcashu mode, Cashu tokens are sent per-request via X-Cashu header and
refunded in the response — no balance is kept with the provider. Falling
back to a Lightning invoice (routstr-core or local wallet) makes no
sense in this mode: it would create invoices that can never be settled
by the per-request token flow.

Added isXcashuMode() check in both installCreateProviderTokenFallback
and installMintFallbackTopUp. When all mints fail in xcashu mode, the
error now propagates immediately so the SDK's provider failover can
try the next available provider instead of attempting Lightning.
2026-07-24 12:52:09 +00:00
Paperclip Deployment Engineer a4a490049e feat: enable xcashu mode in CLI
Removed the 'coming soon' guard that blocked xcashu mode selection.
The SDK fully supports xcashu: Cashu tokens are sent via X-Cashu header
per request, and refunds are received via x-cashu response header.
Verified working: request creates 1-sat token, sends X-Cashu header,
receives response with refund, no balance kept with provider.
2026-07-24 12:32:58 +00:00
Paperclip Deployment Engineer 67a433c86f fix: prevent config override on restart and skip disabled providers in model fetch
Problem 1 — config overridden on restart:
parseArgs() always returned port=8008 when --port was not passed on the
CLI. index.ts then spread { ...config, port, provider } and saved,
silently overwriting the user's persisted port with 8008 on every daemon
restart.

Fix: parseArgs now returns null when --port is absent. index.ts falls
back to config.port (then 8008 as last resort), so the persisted config
is preserved across restarts.

Problem 2 — fetching models from disabled providers:
The SDK's fetchModels() still makes HTTP requests to every provider in
the list, even disabled ones — it only skips adding their models to the
best-priced map. models.ts passed unfiltered provider lists to
fetchModels in three places (bootstrap, getRoutstr21Models, refresh).

Fix: Added filterDisabled() helper that reads disabledProviders from the
store and strips them before fetchModels is called, preventing wasteful
HTTP requests to providers the user has explicitly disabled.
2026-07-24 12:32:58 +00:00
Paperclip Deployment Engineer 9c87a87d50 Merge remote-tracking branch 'origin/main' into mint-fallback 2026-07-22 22:51:53 +00:00
redshift e07098ae8c feat: check for version updates before installing; show update banner in TUI
- Extract version-checking logic into src/utils/update-checker.ts (shared
  between CLI and TUI)
- Updating routstrd...
bun add v1.2.22 (6bafe260)

installed routstrd@0.3.10 with binaries:
 - routstrd

[649.00ms] done
routstrd updated successfully.

Updating cocod...
bun add v1.2.22 (6bafe260)

installed @routstr/cocod@0.0.24 with binaries:
 - cocod

[692.00ms] done
cocod updated successfully.

Both routstrd and cocod have been updated!

Using remote daemon — skipping routstrd daemon restart.

cocod daemon was not running — skipping restart.

✓ All daemons restarted successfully. now checks npm for the latest version of each package
  and only reinstalls when a newer version is available; skips daemon
  restart when nothing was updated
- TUI shows a bold yellow 'UPDATE AVAILABLE' banner below the header on
  all tabs when a new version is detected
- TUI checks for updates at most every 210 minutes to avoid spamming the
  npm registry; first check fires 3s after startup (non-blocking)
- Bump version to 0.3.11
v0.3.11
2026-07-20 20:52:34 +01:00
Paperclip Deployment Engineer 6a5d0982c1 fix: floor fractional sats in Lightning invoice amount
routstr-core's /lightning/invoice endpoint requires integer sats
but options.amount can be a float (e.g. 64.79) derived from msat
costs. Without Math.ceil the fallback crashed with HTTP 422:
'Input should be a valid integer, got a number with a fractional part'

Fix: Math.ceil() in both topUpAmount calculations + defense-in-depth
Math.ceil() inside createInvoiceViaRoutstrCore itself.
2026-07-20 08:11:40 +00:00
Paperclip Deployment Engineer cc1927af84 chore: gitignore local debug/state artifacts
.pi-subagents/, data/, ROUTSTRD_OTRTA_FIX.md, kill-orphan-cocod.sh are
local-only debug/state files that accumulated during incident response
and shouldn't be tracked.
2026-07-19 16:28:00 +00:00
Paperclip Deployment Engineer d870475b65 fix: dedupe controller decl, reduce routstr-core timeout to 3s
- Remove duplicate const controller/timeout declarations introduced during
  sibling-agent parallel edits
- Reduce fetch timeout from 10s to 3s so the fallback chain stays snappy
  and tests don't time out the 5s bun:test budget
2026-07-19 16:26:29 +00:00
Paperclip Deployment Engineer 8e11b28eb3 Merge remote-tracking branch 'origin/main' into mint-fallback 2026-07-19 16:23:57 +00:00
Paperclip Deployment Engineer 4f59168b5f feat: harden fallback chain — routstr-core Lightning invoice + NWC auto-pay
When the local wallet runs out of proofs, the fallback chain now:

1. Retries across ALL configured mints (createProviderToken patched —
   previously only topUp retried on 'Not enough proofs')
2. Creates a routstr-core Lightning invoice (POST /lightning/invoice)
   → uses 'topup' purpose when an existing API key is available
   → pays via NWC (payBolt11) if connected, then retries
   → otherwise surfaces invoice for manual payment + polls until paid/expired
3. Falls back to local wallet Lightning invoice + NWC funding

Hardening (15 new tests):
- SSRF protection: rejects non-HTTPS provider URLs (except localhost)
- Amount validation: clamps to [1, 1_000_000] sats
- bolt11 validation: must start with 'lnbc'
- invoice_id validation: must be non-empty string
- Fetch timeout: AbortController (10s) on invoice creation
- NWC exception handling: caught, not propagated
- Double-install idempotency: patch markers prevent re-patching
- Concurrent calls: no shared state corruption
- API key safety: never logged in error messages
- Poller lifecycle: stops after 84 attempts (~7min) or on expired status

New wallet adapter method:
- payBolt11(bolt11): pays externally-created invoices via NWC

Verified end-to-end:
- 50-request stress test: 44/50 success, 6 fallback triggers (fugu-ultra)
- Routstr-core topup: 64,781 → 114,340 msats (balance increased)
- Daemon stable post-stress (142MB RSS, immediate recovery)
- 48/48 tests pass (33 existing + 15 hardening)
2026-07-19 15:47:27 +00:00
redshift 755dabbc3c Fix: Router for Teams -> Routstr for Teams 2026-07-16 19:41:13 +08:00
redshift 58288114f0 Add Router for Teams section linking to routstrd-auth 2026-07-16 19:39:49 +08:00
Paperclip Deployment Engineer 89d27cb62a fix: exclude failed mint from fallback candidate selection
When routstr.otrta.me rejects a Cashu token with 'mint_unreachable',
the mint fallback handler calls _spendToken with a fallback mint URL
(e.g. cubabitcoin.org). However, _spendToken's internal
_selectCandidateMints puts the highest-balance mint first regardless
of the preferredMintUrl parameter, so the fallback token was still
minted from the same unreachable mint (minibits).

Pass excludeMints: [initialMintUrl] to _spendToken so the failed
mint is excluded from candidate selection, forcing the fallback to
use the requested alternative mint.
2026-07-16 07:25:34 +00:00
Paperclip Deployment Engineer d3426a68c0 better fallback handling 2026-07-15 15:56:33 +00:00
9qeklajc 256cd8aac0 Add mint fallback for Cashu topups 2026-07-15 01:37:44 +02:00
redshift 2a31fef10b chore: bump version to 0.3.10 v0.3.10 2026-07-12 08:26:44 +05:30
redshiftandGitHub e405e685b8 Merge pull request #52 from Routstr/fix-overriding-agent-config
fix breaking hermes config
2026-07-12 02:54:35 +00:00
9qeklajc f90e65aa09 fix breaking hermes config 2026-07-11 16:33:28 +02:00
redshift a7de201330 chore: bump version to 0.3.9 v0.3.9 2026-07-10 12:57:30 +05:30
redshift 9e8f8178a9 chore: bump version to 0.3.8 v0.3.8 2026-07-09 12:27:30 +05:30
redshift 60bc8b4cb2 fix: capture daemon crash logs and prevent silent crashes
- Redirect detached daemon stdout/stderr to ~/.routstrd/debug.log
  instead of ignoring them, so uncaught exception stack traces are
  no longer lost
- Add process-level uncaughtException/unhandledRejection handlers
  that log to the file logger before the process dies
- Wrap setInterval async callbacks (model refresh + refund jobs) in
  IIFE catch chains so rejected promises can't escape and kill the
  process silently
2026-07-09 00:07:13 +08:00
redshiftandGitHub 44cda2577d Merge pull request #50 from Routstr/feat/update-restart
feat(update): restart daemons after update
v0.3.7
2026-07-08 08:52:13 +00:00
redshift cf1ca5941a feat(update): restart daemons after update
The update command previously only downloaded and installed new
binaries for routstrd and cocod without restarting the running
daemons, so updates would not take effect until a manual restart.

- Add restartDaemonsAfterUpdate() helper that gracefully stops and
  restarts both daemons after a successful update
- routstrd: uses POST /stop (drains active connections), polls for
    shutdown, then calls startDaemon() with configured port/provider
- cocod: runs 'cocod stop', then spawns 'cocod daemon' detached,
    polls 'cocod ping' until it comes back up
- Skips restart for daemons that weren't running
- Skips routstrd daemon restart when using a remote daemon
- Collects and reports failures without rolling back the update
- Bump @routstr/sdk to 0.3.15
- Bump routstrd version to 0.3.7

Closes nostr task: update-restart
2026-07-08 16:39:31 +08:00
redshift f58e807807 Add 'update' command to upgrade routstrd and cocod globally v0.3.6 2026-07-06 18:27:00 +08:00
redshift 1a06093b96 bump version to 0.3.5, update @routstr/sdk to 0.3.14 v0.3.5 2026-07-06 11:37:34 +05:30
redshiftandGitHub f77bcf6d00 Merge pull request #47 from Routstr/syncing-sdk
Sync daemon with SDK discovery adapter API
2026-07-05 16:00:51 +00:00
redshift 1ff3b9a4e1 Fix renderToday definite-assignment and usage-summary stale fixtures
renderToday declared todayStats/recentDays/hourlyMap with let but only
assigned inside if (stats.summary); TypeScript flagged them as used
before assignment (TS2454). Initialize with sensible defaults so the
function degrades gracefully when summary is missing.

The usage-summary tz-bucketing test used hardcoded May 2026 timestamps
with a comment dated 2026-06-02. Those entries aged out of
getUsageSummary's 30-day rolling window, so days came back empty.
Recompute timestamps relative to now and assert on dynamically-derived
local-day date strings.
2026-07-05 21:27:20 +05:30
redshift b2738109e3 Sync daemon with SDK discovery adapter API 2026-07-04 21:11:56 +05:30
redshift 3c6161d9bf fix: add periodic Nostr event refresh to daemon
Calls refreshNostrEvents() after initial bootstrap and in the 21-minute
recurring job, covering provider discovery (38421) and lgtm reviews
(38425) which were previously only refreshed on manual trigger.
2026-06-28 10:33:52 +08:00
redshift a5be3b06e9 bump version to 0.3.4 and @routstr/sdk to 0.3.12 v0.3.4 2026-06-22 20:09:31 +08:00
redshift 9cc3b76483 refactor(tui): separate async fetching from sync rendering
Split the monolithic render() into two distinct functions:
- fetchData(): async background fetch that updates state and triggers
  a repaint, guarded against overlapping calls
- render(): synchronous paint that reads current state and writes to
  stdout, safe to call from key handlers without blocking

Run all four daemon calls concurrently via Promise.all to cut the
blocked window. Remove redundant isDaemonRunning() checks from each
fetch function in data.ts — the single check now lives in fetchData().

Key handlers now call the sync render() directly instead of
void render(false), so scrolling and tab switching feel instant.
2026-06-22 16:39:58 +08:00
redshiftandGitHub 8a13d23b2e Merge pull request #46 from Routstr/feat/request-response-logging-config
feat: configure raw request/response logging
v0.3.3
2026-06-20 09:41:32 +00:00
redshift d8385d5c2e verson bump 2026-06-20 17:40:16 +08:00
redshift f15e18cf05 Adapt request response logging to SDK sink 2026-06-20 17:00:52 +08:00
redshift 1735d97a9e feat: configure raw request response logging 2026-06-20 10:10:21 +08:00
redshiftandGitHub eacb75da30 Merge pull request #45 from Routstr/pr-43
feat: server-side /usage/summary endpoint with npub filtering
2026-06-11 02:02:35 +00:00
redshift 8c52c90f94 addeed npub filtering for /usage as well 2026-06-11 09:31:32 +08:00
redshift 61acc0b5fb added npub based filter! 2026-06-11 09:17:13 +08:00
redshift 764d2dd826 removed old functions with benchmark which is clearly not needed 2026-06-11 08:49:15 +08:00
redshift 829681ffca removed legacy usage support 2026-06-11 08:33:15 +08:00
redshift ae91c46f01 fix(usage-summary): compute size buckets in JS after SDK removed token-range filters
The SDK dropped minTotalTokens/maxTotalTokens from AggregateUsageOptions
(routstr-sdk c98de6b), so replace the five aggregate() calls with a single
list() and bucket entries in-process.
2026-06-10 17:29:19 +08:00
redshift 6536333224 Merge remote-tracking branch 'origin/main' into pr-43
# Conflicts:
#	package.json
#	src/daemon/index.ts
2026-06-10 17:14:24 +08:00
redshift 0ab608bf88 fixed provider disabling not working while running 2026-06-08 09:43:15 +08:00
redshift d87a911241 viisble lgos 2026-06-07 11:25:02 +08:00
redshift 4a66af87da feat(providers): add --refresh flag to providers list
routstrd providers list --refresh now:
- Re-fetches Nostr kind 38421 provider discovery events
- Re-fetches Nostr kind 38423 routstr21 model list
- Re-fetches Nostr kind 38425 review events (applies LGTM-based disable)
- Fetches models from all discovered providers
- Syncs fresh provider list and disabled status into the store

Usage: routstrd providers list --refresh
2026-06-07 11:13:39 +08:00
redshift 27b13c30b0 made relays configurable 2026-06-07 10:56:06 +08:00
redshift 70230d9030 cached otkens now appear on the TUI 2026-06-07 09:06:53 +08:00
redshift d86a231ed3 fix: use Bun-safe SDK entrypoints for ModelManager and storage
- Import ModelManager from @routstr/sdk/bun instead of @routstr/sdk
- Import storage helpers from @routstr/sdk/storage/bun instead of @routstr/sdk/storage
- Fix createBunSqliteUsageTrackingDriver call: async and no longer needs
  manual bun:sqlite import (now handled internally by the sdk entrypoint)
- Verified sharded discovery adapter setup matches SDK pattern in
  scripts/routstr-daemon.ts at 6077aa7
2026-06-06 18:13:06 +08:00
redshiftandGitHub 2e8bbc9aa0 Merge pull request #42 from bilthon/fix/monitor-time
Display usage timestamps in local time instead of UTC
2026-06-04 13:45:39 +00:00