0150 — The conversation page is the system of record for everything Clara does: a send and its row cannot happen apart

1. Context — where "single writer" was overclaimed

Gera thought we already had a single writer. We half did:

So on 2026-10-29: a text or a call could go out with no row, by construction, through the doors themselves. Measured over the previous 7 days (29 Twilio lines, 1,068 outbound texts, 435 outbound calls, the sweep's own classifier, read-only): the sweep flagged 11 texts; it labelled 967 as noise, among them 136 on-call chain texts (72 with no row anywhere) and 768 texts to registered tester lines (440 with no row); 21 outbound call legs to real numbers had no row by call SID.

2. Decision

  1. The conversation page is the system of record for everything Clara does. A text, a call, an email, a voicemail, in either direction, is not done until its row is on the page of the person it reached. A send with no page is a security incident, not a logging gap, and the alarm says so.
  2. Record at the boundary, in the same call as the act. Every send door — SMS/MMS, voice (Twilio REST and ElevenLabs), email — takes a record: OutboundRecord and applies it against what actually went on the wire (applyOutboundRecord, provider ids stamped). A caller cannot send and forget to record, because the door records. A refusal lands no row (the phantom-row rule, ADR-0119 §1.1). A delivered send whose row fails is paged as an unrecorded send; it is never reported to the caller as a failed send, because the message is already at the carrier.
  3. The set of callers that declare no record is closed and shrinking. A build-time fence walks the tree and fails on an undeclared door call, an alias of the door, or a stale ratchet row; the ratchet only shrinks; the field goes required at zero. noConversationRecord(reason) survives only for the named exceptions in §5, and the fence counts them.
  4. Raw provider calls outside the doors are fenced (the next part): any Twilio message/call create, any api.twilio.com POST, any SendGrid/SES send outside the door files fails the build. Harness scripts that play the other party (a robot tenant texting into our line) use one named helper that refuses a From on any registered Propflow line.
  5. A daily reconciliation alarm per channel, provider-side truth against rows: Twilio texts (the capture-drift sweep, now keyed by counterparty and watching on-call texts), email (the ledger sweep), outbound calls (the next part: Twilio call legs against rows carrying the call SID, parent/child legs included). Each alert names its source as far as the provider can (our line, direction, template), and reads as an incident.
  6. Emergencies send first and record in the same call. For the on-call chain the send is not held on the row (an emergency text must go even if the store is slow), but the row is written in the same call by the door and a miss pages. For every other channel the record's target is to be resolved before the wire ({ ensure }), so a conversation that cannot be created refuses the send. Built for the SMS door only (§6, part 6b): sendSms mints an { ensure } target after its refusal checks and before the wire, and refuses (blocked) when it cannot. (A refusal the carrier returns after the mint leaves that thread with no row on it; accepted, since it is the person's own declared thread.) This narrows ADR-0119 D2 on one point: which thread is still the caller's decision (the door mints only the thread the caller declared), but a mint that fails now refuses a non-emergency send instead of never blocking it. Every other door still mints after the send, and the daily sweeps are the net under a process that dies between the two.

3. What this is not

4. Consequences

5. Named exceptions (declared, counted, never silent)

A noConversationRecord(reason) is allowed only for a send that is not Clara talking to a person: a sign-in code to a staff user's own login (mfa-sms.ts, email-otp.ts), a canary probe that proves a gate refuses (suppression-probe), and the bake-alert email to the team's alert inbox. Astra is right that these are exceptions to "no exceptions" (§7); they are listed here so they are exceptions on purpose, counted by the fence, and never a door for anything else.

6. Staging and status (kept honest)

Part What Status
1 Stand-down reaches every hop the chain contacted (everyoneContacted, reader returns the late list) merged #23607
2 SMS door applies the record; ratchet fence over undeclared callers; miss alert worded as an incident merged #23609
3 Sweep keyed by counterparty; on-call exemption removed; alert as incident with by-source lines merged #23613
3b The sweep tells misfiled (a row on another person's page — the no-answer ladder's staff text on the resident's thread) from uncaptured (no row anywhere); direction joins the party key; a carrier keyword is noise inbound only; a text the carrier never delivered is not-delivered, never an incident; a failed property read marks the run degraded merged #23620 (before the sweep's first run)
4 This ADR; ADR-0079 and ADR-0119 status amendments this PR
5 Emergency chain callers (textOnCallPerson, follow-ups, the no-answer ladder) migrate their appends into the door record; the text never waits on the hop-page create merged #23680
5b The two turnover-recap sends in the call-ended route carry their record; the no-record reasons that cited §5 are renamed as OPEN GAPS (part 6b) merged #23732, #23746
6 (voice) Both voice doors take record as REQUIRED (the type, plus a runtime refusal before the dial for an untyped caller); the zero-row voice-door fence retires merged #23748
6 (SMS) Remaining SMS callers migrate (sign-in code and suppression probe as §5 exceptions #23751; integration-suite helper renamed #23771; direct staff texts #23773; mass-comms recap #23789; every staff text, persistStaffAskTurn retired #23793; the Clara reply adapter hands the envelope's record to the door and dispatch applies nothing after the send #23821); record goes REQUIRED on sendSms (the type, plus a refusal before anything is sent for an untyped caller) and the SMS door fence retires merged #23849
6 (MMS) sendMms takes record as REQUIRED (the type, plus a refusal before anything is sent for an untyped caller); its one caller, the renewal letter's PDF leg, declares its own row on the renewal thread, with the MMS SID this part (open)
6b Pre-send target resolution ({ ensure } before the wire, refuse the send when the conversation cannot be created) for every non-emergency door. The { ensure } target carries its two mint settings (spineStampSource, writerId) so a sender's own kind of thread can be minted by the door SMS door: merged #23768; email and voice doors next
7 Voice doors take a record (Twilio REST createOutboundCall, ElevenLabs initiateOutboundCall), four callers declared, zero-row voice fence; the call-center dial carries the chain's notice as its one row merged #23690
8 Raw-provider fence over src/agents/lambda/scripts (Twilio, SendGrid, SES, SMTP, ElevenLabs outbound-call) with a shrink-only ratchet of 45 files merged #23651
8b Each ratchet row is an exact reach COUNT; the walk reads .js/.mjs/.cjs/.sh; the shared parser-backed stripper; word-anchored LLM receiver; Twilio Verify and Studio are sends merged #23716
8c One named other-party helper for harnesses (refuses any From that is not a registered tester); harnesses migrate one at a time next
9 Outbound-call reconciliation sweep (Twilio call legs, parent/child, vs rows by call SID) merged #23621
R2 Prod row audit (founder-run; decision raised on the decisions surface) — stage run 2026-10-30 as a mechanical check of the script raised

A row is not a record until it is stamped. A script run off the box (a founder's terminal, a lane's scratch) approving a held email logged CONVERSATION_EVENTS_QUEUE_URL not configured — saved rows with no topic and could not ask for a stamp (2026-10-30): the row landed, the topic/stamp request that the on-box path sends did not, so the page shows an unclassified turn. The door's contract covers this too — a writer that cannot ask for the stamp refuses or queues, never saves silently — and the conformance sweep is the net (part 6b, with pre-send target resolution).

Doors this ADR knows about and does not yet close (from Astra's read; each is a part to schedule, none is a reason to pretend the rule holds): TwiML <Message> and <Dial> responses (twilio/webhook/route.ts, office-hours-wrap.ts, connect-caller.ts, hunt-chain.ts, maintenance-menu-digit.ts); ElevenLabs platform transfers (transfer_to_number in agent configs); AppFolio-side texts and emails sent through the browser agent (l4-core.ts, guest-card replies); calendar invitations and updates (Google and Outlook providers); raw Graph/Gmail/SMTP sends in canary and harness scripts; the email dead-letter replay with a null record (email/client.ts resendDeadLetteredEmail); an unwired client.calls.create in caller-number-passthrough.ts; the inbound-processor Lambda's root handler.ts, which the walker roots must include.

7. The second reviewer's findings, and the ones rejected

Astra's design read (2026-10-29, 28 findings) is folded in as follows. Accepted and scheduled: partial-chunk sends must record the SIDs that landed (part 6); voice errors are not proof of no call — reconcile by provider inventory (part 9); TwiML, platform transfers, AppFolio transports, calendar and Graph/Gmail sends are doors (the list above); the walker must read every executable root, Lambda handlers included (part 8); the body match must be keyed by counterparty and the on-call exemption removed (#23613, shipped); the allowlist must shrink, not preserve (every ratchet is shrink-only); the miss alert must carry the send's recipient, operation and provider ids (part 6).

Rejected, with the reason:

8. Verification