Idempotency

A caller-supplied idempotency key identifies one intended operation; the execution store prevents that operation from running twice.

Every execution request carries a caller-supplied idempotencyKey. Kaji never generates one itself — the caller decides what “one intended operation” means, and Kaji makes sure that operation only ever runs once.

Claiming

Before authorizing or executing anything, the executor claims the idempotency key in the configured ExecutionStore:

  • No existing claim: the executor proceeds to authorization.
  • An existing claim with the same input fingerprint: the caller (or a concurrent duplicate) receives the same recorded or in-flight outcome — execute does not run again.
  • An existing claim with a different input, capability, or principal: the request settles failed as a conflict. One key can’t represent two different actions.

Claiming happens before authorization deliberately: it’s bookkeeping, not the side effect the authorization boundary protects. That way a denied or rejected request still occupies and settles its idempotency key, so a caller retrying a denied request with the same key sees the same denial instead of racing a fresh claim.

Store Contract

type ExecutionClaim = {
  capability: string;
  principalId: string;
  idempotencyKey: string;
  inputFingerprint: string;
};

type ClaimResult =
  | { status: "claimed"; executionId: string }
  | { status: "existing"; outcome: Promise<StoredExecution> }
  | { status: "conflict"; executionId: string };

type ExecutionStore = {
  claim(claim: ExecutionClaim): Promise<ClaimResult>;
  record(execution: StoredExecution): Promise<void>;
};

claim() must be atomic enough that concurrent claims for the same identity produce exactly one "claimed" result — a check-then-create sequence can let two callers both believe they claimed first, which is exactly the race this contract exists to prevent. record() persists the terminal outcome once it’s known.

memoryStore()

import { memoryStore } from "@irogane/kaji";

const kaji = createKaji({ store: memoryStore() });

memoryStore() is a process-local, in-memory ExecutionStore. It honors the full claim/record contract, which makes it useful for local development, tests, and examples — but it has no external persistence and no durability guarantee. Nothing it records survives a process restart, and it does not coordinate across processes. Kaji v0 does not ship a database store, distributed lock, or recovery worker; a production deployment supplies its own durable ExecutionStore implementation.

Evidence

Every settled execution carries minimal ExecutionEvidence:

type ExecutionEvidence = {
  executionId: string;
  capability: string;
  principalId: string;
  idempotencyKey: string;
  inputFingerprint: string;
};

This is deliberately not a general audit or event-sourcing record — it’s only what’s needed to identify an execution and detect conflicting idempotency key reuse. Kaji does not store arbitrary application events, conversation transcripts, or full payload history.

Further Reading