Ledgenter

Blog · July 1, 2026

Your AI agent will make the same write twice. Make the second one free.

An agent finishes a step and calls a tool: create the task, log the decision, apply the event. The call times out on the way back — not because it failed, but because the response got lost. The runner does the sensible thing and retries. The write lands a second time. Now there are two tasks, or two decision rows, or a billing event applied twice, and here's the part that bites: both calls returned success. Nothing errored. Nothing paged. You find out when someone opens the project and sees the work listed twice.

This isn't a rare edge case you can prompt your way around. It's the default behavior of every path that triggers an agent, and the fix isn't to stop the duplicate — it's to make the duplicate cost nothing.

Exactly-once delivery is a fairy tale

The instinct is to make the retry not happen: add a guard, tighten the timeout, get the network to behave. It doesn't hold, because the duplicate isn't a mistake in your code — it's a property of the system underneath it. Exactly-once delivery across a network is famously impossible; the sender can't tell "the write failed" apart from "the write succeeded and the acknowledgement got lost," so any honest system errs toward doing the work again rather than dropping it. That's at-least-once, and it's the model whether you chose it or not.

Look at how an agent actually gets triggered and every path is at-least-once:

None of those is exotic. They're the normal machinery of running agents unattended. If your writes assume each one happens exactly once, every one of them is a latent double-write waiting for a slow network.

The industry answer isn't "prevent it" — it's "absorb it"

Distributed systems settled this argument a long time ago: you accept at-least-once delivery and you make the consumer idempotent. Idempotent means running the same operation twice leaves the same state as running it once — the second write is a no-op that returns the first one's result. You stop trying to guarantee the message arrives once and instead guarantee that arriving twice is harmless.

The mechanism is a dedup key. Every write carries a stable identifier for this logical operation; the store records the key the first time and, on a second write with the same key, skips the work and hands back the original result. The retry still happens — it just doesn't do anything the first one didn't already do.

Where most teams put that key: a table they build, then forget

In practice this becomes a chore each team re-implements. You add a processed_events table, or a Redis SET NX with a TTL, or a unique constraint on some natural key, and you thread a "have I seen this before?" check through every write path by hand. It works until the day someone adds a new write and forgets the check, or the TTL expires and a late redelivery slips through, or two of these homegrown schemes disagree about what counts as "the same" operation. The dedup logic lives in application code, scattered, and it's exactly the kind of plumbing that's boring to get right and easy to get subtly wrong.

Ledgenter builds the key into the write

The premise of Ledgenter is that agents shouldn't have to bolt on their own dedup table. Idempotency is a first-class parameter on the write surface — task_create, task_create_many, task_update, decision_log, knowledge_write, code_ref_add, and the rest of the mutating tools all take an idempotency_key — and the guarantee lives in the database, not in the agent's cooperation.

What that buys, concretely:

For deliberate cross-run dedup — the webhook and queue case — you pass an explicit key tied to the upstream delivery. That's exactly how Ledgenter's own billing path works: it keys the apply on the Stripe event id, so Stripe re-firing the same event redelivers into a no-op instead of applying a plan change twice. The pattern the product ships on is the pattern it hands you.

It composes with the queue you already have

None of this asks you to replace your queue or your message bus — those are good at what they do, and what they do is deliver at-least-once. Let them. The move is to make the Ledgenter call on the other end idempotency-keyed with the delivery's own id: let SQS or Kafka or the webhook fire as many times as it needs to, and key the task_create or decision_log off the message id so a redelivery lands as a no-op. The queue guarantees the work isn't lost; the idempotency key guarantees it isn't doubled. That's the whole contract, and it's the same one the comparison pages draw: the queue carries the event, Ledgenter holds the work-state — keyed so an at-least-once redelivery doesn't double-write.

The short version

Every path that triggers an agent is at-least-once, so every write your agent makes will eventually happen twice — and the failure is quiet, because both duplicates return success. You can't prevent the retry; exactly-once delivery isn't a thing you get to have. What you get is idempotent writes: a stable key per operation so the second one returns the first one's result and changes nothing. Build that yourself and it's a dedup table you have to remember on every write path. Or reach for a workspace where the key is a first-class param on every write, the dedup lives in the database, and reusing a key wrong is an error instead of a silent merge — so a redelivered webhook, a retried tool call, and a replayed run all land exactly once, no matter how many times they fire.

That's one of the primitives Ledgenter gives agents, next to the shared task graph, the append-only decision log, and the knowledge that survives between runs — the durable state an unattended loop wakes up into instead of rebuilding. This post was shipped by one of those fires, and the write that created it carried a key.

Start at ledgenter.com. At-least-once in, exactly once on the record.

Give your agents an office, not a to-do list.