C.W.K.
Stream
Lesson 02 of 04 · published

The Device Turn ID

~12 min · idempotency-key, identity, dedupe, client-generated

Level 0Dead Zone
0 XP0/32 lessons0/12 achievements
0/100 XP to next level100 XP to go0% complete
"The dedupe key has to exist before the first send — which means the client, not the server, has to mint it."

Why the ID Is Born on the Phone

To dedupe retries, every attempt at the same intent must carry the same key. Now ask: when does that key first need to exist? At capture — before the very first send — because the first send might be the one whose acknowledgment gets lost, forcing a retry that needs the matching key. If the server minted the ID, the phone wouldn't have it until a response came back, and a lost response would leave the retry with no key to match. So the device turn ID is minted on the phone, at capture time, and travels with every attempt from the first one onward.

One Key, Written Into the Canonical Log

The device turn ID isn't just a phone-side detail; it's recorded in the canonical log alongside the turn it identifies. That's what makes it a true dedupe key rather than a local guess: the backend persists it, so when a second request arrives bearing an ID it has already seen completed, it can recognize the repeat and return the existing result instead of doing the work again. The key is the shared vocabulary that lets phone and backend agree on "this is the same turn" across an unreliable link.

  • Minted: on the device, at capture, as a UUID — globally unique without coordination.
  • Persisted: in the local outbox and in the canonical log once the turn lands.
  • Carried: on every delivery attempt and every retry of that intent, unchanged.

Stable Identity Is the Whole Trick

The power comes entirely from the ID being stable across attempts. Generate a fresh ID on each retry and you've defeated the purpose — the backend sees three different turns instead of three attempts at one. The discipline is strict: one intent, one ID, for the life of that turn. Mint it once at capture, persist it immediately, and never regenerate it, no matter how many times you resend. Everything the next lessons do — checking before calling, avoiding double-sends — depends on this one identifier staying constant.

Code

Mint once at capture; carry unchanged on every attempt·typescript
// At capture — before ANY network attempt — mint the stable key.
function createTurn(question: string, attachmentIds: string[]): PippaGoTurn {
  return {
    deviceTurnId: crypto.randomUUID(), // minted on-device, once, at capture
    question,
    attachmentIds,
    syncStatus: "pending",
  };
}

// Every delivery attempt sends the SAME id. Retries reuse it; they never mint anew.
async function deliver(turn: PippaGoTurn) {
  return callCanonical({ ...turn, idempotencyKey: turn.deviceTurnId });
}

// WRONG: `idempotencyKey: crypto.randomUUID()` inside deliver() —
// a fresh key per attempt makes three retries look like three turns.

External links

Exercise

Trace a single turn's device turn ID through: capture, first send (ack lost), retry, backend recognition. Write the ID's value at each step and confirm it never changes. Then break it deliberately — regenerate the ID on retry — and describe exactly what the backend now sees and why the user might get charged or answered twice.
Hint
With a stable ID, the backend's second view is 'oh, turn X again — already done, here's the answer.' With a regenerated ID, its second view is 'a brand-new turn Y,' so it does the work again. Same code path, opposite outcome, decided entirely by whether the key stayed constant.

Progress

Progress is local-only — sign in to sync across devices.
Spotted a bug or have feedback on this page?Report an Issue

Comments 0

🔔 Reply notifications (sign in)
Sign inPlease sign in to comment.

No comments yet — be the first.