Skip to content

hir-124: dedupe replyTriage + createReply on duplicate webhook delivery#16

Open
jaredzwick wants to merge 1 commit intopypesdev:hir-120/warm-reply-triagefrom
jaredzwick:hir-124/dedupe-reply-triage
Open

hir-124: dedupe replyTriage + createReply on duplicate webhook delivery#16
jaredzwick wants to merge 1 commit intopypesdev:hir-120/warm-reply-triagefrom
jaredzwick:hir-124/dedupe-reply-triage

Conversation

@jaredzwick
Copy link
Copy Markdown
Collaborator

Summary

  • Short-circuits recordInboundReply on alreadyReplied so a redelivered Gmail Pub/Sub (or repeated IMAP poll) hit doesn't double-write the reply row, double-bill the LLM triage call, double-cancel a still-scheduled silent follow-up, or duplicate cards in /dashboard/replies. Returns reason: "duplicate_delivery" so callers can log the dedup explicitly.
  • Adds a unique index on reply (contact_id, event_id) (drizzle migration 0009_damp_mulholland_black.sql) as belt-and-braces in case the eventExistsForTracking check ever races. Postgres treats NULL as distinct, so legacy rows with a null event_id won't collide.
  • Test additions: a two-delivery test that asserts triageReply, createReply, createEmailEvent, createReplyFollowup, and incrementCampaignStat each fire exactly once across the two calls. Rewrote the existing "alreadyReplied" test to match the new short-circuit semantics — the old test was implicitly conflating duplicate webhooks with genuine new replies, which is the bug HIR-124 is fixing.

Why this branches off PR #14

This is the follow-up CTO review surfaced on HIR-122 / PR #14. Branching off hir-120/warm-reply-triage so it can land in the same week as v1 of the warm-reply UI without sitting behind a main merge.

Behavior note worth a CTO eye

With this change, a genuine second reply from the same prospect against the same outbound (same tracking_id) will also short-circuit — v1 of the warm-reply UI doesn't model multi-turn replies. That's the correct trade for v1 (the founder will almost always notice the new reply via Gmail UI), but if we want multi-turn we'll need a different dedup key (e.g. message-id, not tracking-id).

Test plan

  • pnpm vitest run tests/int/replyFollowup.int.spec.ts — 14/14 pass
  • pnpm tsc --noEmit -p tsconfig.json — clean
  • pnpm vitest run — 110/110 pass (the one failed file, api.int.spec.ts, is a pre-existing Payload secret-key env issue on the base branch, not introduced here — verified by re-running it from base)
  • CTO review
  • After merge: run pnpm db:migrate against staging to apply 0009_damp_mulholland_black.sql

Gate the warm-reply triage path on the same alreadyReplied check the
event-write already had, so a redelivered Gmail Pub/Sub (or repeated
IMAP poll) hit doesn't double-write the reply row, double-bill the LLM
triage call, or cancel a still-scheduled silent follow-up twice.

Adds a unique index on reply (contact_id, event_id) as a belt-and-braces
DB-layer invariant — Postgres treats NULL as distinct so legacy rows
with a null event_id won't collide.

Test additions: a two-delivery test that asserts triageReply, createReply,
createEmailEvent, createReplyFollowup, and incrementCampaignStat each
fire exactly once across the two calls; and rewrites the existing
"alreadyReplied" test to match the new duplicate_delivery short-circuit
(the old test was implicitly conflating duplicate webhooks with genuine
new replies, which is the bug HIR-124 is fixing).
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