Repository navigation
Description
aggregator(alerts): getSupabaseClient and getAlertsStore both memoize a transient null forever — the alerts ledger stays "unavailable" until restart
Evidence
Two stacked "resolved once" latches in the alert-throat landing (#3676),
both in pm-stack/aggregator/src/ (main d3225b99):
Layer 1 — supabase.ts:48-68:
let clientPromise: Promise<SupabaseClient | null> | null = null;
export function getSupabaseClient(): Promise<SupabaseClient | null> {
if (!clientPromise) {
clientPromise = (async () => {
const token = await resolveBwsSecret("supabase.access-token-muse-cli");
if (!token) return null;
...
})().catch(() => null);
}
return clientPromise;
}Layer 2 — alerts.ts:482-489:
let storePromise: Promise<AlertsStore | null> | null = null;
export function getAlertsStore(): Promise<AlertsStore | null> {
if (!storePromise) {
storePromise = getSupabaseClient().then((db) => (db ? new AlertsStore(db) : null));
}
return storePromise;
}resolveBwsSecret (src/secrets.ts) fails soft to null on any
transient problem (bws unreachable, token file briefly unreadable, 30s
timeout) — by design, and it caches that null for only 5 minutes so
the failure heals. Neither consumer honors that design: the first
null is memoized in a module-level promise that is never cleared, at
BOTH layers independently. Fixing only the client layer would not heal
the store, whose settled null promise never re-asks the client; and
the store layer is the only path the alerts surface uses, so the whole
alert throat (intake ledger, dedupe, delivery state, ack) reports
itself unavailable for the process lifetime after one transient hiccup
at first use.
This is the same defect shape as #3729 (push.ts
ensurePushConfigured), filed separately because the fix sites and the
affected surface differ: there, push delivery; here, the ledger/store
every alerts route depends on.
Verified (laptop probes, verbatim files)
Probe: ~/git/fleet/probe-2026-11-01-laneB-supalatch/ on the laptop.
supa/— the repo's realsupabase.ts(sha2567a0969f7…, identical
on sandbox maind3225b99and the laptop clone) with its
./secrets.tsimport stubbed fail-once-then-succeed
(probe-supabase.log):
1st getSupabaseClient (bws transiently down): null resolveCalls=1
2nd getSupabaseClient (bws recovered): null resolveCalls=1
RESULT: LATCH CONFIRMED — null client memoized forever, secret never re-read
alerts/— the repo's realalerts.ts(sha256fb07caf8…) with
only its./supabase.tsand./push.tsimports stubbed; the stub
client is unavailable on the first ask and available thereafter
(probe-alerts.log):
1st getAlertsStore (client unavailable): null clientCalls=1
2nd getAlertsStore (client available): null clientCalls=1
RESULT: LATCH CONFIRMED — storePromise memoizes the null store; the client is never re-asked
Why it matters
The alerts surface is deliberately fail-soft ("callers fail soft — the
alerts surface reports itself unavailable instead of taking the
dashboard down", supabase.ts header). A transient secret-store failure
around the first alerts request after a restart turns that soft
degradation into a hard, silent, permanent one: sentinels keep posting,
but intake/dedupe/delivery bookkeeping has no store until someone
restarts the aggregator. Nothing distinguishes "no token configured"
from "failed once at startup" in the outcome.
Acceptance criteria
- A
nullresult is not memoized at either layer: after a
transient failure, a latergetSupabaseClient()/getAlertsStore()
retries resolution and can return a live client/store without a
process restart. - A successful resolution is still memoized (no per-request bws
spawns), and concurrent first callers still share one in-flight
resolution. - Regression tests cover fail-then-recover at both layers
(client: first tokennull, then a token →nullthen a client;
store: first clientnull, then a client →nullthen a store). - Genuinely unconfigured (no token reachable at all) still fails
soft: callers getnull, no throw, the dashboard stays up.
Distinct from: #3729 (the push.ts VAPID configure latch — same shape,
different module and surface), #3685 (closed; delivery marking),
#3693 (subscriptions GET key leak).
Activity
developing-today commentedon Nov 1, 2026
CODE-1515
dezren39 commentedon Nov 1, 2026
🤖 Agent note · filed
dezren39 commentedon Nov 1, 2026
🤖 Agent note · adopted as Phase 1 of design #3879
null— at BOTH layers independently (your own evidence: the store latch survives a client-only fix); do NOT substitute a consumer-side failure TTL (secrets.ts's 5-minute null cache already bounds retry cost). (2) The success test is on the resolved value (non-null), not on non-rejection — both sites fail soft via.catch(() => null). (3) Clear at settle only, so concurrent first callers still share one attempt.source/design-architect. Existing queued triage packet stands.49 remaining items
dezren39 commentedon Nov 4, 2026
Triaged: OpenChamber scheduled lane completed task 7407; fix task 9750 is queued.
dezren39 commentedon Nov 5, 2026
🤖 Agent note · picked up
dezren39 commentedon Nov 5, 2026
🤖 Agent note · done
bun test test/supabase-export.test.ts test/alerts.test.tsat the merged tree — 55 pass, 0 fail, 193 assertions; Codex review on head 7803c6f completed with no findings; PR mergeable_state clean before merge.