fix(mcp): account-sync resilience — bound account-map cache + surface status#22
Merged
Conversation
…lience) Root cause of the multis-reported wedge (WhatsApp added via noVNC didn't surface; a plain `docker restart` didn't recover it): getAccountMap() cached the accountID->network map for the whole process lifetime with no TTL/ invalidation, and froze an EMPTY map captured during the backend's post- restart sync window. So a runtime account-add stayed network:"unknown" until the process restarted, and a cold cache re-poisoned itself from a still- syncing backend on the next restart. - getAccountMap: TTL-bound the cache (BEEPERBOX_ACCOUNT_CACHE_TTL_MS, default 60s; 0 = always live) and NEVER cache an empty result — serve it for the one call, re-read next call, so the map self-heals when Beeper finishes syncing. - list_accounts: surface the backend per-account `status` (connected/ connecting/…) so a syncing bridge != a missing account; new schema field. - Observability: warn to stderr when /v1/accounts returns 0 (list_accounts + account-map refresh) instead of silently relaying an ambiguous 0. - Tests: 4 new unit tests (empty-not-cached, within-TTL caching, runtime add visible after TTL without restart, status surfaced); each written to fail against the old unbounded cache. 48/48 green. - Live-validated against the running container: list_accounts now returns status for all 5 accounts; list_inbox resolves 0 unknown-network chats. - Docs: context.md schema + retry guidance, PRD env table. Healthcheck left unchanged on purpose: failing it on 0 accounts would wedge first-run/fresh-login containers (legitimately 0) and the release gate; the stderr WARN is the observability path instead. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes the account-sync wedge filed by multis (
docs/01-product/beeperbox-asks/account-sync-resilience.md): a WhatsApp account added via noVNC didn't surface, and a plaindocker restartdidn't recover it — it self-corrected only later.Root cause (debug-method, evidence-grounded)
getAccountMap()— theaccountID → networkmap every chat verb uses for network labels — cached its result for the whole process lifetime with no TTL and no invalidation (if (accountCache) return accountCache;), and happily froze an empty map captured during the backend's post-restart sync window (when/v1/accountsbriefly returns[]).Consequences, all matching the report:
network:"unknown"in every chat verb until the MCP process restarted.docker restartre-poisoned it from a still-syncing backend → wedge survived, recovering only on a luckier restart / fulldown+up.list_accountsitself is stateless (live read), so its0was the backend's transient empty during sync — relayed silently, indistinguishable from "no accounts." The backend's per-accountstatusfield was dropped entirely.Fix
BEEPERBOX_ACCOUNT_CACHE_TTL_MS(default 60s;0= always live) and never cache an empty result — served for the one call that saw it, re-read next call, so the map self-heals the instant Beeper finishes syncing. Runtime account-add now reflected within the TTL, no restart.statusonlist_accounts(connected/connecting/… — new schema field) so a syncing bridge ≠ a missing account./v1/accountsreturns0, instead of silently relaying an ambiguous empty list.Verification
list_accountsreturnsstatusfor all 5 accounts (WhatsApp incl.);list_inboxresolves 0 unknown-network chats.beeperbox.context.mdschema + retry guidance,docs/PRD.mdenv table.Acceptance (from the ask)
list_accountsreturns N (live read).list_accountsinstant).docker restartrecovers a wedged relay (empty is never frozen).Healthcheck left unchanged on purpose — failing it on 0 accounts would wedge first-run/fresh-login containers (legitimately 0) and the release gate; the stderr WARN is the observability path instead.
Release scope: MINOR (new
statusschema field) when cut.🤖 Generated with Claude Code