Every sibling of the dedupe marker that posted 96+ ops cards into a paying customer's Slack — which of them share the defect, which can reach a human, and which are being fixed.
2026-10-25 · dock tour-alert-flood row #4 (marker-fail-open-family) · census complete, fixes in flight
The tour marker was ordinary in shape and special in position. Eight more fail-open sites share its exact defect, and three of them can text or email a customer's own residents — but none sits where the flood happened, so each fails open once per event rather than once per call.
The whole census is below, folded by section — open what you need. Written for anyone picking up one of the slice PRs that links here.
On 2026-10-24 a read-only Clara Bench replay posted at least 96 identical ops cards into a live customer's Slack in 46 seconds. The cause was not a sick database: claimTourCalendarAlert's conditional write was refused before it reached DynamoDB by a request-scoped read-only guard, which threw a plain Error. The marker's classifier tested err.name === 'ConditionalCheckFailedException' exactly, so a refusal fell through to a bare return true fail-open — and because every write inside such a scope is refused, "fail open" stopped meaning "alert once instead of going dark" and started meaning "alert once per call".
That marker was fixed in #22322, which also introduced the typed DdbWriteRefusedError and the isDdbWriteRefused helper. This page is the census of its siblings: it exists because the marker that fired was not the only one with that shape, and it is the evidence behind the slice PRs listed at the bottom.
The lead that opened this row was a grep counting bare return true lines against file-wide log calls. That cannot distinguish a fail-open from any other return true, nor tell whether a log call sits on the path that matters, so it was replaced rather than acted on.
The real instrument masks comments and string bodies at preserved offsets, brace-matches every catch (…) { }, pairs each one with its own try body, and records per site: whether the try issues a conditional write · whether the catch classifies the conditional failure · whether it rethrows · log calls inside the catch (not file-wide) · every return expression. Two masks deliberately: structure is matched with string bodies blanked, semantics with comments only — so a comment mentioning the exception does not count as a classifier, while a string literal inside a comparison does.
Swept src/ agents/ lambda/ — 5,774 files, 6,031 catch blocks, 285 catches paired with a conditional write, 27 matching the shape. Tests, *.test.ts and gitignored lambda/*/dist/ build output excluded.
| Input | Required | Result |
|---|---|---|
ntv-escalation-marker.ts (known true positive) | 2 catches, conditional write, classifies, no rethrow, zero logs in catch | exactly that at L95 and L164 |
Synthetic file: a catch inside a comment; both identifiers in prose and in literals; rethrows | comment catch NOT found; no conditional write; no classifier; rethrow seen | exactly that |
clara-first-email-intent.ts — the grep's top hit (7) | does it have the shape? | 10 conditional writes, ALL rethrow → not the shape |
os.walk on a file argument scanned an empty set, so the positive control first read 0 catches on a file known to have 2. It now exits non-zero rather than reporting a zero when the file set is empty.'ConditionalCheckFailedException' read as not classifying. Hence the two-mask split.ConditionExpression; this repo also conditions through wrapper helpers taking a lowercase conditionExpression, plus attribute_not_exists and transact ConditionCheck. Widening took conditional-write catches from 90 to 285 and candidates from 17 to 27 — and added no new fail-open site.Every classifier in this family is a byte-identical copy of:
(err as { name?: string }).name === 'ConditionalCheckFailedException'
at calendar-provider-page-marker.ts:116, call-team-notice-claim.ts:93, hunt-nopickup-marker.ts:172, rating-ask-tenant-cap.ts:110, ntv-escalation-marker.ts:170, and inline at reply-not-sent-alert-marker.ts:65. Every one reads a guard refusal — or any wrapped error — as a store outage.
Each sink below was traced to the line that performs the send and then verified by hand, not inferred from a filename or a grep count.
| File | Catch | Human reached | Verified sink | Logged? |
|---|---|---|---|---|
agents/clara/lib/email/outbound-dedup.ts | L112 | external prospect / resident — email | property-graph-sender.ts:861 / :1099 | yes |
src/lib/temporal/renewal-receipts.ts | L262 | external resident — SMS | activities/renewal.ts:1064 dispatch(phone, …) | yes |
dynamo/rating-ask-tenant-cap.ts | L76 | external resident — SMS | maintenance/rating.ts:916 | yes |
dynamo/call-team-notice-claim.ts | L212 | customer's PM staff — email + SMS | tell-staff.ts:86, open-matter.ts:741 | no |
dynamo/hunt-nopickup-marker.ts | L124 | customer's PM staff — email | tell-staff.ts:86 | no |
dynamo/ntv-escalation-marker.ts | L95 + L164 | internal ops — Sentry fatal page | platform/logging.ts:293 | no |
dynamo/reply-not-sent-alert-marker.ts | L65 | internal ops — Slack #alerts | notify.ts:557 chat.postMessage | no |
dynamo/calendar-provider-page-marker.ts | L109 | internal ops — Sentry fatal page | platform/logging.ts:293 | no |
Five of the eight fail open with no log line at all, so if it happens an operator cannot tell that it did. That, and not an imminent flood, is the strongest part of the case for fixing them.
check_availability is in the bench door's read-only allowlist and does run for real inside the scope — but its handler, handle-check-unit-availability.ts, holds zero references to findTourSlot or calendar access. It is unit availability. The obvious route is falsified, not merely unchecked.src/lib/domain/clara-bench references the email send or record paths (process-email-record, process-pms-email, acquireSendLock, recordReplyNotSent, sendPropertyEmail). This was the one intersection that would have recreated the flood one module over.lease_states per-minute cron, a Temporal activity worker, the inbound-email Lambda.So these fail open once per event, not once per call. None is a 96-card flood waiting to happen, and the row should not be sold to anyone as one. What remains true is the classifier: any wrapped or re-thrown error still misreads as an outage, and installDynamoMutationGuard() is a second, process-wide refusing door that wraps the onboarding-import dry run and roughly a dozen scripts.
Six files whose fail-open cannot reach a human — the cost is a duplicate internal row, a stale admin-grid pointer, or a UI value. Not worth new state.
dashboard-stats-snapshot.ts — 'error' folds into the same skip branch as 'held'; renders in UI.recording-peaks-cache.ts — returns false on both branches; the value gates nothing.deck/access-log.ts — slide telemetry; null and true both proceed.automation/automation-run.ts — a stale "latest run" pointer in the admin Jobs grid.appfolio/durable-account-budget.ts — void; internal request pacing and a counter.platform/auth/mfa-attempt-throttle.ts — see the adjacent findings below.A census where everything examined is a hit is not trustworthy, so two were required:
clara-first-email-intent.ts — the grep's #1 at 7 hits. It genuinely conditional-writes (10 sites) with the same exact-.name classifier, and is still not in the family for the right reason: every catch does if (!isConditionalCheckFailed(err)) throw err. It rethrows. This is the shape done correctly.
automation/automation-run.ts:194 — a conditional "latest run pointer" write with the identical classifier. Its own comment states the consequence: the automation grid shows an older run. The run row is already persisted; nothing leaves the system.
Also absent from the real census despite grep counts of 2–4: pms-records.ts, organization.ts, lead-received-outbox.ts, pms-mirror-store.ts, pms-write-intent-outbox.ts, website-leads.ts. And the transact-write twin is empty: a conditional transact throws TransactionCanceledException, and the shared helper at helpers.ts:3382 rethrows anything else, so no transact site falls through to a grant.
| Site | Its own header says | Verdict |
|---|---|---|
mfa-attempt-throttle.ts:248 | "Fail-open on storage errors, by choice: a DynamoDB outage must not lock every customer out of their own account. The throttle is a brake on guessing, not an authentication decision" (L43-46) | NOT a defect — killed |
outbound-dedup.ts:112 | "Deliberate availability posture — better to risk a duplicate than silently drop a prospect's reply during a DDB outage" | defect in the CLASSIFIER only; the fix must not reverse the posture |
doorRoute | documented — but the comment overstated what the code does | filed as #22340 |
The MFA case is worth stating plainly because this page previously got it wrong: an outage there removes a rate brake temporarily and never locks an account, never touches enrolment, and never blocks a correct code before the fifth failure. The code check itself is Better Auth's and is unaffected. So it is not an authentication weakening, no issue was filed, and nothing is escalated. It was Q4 = no all along; only the word "weakening" overstated it.
And the sharper point for this family: every one of the eight in-scope sites documents its outage fail-open as a deliberate choice. So the in-scope list is not eight files with a bad posture — it is eight files with a correct, documented posture and a broken classifier. That is precisely why the rule below is "fix the classifier, never the posture", and why a fix that made any of them fail closed on a genuine outage would be reversing a decision somebody made on purpose and wrote down.
Each fix adds one branch before the outage arm, reusing isDdbWriteRefused rather than re-deriving it, and leaves the outage arm's return value untouched while giving it a log line. A refusal needs no bookkeeping to fail closed — nothing was posted, so there is nothing to bound — and no module-level mutable state is introduced: in #22322 a module-level bound outlived the scope it described, carried between test cases, and silently cost a planted-defect control its ability to discriminate at all.
The defect is not “a fail-open”. It is an unclassified case falling through to the loud outcome — and it recurs in whatever is built to fix it.
The census found eight correct postures with one broken classifier: every site's fail-open on a store outage was deliberate and documented; the bug was that an unclassified error reached “alert”.
Then the pull request that closed the family produced three recurrences of the same shape, in our own work:
const _: never exhaustiveness arm, which then immediately surfaced a fifth stale mock that a list of line numbers had missed.exit 1, indistinguishable from the control working.Each was caught — twice by review, once by reading a log that could have been skipped. For any assertion whose expected outcome is failure, the exit code alone is never evidence: name the failing tests, confirm a test-summary line exists at all so you know the suite started, and run the unmodified baseline in the same session — exit 0 on the branch is half the evidence.
One marker end-to-end per PR, each branched off origin/main, never off a sibling.
| # | Slice | Why here | Status |
|---|---|---|---|
| 1 | ntv-escalation-marker.ts | Pattern-setter: the verified control, already tested, plain false suppress value | #22344 — merged · live · exercised by test, not yet by production |
| 2 | reply-not-sent-alert-marker.ts + extract the shared classifier | Second copy would otherwise be written anyway; migrates all existing copies in the same PR | #22353 — merged · live |
| 3 | calendar-provider-page-marker.ts | The one real variation: suppress must stay the full { owns:false, firstSeenAt } object | #22356 — merged · live |
| 4 | call-team-notice-claim.ts + hunt-nopickup-marker.ts | One concern: two guards in series on the same route, same sink | #22358 — merged · live |
| 5 | rating-ask-tenant-cap.ts | External resident SMS; needs a new test file | #22362 — merged · live |
| 6 | outbound-dedup.ts | Most delicate: the file already argues for preferring a missed reply to a duplicate, and the fix must mirror that rather than import a new posture | #22359 — merged · live |
| 7 | renewal-receipts.ts | Separate: returning false hits claimRenewalSend's hard block, which fires a renewal_send_double_block Sentry page — a false statement about a refusal. The branch belongs at that layer. | #22363 — merged · live |
claimReplyNotSentAlert's two callers are asymmetric: lead-outcome.ts:120 checks isTestScope and returns before taking the claim; reply-not-sent.ts:197 has no equivalent, so harness traffic both burns the durable claim and posts to #alerts. Filed as #22339, deliberately not folded into the slice that fixes that marker's classifier. The issue itself notes the asymmetry may be deliberate-but-undocumented, so it wants a human read rather than a reflex copy of the sibling guard.The instrument is a standalone script; point it at the trees and read the shape columns. Re-prove it both ways before trusting any new number — it must flag ntv-escalation-marker.ts's two sites and must not flag a catch that appears inside a comment. A count whose instrument was not challenged in both directions is a statement about the instrument, not about the codebase.