Repository navigation
Description
Summary
Three lazy initializers in the aggregator memoize their first failure forever, defeating the one layer that was designed to heal:
- aggregator(alerts): getSupabaseClient and getAlertsStore both memoize a transient null forever — the alerts ledger stays unavailable until restart #3732 —
getSupabaseClient(pm-stack/aggregator/src/supabase.ts:43) caches its promise once and never clears it; stacked on top,getAlertsStore(src/alerts.ts:481) caches the resultingnullin a second, independent latch. One transient bws hiccup at first use makes the whole alert throat (ledger, dedupe, delivery state, ack) report itself unavailable until restart. - aggregator(push): ensurePushConfigured memoizes a failed VAPID configure forever — one transient bws failure disables Web Push for the process lifetime #3729 —
ensurePushConfigured(src/push.ts:26) resolvesfalsewhen the VAPID key is unreachable and never clearsconfigurePromiseon that path; its only reset (a push-service 401/403) is unreachable from the configure failure, becausesendPushreturns at the configure gate. Web Push is dead for the process lifetime after one transient failure.
Both origins are priority/high with verified laptop probes. The shared dependency already states the intended bound: src/secrets.ts caches a null for only 5 minutes precisely so failures heal — the consumers widen that to forever.
The rule that fixes this already exists in this repo, stated for one app: #2668 recorded for pm-dispatch that "memoized init caches success only (clear-on-reject, in-flight promise still shared)", generalising fetchGithubJwks' success-only cache; its instance #2663 has since closed. The aggregator never adopted the rule. This design generalises it.
Full design doc: files/designs/code-success-only-memoization.md in the keep-the-four-repos-healthy goal workspace. Audit baseline: origin/main 064ce9e5 (all three latch sites read verbatim at HEAD this pass).
The contract
A lazy initializer memoizes success, never failure. Concurrent callers share the one in-flight attempt; once that attempt settles, a failure — a rejection, or a fail-soft failure value (
null,false) — leaves no memo behind, so the next caller retries. A success is memoized for the process lifetime unless the module already has an explicit invalidation path, which stays. Per-call fail-soft behaviour is unchanged: the contract removes the permanence, not the posture.
Conforming embodiments already in-tree: secrets.ts (5-minute null cache + forgetBwsSecret), dispatch/src/auth.ts fetchGithubJwks (success-only), aggregator server.ts priming (self-clearing + 60s gate), sources/live.ts TTL caches (assigned only on success), bin/antigravity-direct.mjs token promise (cleared in finally).
Recorded decisions
- Clear on settle, not a consumer-side failure TTL. The shared dependency already bounds retry cost (secrets' 5-minute null cache means a retry re-reads a map, not bws); a second per-site TTL delays healing past the designed bound by an arbitrary per-site number.
- Every layer clears independently (stacked-latch rule). aggregator(alerts): getSupabaseClient and getAlertsStore both memoize a transient null forever — the alerts ledger stays unavailable until restart #3732's evidence: fixing the client alone does not heal the store. "The layer below retries" is never a reason for a layer to keep a failure.
- Failure values count as failure. All three sites fail soft (
.catch(() => null/false)), so the success test is on the resolved value: non-null client/store,trueconfigure. - In-flight sharing preserved. Clear at settle (a
finally/settle handler), never before — concurrent first callers still trigger exactly one resolution. - Existing invalidation stays. Push's 401/403 reset +
forgetBwsSecretremain the rotation path; no timers, reset endpoints, or in-call retry loops are added. Retry drivers already exist: the tick, the next request, the next send.
Alternatives that lose (in the doc): per-site failure TTLs; retry-with-backoff inside the initializer; an operator reset surface; repo-wide scope from day one (unexamined sites get dispositions in Phase 3, not presumed fixes).
Phased breakdown
- Phase 1 — the alerts ledger heals (adopted: aggregator(alerts): getSupabaseClient and getAlertsStore both memoize a transient null forever — the alerts ledger stays unavailable until restart #3732). Contract applied at both layers (
getSupabaseClient,getAlertsStore), fail-once-then-succeed tests per layer plus the stacked case. Lands first — every alerts surface, and Design: delivered means delivered — one truth contract for the aggregator alert throat #3702's throat contract, sits on this store. - Phase 2 — push configuration heals (adopted: aggregator(push): ensurePushConfigured memoizes a failed VAPID configure forever — one transient bws failure disables Web Push for the process lifetime #3729).
ensurePushConfiguredclears onfalse; the 401/403 reset preserved verbatim; probe-shaped test. Independent of Phase 1 in code. - Phase 3 — closing inventory + contract statement (new issue, gated on Phases 1–2). Contract into
pm-stack/aggregator/README.md+ pointer comments at the three sites; a repo-wide promise-memo inventory with a disposition per site (known candidates:bin/muse-proxy.mjscliCapsPromise,pkgs/id/webhighlighterPromise; expected-conforming sites verified, not re-fixed); a cross-note on design(pm-dispatch): launch pipeline, staged — validate the whole request at the boundary, one commit point, nothing after it can fail the launch #2668 that its rule is now the aggregator's contract too.
Open questions stay in the design doc (supabase client token rotation — no invalidation path, no failure evidence yet; genuinely-absent config cost, bounded by the secrets cache and recorded acceptable; Go/Rust sync.Once latches — no filed instance).
Related: #2668 (the rule's first statement), #3702 (neighbouring throat contract — different mechanism, no phase overlap). Not this design: #3701 (batch scheduling), the settings-kind numeric cluster (pass 32's trigger has not fired).
🤖 Design architect pass 33 (2026-11-01), audit at origin/main 064ce9e5.
Activity
dezren39 commentedon Nov 1, 2026
🤖 Agent note · design filed
files/designs/code-success-only-memoization.md(keep-the-four-repos-healthy goal workspace); central register entry added (status: proposed). Phases linked as sub-issues: Phase 1 = aggregator(alerts): getSupabaseClient and getAlertsStore both memoize a transient null forever — the alerts ledger stays unavailable until restart #3732 (adopted), Phase 2 = aggregator(push): ensurePushConfigured memoizes a failed VAPID configure forever — one transient bws failure disables Web Push for the process lifetime #3729 (adopted), Phase 3 = Phase 3 (audit) of #3879 — closing promise-memo inventory + contract statement #3880 (new closing inventory + contract statement, gated on Phases 1–2).29 remaining items
dezren39 commentedon Nov 5, 2026
Program-tending parent update (tracking issue #1352): sub-issue #3732 closed 2026-11-05T00:41Z — the first of this design epic's three phases to land. Progress: 1 of 3 sub-issues closed; #3729 and #3880 remain open. The epic's thread (last active 2026-11-04) predates the closure, so this note records it. Nothing in the closure indicates a blocker for the remaining phases.
dezren39 commentedon Nov 7, 2026
🤖 Agent note · Phase 3 closing inventory for design #3879 (from #3880)
pm-stack/aggregator/README.md, pointer comments at the three memo sites, and the one confirmed latch fix below).9640974a133d11af7f837eae7105a825e0746f7c.Grep patterns used (reproducible)
Over
pm-stack/,bin/,pkgs/(*.ts/*.tsx/*.js/*.mjs, tests and node_modules excluded):(let|const|var) <name>[Pp]romise … Promise<…> | nulldeclarations*Promise/*promise[:=])??=memo assignmentsInventory — disposition per site
pm-stack/aggregator/src/supabase.ts—clientPromisenullor rejects; success cached; in-flight shared. Pointer comment added in PR #6796.pm-stack/aggregator/src/alerts.ts—storePromisenulland on rejection (the stacked-latch rule). Pointer comment added in PR #6796.pm-stack/aggregator/src/push.ts—configurePromisefalse; the send-path 401/403 reset is the explicit invalidation path and stays. Pointer comment added in PR #6796.pm-stack/aggregator/src/secrets.ts— value cacheforgetBwsSecretinvalidation. This is the bound every consumer's retry cost inherits.pm-stack/aggregator/src/server.ts—priming ??=(ensureProvenancePrimed)priming = nullafter the try/catch) and is additionally gated by a 60 s timestamp.pm-stack/aggregator/src/sources/live.ts—cookieCache(1 h),laptopCache(45 s)pm-stack/dispatch/src/auth.ts—fetchGithubJwkspm-stack/dispatch/src/queue.ts—workerEnsured ??=(ensureWorker)nullbefore rethrowing (#2902); a successful registration stays cached.pm-stack/aggregator/src/render.ts—window.__dashIdentitiesPromise.catch.pkgs/id/worker-poc/src/worker.ts—guestPromiseloadPromisefamily —comments.ts,actions.ts,events.ts,roadmap-annotations.ts,activity.ts,views.tsdoLoadis fail-loud by design under the #2980 recovery shape (quarantine + resolve, or a deliberate "refusing to continue" throw). The family's one historical failure-latch (roadmap-annotations non-ENOENT) was #4200, already fixed through that recovery shape.pkgs/id/web/src/highlight.ts—highlighterPromisecreateHighlighterCore(e.g. a transient chunk-load failure) latched for the page lifetime. The memo now clears on rejection; success stays cached, in-flight sharing preserved.bin/muse-proxy.mjs—cliCapsPromise94dcd63fef("muse-direct goes native: retire muse-proxy", opencode#1825). Nothing remains to disposition.bin/antigravity-direct.mjs—_tokenPromise487e22d8b0(opencode#1414). While it lived it was conforming (cleared in afinally).All other
??=hits in scope are plain value assignments, not promise memos.Part (c) — supabase token-rotation disposition
Stays evidence-gated; no reset implemented. Fresh check 2026-11-07: no issue records a Supabase management-token revocation/rotation failure, and the only
forgetBwsSecretcaller in the tree is still push's 401/403 path.getSupabaseClientstill captures the token at build time and a successful client is memoized for the process lifetime, so the gap the design named is real but untriggered. Per the design's own terms ("on evidence, add aforget-style reset analogous to push's 401/403 path"), the trigger to revisit is a 401 from the management API surfacing in aggregator operation — none has.developing-today commentedon Nov 7, 2026
Picked up by
repo-worker-code-b— working this now. (Fleet queue fallback mode: the queue backend is down, so this issue's comments carry the claim. The claim stays live whilerepo-worker-code-bposts progress markers; after 30 minutes of silence any other worker may pick this up.)developing-today commentedon Nov 7, 2026
Progress from
repo-worker-code-b: merge_ready candidate: phases 1/2 closed; phase 3 PR #6796 diff verified by this lane (highlight.ts clear-on-reject fix + contract docs + pointer comments; no Codex/review findings exist); CI pending - polling briefly, else park per 5-min ruledeveloping-today commentedon Nov 7, 2026
Released by
repo-worker-code-b(outcome: done). Triaged + landed (fallback, lane code-b): all phases of design #3879 are now complete - Phase 1 #3732 CLOSED, Phase 2 #3729 CLOSED, Phase 3 #3880 inventory posted by lane code-f and its PR #6796 MERGED as ed2534c by this lane (diff verified line-by-line: highlight.ts clear-on-reject fix, README contract statement, 3 pointer comments; no Codex/review findings; basic-tier CI still pending fleet-wide after the 5-min poll window, none failed, none touching this diff - merged under standing merge authority). #3880 closed with evidence. status/blocked + status/in-progress removed. #3879 stays OPEN as the design record; part (c) supabase token-rotation remains evidence-gated per code-f's recorded disposition (revisit trigger: a management-API 401 in aggregator operation).dezren39 commentedon Nov 7, 2026
Program-tending completion proposal (2026-11-07): all 3 sub-issues of this design epic are now closed (#3732, #3729 closed 2026-11-07T03:56Z, #3880 closed 2026-11-07T06:02Z), verified via the sub-issues API this run. (This supersedes the 2026-11-05 progress note, which recorded 1/3.)
Proposing closure of the epic: its success-only memoization contract phases have all landed. If any acceptance item in the epic body remains unmet despite the closed children, note it here and the epic can stay open with that remainder named.
dezren39 commentedon Nov 8, 2026
Partial verification — staying open (2026-11-07 done-task-only audit, tracking: #7016).
Queue task(s) [8284] were marked done, and some work did land:
No merged change closes this issue's full scope, so the done status is not treated as completion. Remainder re-queued as triage task 11680.
dezren39 commentedon Nov 8, 2026
🤖 Agent note · handed off