0146 — Team notices and staff replies live on the source conversation
- Status: Accepted
- Date: 2026-10-19
- Deciders: Gera (ruling, 2026-10-19, in chat). Fede for §4, whether and when Clara answers a staff reply (he owns the shared-inbox reply gating design, Gera and Fede meeting, 2026-10-19).
- Related: ADR-0079 (one writer), ADR-0119 (every send declares its record), ADR-0141 §5 (one staff-reply door)
- Plan: ws-emergency-rearchitecture, workstreams C and D
Context
On 2026-10-18 Clara emailed Western Slope's leasing inbox about a leak call. The email went out, and
its body even linked to the conversation. But nobody could show from our side that the team had been
told: the conversation page showed only the call. For companies on the team-notification engine
(Western Slope and JPCO), the engine records the send as a TeamNotice row in its own partition
(src/lib/team-notify/live-deps.ts:48, noConversationRecord), and the page never reads that partition.
#19577 added the thread write only on the pre-engine send path, which those companies no longer use.
The team's replies were lost as well. The copy that matched our notice only closed it
(team-notify/ingest.ts:74-77), and the reply text was written nowhere. A header-less sent copy
was matched by sender address to an unrelated placeholder renter. Clara answered it as that renter,
and a team note went out saying "no leak report appears in this conversation thread", which was
false. #20949 stops staff mail being treated as customer mail and is live in production. The
placeholder's claim on the leasing inbox was retired, and its stale open question closed, on 2026-10-19.
About 40 staff-notice paths exist, and most leave no row on any conversation.
Decision
- Every team notice is a row on its source conversation. Any channel (email, SMS, call, Slack),
written through the ADR-0119 door as
kind: team_noticewith the recipient, channel and provider id, and only when delivered. A notice whose conversation id does not name a real conversation (a PMS id, a tour id) writes nothing and alerts. A conversation is never created for a notice. - Every staff reply is a row on the same conversation. It is a new
team_notice_replykind, matched through the one staff-reply door (ADR-0141), with an idempotency key on the reply's Message-ID or SID, so duplicate copies make one row. A staff reply is never matched to a person by sender address alone. - Every team notice carries the conversation link, built by one helper and always opening the right thread.
- Whether Clara answers a staff reply follows Fede's shared-inbox reply gating design (Fede, 2026-10-19), and it runs in Supervised first. The gate has three layers: is the message for Clara at all (if not, she stays quiet); can she actually help (no "I'll check with the team" replies); and does she reply only when about 95% confident nobody else has handled it. This ADR decides only that a staff reply, and any answer Clara sends, is recorded on the source conversation. The #20949 guards stay: staff mail is never a customer turn.
- One team-notice door. The pre-engine send path folds into the one team-notice door
(
sendOrHoldTeamNotice) in the workstream-C PRs, so the thread row is written in one place. No second send path stays alive alongside it. - Every email sent to staff carries a reply tag, including the forward-to-property-manager email
(
handle-forward-to-property-manager.ts:1700, which has none today).
Consequences
- The conversation page becomes the proof of what we told the team and what they said back. Gera asked for this directly.
- A one-off backfill re-files the 24 engine notices missing from their conversations (Western Slope 21,
JPCO 3) from the
TeamNoticerows. It only adds rows, is idempotent, and was approved by Gera on 2026-10-19. Each run still needs its own go-ahead with fresh dry-run counts. Older pre-engine sends can only be recovered from SendGrid, without a body. Numbers come from the read-only scoping in the plan (prod reads, 2026-10-19). - Slack is shown as a read-time "Posted in #channel" line from
slackTs. Addingslackto the message channel union is deferred. - How Clara answers staff, and when, is Fede's gating design; it starts in Supervised so nothing she writes to a customer's team goes out unreviewed.
Alternatives considered
- Show
TeamNoticerows on the page at read time without writing thread rows. Rejected: there would be two sources of truth for "what was said on this conversation", against ADR-0079. - Let Clara answer staff under conditions set in this ADR. Superseded by Fede's gating design (2026-10-19), which owns that decision.