Oban worker that performs the per-recipient outbound delivery for a system_outbound
message previously inserted by Cairnloop.Outbound.trigger/2 (sealed, Phase 22/23).
Job args shape (Phase 25 expanded — sealed-additive, WR-01)
As of Phase 25, EVERY job enqueued by Cairnloop.Outbound.trigger/2 (including
Phase 24 single-recipient callers) carries the following args map:
%{
"message_id" => integer(), # carried over from Phase 22/23
"conversation_id" => integer(), # ADDED in Phase 25 for dedup
"template_id" => binary(), # ADDED in Phase 25 for dedup
"bulk_envelope_id" => binary() | nil # ADDED in Phase 25; `nil` for Phase 24 callers
}This shape is additively required by D-11 — the three new keys form the Oban
unique: dedup tuple (see "Idempotency" below). Pre-Phase-25 callers enqueued
jobs with args: %{"message_id" => id} only; the new keys are now ALWAYS present,
even on the Phase 24 single-conversation lane (bulk_envelope_id is nil there).
Host-side compatibility note. A host with a custom Oban consumer (e.g., a
bespoke retry policy, an Oban.Telemetry handler that introspects args, or a
notifier that pattern-matches args strictly) will OBSERVE the new keys.
Subset-matching patterns like %{"message_id" => id} = args still succeed (map
pattern matching is subset-based). Hosts that treat the job args as an opaque
pass-through are unaffected.
Idempotency (Phase 25 D-11)
Per-recipient at-most-once delivery is enforced at the Oban job-uniqueness layer:
unique: [
period: :infinity,
fields: [:worker, :args],
keys: [:conversation_id, :template_id, :bulk_envelope_id]
]This is the same unique: shape used by Cairnloop.Workers.ApprovalResumeWorker and
Cairnloop.Workers.ToolExecutionWorker. The three keys form the dedup tuple:
:conversation_id— recipient identity.:template_id— the outbound template that was rendered.:bulk_envelope_id— theCairnloop.Outbound.BulkEnvelopecorrelation key for bulk fan-out (Phase 25). Phase 24 single-conversation callers (e.g.ConversationLive's "Send recovery follow-up" handler) passnilfor this key — Oban treatsnilas a valid dedup value, so two same-template Phase 24 recoveries for the same conversation in quick succession will be deduped. This is the documented desired behavior per D-11 ("at-most-once delivery") and was a locked decision in Phase 25's research Open Question 2.
Backwards compat (Phase 24)
perform/1 pattern-matches only on "message_id". Phase 24 callers that do NOT
include "bulk_envelope_id" (or "conversation_id" / "template_id") in their job
args still run successfully; the missing args are simply unused at perform time.
The dedup keys are only consulted by Oban at INSERT time when constructing the
uniqueness fingerprint — a Phase 24 caller that omits a key behaves identically to
one that passes nil.
Summary
Functions
Callback implementation for Oban.Worker.perform/1.
Functions
Callback implementation for Oban.Worker.perform/1.