Skip to content

Count only real replies, keep our own domains out, write real subject lines - #172

Merged
ralyodio merged 1 commit into
mainfrom
fix/reply-matching-own-domains
Oct 12, 2026
Merged

ralyodio merged 1 commit into
mainfrom
fix/reply-matching-own-domains

Conversation

@ralyodio

Copy link
Copy Markdown
Contributor

Why

Prod numbers (2026-10-12), Anthony's workspace:

  • Replies: 1,247 people emailed, and the only "reply" was 20 Porkbun receipts and renewal notices. They were matched on address alone because Porkbun is both a prospect and our registrar.
  • Own domains: 18 cold emails went to our own products (all before Stop cold-emailing support/abuse desks and the workspace's own products #105). Teammate workspaces still only know their own offering, and own-domain prospects still got cards.
  • Subjects: all 1,310 cold emails had the same subject, "Quick question about {Company}". All 2,395 email drafts have no subject, because the composer was told never to write one.

What

  • receive-email.ts: a human message counts as a reply only if In-Reply-To/References names a Message-ID we sent (actions.external_id). If the reply comes from a colleague's address, it is still attributed to the thread's person. Fallback when headers are stripped: an address we wrote to and a reply subject (Re:/Aw:/SV:/回复…). Bounce and auto-reply filing is unchanged.
  • recipient-guard.ts: ownDomains now covers offerings across the organization, plus members' and sending mailboxes' domains. It skips webmail, shared hosts (github.com, vercel.app…) and RFC 2606 names. New personOnOwnDomain.
  • pipeline.ts: card creation skips people on our own domains, which covers every intake path and every network.
  • composer.ts / draft.ts: email drafts get a model-written subject (2–6 words, about their specific thing). It is checked for grounding, template phrases, fake "Re:", "!", caps, emoji and naming our product. A subject that fails is dropped and the existing default is used.
  • Migration 0067 (sqlite + pg): refiles inbound mail that neither threads to our mail nor has a reply subject as automated. A prod dry run matched exactly the 20 Porkbun rows.

Tests

New tests: the Porkbun receipt, threaded colleague replies, foreign References, threadIds/isReplySubject, org-wide ownDomains, personOnOwnDomain, subject split/drop/fallback, and subjectProblem. Locally: receive-email, recipient-guard, social-intake, composer, draft and composer-reply all pass; typecheck and format are clean. The full suite was left to CI (Postgres), since the local SQLite run is too slow.

Not in this PR

The 694 pending and 187 queued email drafts have no subject and will still use the default until they're redrafted.

🤖 Generated with Claude Code

… lines

Replies: a human message counts only when its In-Reply-To/References names a
Message-ID we sent, or it comes from an address we wrote to under a reply
subject. The only "reply" in 1,247 prod sends was 20 Porkbun receipts matched
on address alone; migration 0067 refiles them as automated.

Own domains: ownDomains spans the organization's offerings, members and
mailboxes (skipping webmail, shared hosts and RFC 2606 names), and card
creation skips people on those domains, so no intake path queues our products.

Subjects: every cold email went out as "Quick question about {Company}"
because the composer was told never to write one. Email drafts now carry a
model-written subject, checked for grounding and shape; a failing one falls
back to the old default.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@ralyodio
ralyodio merged commit 25011ec into main Oct 12, 2026
4 checks passed
@ralyodio ralyodio mentioned this pull request Oct 13, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant