0005 — Outbound send-lock for the inbox_email path

Context

On April 15, a prospect (Perrin Contrivo) received an email from Clara that read:

This is a duplicate of the original lead email — the tour request has already been processed and I've already sent Perrin a reply with available Monday April 13th time slots. No further action needed on this one.

Root cause:

The SES path has one (acquireSendLock/markResponseSent at src/lib/email/process-inbound-email.ts:300-325). The Graph path was never ported.

Decision

Port the send-lock pattern to graphDeliver.

Before calling client.sendReply():

  1. Call acquireSendLock(dedupKey, { ... }) where dedupKey is derived from the RFC 5322 internetMessageId (preserved across Fwd: of the same original message), falling back to s3Key.
  2. On success, call markResponseSent(dedupKey, { ... }).
  3. If acquireSendLock fails (another process already sending or sent), log and skip — do not throw.

Also suppress outbound when the agent's response matches a duplicate-detection pattern. If the reply body includes phrases like "duplicate of the original", "already processed", or "no further action needed", drop the outbound and log. Clara should not be the last line of defense, but if she correctly detects a duplicate, we should not email the prospect our reasoning.

Consequences

Easier

Harder

Follow-up work

Alternatives considered

Dedup at the SQS publish layer by hashing internetMessageId. Would prevent the duplicate from ever reaching Clara. Considered for a follow-up; send-lock is the immediate fix.

Rely on the agent not producing duplicate-detection text. Fragile. The agent got it right most of the time, but "most of the time" once cost a prospect a confusing email.