Skip to content

Reduce duplicate-send risk for ambiguous provider outcomes #101

Description

@patoperpetua

Parent: #6
Depends on: #42

Goal

Close the remaining gap left by #42 / PR #99: when the email provider may already have accepted a message but PostKit did not observe a clear success (timeout, transport drop, or similar), retries must not casually produce a second delivery.

Today the application Idempotency-Key ledger prevents a second successful acknowledgement for the same key and gates internal retries of clear HTTP transient failures (statusCode present). It does not provide provider-side deduplication. Ambiguous outcomes are not retried internally, but a caller retry of the same key after release can still double-deliver if the first POST was accepted.

Background

Raised on PR #99 (CodeRabbit); declined for #42 because that issue forbids new queues/durable orchestration and the current provider has no documented send idempotency key. Track the follow-up here instead of leaving an unresolved PR thread as the only record.

Scope (options to refine before implementation)

Pick one primary approach (or a sequenced combination) in refinement; do not implement all at once without a written decision:

  1. Provider-side deduplication — if/when the outbound provider supports an idempotency or dedupe key, pass the application Idempotency-Key (or a derived stable key) on the provider request and document the contract.
  2. Ambiguous-outcome claim policy — on timeout/transport (no HTTP status), do not release the in-progress claim immediately; keep 409 IDEMPOTENCY_IN_PROGRESS (or a dedicated status) until TTL/reconciliation so callers cannot re-POST blindly. Document caller guidance and ops escape hatches.
  3. Reconciliation / outbox — only if (1) and (2) are insufficient: durable ambiguous state and a later resolve path. If this needs a queue or durable orchestration, document why and stop for human design approval (same constraint spirit as Define timeout policy, failure classification, and safe retry for sends #42).

Also update:

  • docs/architecture/send-timeout-retry.md
  • docs/architecture/send-idempotency.md
  • docs/operations/troubleshooting.md

…so operators can tell replay-safe failures from ambiguous ones.

Out of scope (for the first cut unless refinement says otherwise)

  • Changing the public send API shape beyond what the chosen approach requires
  • Provider-specific DTOs leaking into @singleton-sd/post-kit-types
  • Silent “always safe” claims in docs without an enforceable mechanism

Constraints

  • Public-repo safety: no secrets, customer data, or real tenant identifiers in issues/PRs
  • Prefer the smallest mechanism that materially cuts duplicate risk
  • Do not invent a queue just to look complete — justify any durable orchestration

Acceptance criteria

  • Written decision on approach (1), (2), and/or (3) with trade-offs
  • Ambiguous provider outcomes cannot trigger an automatic second provider POST from PostKit’s internal retry path (already true after Define timeout policy, failure classification, and safe retry for sends #42 — keep covered by regression tests)
  • Caller-retry guidance after ambiguous outcomes is explicit and matches implementation (no “always safe” wording)
  • If provider dedupe is chosen: the provider request carries a stable dedupe key derived from the application idempotency model, with tests
  • If claim-hold is chosen: timeout/transport failures do not release the claim in a way that invites immediate duplicate send; TTL/ops behaviour is documented
  • Docs updated for operators and consumers
  • pnpm -r --if-present run test passes

Agent implementation notes

Do not start coding until this issue is refined to agent-ready (section 4 of docs/github-source-of-truth.md) — especially a single chosen approach. Read apps/api/src/send-delivery.ts, docs/architecture/send-timeout-retry.md, and the PR #99 review thread that declined the heavy lift before planning.

Related: #37 (idempotency ledger), #42 (timeouts / classification / safe internal retry).

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or requestneeds-requirementsGoal, scope, or acceptance criteria are not yet resolved — refinement work, not implementation work

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions