Duplicate opening private replies after a polling sweep
Summary
When a campaign uses an opening DM, the same Instagram comment can receive the opening private
reply repeatedly on each polling sweep. The worker and diagnostics then report repeated Meta API
errors for the same comment.
This was reproduced against upstream commit 0d723ef with direct Instagram Login and Meta Graph
API v25.0.
Reproduction
- Enable a campaign with an opening DM for a keyword comment.
- Let the comment be processed by the normal comment path and recorded in
DmLog.
- Allow a second polling or redelivered queue job for the same
(automationId, commentId) pair to
overlap the first job.
- Observe the worker logs and
DmLog row.
What happens
The first request can successfully send the opening private reply and the public comment reply.
The overlapping request then tries to send the private reply again. Instagram permits only one
private reply per comment, so the second request returns a generic Meta code 1 or 2 error.
OpenReply updates the existing row to FAILED even when dmSentAt is already populated. The
polling reconciler considers only status = SENT or dmDeliveryUnconfirmed = true to be handled,
so it enqueues the comment again on the next sweep. Because the campaign still has
openingDmEnabled, every reprocessed comment attempts the opening DM again.
Impact
Users can receive repeated opening messages, while the dashboard reports a failure for a message
that may already have been delivered. Repeated retries also create avoidable Meta API errors and
can damage the account's messaging experience.
Proposed fix
- Treat
dmSentAt IS NOT NULL as an authoritative comment-stage delivery signal, independent of
the final status string.
- Atomically claim
(automationId, commentId) before making the external private-reply call.
A unique-create race or a fresh PENDING claim must not send a second request; stale claims
can be reclaimed after a bounded lease.
- Treat Meta code 1 and 2 from the private-reply path as delivery-uncertain and stop automatic
BullMQ retries. The row should remain handled until an operator reviews it.
I have implemented and tested this fix locally. The relevant checks pass with 275 tests and a
production Next.js build. The patch is intentionally small and does not change the opening-DM or
reveal UX.
Duplicate opening private replies after a polling sweep
Summary
When a campaign uses an opening DM, the same Instagram comment can receive the opening private
reply repeatedly on each polling sweep. The worker and diagnostics then report repeated Meta API
errors for the same comment.
This was reproduced against upstream commit
0d723efwith direct Instagram Login and Meta GraphAPI v25.0.
Reproduction
DmLog.(automationId, commentId)pair tooverlap the first job.
DmLogrow.What happens
The first request can successfully send the opening private reply and the public comment reply.
The overlapping request then tries to send the private reply again. Instagram permits only one
private reply per comment, so the second request returns a generic Meta code 1 or 2 error.
OpenReply updates the existing row to
FAILEDeven whendmSentAtis already populated. Thepolling reconciler considers only
status = SENTordmDeliveryUnconfirmed = trueto be handled,so it enqueues the comment again on the next sweep. Because the campaign still has
openingDmEnabled, every reprocessed comment attempts the opening DM again.Impact
Users can receive repeated opening messages, while the dashboard reports a failure for a message
that may already have been delivered. Repeated retries also create avoidable Meta API errors and
can damage the account's messaging experience.
Proposed fix
dmSentAt IS NOT NULLas an authoritative comment-stage delivery signal, independent ofthe final status string.
(automationId, commentId)before making the external private-reply call.A unique-create race or a fresh
PENDINGclaim must not send a second request; stale claimscan be reclaimed after a bounded lease.
BullMQ retries. The row should remain handled until an operator reviews it.
I have implemented and tested this fix locally. The relevant checks pass with 275 tests and a
production Next.js build. The patch is intentionally small and does not change the opening-DM or
reveal UX.