Skip to content

fix(worker): treat Meta code 1 as delivery-unconfirmed, not a plain failure - #70

Open
valentinpanizza wants to merge 1 commit into
diwenne:mainfrom
valentinpanizza:fix/meta-code-1-duplicate-sends
Open

valentinpanizza wants to merge 1 commit into
diwenne:mainfrom
valentinpanizza:fix/meta-code-1-duplicate-sends

Conversation

@valentinpanizza

Copy link
Copy Markdown

What happens

A single commenter can receive the same DM dozens of times.

In production on my instance, 11 people got between 3 and ~40 copies of the same
private reply over a few hours, and one of them messaged me asking me to stop.
DmLog showed one row each, so nothing looked wrong until I checked the worker
logs: 828 failed (attempt N) lines, all the same error.

[DM Worker] Job 849 failed (attempt 3): An unknown error has occurred.
(/v25.0/<ig-id>/messages) [code=1 sub=- type=OAuthException]

Root cause

Meta answers some sends on /messages with the generic code 1 OAuthException
after the DM has already been delivered
. What made me certain it delivers: a user
tapped the button of a reply the worker had marked FAILED 30 seconds earlier —
they cannot tap a button in a message they never received.

Recording that as a plain failure feeds two amplifiers that compound:

  1. Retries. processJob only treats ZernioDeliveryUnconfirmedError as
    unrecoverable, so a MetaApiError is retried up to attempts: 3 with the
    5/15/45-minute backoff. Every retry delivers another copy.

  2. The reconciler. reconcileComments considers a comment handled only when
    its DmLog row is status: "SENT" or dmDeliveryUnconfirmed: true. A FAILED
    row satisfies neither, so every five-minute sweep re-enqueues the same comment
    for the entire COMMENT_POLL_LOOKBACK_HOURS window.

Roughly 30 re-enqueues x 3 attempts each. The damage scales with the lookback
window: I had it at 3 hours, which capped it near 40 copies. At the default 72
hours the same bug would send on the order of 850.

This is also why it is easy to miss in the data — DmLog is unique on
automationId_commentId, so every repeat updates the existing row instead of
inserting one. Row counts look clean; only updatedAt drifting hours past
createdAt gives it away.

The fix

isDeliveryUnconfirmed() now covers Meta code 1 alongside the existing Zernio
case, and is used in both places that matter:

  • the stored dmDeliveryUnconfirmed / publicReplyDeliveryUnconfirmed flags, so
    the sweep's dedup treats the comment as handled;
  • the retry decision in processJob, so the job is not attempted again.

That is what the dmDeliveryUnconfirmed column already exists for, and
processComment honours it (needsDm = !alreadyDmd && !existingLog?.dmDeliveryUnconfirmed),
so an ambiguous send is now recorded once and never repeated.

Left alone deliberately:

  • the postback path already converts any non-confirmed rejection into
    ZernioDeliveryUnconfirmedError, so it is covered;
  • the read-fallback branch returns early on a plain error, which is the safe
    behaviour — routing code 1 through isDeliveryUnconfirmed there would make it
    fall through to the failure path instead.

Trade-off

A code 1 that genuinely did fail now means that person receives no DM and can
comment again, instead of getting more copies of a message they already have. For
a tool whose whole job is DMing strangers, erring toward under-delivery seems
clearly right.

Verification

Deployed on my instance: 0 failed attempts since, and the ~20 already-queued
delayed jobs drained without sending anything, because processComment saw the
flag and skipped them.

@vercel

vercel Bot commented Sep 22, 2026

Copy link
Copy Markdown

@valentinpanizza is attempting to deploy a commit to the diwenne's projects Team on Vercel.

A member of the Team first needs to authorize it.

…ailure

Meta answers some sends on /messages with the generic code 1 OAuthException
*after* the DM has already reached the recipient. Observed in production: a
user tapped the button of a reply this worker had just marked FAILED, 30
seconds earlier.

Logging that as a plain failure caused duplicate sends through two separate
paths that compounded each other:

  * the job was retried up to 3 times (5/15/45 min backoff), and every retry
    delivered another copy to the same inbox;
  * the DmLog row never satisfied the reconciler's handled test
    (status SENT or dmDeliveryUnconfirmed), so each five-minute sweep
    re-enqueued the same comment for the whole lookback window.

Together those sent single recipients dozens of identical DMs, with only one
DmLog row to show for it because the row is updated rather than inserted.

isDeliveryUnconfirmed() now covers code 1 alongside the Zernio case, and is
used both for the stored flag and for the retry decision in processJob, so an
ambiguous send is recorded once and never repeated. The public-reply failure
path gets the same treatment, since the sweep's dedup reads that flag too.

The trade-off is deliberate: a code 1 that really did fail now means the
person receives no DM and can comment again, which is far better than sending
more copies to someone who already received it.
@valentinpanizza
valentinpanizza force-pushed the fix/meta-code-1-duplicate-sends branch from 87fa4e9 to eaf8d69 Compare September 23, 2026 00:11

This branch has not been deployed

No deployments
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