From 9231084ee31c70b5ee09d937bffe79a79aaaf822 Mon Sep 17 00:00:00 2001 From: BSVanon Date: Sat, 28 Feb 2026 21:45:16 -0600 Subject: [PATCH] Cache wallet status in /api/status to fix Mission Control showing 0 balance wallet.balance() + listOutputs() were timing out (>10s) on every 15s poll, causing wallet: null in the response. 30s cache with stale-while-revalidate prevents DB contention. Timeout raised to 15s, failures now logged. Co-Authored-By: Claude Opus 4.6 --- clawsats-wallet/src/server/JsonRpcServer.ts | 64 ++++++++++++++------- 1 file changed, 42 insertions(+), 22 deletions(-) diff --git a/clawsats-wallet/src/server/JsonRpcServer.ts b/clawsats-wallet/src/server/JsonRpcServer.ts index 7026b5a..bcc9053 100644 --- a/clawsats-wallet/src/server/JsonRpcServer.ts +++ b/clawsats-wallet/src/server/JsonRpcServer.ts @@ -53,6 +53,8 @@ export class JsonRpcServer { private indelibleAuthToken: string; private indelibleCache: { data: IndelibleStatusData | null; expiresAt: number } = { data: null, expiresAt: 0 }; private agentCertHex: string | null = null; + private walletStatusCache: { data: WalletStatus | null; expiresAt: number } = { data: null, expiresAt: 0 }; + private walletStatusRefreshing = false; constructor(walletManager: WalletManager, options: ServeOptions = {}) { this.walletManager = walletManager; @@ -346,28 +348,8 @@ export class JsonRpcServer { const completedJobs = jobs.filter(j => j.status === 'completed').length; const approvalJobs = jobs.filter(j => j.status === 'needs_approval').length; - // Wallet fields (V2-0 + V2-A expanded) — 10s timeout to prevent deadlock - let walletStatus: WalletStatus | null = null; - try { - const walletPromise = (async () => { - const balanceSats = await this.walletManager.getBalance(); - const utxo = await this.walletManager.getUtxoStatus(); - return { balanceSats, utxo }; - })(); - const timeoutPromise = new Promise((_, reject) => - setTimeout(() => reject(new Error('wallet timeout')), 10000) - ); - const { balanceSats, utxo } = await Promise.race([walletPromise, timeoutPromise]); - walletStatus = { - balanceSats, - utxoTotal: utxo.total, - utxoAvailable: utxo.available, - utxoLocked: utxo.locked, - utxoStuck: utxo.stuck, - fundingAddress: this.walletManager.getFundingAddress(), - concurrencySlots: utxo.available - }; - } catch {} + // Wallet fields — served from 30s cache to avoid DB contention on frequent polls + const walletStatus: WalletStatus | null = await this.getCachedWalletStatus(); // Brain stats extracted from brain-events.jsonl const brainStats = this.extractBrainStats(brainEvents); @@ -2267,6 +2249,44 @@ export class JsonRpcServer { * Results are cached for 10 seconds to avoid hammering on 15s dashboard refresh. * Returns null when Indelible is disabled or all calls fail. */ + private async getCachedWalletStatus(): Promise { + if (this.walletStatusCache.data && Date.now() < this.walletStatusCache.expiresAt) { + return this.walletStatusCache.data; + } + if (this.walletStatusRefreshing) { + return this.walletStatusCache.data; + } + this.walletStatusRefreshing = true; + try { + const walletPromise = (async () => { + const balanceSats = await this.walletManager.getBalance(); + const utxo = await this.walletManager.getUtxoStatus(); + return { balanceSats, utxo }; + })(); + const timeoutPromise = new Promise((_, reject) => + setTimeout(() => reject(new Error('wallet status timeout')), 15000) + ); + const { balanceSats, utxo } = await Promise.race([walletPromise, timeoutPromise]); + const status: WalletStatus = { + balanceSats, + utxoTotal: utxo.total, + utxoAvailable: utxo.available, + utxoLocked: utxo.locked, + utxoStuck: utxo.stuck, + fundingAddress: this.walletManager.getFundingAddress(), + concurrencySlots: utxo.available + }; + this.walletStatusCache = { data: status, expiresAt: Date.now() + 30000 }; + return status; + } catch (err) { + const msg = err instanceof Error ? err.message : String(err); + console.warn(`[STATUS] Wallet status query failed: ${msg}`); + return this.walletStatusCache.data; + } finally { + this.walletStatusRefreshing = false; + } + } + private async fetchIndelibleData(): Promise { if (!this.indelibleEnabled) return null;