DEV Community

Mason Delan
Mason Delan

Posted on • Edited on

My AI agent tried to ship a mistake we'd already reverted

Vanishing reasoning between AI sessions

A month ago we added a card_token column to the users table so a background job could retry failed Pro charges. It lasted about two days. Storing card data in your own database drops you into PCI-DSS (the compliance standard that kicks in the moment card data touches your systems), so we pulled it and moved to Stripe-managed payment methods.

Last week the charges started failing again. New Claude Code session, no memory of any of that. Its plan? Add a card_token column to users and retry.

I don't really blame the agent. It had the context the first time and it was right. The problem is that context died when the session closed. That's the part I never see mentioned about building with agents: the code sticks around, the reasoning doesn't. People leave a trail without trying. A commit message, a PR comment, the Slack thread before it. Agents don't, and the prompt that explained everything is gone by morning.

So I built Selvedge to hold onto the reasoning.

What happened the second time

Selvedge is a local MCP server the agent calls as it works. There's a four-line block in our CLAUDE.md that says, roughly: before you touch an entity, check if we've been here before.

$ selvedge prior-attempts users.card_token

  users.card_token
  Prior attempt   28 days ago  (reverted after 2 days)
  Reasoning       Added to store card tokens for one-click retries.
  Outcome         REVERTED — kept card data out of our own DB to stay
                  clear of PCI-DSS scope; moved to Stripe-managed methods.
Enter fullscreen mode Exit fullscreen mode

So it didn't add the column. It charged off_session against the saved Stripe PaymentMethod instead. Charge retried, no card data in our database, done. We paid for that lesson once.

That question — how do you stop an AI coding agent from repeating a mistake it already made and reverted — is the whole reason prior_attempts exists. There's a longer teardown with the raw JSON shapes at selvedge.sh/prior-attempts.

How it works

The agent writes down why live, in the moment, from the same context that made the change. That's the whole trick. A lot of the "git blame for AI" tools take your diff afterward and ask a second model to explain it. That's a guess. It reads well, but you can't really build on it. Selvedge stores what the agent actually meant, in its own words.

It's 8 tools, over MCP and as a plain CLI: prior_attempts, blame, diff and history for reading the past; log_change to record a change; and changeset, search and stale_decisions for working with what's stored. Data sits in a SQLite file under .selvedge/ next to your code. No account, no telemetry, no network calls in the core, and zero LLM calls in that core path (on purpose). Your reasoning text is whatever the agent wrote, stored as-is.

You don't have to be a "real" engineer for this to pay off. If you can run pip install and paste four lines into a file, you're set:

pip install selvedge
selvedge setup
Enter fullscreen mode Exit fullscreen mode

New in v0.3.9

selvedge export --format agent-trace writes your history out as Agent Trace v0.1.0, the open attribution format from Cursor and Cognition AI — and selvedge import reads it back, so your history round-trips instead of being stuck inside Selvedge. Export it, hand it to another tool, import it back. Selvedge does the live capture; the format is shared.

Try it

pip install selvedge && selvedge setup
Enter fullscreen mode Exit fullscreen mode

If you've ever reopened your own AI-built project and thought "wait, why did I do it like this," that's the whole pitch. git blame tells you what changed and when. Selvedge tells you why, after the session and the model that wrote it are both long gone.

selvedge.sh · pip install selvedge · github.com/masondelan/selvedge

I'm building this in the open. If you run it and it breaks, I want to hear about it.

Top comments (39)

Collapse
 
nazar-boyko profile image
Nazar Boyko

Capturing the reasoning live from the same context that made the change, instead of asking a second model to guess at the diff afterward, is the part that makes this worth more than a git-blame wrapper. Here's the wrinkle I keep coming back to. Some reverted decisions stop being wrong. Storing card tokens was right until PCI made it wrong, and if Stripe ever became the thing you moved away from, "reverted 28 days ago" would be steering the agent away from the current best answer. How does a decision get un-retired? A stored "we did X, then reverted, then it became correct again" feels harder to represent than a flat prior attempt, and it's the case that'll bite quietly.

Collapse
 
armorer_labs profile image
Armorer Labs

Exactly. I would treat “reverted” as a decision state with a review predicate, not as a permanent ban.

The durable record probably needs three separate facts: what was tried, why it was rejected in that environment, and what evidence would make the rejection stale. For the card-token case, the stale condition might be “payment provider changed” or “compliance boundary moved,” while the live gate still blocks the old implementation until someone records that new evidence.

That also means the agent should not only ask “was this tried before?” It should ask “what prior decision governs this target, and is the decision still fresh against the current dependency graph?” If the answer is stale, the right behavior is escalation or a new proposal, not silent resurrection.

Disclosure: I work on Armorer Labs. This is close to how we think about run records and recovery: keep the past decision visible, but make freshness and authority explicit so old lessons do not become stale policy.

Collapse
 
masondelan profile image
Mason Delan

That three-fact split is the right decomposition. Today the first two are captured; the "what would make this rejection stale" part only lives in free text. Promoting the stale condition to its own field the gate can evaluate is planned for a following version. And agreed: the right move on stale is escalation with the old record in view, never silent resurrection.

Collapse
 
nazar-boyko profile image
Nazar Boyko

thanks for sharing

Collapse
 
masondelan profile image
Mason Delan

This is the case I worry about most too. Records are append-only, so un-retiring means superseding: you log a new entry on the same entity saying what changed, and the trail reads tried, then reverted, then re-opened, with the why at each step. What Selvedge can't do yet is notice on its own that the world changed. A proper supersede flow with a status on each decision (active or superseded) is planned for a following version.

Collapse
 
alex_spinov profile image
Alexey Spinov

The staleness thread above, Nazar and Armorer, is the right worry, so let me add the cheap layer that sits under it, because one assumption running through the comments is doing too much work: that the revert is invisible unless you maintain a separate decision log or a project-instructions line.

A lot of the time it isn't. The revert is a commit. git log --grep=revert, --diff-filter=D on the path, the diff that dropped the card_token column, all of it is already on disk, greppable, with zero log to keep in sync. So the pre-mutation gate Tae described can read its "prior reverted entry" straight from git history for the exact entity a proposed diff reintroduces, and it works for the whole backlog you reverted before any decision log existed. That is a direct answer to Edu's caching case: the revert commit was invisible to the agent, but it is not invisible to a deterministic check that greps it before the diff lands.

Keeping that read deliberately dumb is the point. It only decides "was this exact entity explicitly reverted," blocks the re-add, and prints the old revert message. Whether the rejection is now stale, PCI moved, the provider changed, is exactly the escalation Armorer described, and it belongs one layer up, on top of the block, not inside it. Cheap gate catches the silent resurrection, the freshness judgment stays a human or a separate authority check.

The honest limit is also Edu's exact case: git only sees entities named in a revert commit. A revert folded into an unrelated "cleanup" commit with no marker is invisible even to the grep, and that is where a live-captured note earns its keep. So I read these as stacked, not competing. Git revert history for the free part, captured reasoning for the folded reverts and the why.

Collapse
 
masondelan profile image
Mason Delan

Stacked, not competing, is exactly how I see it. Git reverts as free deterministic backfill is a great layer under this. I want selvedge import to read them the way it reads Agent Trace: grep the revert commits, seed the records, and let live capture handle the folded reverts and the why. Importing from git history is on the list for a following version if possible.

Collapse
 
alex_spinov profile image
Alexey Spinov

Importing them as seeded records is the right end state, and gate time is the reason: one query surface, no second lookup to keep in sync. One design choice will decide how much authority the seeded slice carries: keep provenance queryable per record. A record seeded from git is re-derivable, anyone can re-run the importer against the repo and diff its output against the store, so drift or tampering is checkable by anyone. A live-captured record is testimony, nothing regenerates it after the fact. That also makes the git slice stronger than the Agent Trace import: a trace is written by the same actor whose future proposals you want to check, while revert commits come from whoever shipped the fix, long before the current session existed. So a gate can put hard blocks on the re-derivable slice and treat testimony as advisory, two trust tiers inside one store.

For that to hold, the importer has to be a pure function of repo state. Key each seeded record to the revert commit sha, so re-runs on a moving repo upsert instead of duplicating, and the slice cannot drift from what git says.

The boundary worth settling before you ship it: the import inherits git's identity model, paths and hunks, while the decision lives at entity level. A reverted column can resurface in a different file, migration versus model versus serializer, and an exact-path seed misses it. Extracting identifiers from the revert diff at import time and indexing on those widens the match without giving up determinism.

And once live capture runs alongside, every new explicit revert lands in both slices. Diffing them gives you a measured miss rate for the capture layer, which is exactly the layer you cannot re-derive and most want a number for.

Thread Thread
 
masondelan profile image
Mason Delan

This is the comment I keep rereading. The import shipped as selvedge import --from-git, keyed on the revert sha so re-runs upsert instead of duplicating. The provenance split is the part I most want to get right — a git-seeded record is re-derivable and a captured one is testimony, so the gate can hard-block on the first and treat the second as advisory. That's on the roadmap, with a --verify that re-derives the slice and diffs it against the store, since that's what actually earns the hard block. The identity boundary you flagged, git thinking in paths and hunks while the decision lives at the entity level, is the real limit: right now only a DROP COLUMN gets seeded at the entity level and everything else falls back to the file. Pulling the identifiers out of the diff with their scope is the fix, same cluster of work. Appreciate you going this deep.

Collapse
 
alexshev profile image
Alex Shev

The reverted-mistake story is exactly why agents need project memory with conflict checks. "I saw this pattern before" is not enough; the agent has to know whether the pattern was kept, reverted, or superseded.

Collapse
 
masondelan profile image
Mason Delan

Right. Kept, reverted, or superseded is the distinction that matters, and it's why every record carries an outcome instead of just a description. "Seen before" without the verdict reads like precedent, which is worse than nothing.

Collapse
 
alexshev profile image
Alex Shev

That outcome field is the part most memory systems skip. A memory item without a verdict can accidentally become permission to repeat the same mistake. I like treating reverted and superseded decisions as first-class records, because the negative memory is often more valuable than the success case.

Collapse
 
theuniverseson profile image
Andrii Krugliak

The fix here isn't better agent memory, it's making the reason survive next to the code. A revert that drops the column but leaves no constraint or comment saying why is an open invitation for the next session to add it right back. We started writing the 'don't do this again' as an artifact the code itself carries, so a fresh agent hits a wall instead of the old idea.

Collapse
 
masondelan profile image
Mason Delan

Agreed, the reason has to travel with the code. That's why the store is a SQLite file in .selvedge/ you can commit with the repo. Having the code itself carry the wall (a comment or a constraint) works with that, not against it. Belt and suspenders.

Collapse
 
sarracin0 profile image
Raffaele Zarrelli

The un-retire case Nazar and Armorer raised is the sharp one, and I think it stays hard as long as the record stores the what (reverted card_token) instead of the constraint that drove it. Key the record on the reason as a constraint (card data in our own DB pulls us into PCI scope) and freshness becomes answerable: is that constraint still true? The day Stripe stops being the answer you do not delete the old record, you supersede it with one that names the changed constraint, so the trail reads reverted, then un-retired, and why, which is the shape Nazar said was hard to represent.

It also covers Dipankar's near-miss: a verdict keyed on the principle still trips when the name drifts from card_token to payment_token, because the match is on the constraint, not the string. Capturing the why live, from the same context that made the change, is the hard part and the right part. The lighter layer on top is giving each captured why a status (active, superseded, expired) and keeping it in plain readable files a human can correct, which is the operating layer I ended up building as cowork-os.

When you log a reverted decision, is the constraint that drove it its own field, or is the reasoning one free-text blob right now?

Collapse
 
masondelan profile image
Mason Delan

Free-text blob today, plus a structured outcome. You're right that making the constraint its own field is what makes freshness answerable, and it catches name drift because the match is on the principle, not the string. A constraint field plus supersede instead of delete is planned for a following version. Will look at how cowork-os handles status.

Collapse
 
sarracin0 profile image
Raffaele Zarrelli

It is a plain status field on each decision record, not a separate service: proposed, active, superseded, or expired, plus an owner and a review date. Nothing gets deleted, a supersede writes a new entry that names the old one and the reason it stopped holding, so the file reads tried, then reverted, then un-retired, and why, close to your shape. The record is keyed on the reason, not the entity name, same split we landed on above, so a rename from card_token to payment_token would not break the match. It is plain markdown in the repo, decisions/decisions_log.md and decisions/open_questions.md, open in cowork-os (github.com/yempik-ai/cowork-os) if you want the actual file shape. What it does not do yet: nothing forces a fresh session to check the log before acting, same gap Rene and Tae raised about CLAUDE.md rules getting read once and then ignored. Today it is convention plus a weekly review pass, not a tool level gate. Did the PreToolUse hook end up living inside Selvedge itself, or is it something each user wires up separately in their own repo?

Thread Thread
 
masondelan profile image
Mason Delan

Both, but the useful answer is which part lives where. The hook itself is inside Selvedge — selvedge-hook is a console script that installs with the package, same as selvedge and selvedge-server. What's per-repo is just the wiring: selvedge setup drops a single PreToolUse entry into the project's .claude/settings.json pointing at that shared binary. So you're not copy-pasting a hook into every repo, and pip install -U upgrades the gate everywhere at once — each repo just carries four lines of config setup manages for you.

And the two things you pushed hardest on, constraint as its own field and supersede instead of delete, both landed in this release. The reason-keyed matching you and cowork-os do is the one I'm still chewing on — Selvedge keys on the entity, and --fuzzy (semantic match on the reasoning) is the overlay for the rename case, but moving the primary key onto the reason itself is a bigger call I'm saving for v0.4.0. Going to read your decisions_log.md before I decide.

Collapse
 
dipankar_sarkar profile image
Dipankar Sarkar

The keyed lookup is the load-bearing part, and it's also where I'd expect the second miss. selvedge prior-attempts users.card_token only fires if the fresh session reaches for that exact entity. But the whole reason context died is that the agent lost the thread, so the retry might land as payment_token, or on a subscriptions.card_ref, and the exact key misses the semantic dupe. That's the worst case, not an edge case: the agent most needs the warning precisely when it has forgotten the canonical name. Feels like recall has to key off the reasoning ('storing card data in our own DB pulls us into PCI scope') rather than the entity string, so a near-miss still trips it. Do you index the reasoning text for retrieval, or is lookup entity-keyed only right now?

Collapse
 
masondelan profile image
Mason Delan

Honest answer: entity-keyed lookup, plus full-text search over the reasoning. So searching "PCI" finds the record even after the column name drifts, but the agent has to think to search, and full-text won't catch a pure synonym. You're right that the rename near-miss is the sharpest gap. The core stays zero-LLM on purpose, so semantic recall would come as an optional local layer. That's planned for a following version if I can keep it lightweight.

Collapse
 
hannune profile image
Tae Kim

The session-memory problem is real, but I've found the prior-decision check also needs to live at the tool level rather than just in documentation: we added a schema-level constraint that fires before any table-mutation tool executes, querying a decision log and blocking if the target column has a prior "reverted" entry. The agent doesn't have to remember to check Selvedge; the tool itself refuses and returns the prior reasoning as context. Moving the gate into the tool definition rather than the prompt means it survives model upgrades and new sessions without any prompt maintenance.

Collapse
 
masondelan profile image
Mason Delan

This is where I want it to go. The CLAUDE.md line is the soft version. A gate at the tool boundary that refuses and returns the prior reasoning survives model upgrades and prompt drift. Rene found the same thing elsewhere in this thread. Shipping a ready-made hook as part of selvedge setup is planned for a following version.

Collapse
 
tecnomanu profile image
Manuel Bruña

This is exactly the failure mode I keep seeing too: the code survives, but the reasoning that produced or rejected it disappears. I like the “before touching an entity, check if we’ve been here before” shape. Feels more durable than trying to stuff everything into one giant prompt.

Collapse
 
masondelan profile image
Mason Delan

Thanks. That shape came from watching the giant prompt version fail. Small lookups at the point of contact have held up much better than one big context dump.

Collapse
 
kartik-nvjk profile image
Kartik N V J K

The line that lands for me is that the code survives but the reasoning behind the revert does not, so git blame tells you what changed and never why you walked it back. A commit that says "remove card_token" reads as cleanup, not as a PCI-DSS boundary you chose on purpose. Have you found a way to capture the negative decisions, the "we tried this and backed it out," without the agent later reading that note as a to-do to re-implement?

Collapse
 
masondelan profile image
Mason Delan

What's prevented it so far is the explicit verdict. A note saying "card_token was removed" invites re-adding. "REVERTED: keeps us out of PCI scope" reads as a wall. Models treat an explicit outcome very differently from a floating note. Not bulletproof, but I haven't caught an agent re-implementing from a verdict yet.

Collapse
 
reneza profile image
René Zander

The four-line CLAUDE.md block is the part I would stress-test first. In production I have watched runtime instructions like "before you touch an entity, check prior-attempts" get skipped a meaningful fraction of the time: the agent reads CLAUDE.md once at session start, and by the time it is mid-plan its trained default (add the column, retry) outweighs a soft rule. Storing the reasoning solves recall, but the trigger to consult it is still a probabilistic instruction, not a guarantee. What made this reliable for me was moving the check out of CLAUDE.md into a PreToolUse hook that intercepts schema and migration edits and blocks until prior_attempts has been queried, so the lookup fires deterministically instead of when the model happens to reach for it. Then Selvedge is the source of record and the hook is the thing that forces the read. I wrote up why most CLAUDE.md rules get read once and then ignored here: renezander.com/blog/claude-md-rule...

Collapse
 
masondelan profile image
Mason Delan

Matches what I've seen. The CLAUDE.md line works most of the time, which isn't enough. A PreToolUse hook that blocks schema edits until prior-attempts ran is the deterministic version, and it's where setup should land: Selvedge as the record, the hook as the forced read. Planned for a following version. Reading your writeup now.