Repository navigation
Description
Summary
The alert throat (pm-stack/aggregator/src/alerts.ts + push.ts, landed 2026-11-01 in #3676 under the #1254 push design) is the one PM-stack system whose entire value is that Drew can believe it — and within hours of landing, three filings, one per trust boundary, showed its claims about itself going unchecked:
- aggregator(alerts): runAlertTick marks incidents delivered when every push send failed — critical incidents are silently lost, never retried #3685 —
runAlertTickmarked incidents delivered (and escalated) even when every push send failed: a transient VAPID/bws outage permanently swallowed critical incidents, never retried, with the ledger asserting delivery that never happened. Landed during this design's audit (PR fix(aggregator): retry failed incident pushes (#3685) #3690, mergea3acf3b0): both marks now gate onsent > 0, with an injectablePushSenderseam and a warning on all-failed ticks. It is Rule 1's embodiment — cited here, not adopted as a phase (the id(web): detectLanguage returns Object.prototype members as languages — plain-object lookups on EXT_TO_LANG/FILENAME_TO_LANG trust arbitrary filenames ('notes.constructor' returns the Object constructor, '__proto__' returns a prototype object) #2787/meta-exporter: parseSubscriptionQuota's num() reinterprets empty, hex and scientific strings as real measurements — a drifted '' used value exports as a measured 0% window #2789 precedent). - aggregator(alerts): GET /api/push/subscriptions returns every subscription's p256dh + auth secret — unauthenticated, unused by any client, and the /alerts page for the same data redacts them #3693 —
GET /api/push/subscriptionsreturns every subscription's full store row —endpoint,p256dh, and the RFC 8291authsecret — unauthenticated, while the /alerts page for the same data projects the keys away and no client uses the GET. - aggregator(alerts): mergeQuiet accepts out-of-range times (99:99, 24:60) — a malformed quiet window is silently reinterpreted instead of falling back to the default #3686 —
mergeQuietvalidates quiet times by shape (/^\d{2}:\d{2}$/) only, so99:99/24:60are stored and silently reinterpreted as a different suppression window, in the one parser whose established contract is reject-to-default.
Full design doc: files/designs/code-aggregator-alert-throat-contract.md in the keep-the-four-repos-healthy goal workspace.
The contract (stated once, in the alerts.ts module header)
The alert throat's records and responses tell the truth and keep its secrets. A state transition is recorded only when the event it names happened (delivered / escalated require at least one accepted send;
goneis not a send). Subscription key material (p256dh,auth) never leaves the server: store rows are internal, and any HTTP surface projects — the page's projection is the only read shape. Subscription preferences are validated as values, not shapes, and an invalid value falls back per-field to the default — never stored, never reinterpreted.
Recorded decisions
- The unused subscriptions GET is removed, not projected (aggregator(alerts): GET /api/push/subscriptions returns every subscription's p256dh + auth secret — unauthenticated, unused by any client, and the /alerts page for the same data redacts them #3693 allows either). No in-repo client calls it (push-client.js POST/DELETE only; /alerts is server-rendered; the engine reads in-process), and even a projected GET would enumerate every subscription endpoint — the input the unauthenticated DELETE needs. A future consumer re-adds a read returning the page's projected shape; the projection rule is the standing requirement for that day.
- Fixes stay on their own side of the store seam.
listSubscriptions()has two consumers with opposite needs (engine: full rows, in-process; HTTP: no secrets, ever). aggregator(alerts): GET /api/push/subscriptions returns every subscription's p256dh + auth secret — unauthenticated, unused by any client, and the /alerts page for the same data redacts them #3693's fix removes the HTTP surface, never narrows the store read — projecting the store query would breakdeliverToAll. - Quiet validation is per-field value validation at merge time (hour 00–23, minute 00–59, else that field's default) — the parser's existing reject-to-default contract extended to digit-shaped input. Already-stored out-of-range rows are not silently rewritten; their disposition is a required output of Phase 3.
Phased breakdown
- Phase 1 — subscription secrets never leave the server (adopted: aggregator(alerts): GET /api/push/subscriptions returns every subscription's p256dh + auth secret — unauthenticated, unused by any client, and the /alerts page for the same data redacts them #3693). Remove the GET handler on
/api/push/subscriptions; engine reads unchanged; sentinel-row test pins that no subscriptions response containsp256dh/auth; /alerts rendering unchanged. - Phase 2 — quiet prefs validated as clock values (adopted: aggregator(alerts): mergeQuiet accepts out-of-range times (99:99, 24:60) — a malformed quiet window is silently reinterpreted instead of falling back to the default #3686).
mergeQuietrange validation + per-field fallback, tests per the origin's acceptance. Independent of Phase 1. - Phase 3 — closing audit + contract statement (new issue, gated on Phases 1–2). Contract into the
alerts.tsheader; seam check (no HTTP route returns a store row verbatim); required dispositions recorded here with reasons: stored out-of-range quiet rows, the unauthenticated DELETE + ack/close posture (aggregator-wide stance),mergeQuiettz acceptance (fails open on unknown zones),mergeClassesunbounded class keys; closing evidence incl. Rule 1 as-built (PR fix(aggregator): retry failed incident pushes (#3685) #3690) and a note to feat(aggregator): notification engine — class triggers, dedupe, quiet hours, escalation (#1254 slice c) #3317/feat(aggregator): notification measurement — weekly sent-vs-acted-on roll-up (#1254 slice e) #3319 that quiet semantics and delivery state are now contractual.
Open questions stay in the design doc (an aggregator-wide auth model; digest-class state following Rule 1; delivery backoff/dead-letter on operational evidence; sends-ledger retention).
Related: #3676 (the throat build), #1254 (push design), #3317 / #3319 (consumers of quiet + delivery truth), #1260 (pm-stack SLOs — the layer above, not this contract). Not this design: #3681 (chargesAttention shape — #3068/#3161's module), the feed gates (#2576's family).
🤖 Design architect pass 32 (2026-11-01), audit at origin/main a3acf3b0.
Acceptance criteria
- The design names the alert throat's truth contract: what 'delivered' (and each sibling claim) means, which evidence backs it, and what happens when that evidence is absent.
- Every phase of the design is filed as its own implementation issue (or explicitly marked no-action with rationale) and linked from this epic — the design does not sit without an implementation path (protocol §11).
- The already-landed aggregator(alerts): runAlertTick marks incidents delivered when every push send failed — critical incidents are silently lost, never retried #3685 fix is cited as Rule 1's embodiment, not re-adopted as a phase.
Activity
developing-today commentedon Nov 1, 2026
CODE-1506
dezren39 commentedon Nov 1, 2026
🤖 Agent note · design filed
files/designs/code-aggregator-alert-throat-contract.md(keep-the-four-repos-healthy goal workspace); central register entry added (status: proposed). Phases linked as sub-issues: Phase 1 = aggregator(alerts): GET /api/push/subscriptions returns every subscription's p256dh + auth secret — unauthenticated, unused by any client, and the /alerts page for the same data redacts them #3693, Phase 2 = aggregator(alerts): mergeQuiet accepts out-of-range times (99:99, 24:60) — a malformed quiet window is silently reinterpreted instead of falling back to the default #3686, Phase 3 = Phase 3 (audit) of #3702 — alert-throat contract statement, seam check + dispositions #3703 (new closing audit, gated on Phases 1–2). Rule 1 (delivered only on a successful send) is NOT a phase — it landed during the audit via aggregator(alerts): runAlertTick marks incidents delivered when every push send failed — critical incidents are silently lost, never retried #3685 / PR fix(aggregator): retry failed incident pushes (#3685) #3690 and is cited as the embodiment; Phase 3 (audit) of #3702 — alert-throat contract statement, seam check + dispositions #3703 verifies it as-built.listSubscriptionsseam; quiet validation is per-field at merge time, stored-row disposition belongs to Phase 3 (audit) of #3702 — alert-throat contract statement, seam check + dispositions #3703.dezren39 commentedon Nov 1, 2026
Intake review
Fleet monitor intake pass (protocol §5), 2026-11-01.
Related: code#3685 / PR #3690 (Rule 1, landed), code#3693 (subscription secrets exposure), code#3676 / #1254 (the throat's origin design).
Notes: Design epic filed by design-architect with kind/design + app/aggregator but no priority, effort, or acceptance criteria until this pass. It is a design, so the criteria above govern the design's completeness and hand-off, not code.
Risks: Designs without acceptance criteria drift; the §11 risk is a finished design with no implementation issues filed.
Suggested approach: Complete the audit phases, then file/link the implementation issues natively (sub-issues) in the same pass the design is declared done.
dezren39 commentedon Nov 3, 2026
Phase 3 (closing audit) — contract statement, seam check, dispositions
🤖 Agent note · phase complete
Gate (revalidated fresh 2026-11-03): #3693 CLOSED (PR #4413), #3686 CLOSED (PR #4786) → Phase 3 unblocked. #3703 is the audit issue; contract statement is PR #4977.
(a) The contract statement is in the code
pm-stack/aggregator/src/alerts.ts, module header beside the #3676 design notes (PR #4977, comment-only):(b) Seam check — routes and fields reviewed
src/server.ts)GET /api/push/subscriptions(1616–1621).map(toPublicSubscription)— id, endpoint, classes, quiet, user_agent, created_at, last_seen_at, gone_countPOST /api/push/subscriptions(1622–1626)DELETE /api/push/subscriptions(1627–1636){deleted}/{error}POST /api/push/test(1643–1649)POST /api/alerts(1652–1663){incident, created}— Bearer intake token (alertsIntakeAuthorized, 322–331)POST /api/alerts/:id/(ack|close)(1665–1674){incident, transitioned}GET /alerts(1675–1702)endpointas the unsubscribe key (data-unsub+SUBSscript constant).p256dh/authnever rendered.GET /api/alerts.json(1694){incidents, events}verbatimAlertsStore.listSubscriptions()(alerts.ts:330) still returns full rows and only the in-process engine (runAlertTick, 604+) and the server-rendered page consume them; delivery is untouched. Verdict: no HTTP route returns a store row verbatim.One divergence from record: decision 1 said the unused subscriptions GET is removed; #3693 as landed projected it. The secrecy clause holds, but the projected GET still enumerates every endpoint — the input the unauthenticated DELETE needs. Filed as #4983 (remove it; no in-repo client uses it).
(c) The four dispositions, with reasons
inQuietHours(alerts.ts:168) stilltoMin()s the stored string, so a pre-aggregator(alerts): mergeQuiet accepts out-of-range times (99:99, 24:60) — a malformed quiet window is silently reinterpreted instead of falling back to the default #368699:99/24:60silently becomes a different window. Disposition: no silent rewrite; fix at read time (sanitize/fallback) and/or one-off data fix with row ids recorded → aggregator(alerts): sanitize stored out-of-range quiet prefs at read time (#3702 disposition 1) #4978. Reason: rewriting hides that the value was bad, and the design forbids silent reinterpretation — but reading it raw is reinterpretation.DELETE /api/push/subscriptions+ ack/close posture — recorded exposure: anyone with an endpoint can unsubscribe it (and the page hands endpoints out); anyone can ack/close an incident, suppressing a critical escalation; anyone can fan outPOST /api/push/test. Disposition: accepted as the aggregator-wide stance for v1 — no partial auth on one route while the page that supplies the inputs is public, and auth on DELETE alone would breakpush-client.js; the mitigation is the private origin. The shared posture is aggregator(alerts): aggregator-wide auth/CSRF posture for mutating routes (#3702 disposition 2) #4980. Reason: a one-route fix would be security theatre, not a boundary.mergeQuiettz acceptance (fails open on unknown zones) —mergeQuietvalidates the zone by constructingIntl.DateTimeFormatand falls back toAmerica/Chicago(alerts.ts:141–158, test attest/alerts.test.ts:102);inQuietHourscatches an unknown stored zone and returnsfalse= no suppression (alerts.ts:168–186). Disposition: accepted — delivery failing open is the deliberate direction (silently suppressing is the worse failure, and the contract is about not claiming things), and no new invalid zone can be stored. Stored bad zones are covered by aggregator(alerts): sanitize stored out-of-range quiet prefs at read time (#3702 disposition 1) #4978 if we want default-fallback instead.mergeClassesunbounded keys — copies every boolean key (alerts.ts:119), so a client can growclasseswithout bound; unknown keys are semantically inert (classEnabled= on unless explicitfalse; pass-through pinned attest/alerts.test.ts:53). Disposition: accept for v1 — no server-side amplification, one row per subscription, and feat(aggregator): notification engine — class triggers, dedupe, quiet hours, escalation (#1254 slice c) #3317 is about to introduce real classes, so a strict allowlist now would drop them. Bound it once a class registry exists → aggregator(alerts): bound mergeClasses keys to a class registry (#3702 disposition 4) #4981.(d) Closing evidence
a3acf3b0):runAlertTickcallsmarkDelivered/markEscalatedonly whensummary.sent > 0(alerts.ts:604–631);PushSenderis an injected seam (push: PushSender = sendPush); an all-failed tick warns withsent/gone/errorsand leaves the incident pending.goneis counted separately and is never a send.10b34158) — GET/POST projected throughtoPublicSubscription, sentinel test pins the exact serialized key set; aggregator(alerts): mergeQuiet accepts out-of-range times (99:99, 24:60) — a malformed quiet window is silently reinterpreted instead of falling back to the default #3686 (PR fix(aggregator): reject out-of-range quiet-hour times in mergeQuiet (#3686) #4786) — clock-shape and range validation with per-field fallback (test/alerts.test.ts:79–94).bun test pm-stack/aggregator/test/alerts.test.ts→ 34 pass, 0 fail (PR docs(aggregator): state the alert throat's truth contract in alerts.ts (#3703) #4977 head).Phase 3 is complete with PR #4977 merged; #3703 closes with it. Follow-ups from (c)/(b): #4978, #4980, #4981, #4983.
dezren39 commentedon Nov 3, 2026
🤖 Agent note · design complete — closing
All three phases have landed and the Phase 3 audit is recorded above:
/api/push/subscriptionsproject throughtoPublicSubscription; key material never serialised.mergeQuietvalidates clock values with per-field fallback to default.b9e2eaf2); seam check, four dispositions and closing evidence in the comment above.Acceptance criteria met: the contract is named in the code, every phase had its own implementation issue, and Rule 1 is cited as-built (#3685/PR #3690) rather than re-adopted.
Carried forward as named issues, not comments-only: #4978 (stored out-of-range quiet rows), #4980 (aggregator-wide auth/CSRF posture), #4981 (bound
mergeClasseskeys), #4983 (remove the now-projected GET). Open questions remain in the design doc (files/designs/code-aggregator-alert-throat-contract.md).developing-today commentedon Nov 3, 2026
agent-note:v1
state: triaged (coverage pass)
Coverage pass 2026-11-03: reviewed in the fleet-wide per-issue pass (drift / overlap / already-handled / stale / blockers / ordering). Verdict: still live; queued in repo-health-code-pilot as triage at priority 40 (priority/normal). If this is already handled at current main, the triage closes it with evidence; if it needs splitting, the triage files the split.
Found via: coverage pass 2026-11-03
dezren39 commentedon Nov 3, 2026
#4978 production-row disposition (2026-11-03)
A read-only query of the live
muse-dev2alerts.push_subscriptionstable found 1 subscription and no rows with invalidquiet.start/quiet.endclock values or an unrecognizedquiet.tz. No stored rows were rewritten. This fix appliesmergeQuiet()to rows returned bylistSubscriptions(), so any legacy bad preference is defaulted in memory on read while the stored value remains unchanged, as required by decision 3.dezren39 commentedon Nov 4, 2026
#4978 landed
The read-side sanitize for stored quiet preferences is merged: PR #5169 →
d153d51b8.AlertsStore.listSubscriptions()now runs each storedquietthroughmergeQuiet(), so a legacy out-of-rangestart/endor unknowntzis defaulted in memory per field; the stored row is never rewritten (decision 3).99:99,24:60, an unknown zone, valid preferences, and the no-write path (pm-stack/aggregator/test/alerts.test.ts).muse-dev2and no invalid quiet values; nothing was rewritten.