An AI agent can remember a 30-page conversation and still perform the same action twice.
It sends a request. The connection times out. The agent remembers the goal, the plan, and the tool
call—but not whether the outside system changed. So it tries again.
That is not a vector-memory problem. It is an action-receipt problem.
The missing memory layer
“Agent memory” often means conversation history, retrieved documents, or durable project knowledge.
Those are useful, but they answer questions about what the agent knew—not what happened in another
system.
I find it useful to separate four layers:
| Layer | Question it answers | Typical retention |
|---|---|---|
| Context | What did the agent know? | Task-scoped |
| Plan | What did it intend to do? | Until the task is reviewed |
| Attempt | What request did it submit? | Until reconciled |
| Effect | What external change was verified? | Durable audit record |
The first two help reasoning. The last two prevent duplicate emails, repeated publications,
double-created listings, and other expensive “helpful” retries.
More context does not close this gap. A model can recall the exact request and still not know
whether a server committed it before the connection disappeared.
Timeout is not failure
Before submission, failure is simple: nothing was sent, so retrying may be safe.
After submission, failure is ambiguous. A timeout, connection reset, or unreadable response can
mean either:
- the platform never received the request; or
- the platform completed it, but the response never reached the agent.
Treating both cases as “failed” converts a transport problem into a duplicate-action bug.
The operation therefore needs a state that most happy-path workflows omit:
planned -> submitted -> succeeded
\-> rejected
\-> outcome_unknown -> reconciling
\-> succeeded
\-> safe_to_retry
\-> manual_review
outcome_unknown is not an error message to hide. It is durable knowledge about the limit of what
the system can currently prove.
What an action receipt records
A receipt should be written before the external request. Otherwise the precise failure that makes
it valuable can also prevent it from existing.
A small receipt can contain:
{
"operation_id": "20260816T012030Z-1fd54b31a2",
"operation": "articles.create",
"target": "/api/articles",
"state": "submitted",
"intent_fingerprint": "sha256:…",
"submitted_at": "2026-08-16T01:20:30Z",
"external_id": null,
"authentication_recorded": false
}
The fingerprint should be calculated from an allowlisted or redacted representation of the intent,
not from secrets. The receipt needs enough identity to recognize the effect later; it does not need
to become a second credentials store.
This is also different from a log line. Logs describe events. A receipt is an operation record with
a lifecycle. The system updates the same record as its knowledge changes.
I added this to a publishing CLI
I tested the pattern in a small CLI that writes to the DEV API. The CLI already previewed mutations,
required explicit confirmation, wrote a private intent file, and never retried a write after a
network failure.
But the intent file stayed an intent file forever. A successful response did not advance it to
succeeded, and an ambiguous transport failure did not advance it to outcome_unknown. The safety
rule existed in the client, while the durable state lagged behind it.
The corrected shape is deliberately boring:
receipt = record_intent(operation, sanitized_request)
update(receipt, state="submitted", submitted_at=now())
try:
response = send_once()
except ExplicitRejection as error:
update(receipt, state="rejected", error=error.code)
raise
except TransportFailure as error:
update(receipt, state="outcome_unknown", error=error.code)
raise
else:
update(
receipt,
state="succeeded",
external_id=response.id,
completed_at=now(),
)
There is intentionally no retry in that exception path. Tests cover success, explicit rejection,
and ambiguous failure as different receipt states.
One implementation detail mattered more than I expected: state updates should be atomic. Replacing
the receipt through a private temporary file avoids turning a process interruption into half a JSON
document—the audit system creating its own ambiguous evidence.
Reconcile the world, not the agent's story
An unknown outcome is resolved with a read, not another write.
The reconciler should:
- Query the external system by its idempotency key or returned identifier when one exists.
- Otherwise perform a bounded search and match the smallest safe intent fingerprint.
- Mark the operation
succeededif the intended effect exists. - Mark it
safe_to_retryonly when absence is actually provable and the operation permits retry. - Send every remaining case to
manual_review.
This is where API design changes the safety envelope. A platform with idempotency keys and exact
read-after-write lookup is much easier to automate safely than one with neither. When the platform
offers no reliable way to prove absence, “I cannot tell” is the correct answer.
Receipts are evidence, not truth
Receipts solve one narrow problem: what request was attempted, what the transport reported, and
what external effect was later observed.
They do not prove that the request was wise, that the payload was semantically correct, or that the
verification query inspected the right thing. A perfectly maintained receipt can preserve a bad
decision with excellent fidelity.
That means receipts belong beside—not instead of—policy checks, human approval for consequential
actions, semantic validation, and negative controls for the verifier itself.
There are other limits too:
- eventual consistency can make a successful effect temporarily invisible;
- two similar operations may not have a unique fingerprint;
- some APIs expose no idempotency key or stable lookup;
- a crashed process can leave a submitted operation that still needs recovery;
- retention and redaction rules must match the sensitivity of the action.
Those limits are exactly why manual_review belongs in the state machine.
What should an agent be allowed to forget?
Scratch context can expire. Old plans can be archived. Failed approaches can become history.
But an externally visible attempt with an unknown outcome should not be forgotten or summarized
away. Keep it until the world has been reconciled with the agent's intent.
The practical question is not only, “What does the agent remember?”
It is: What can the system prove happened before the agent acts again?
Which external write in your system is hardest to reconcile after a timeout?
Top comments (11)
@polycratia I lean towards adjustments as first-class allocations, acting as explicit compensating transactions.
Versioning the basis with explicit supersession keeps the core logic conceptually clean, but in practice, you want the ledger to reflect when a correction became known just as much as what the new truth is.
Yes, treating adjustments as first-class allocations forces downstream consumers to understand them to maintain the "sum of allocations equals repayment" invariant. But hiding that complexity behind supersession often leads to downstream systems silently holding onto stale truths. If a downstream service is calculating payouts or tax, they absolutely need to see the discrete adjustment event to trigger a recalculation or re-filing, rather than just silently drifting from a magically rewritten history.
It pushes the complexity to the consumer, but the alternative—silently breaking external state because a consumer missed a supersession—is usually a much more dangerous failure mode.
Spot on about the difference between principle and practice with explicit feedback. Relying on someone to manually report a failure means you only hear about the most frustrating ones, and you miss the silent degradation entirely.
The receipt layer changes the game because it makes certainty a mechanical baseline rather than relying on human motivation. The lane split forces the agent to prove its certainty mechanically before it takes the unattended lane. I'd love to hear how it changes your operational metrics once you ship that version.
That's a fantastic point about refunds and the intent fingerprint needing to carry the originating transaction. Treating the originating transaction as part of the idempotency key instead of just the payload is exactly how you prevent that invisible duplicate.
I love the parallel to regulated payments—the idea that
outcome_unknownhas a regulatory clock attached to it makes perfect sense. The stakes are higher, but the required state machine is exactly the same. Thanks for sharing that lesson!That refund case is a strong counterexample to fingerprints built only from amount, payee, and a time window. The originating transaction is the causal identity; without it, a legitimate second refund and an accidental replay are indistinguishable.
I would scope the refund intent around the original transaction ID, refund sequence or reason, amount, currency, recipient, and the originating workflow step. The point that the counterparty is happy either way is especially useful: monitoring may never expose the duplicate, so correctness has to come from the operation identity rather than a complaint. Thanks—this is exactly the kind of failure mode the simplified payment examples miss.
Thanks for reading and for this great breakdown, Mikhail!
That "Verify-On-Read" layer you described is a fantastic parallel on the read side. Refusing to collapse an unproven claim into a false binary is exactly the principle that prevents hallucinated drift.
On the intent fingerprint "trap" case: you've hit on a critical nuance. If the fingerprint is too shallow (e.g. just endpoint + entity ID), two distinct operations in close sequence (like modifying status vs updating metadata on the same record) could collide or match the wrong prior intent. In our design, the fingerprint hashes the canonicalized semantic parameters (normalized payload fields, mutation type, and account namespace) rather than just structural tags. But your suggestion to write a dedicated regression test specifically testing near-duplicate operations with identical targets and distinct payloads is spot on—I'm adding that exact test case to our validation suite.
Regarding atomic writes: we treat torn/uncommitted temporary files as distinct from network transport
outcome_unknown. If an atomic rename never completes (e.g., node crash before replace), the original receipt remains unmodified, and the orphaned temp file triggers an explicit recovery log upon next startup rather than a generic network timeout error. Keeping that distinct helps isolate process crashes from external system unreliability.Really appreciate the thoughtful observations!
Thanks a lot for the insightful comment, Edward!
You're 100% right on the connection to idempotency keys. Framing it as the local half of the classic at-least-once + server-side deduplication contract makes the architectural lineage immediately clear to anyone who has worked with payment ledger systems. When external APIs natively support an idempotency key header, reconciliation is trivial; when they don't, our local intent fingerprinting and readback verification have to do the heavy lifting to bridge that gap safely.
Your second point about audit trails is crucial: when reconciling an
outcome_unknown, we preserve the state transition history (submitted -> unknown -> confirmed_succeeded) in the receipt record rather than overwriting it in place with a naivesucceeded. Knowing that an action lived in an ambiguous state for hours before reconciliation explains potential downstream race conditions or duplicate reviews during post-mortems.That principle—never destroying the audit trail of what was believed to be true while it was happening—is just as vital for agent state as it is for knowledge graphs. Checked out your work on Mnemoverse; great to see others thinking seriously about rigorous state boundaries in agent systems!
This is the write-side mirror of something I've been experimenting with on the read-side. I built a Verify-On-Read layer that challenges memory claims against live git HEAD at retrieval time — VERIFIED/REFUTED/INCONCLUSIVE instead of trusting whatever's stored. Your outcome_unknown is doing structurally the same job for actions: refusing to collapse "I can't prove it happened" into a binary success/fail.
One thing from my experiments that might be relevant to your reconciler design (step 2, "match the smallest safe intent fingerprint"): I hit a version of this on the read side that I didn't expect. My verification anchors were structural (file path, import string, token presence), and a claim got marked VERIFIED just because the token existed somewhere in the codebase — right token, wrong subject. Same shape of bug you'd get if two operations share an intent fingerprint that's precise enough to look unique but isn't precise enough to actually distinguish them. I only closed it by widening the anchor from "token present" to "surrounding context matches" — recall went up 4-11x once the model could see real evidence instead of a bare pattern string. Might be worth a "trap" case in your reconciler tests: two near-duplicate operations, same type, same target, different payload, and see if the fingerprint actually tells them apart.
Also curious about the atomic write detail — you mention swapping the receipt via a temp file so a crash mid-update doesn't leave a half-written JSON. Did you consider making that itself the source of truth for outcome_unknown — i.e., if the reconciler ever finds a receipt in a torn/uncommitted state, does it get treated as its own distinct case, or does it collapse into the same outcome_unknown bucket as a transport failure?
Writing the receipt before the request is the detail that makes this work, and it's the one people skip. Write it after and the exact failure you built it for is the failure that stops it existing.
Your four layers map almost exactly onto what regulated payments forces you to keep, except we're not allowed to treat the last one as optional. outcome_unknown has a regulatory name and a clock attached to it.
To your closing question: the hardest write to reconcile after a timeout is a refund. A payment has a natural fingerprint, amount plus payee plus a narrow window, and duplicates are loud because someone complains. A refund to the same customer for the same amount is a completely plausible genuine second event, the counterparty is happy either way, and nobody reports it. So the intent fingerprint has to carry the originating transaction, not just the transfer details. We learned that the expensive way.
The four-layer split is the useful part here, and the line you draw between Attempt and Effect is the one most agent frameworks never draw at all. Worth saying out loud: those two layers are not "memory" in the sense the category usually means, which is exactly why they get dropped.
One thing that might save your readers a search. Your intent_fingerprint is an idempotency key, and naming it that connects this post to a large body of settled practice. Payment APIs worked this out years ago, and the underlying result is worth knowing on its own: exactly-once delivery across a network is not achievable, so what you actually build is at-least-once delivery plus a server-side dedupe on that key. Your outcome_unknown is the honest local half of that contract. If the external system also accepts the key, reconciliation gets much cheaper than hunting for an external_id afterwards.
The second thing I would add comes from the knowledge side of memory, because the failure mode turns out to be identical. When reconciliation finally resolves an outcome_unknown, keep the ambiguity in the record rather than overwriting the state. A receipt that reads "submitted, unknown for six hours, then confirmed succeeded" answers a question later that a bare "succeeded" cannot: whether the duplicate someone found in the external system came from you. Updating in place is cheaper right up until the audit.
That is the same argument that applies to ordinary agent memory, where replacing a fact instead of closing it destroys the explanation for everything built while it was true. I wrote that one up here rather than repeating it in a comment. The parallel is the interesting part: your Attempt and Effect layers are evidence in the strongest sense, and evidence records earn their keep by staying complete.
Disclosure: I work on Mnemoverse, a memory service, so I think about the knowledge half of this daily. Your half, the one about effects in other systems, is the half my own field keeps ignoring.
Some comments may only be visible to logged-in visitors. Sign in to view all comments.