Skip to content

REQ-C-007: Mutating Commands Accept --idempotency-key

Tier: Command Contract | Priority: P1

Source: §12 Idempotency & Safe Retries

Addresses: Severity: Critical / Token Spend: High / Time: High / Context: Medium


Description

Every command with danger_level: "mutating" or "destructive" MUST accept an --idempotency-key <string> argument. The command MUST use this key to detect and short-circuit duplicate invocations, returning the original result with effect: "noop" and the original response data. When no key is supplied, the framework MAY auto-generate one (deterministic, based on command + args + session) or MUST document that the operation is not deduplication-safe.

A streaming command (one that declares supports_streaming: true, REQ-O-004) is exempt and MUST NOT accept --idempotency-key, in its buffered answer as well as with --stream: a replay returns one stored result, and a stream is a sequence of events with no single result to return. The framework MUST refuse to register a streaming mutating command that accepts the flag. A streaming command is never destructive (REQ-O-004), so the exemption covers only danger_level: "mutating"; every non-streaming mutating or destructive command still MUST accept the key.

The key deduplicates one request; it does not say that a repeat with the same arguments is safe. That property is the command's idempotent declaration (REQ-C-002), which tells an agent to recover a partial failure by rerunning the identical command. A non-streaming command declared idempotent still accepts --idempotency-key.

Acceptance Criteria

  • Invoking a mutating command twice with the same --idempotency-key returns effect: "noop" on the second call
  • The second call's response data matches the first call's response data
  • An auto-generated idempotency key is deterministic for the same command arguments within a session
  • Registering a streaming mutating command that accepts --idempotency-key raises a registration error
  • Registering a non-streaming mutating command without --idempotency-key still raises a registration error

Schema

Types: manifest-response.md · response-envelope.md

The --idempotency-key flag appears in the schema for all mutating and destructive commands except streaming ones.

{
  "flags": {
    "idempotency-key": {
      "type": "string",
      "required": false,
      "description": "Caller-supplied key; duplicate calls with the same key return the original result without re-executing"
    }
  }
}

Wire Format

First call:

$ tool create-order --amount 100 --idempotency-key order-abc123
{
  "ok": true,
  "data": { "effect": "created", "id": 42, "amount": 100 },
  "error": null,
  "warnings": [],
  "meta": { "exit_code": 0, "duration_ms": 91 }
}

Second call with the same key:

$ tool create-order --amount 100 --idempotency-key order-abc123
{
  "ok": true,
  "data": { "effect": "noop", "id": 42, "amount": 100 },
  "error": null,
  "warnings": [],
  "meta": { "exit_code": 0, "duration_ms": 9, "idempotency_hit": true }
}

Example

A mutating command stores the result keyed by the idempotency key and returns it on subsequent calls without re-executing.

register command "create-order":
  danger_level: mutating
  exit_codes:
    SUCCESS  (0): description: "Order created or already existed", retryable: false, side_effects: complete
    ARG_ERROR(2): description: "Invalid amount",                   retryable: false, side_effects: none

  execute(args):
    cached = idempotency_store.get(args.idempotency_key)
    if cached:
      return response(effect="noop", **cached, meta={idempotency_hit: True})
    order = create_order(amount=args.amount)
    idempotency_store.set(args.idempotency_key, {id: order.id, amount: order.amount})
    return response(effect="created", id=order.id, amount=order.amount)

Requirement Tier Relationship
REQ-C-002 C Provides: danger_level: mutating/destructive triggers --idempotency-key requirement
REQ-C-003 C Composes: effect: "noop" is the canonical response value for an idempotency-key hit
REQ-C-002 C Composes: idempotent declares convergence on rerun, which the key does not imply
REQ-F-004 F Wraps: idempotency response uses ResponseEnvelope
REQ-C-001 C Composes: SUCCESS (0) covers both the live execution and the noop case
REQ-O-004 O Specializes: a streaming mutating command takes no --idempotency-key
REQ-C-032 C Specializes: a protocol command takes no --idempotency-key