Skip to content

IdempotencyGuard: crash between side effect and record() still duplicates on retry #1

Description

@impartshadow

IdempotencyGuard.record() appends to the ledger after the side effect returns. A crash in the window between the external call succeeding and the ledger append leaves no evidence of the call, so the retry re-executes it — the exact duplicate the contract exists to prevent.

The window is small but it is the highest-consequence one: it is open precisely while the payment/email/webhook is in flight.

Fix is a pending/terminal split:

  1. Before the external call, append a pending record keyed on the same key_for(ctx).
  2. After it returns, append a terminal record with the result, linked by the same key.
  3. check_pre blocks on either. A pending hit means "a prior attempt reached the external system and we do not know the outcome" — which must block re-execution and surface for reconciliation, not silently replay a result we never recorded.

Point 3 is the part that matters: a pending hit is not the same as a terminal hit, and collapsing them either duplicates the effect or fabricates a result. Violation.recovery should say which case it is.

Prior art for the same split: idempotency-ref-v1 in giskard09/argentum-core (docs/spec/idempotency-ref.md), which separates the pre-call reservation from the post-call anchor for this reason.

Surfaced while answering the durability question in crewAIInc/crewAI#5802. Relates to FM-037.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions