Repository navigation
Conversation
A code send without `from` now tells cdp-api its sender is the project's default (`from.isDefault`). cdp-api keys an Idempotency-Key retry on the sender the caller chose, so a retry after the default sender changed replays the first send instead of answering that the key was reused. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…oute-idempotency Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…oute-idempotency Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…oute-idempotency Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…oute-idempotency Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
🤖 CI report
|
A code send without `from` now tells cdp-api its sender is the project's default (`from.isDefault`). cdp-api keys an Idempotency-Key retry on the sender the caller chose, so a retry after the default sender changed replays the first send instead of answering that the key was reused. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…route-idempotency The scope cut parks local templates. This merge takes the tree of this layer's own commits replayed onto #127167, so the branch drops the parked layers that sat below it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
Closing. Marking a send that uses the default sender changed its idempotency key, so a send accepted before the marker existed and retried after this deploy, or the other way across a rollback, would get 422 instead of a replay. #127129 now keys every code send on the sender Django resolves, so retries replay across deploys. The cost: a retry after the project's default sender changed between a send and its retry gets 422. |
Moved from Silthus#578.
Re-cut on 2026-11-02 onto #127167, after the scope cut parked local templates and sends by key. This layer keeps only what still applies to a code send.
Part of the code-first rebuild of the email send stack; replaces parts of #121867 (one send per idempotency key on
send/email/).Problem
422 idempotency_key_reused, and the first send's answer is lost.Impact
Pays into Grow Workflows through billed
workflows_emailsactions per paying org: a retry with the same key replays instead of failing. In scope: Messaging, email API sends.Merge Danger
Note
🟢 Merge risk: low · two-way door: a revert keys a default-sender send on the resolved sender again
send/email/only, behindworkflows-email-from-code. Template sends keep the template's sender.422once. Code sends are not on master, and the flag is on only for the PostHog organization, so no customer has such a send queued.Changes
fromreplays after the project's default sender changed. Django marks the sender it resolved as the project default (send.from.isDefault), and cdp-api leaves it out of the key.fromkeys on that address, as before.replayedanswer and the422and409refusals are master's, unchanged.How did you test this code?
Test seams:
send/email/through the DRF client, asserting the body posted to cdp-api (test_transactional_email.py).Test rationale: the code-send rows that compare the whole
senddict now expectisDefault: truewithoutfrom, and no marker with one. A marker that is missing, or set on a named sender, fails them.Proofs at
1aad7ab(saved withfleet proof):44eadf8be13from feat(cdp): key a code send on the sender the caller chose #127129. A code send withoutfrompostsfrom.isDefault: true. A send with a namedfromposts no marker. All three sends deliver.isDefault: trueon the no-frompayload differs. The answers and the mail match.27bfa23ee55, which takes code sends and has no marker. All three sends answer202and deliver. Django at feat(messaging): point a sender refusal at the from a send names #127181 against cdp-api44eadf8be13delivers too.Earlier runs at the pre-cut stack
Live run, 2026-10-31. Django at the top of the stack (
3a30f51c529), cdp-api and both CDP workers at feat(cdp): send a template from the workflow version Django graded #127130 (f9abed0841c), mail caught in maildev.fromsender@example.comother@example.com, then resend the same key and bodyreplayed: truewith the same id, one email in totalfromother@example.comreplayed: true, one emailRelease status
Docs update
None.
🤖 Agent context
Autonomy: Human-driven (agent-assisted)
Agent: Claude Code, Claude Opus 5.5 (claude-opus-5-5)
dj3of map Map: send code-managed email templates with one PostHog call Silthus/posthog#461 onto master's idempotency store, on feat(messaging): point a sender refusal at the from a send names #127181 atf3b92ce. One GPT-6.1 Sol review round found nothing.qa-swarm,modernize-pr,tdd,writing-tests,pr-description,writing-pr-descriptions.PIPELINE_REFUSALSdecoderInvolvement: one brief with Michael's shrink decision; one review round (GPT-6.1 Sol) with no findings; QA swarm on 2026-11-01 with one panel round (GPT-6.1 Sol review, Opus 5.5 fresh eyes): 1 finding fixed by running the owed proofs; later changes are merges from below; live, shadow-diff and cross-version proof captured. After the re-cut, one panel round (GPT-6.1 Sol lenses, Opus 5.5 fresh eyes): 1 finding answered with proofs at this head, 2 rejected with reasons.
🤖 Generated with Claude Code