Skip to content

Design: failure is not a result — one success-only memoization contract for lazy initializers (aggregator first) #3879

Open
3 of 3 issues completed

Description

@dezren39

Summary

Three lazy initializers in the aggregator memoize their first failure forever, defeating the one layer that was designed to heal:

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

  1. 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.
  2. 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.
  3. 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, true configure.
  4. In-flight sharing preserved. Clear at settle (a finally/settle handler), never before — concurrent first callers still trigger exactly one resolution.
  5. Existing invalidation stays. Push's 401/403 reset + forgetBwsSecret remain 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

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

  1. added
    priority/normalOrdinary priority (the default)
    epicMeta-issue grouping multiple items (org Project Type=Epic)
    effort/lLarge: multi-day or open-ended; expect staged check-ins
    app/aggregatorPackage/app: aggregator (pm-stack in this repo)
    kind/designA design proposal (design-architect): multi-step, phased, decision-bearing
    on Nov 1, 2026
  2. dezren39 commented on Nov 1, 2026

    @dezren39
    MemberAuthor

    🤖 Agent note · design filed

  3. added
    queue/queuedHas a queued task in the Supabase agent queue
    task/triageQueue packet kind: verify, reproduce, and route this item
    on Nov 3, 2026
  4. 29 remaining items

  5. dezren39 commented on Nov 5, 2026

    @dezren39
    MemberAuthor

    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.

  6. dezren39 commented on Nov 7, 2026

    @dezren39
    MemberAuthor

    🤖 Agent note · Phase 3 closing inventory for design #3879 (from #3880)

    Grep patterns used (reproducible)

    Over pm-stack/, bin/, pkgs/ (*.ts/*.tsx/*.js/*.mjs, tests and node_modules excluded):

    1. (let|const|var) <name>[Pp]romise … Promise<…> | null declarations
    2. Promise-named variable assignments (*Promise / *promise [:=])
    3. ??= memo assignments
    4. Name lookups for every candidate the design named

    Inventory — disposition per site

    Site Disposition
    pm-stack/aggregator/src/supabase.ts — clientPromise Conforming — fixed in Phase 1 (#3732): clears when an attempt resolves null or rejects; success cached; in-flight shared. Pointer comment added in PR #6796.
    pm-stack/aggregator/src/alerts.ts — storePromise Conforming — fixed in Phase 1 (#3732): clears independently of the client layer on null and on rejection (the stacked-latch rule). Pointer comment added in PR #6796.
    pm-stack/aggregator/src/push.ts — configurePromise Conforming — fixed in Phase 2 (#3729): clears when an attempt settles false; 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 cache Conforming — not a promise memo: 5-minute TTL on success and failure alike, plus forgetBwsSecret invalidation. This is the bound every consumer's retry cost inherits.
    pm-stack/aggregator/src/server.ts — priming ??= (ensureProvenancePrimed) Conforming — clears itself on settle (priming = null after 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) Conforming — value TTL caches assigned only on success.
    pm-stack/dispatch/src/auth.ts — fetchGithubJwks Conforming — the rule's origin: cache written only after a successful fetch; failures throw and are never stored (1 h TTL).
    pm-stack/dispatch/src/queue.ts — workerEnsured ??= (ensureWorker) Conforming — the #2668 rule's instance: the catch resets the memo to null before rethrowing (#2902); a successful registration stays cached.
    pm-stack/aggregator/src/render.ts — window.__dashIdentitiesPromise Conforming — clears itself in its .catch.
    pkgs/id/worker-poc/src/worker.ts — guestPromise Conforming — memo is nulled on its settle/reset paths.
    Aggregator store loadPromise family — comments.ts, actions.ts, events.ts, roadmap-annotations.ts, activity.ts, views.ts Deliberately local, with reason — these are load-once state initializations (the #1509 race fix), not lazy service initializers: the in-memory state IS the load result, and re-loading would re-read stale disk over live state. doLoad is 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 — highlighterPromise CONFIRMED same-shape latch → FIXED in PR #6796 — a rejected createHighlighterCore (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 — cliCapsPromise No longer in the tree — muse-proxy was retired in 94dcd63fef ("muse-direct goes native: retire muse-proxy", opencode#1825). Nothing remains to disposition.
    bin/antigravity-direct.mjs — _tokenPromise No longer in the tree — retired in 487e22d8b0 (opencode#1414). While it lived it was conforming (cleared in a finally).

    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 forgetBwsSecret caller in the tree is still push's 401/403 path. getSupabaseClient still 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 a forget-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.

  7. developing-today commented on Nov 7, 2026

    @developing-today
    Contributor

    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 while repo-worker-code-b posts progress markers; after 30 minutes of silence any other worker may pick this up.)

  8. developing-today commented on Nov 7, 2026

    @developing-today
    Contributor

    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 rule

  9. developing-today commented on Nov 7, 2026

    @developing-today
    Contributor

    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).

  10. dezren39 commented on Nov 7, 2026

    @dezren39
    MemberAuthor

    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.

  11. dezren39 commented on Nov 8, 2026

    @dezren39
    MemberAuthor

    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.

  12. dezren39 commented on Nov 8, 2026

    @dezren39
    MemberAuthor

    🤖 Agent note · handed off

  13. added
    work/assignedQueue worker lifecycle state
    queue/parkedQueue task is parked (waiting on CI, time, or an event)
    status/parkedDeliberately paused with a recorded resume condition
    on Nov 8, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

app/aggregatorPackage/app: aggregator (pm-stack in this repo)app/pm-stackpm-stack (Pad/Grist) in developing-today/codeeffort/lLarge: multi-day or open-ended; expect staged check-insepicMeta-issue grouping multiple items (org Project Type=Epic)kind/designA design proposal (design-architect): multi-step, phased, decision-bearingpriority/normalOrdinary priority (the default)queue/parkedQueue task is parked (waiting on CI, time, or an event)source/design-architectFiled by the repo design architect jobstatus/parkedDeliberately paused with a recorded resume conditiontask/triageQueue packet kind: verify, reproduce, and route this itemwork/assignedQueue worker lifecycle state

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions