Story-2860

Refused-sender backoff + burst anomaly + alert transport

RFD-AGT-013 T2f-b (D23, CC-41/CC-47; CC-34 retired by Bug-3142) — the Q1 compensating stack that justifies skipping step-up auth (founder posture: contain + detect + accept). Contain: repeat refused senders earn geometrically escalating silence on the Story-2856 attempt ledger. Detect: list+fetch bursts on bound identities trip one deduped, metadata-only alert per bucket. Accept: no step-up, no new blocking — enforcement caps stayed in T2f-a (caps.py, Story-2858). Channel alerts reuse the audit ledger and the scheduled email worker; no parallel refusal or alert ledger was created.

The three layers

Backoff escalation (CC-41)Burst anomaly (CC-47)Alert email transport
Where src/domains/channel/refusal.py — inside the existing zero-amp shield, no fork. src/domains/channel/anomaly.py, hooked after the fetch-token mint (fetch_link.py) and each logical disambiguation listing (disambiguation.py); transport chunks never inflate the count. src/domains/channel/alerts.py + src/workers/channel_alert_digest/.
Mechanism Refusal window × multiplier^min(rate_limited rows in horizon, max_exp). Defaults 1h × 2^k, cap 64×, horizon 24h — quiet for the horizon decays back to base. Counts records the seams already write: channel_delivery_records (kind disambiguation — every menu leaks portfolio shape, A-6) + channel_fetch_tokens, per binding per 15-min window; thresholds 5 lists / 8 fetches. The scheduled worker sends every pending alert email on its next run. Counsel replies dispatch immediately on their existing WhatsApp paths.
Alert row channel_refusal.alerted (2856): known revoked/expired identities retain their historical firm; a truly unregistered sender remains NULL-firm/staff. channel_anomaly.alerted — FIRM-scoped (the firm audit feed / T2k register is the in-app transport), masked number + counts only. channel_alert_digest.completed — an opaque per-recipient audit-sequence cursor. A successful admin advances independently; a failed recipient remains pending.

Escalating silence (CC-41)

flood → cap hit → rate_limited rows land (counted silence, Story-2856)
  next attempt: k = rate_limited rows in 24h horizon
                window = 1h × 2^min(k, 6)
  → a sender who flooded an hour ago is STILL silent (window grew)
  → a sender quiet for the horizon decays to the base window and
    earns the ordinary one reply per window again
  every silenced attempt still lands ONE ledger row + feeds the
  windowed CC-41 alert digest — suppression is counted, never dropped

Alert transport — one immediate email door

Seams write audit rows only (in-app transport: firm audit feed/admin channel register T2k; staff feed for NULL-firm alerts). The schedule-triggered worker sends each firm-scoped alert digest to every active Firm Admin and sends truly unregistered-sender alerts to CHANNEL_ALERT_OPS_EMAIL. Per-recipient high-water marks mean one failed admin retries without duplicating mail to admins who already received it. Bodies contain fixed labels, masked numbers, counts, and timestamps only — never message, matter, or document content.

Infra twin (R2)

Required twin Chore-2961 owns the VPC Lambda shell, EventBridge cadence, DB-secret/SES endpoint paths, the ses:SendEmail grant, and CHANNEL_ALERT_OPS_EMAIL. Functions CI publishes code and the live alias only. Its R4 canary performs a staff-scoped real alert-drain path and sends to the AWS SES mailbox simulator. The deploy also requires an enabled EventBridge schedule targeting :live; any missing capability stays red.

LIVE line — staged burst → alert received

scripts/channel_burst_alert_probe.py fires a signed “list my matters” burst from a dedicated bound staging number, then polls ops.audit_log for the firm-scoped channel_anomaly.alerted row and rejects raw-number or unexpected metadata. After Chore-2961 applies, the human ceremony confirms the next scheduled-run admin email lands.

Proofs