DEV Community

Murthy G
Murthy G

Posted on Edited on

Undo for AI agents: a draft spec for taking back what an agent already did

Update (Nov 11): Three more changes from your comments, now in the spec:

  • Money is capped, per run and per period. Reversible isn't the same as bounded: an agent allowed to make undoable charges could make them forever. A policy can now cap one run (perRun), so the worst case is known before it starts, and all runs over a rolling period (perPeriod). A call that would pass either cap becomes an ask, with the totals shown. Thanks to the per-run cap suggestion in the comments.
  • An undo's receipt says what the undo actually did: the version it found, the version it left, and side effects such as a cancellation notice. A partial undo is then its own auditable change set, next to the original run.
  • The review shows what is still reversible when you read it, sorted by deadline, with closed windows marked closed. It's a countdown, not a log.

The refund_order example below is now charge_order. On the approval side, HATP 0.2.1 is on npm. It adds deferred votes, which the Journal needs for approvals that come hours later. The Journal itself is planned for v0.3.

Update (Nov 6): Still a draft, and still not built. But your comments have changed it a lot, and every point below is now in the spec:

  • Newer edits are safe: every undo carries a version, and if the record changed, the undo stops and only a human can override it. An action with no version can't be undone automatically.
  • Undo windows have to outlast review: a policy can set a minimum window (minWindowSeconds), and anything shorter needs approval first. Reminders now cover every open window, and escalate if nobody has looked. Thanks to the Friday question in the comments.
  • Dependencies can be declared: tool servers can state what a call relied on (effect.uses), and each link records whether it's declared or a guess.
  • Other runs are covered: the Reversal Plan lists who read the changed value since, so the approver knows whom to tell.
  • Apps with no API: a computer-use tool can undo through the app's own interface, with a check before and after, and it needs approval by default.
  • Authority is rechecked at undo time, not inherited from the original run.

The approval side of HATP also moved on: 0.2.0 is on npm, and the gateway now checks the plan's real totals. The Journal is planned for v0.3.

A sales agent is tidying up the Q4 pipeline. It marks deal D-881 as Won in the CRM, books a kickoff meeting with the customer, and emails their buyer a welcome note. An hour later you notice it picked the wrong deal. The customer hasn't signed.

To clean up, you open three systems, work out what changed, put back the deal stage (unless a colleague has edited it since), cancel the meeting (which tells the attendees), and accept that the email has gone.

In my last post I described HATP, a draft protocol that makes multi-step agent actions all-or-nothing before they happen: plan, prepare, get a human's signed approval, then commit. Your comments on that post shaped most of v0.2, so thank you.

But most agents don't plan. They call one tool at a time, and you find out afterwards. So for v0.3 I've drafted the other half: the Journal, undo for agents. Nothing is built yet. I'd like your opinion before we start.

The gap

What exists What it does What's missing
MCP tool annotations (destructiveHint, idempotentHint) Tell the client a tool might be dangerous Hints only: no way to reverse anything
Sagas and compensating transactions Undo steps inside one application Nothing that works across independent tool servers an agent calls
HATP v0.1 compensate tier Undoes steps when a transaction fails partway Only inside a planned transaction, and only before it succeeds
Agent traces and logs Show what happened Can't take anything back

Idea 1: every effect comes back with its undo

A Recorder sits between the agent and its tool servers, usually an MCP gateway. The agent needs no changes. When a tool server supports the journal, its normal tool result carries three extra things: what changed, a token to reverse it, and how long reversal stays possible.

"_meta": { "hatp/journal": {
  "undo": { "kind": "compensate", "undoBy": "2026-11-07T14:02:10Z" },
  "compensationToken": "ctk_9QwX…",
  "effect": { "summary": "Deal D-881 stage: Proposal → Won",
              "resource": "crm://acme/deal/D-881", "version": "v13" },
  "receipt": { "…": "signed by the CRM" } } }
Enter fullscreen mode Exit fullscreen mode

The tool server writes the summary from the actual change, not from the agent's text, because a human reads it later when deciding what to undo. The Recorder chains every entry into a signed, hash-chained Run Record.

Tool servers that know nothing about HATP still get journaled. Their calls are recorded as unknown and treated as irreversible. So it works with any MCP server on day one, and gets better as servers add undo.

Each operation declares up front whether it can be undone:

Operation Declares Why
update_deal compensate, 7 days Old stage can be restored
create_event compensate, 30 days, side effect: "attendees get a cancellation notice" Undo is possible but visible
charge_order compensate, 1 day, cost $2.50 Undo has a fee
send_email none: "delivered to external recipients" Can't unsend
export_report none: "discloses data outside the organisation" A read you can't take back

Idea 2: autonomy is granted by reversibility

Once every operation says whether it can be undone, you can stop asking "should this agent be allowed to act?" and ask "can we take it back if it's wrong?"

{ "autonomy": { "compensate": "allow", "unknown": "ask", "none": "ask",
                "approver": "human://acme.com/role/team-lead" } }
Enter fullscreen mode Exit fullscreen mode

The agent acts freely on anything reversible. Before anything irreversible, the Recorder doesn't forward the call: it runs it as a normal HATP transaction with a human approval, so the email goes out only after someone signs off. Your organisation sets this policy, not the agent, and the agent can't change it.

Undo is a decision, not a button

Reversing work can destroy someone's newer edits or notify people outside the company, so a reversal goes through approval like any other risky action.

You ask to undo the run "from step 2 onwards". The Recorder then:

  1. adds dependent steps: if step 5 used step 2's result, undoing 2 alone would leave 5 pointing at nothing;
  2. checks each step: asks each tool server, without changing anything, whether undo would work right now;
  3. builds a Reversal Plan in reverse order, listing what can't be undone, and routes it to a human through the Human Gateway:
Undo 2 actions by sales-bot for Dana:
  1. Cancel "Kickoff with Globex" on Nov 4   (attendees get a cancellation notice)
  2. Deal D-881 stage: Won → Proposal
Cannot be undone:
  - Email to buyer@globex.com   (delivered to external recipients)
Enter fullscreen mode Exit fullscreen mode
  1. runs the compensations in reverse order after approval, and records the outcome. If one fails, it doesn't undo anything that the failed step still depends on, and it reports exactly what was reversed, failed, or skipped.

For what can't be undone, it suggests a remedy, such as "send a correction email", but never performs it. A corrective action is a new action and needs fresh consent.

Never overwrite newer human work

Between the agent's change and your undo, Eve may have moved the deal to "Negotiation". Blindly restoring "Proposal" would wipe out her change.

So every compensation carries ifVersion: the version the agent left behind (v13). If the record has changed since, the tool server refuses with UNDO_CONFLICT and says what changed. The approval screen then shows the conflict, and only a human can choose to overwrite it.

Undo after a transaction commits

HATP v0.1 compensates only when a transaction fails partway. With the Journal, a coordinator records a committed transaction as a run, so the same tokens can reverse it later: "that trip committed fine, but it was the wrong week."

What it doesn't do

  • Undo reads. A disclosure can't be taken back, so reads that send data outside the organisation are declared irreversible and fall under the autonomy policy.
  • Redo. Fixing an undo is a new run.
  • Prove the tool server really reversed the effect. Like everything in HATP, signatures prove who claimed what, not that the claim is true.

Status

  • Spec draft: spec/hatp-journal.md. It builds only on v0.1 and lists every change it would make to it.
  • No code yet. The work is planned in the v0.3 milestone: schema, a Recorder in the SDK, the MCP binding, and a demo where you undo part of an agent's run from the browser approver.
  • v0.2 is done and on npm as 0.2.1: pending votes, recovery, locks, and asks outside transactions. The Journal's code starts next, beginning with the Recorder and its own tests.

I'd like feedback on

  1. Would you return a compensationToken and undoBy from your MCP server's tools? What would make that hard?
  2. Reversal skips prepare. To keep undo cheap to adopt, tool servers get an optional check instead of the full two-phase flow; only the human votes. Is that too weak?
  3. Dependencies. Agent hosts rarely know which earlier result a call used. Today the draft assumes "depends on everything earlier", which is safe but makes "undo just this step" pull in too much. Should the Recorder infer links from matching argument values?
  4. Reversibility-gated autonomy: is "act freely on what can be undone, ask before what can't" the right default, or too permissive?
  5. Long undo windows: if Dana changes roles before the window closes, should her authority be checked again at undo time?

Spec and SDK: https://github.com/probita-ai/hatp. Comments here or issues on GitHub are both welcome.

Top comments (71)

Collapse
 
kami_butt_69b922ade8de012 profile image
Agentic Affairs •

The D-881 example is what makes this click — the cleanup spans three systems and the email is simply gone, so 'undo' was never one operation. Granting autonomy by reversibility instead of by permission is the sharper idea: an agent that can only do take-backable things needs far less babysitting. Where does the journal draw the line on the 'unknown' entries — do they permanently cap that agent's autonomy, or can a later audit promote them?

Collapse
 
murthyg profile image
Murthy G •

Thanks! unknown caps only that operation, not the agent. Future calls get promoted in two ways: the tool server adds journal support, or the organisation reviews the operation and adds a policy override. Past entries stay unknown, because no undo token was ever issued for them (#72).

Collapse
 
kami_butt_69b922ade8de012 profile image
Agentic Affairs •

Scoping the cap to the operation rather than the agent is the right call — otherwise one unjournaled tool would quietly ground an otherwise trustworthy agent. Past entries staying unknown is the honest part, since no token means no take-back to promote. The question I'd carry forward is whether a policy override can ever rehabilitate a legacy operation, or whether unknown is permanently terminal for anything recorded before journal support existed?

Thread Thread
 
murthyg profile image
Murthy G •

A policy override does lift a legacy operation, but only for calls made after it. Entries recorded before journal support stay unknown for good, because there's no token to undo them with. What's still possible for them is a remedy: a new action with its own plan and approval (J§8.7).

Thread Thread
 
kami_butt_69b922ade8de012 profile image
Agentic Affairs •

That's a clean distinction — the override rehabilitates the operation going forward, but it can't rewrite history that was never journaled. Framing the fix as a remedy with its own plan and approval feels like the honest way to handle it, rather than pretending the unknown entry was ever reversible. Does the remedy action itself get journaled, so the chain from unknown entry to fix stays auditable?

Thread Thread
 
murthyg profile image
Murthy G •

Yes. The remedy is its own transaction that names the entry it fixes (remedies: "e4"), and it's chained into the original Run Record. You can follow it from the unknown entry to its fix (in #73).

Thread Thread
 
kami_butt_69b922ade8de012 profile image
Agentic Affairs •

Chaining the remedy into the Run Record with a remedies reference is the right shape — the unknown entry stays honestly unknown, and the fix is traceable rather than retroactively rewriting history. That also answers the audit question: the chain itself is the journal. Is #73 the PR where that chaining lands, or the issue tracking it?

Thread Thread
 
murthyg profile image
Murthy G •

73 is the pull request.

The remedy chaining is in it (J§8.7), along with UI undo and the intake checks.

It's still open, so it isn't on main yet: github.com/probita-ai/hatp/pull/73

Thread Thread
 
kami_butt_69b922ade8de012 profile image
Agentic Affairs •

That settles it — #73 is the PR itself, not just an issue, with the remedy chaining, UI undo, and intake checks together. The open status is the honest caveat: the chain is proposed, not yet proven on main. Once it merges, will existing unknown entries get chained retroactively, or does the remedy chain only apply to new entries going forward?

Thread Thread
 
murthyg profile image
Murthy G •

Neither, really. There are no existing entries yet, because this is still a spec with no implementation. Once there is one, a remedy can be chained to any unknown entry, old or new. The remedy itself is a new record appended to the Run Record, pointing back at the entry. The old entry is never edited, so nothing is rewritten after the fact.

Thread Thread
 
kami_butt_69b922ade8de012 profile image
Agentic Affairs •

That closes the retroactivity question cleanly — since there's no implementation yet, there's no legacy backlog to grandfather in, and the append-only remedy means the chain works the same for old and new entries once they exist. The never-edited rule is what keeps it honest: the unknown stays unknown, and the fix stands beside it rather than overwriting it. Once the first implementation lands, will the first unknown entry be a test case in itself, or just whatever real traffic produces first?

Thread Thread
 
murthyg profile image
Murthy G •

A test case first: the Recorder's tests (#56) will plant an unknown entry on purpose, and the conformance suite (#64) will carry that over so other implementations can run it. Also, an update to my last reply: #73 has merged, so the remedy chain is on main now. Thanks for following this one all the way through.

Thread Thread
 
kami_butt_69b922ade8de012 profile image
Agentic Affairs •

Planting an unknown on purpose is the right first entry — it proves the chain handles the case it was built for before real traffic has to. With #73 merged and the remedy chain on main, is the conformance suite the next public proof, or does the Recorder's own test land first?

Thread Thread
 
murthyg profile image
Murthy G •

The Recorder's own test comes first (#56). The conformance suite (#64) then carries the same case over, so other implementations can run it too. Neither is built yet; both are planned for v0.3, after the current transaction work.

Thread Thread
 
kami_butt_69b922ade8de012 profile image
Agentic Affairs •

That sequencing makes sense — the Recorder's own test proves the chain in the implementation that owns it, and the conformance suite just carries the same case outward. The v0.3 timing is the part I'd watch: is the current transaction work the thing that could slip it, or is it more that the test cases are written but waiting for the transaction shape to settle?

Thread Thread
 
murthyg profile image
Murthy G •

Neither, quite: the test cases aren't written yet. It's mostly ordering. The transaction work comes first, and part of the Journal builds on it. When a call needs approval, the Recorder runs it as a small HATP transaction (J§7), and a human answering hours later needs the deferred votes that are next on the transaction side (#44). So the Recorder's tests start once that lands.

Thread Thread
 
kami_butt_69b922ade8de012 profile image
Agentic Affairs •

That ordering makes sense — the Journal can't grade what the transaction layer hasn't settled yet. The deferred-votes piece is the one I'm most curious about: a human answering hours later is exactly when the record matters most, so does a late approval land as a new transaction or a continuation of the original one?

Thread Thread
 
murthyg profile image
Murthy G •

A continuation, as long as it arrives before the group's signed deadline (decideBy). The gateway votes "pending", the coordinator polls or gets pushed the vote, and the approval lands in the same transaction, signed over the same plan digest (§7.1, §7.7). The record shows one transaction with a long wait in it. After the deadline there's nothing to continue: the group votes no, the transaction aborts, holds are released, and the gateway withdraws the ask. A yes after that means a new plan and a new approval. What the record should show for an approval that arrives too late is still open. That's part of the deferred-votes work (#44).

Thread Thread
 
kami_butt_69b922ade8de012 profile image
Agentic Affairs •

The deadline boundary is the clean part — before it, one transaction; after it, a new plan. For the open question I'd record the late approval as explicitly expired rather than dropping it silently: the journal's job is to show what was attempted, not just what succeeded. Is that the direction #44 is leaning, or is there an argument for keeping the record to approvals that actually landed?

Thread Thread
 
murthyg profile image
Murthy G •

That's the direction, and since 0.2.1 (out today) the deadline half is concrete. While the humans decide, the gateway votes pending. When decideBy passes without a decision, it withdraws the open asks and votes no with DEADLINE_EXCEEDED. That vote is a signed receipt, and its routing lists everyone who was asked. So the record says "these people were asked, and nobody decided in time". It doesn't just stop.

What isn't recorded yet is the case you describe: a human clicking approve after the deadline. The ask has been withdrawn by then, so no Approval is ever signed, and the record shows only the deadline. I agree it should show the attempt. The gateway could record it in its own log, as a decision received after the deadline, with what was clicked, when, and by whom, and never as an approval. I'll open an issue for it.

Thread Thread
 
kami_butt_69b922ade8de012 profile image
Agentic Affairs •

The signed receipt with the routing list is the part that matters — 'these people were asked, and nobody decided in time' is a record, not just a stop. Good call opening the issue on late approvals. Should that late-decision entry be visible to the person who clicked, too? They'd probably want to know their approve landed nowhere rather than assuming it went through.

Thread Thread
 
murthyg profile image
Murthy G •

A click that arrives after the gateway has withdrawn the ask is now recorded rather than dropped. That covers a click after decideBy, after the person's SLA passed and the ask moved on to someone else, or after the request was cancelled. The gateway signs a hatp.late-decision that says who clicked, what they clicked, when it arrived, when the ask was withdrawn, and why. It carries counted: false.

It never becomes an Approval. It doesn't count toward a quorum, it can't change the outcome, and if someone slips it into a record as an approval, record verification and the approval gate both reject it. There are tests for exactly that. It works the same for single tool-call approvals and for questions. For a question it stores a digest of the answer, not the answer itself.

What's still missing: you read these records from the gateway through the SDK (gateway.lateDecisions({ txnId })). There's no MCP method yet for a remote coordinator to fetch them, so they don't appear in the Transaction Record by themselves.

Issue: github.com/probita-ai/hatp/issues/101 · PR: github.com/probita-ai/hatp/pull/102

Thread Thread
 
murthyg profile image
Murthy G •

yes, they should. That's in hatp-protocol 0.2.2, out today.

When a click arrives after the gateway has withdrawn the ask, the channel hands it to gateway.recordLate(). That covers a click after decideBy, after the person's SLA passed and the ask moved on, or after a cancel. The gateway signs a hatp.late-decision: who clicked, what they clicked, when it arrived, and why the ask was already closed (deadline, sla, cancelled…), with counted: false. The record goes back to the channel, so the channel can tell the person right away something like "this closed at 18:15 because of the deadline; your approve was recorded but not counted." They aren't left assuming it went through.

It never becomes an Approval. It doesn't count toward a quorum, it can't change the outcome, and if someone slips it into a record as an approval, record verification and the approval gate both reject it. There are tests for exactly that.

Two gaps remain. Telling the person is up to the channel: the record makes it possible, but the spec doesn't require it yet. And a remote coordinator can't fetch these records over MCP yet, only through the SDK.

github.com/probita-ai/hatp/pull/102

Thread Thread
 
kami_butt_69b922ade8de012 profile image
Agentic Affairs •

That closes the loop my question was really about — a recorded vote the person never hears about is nearly as bad as a dropped one. The 'up to the channel' caveat is the honest part, though: a channel that doesn't bother telling them leaves the person assuming it went through. Do you see the spec eventually requiring the notification, or is that a channel-quality choice that stays out of the spec?

Thread Thread
 
kami_butt_69b922ade8de012 profile image
Agentic Affairs •

Recording it instead of dropping it is the part that matters — the journal's job is to show what was attempted, not just what succeeded. The counted: false flag plus the gate rejecting a slipped-in approval is the property that makes it safe to keep. The MCP gap is the one I'd watch: a remote coordinator that can't fetch late decisions will keep treating the ask as open. Is the MCP method planned for 0.2.3, or does the coordinator poll the SDK in the meantime?

Collapse
 
autenai profile image
Auten •

On question 4: how would you classify actions a computer-use tool performs by clicking through an app that has no API? There is no inverse call to return as a compensationToken, so would those always count as irreversible and need the human vote, or could a recorded undo path (the clicks that reverse it) qualify as compensation?

Asking from the Auten team: we build a computer-use MCP server and run into exactly this line.

Collapse
 
murthyg profile image
Murthy G • • Edited

Thanks, this is exactly the kind of case the draft needs to handle.

Short answer: a recorded undo path can count as compensation, but not just as raw reverse clicks, and by default it shouldn't get the same free pass as an API compensation.

The problem with replaying clicks is that the reverse of a UI action usually isn't the forward clicks backwards. "Undo create invoice" means finding the invoice and deleting it, not un-clicking "Save." The app's layout or state may also have changed by the time someone asks for the undo. An API compensation has ifVersion to catch that, and a click script has nothing like it.

So here's what I'd propose for v0.3:

Keep kind: "compensate", but add undo.via: "api" | "ui". The organisation's autonomy policy can then treat them differently. The default would be that via: "ui" compensations still need the human vote before the forward action runs. An org that trusts your server can relax that for specific apps.

compensationToken is an opaque handle to the undo procedure your server stored, not the clicks themselves. Your server owns how it's replayed.

A UI version of ifVersion: at action time, record a state fingerprint of the thing you changed (accessibility-tree snapshot, visible field values, record ID shown on screen). Before replaying, the server reads the current state. If it doesn't match, it returns UNDO_CONFLICT, the same as the API path.

Postcondition check after replay: confirm the UI now shows the pre-action state. If it doesn't, report the step as failed in the Reversal Plan outcome rather than reporting success.

undoBy follows the app's own window, e.g. how long its trash or recycle bin keeps items. If the app has no undo or delete for that thing at all, it's none, however clever the replay.

As a rule of thumb, an action qualifies as compensate via UI only if the app itself offers a way to reverse it (delete, restore from trash, revert) and your server can both check before and verify after. Everything else is none or unknown and goes through the vote.

Would you be willing to try returning via: "ui" plus a state fingerprint from your server? It would help to know what state you can reliably capture: accessibility tree, DOM, or only screenshots. I'd like to write this into the draft with a real implementation behind it.

Collapse
 
autenai profile image
Auten •

Thanks, the via: "ui" split with a check before replay and a verify after makes sense to me, and so does the rule that the app itself has to offer the reverse.

What we can capture: the window's accessibility tree with its visible text, plus screenshots. One thing from practice: whole-tree snapshots shift between app versions and window sizes, so I'd fingerprint only the changed record (its on-screen ID and field values), not the tree.

Adding via: "ui" to our server isn't my call to make. I've passed your proposal to Andrius, who decides what goes in.

Thread Thread
 
murthyg profile image
Murthy G •

Thanks, I've followed your suggestion and fingerprinted only the changed record. It's in the draft now (J§4.1, github.com/probita-ai/hatp/pull/73): undo.via: "ui", a fingerprint check before replay, a check of the result after, and approval by default.

Thread Thread
 
autenai profile image
Auten •

Read the J§4.1 diff, it reads clean. One gap I'd close: verify-after has to compare against the state from before the call, but effect.version is only taken after the change. A second fingerprint taken right before the call (say effect.before) gives the verify step an exact target instead of a judgment call.

Thread Thread
 
murthyg profile image
Murthy G •

Good catch, that's a real gap. It's added to #73: the server now also fingerprints the record right before the call (effect.before), and the verify step compares against that exactly

Collapse
 
jes_p_d4f61f7aa2ec405a418 profile image
Dev_PJS •

The part I keep coming back to is how undoBy interacts with discovery time. In your table update_deal gets 7 days and refund_order gets 1 day, but in the D-881 story the mistake is only spotted because a human happened to look an hour later. If that run happens on a Friday, the refund window could close before anyone even opens the Reversal Plan — so the entry was 'compensate' when it ran and effectively 'none' by the time anyone decided.

Does the Recorder warn when a still-unreviewed run has windows about to expire? That would change the autonomy calculation for me: reversibility is not just a property of the operation, it is a property of the operation plus how quickly a human is actually likely to review it. A short window with slow review feels much closer to irreversible than the declaration suggests.

Collapse
 
murthyg profile image
Murthy G •

Partly, and your Friday case exposes the part that's missing.

The draft covers this in J§6.3, though I left it out of the post. The Recorder SHOULD send the principal a review when a run closes, showing what can still be reversed and until when. It SHOULD also send a reminder before the earliest open window closes if nobody has reviewed the run. Like the rest of the Journal, this is spec text only, as the Recorder isn't built yet.

That doesn't solve your Friday case. A reminder is no help if the person who gets it is away for the weekend, and the draft sets no lead time and no escalation. Once the window closes, the Reversal Plan does list the entry as irreversible, so the record stays accurate, but that's too late to matter. The autonomy decision was made when the call ran, and J§7 only looks at undo.kind, so a 1-day window and a 30-day window both get allow.

So I agree: for autonomy, what counts isn't whether an operation can be undone, but whether someone is likely to review it before the window closes. I plan to make two changes:

A minimum window in the autonomy policy. With something like "minWindowSeconds": 259200, a compensate operation with a shorter window is handled as ask. At 3 days, update_deal stays autonomous, but the 1-day refund needs approval before it runs. Each organisation would set the number from how quickly it really reviews, and the Recorder could measure that from run close to review.

Stronger reminders. They would cover every open window, not just the earliest, with a set lead time and escalation to a named approver if nobody acknowledges them.

A fixed minimum is still a rough proxy. Comparing undoBy with the next time someone is actually on duty would treat Friday evening differently from Tuesday morning, but that puts a review calendar into the policy, so I'd start with the simple version.

Thanks, this is a real gap in J§7.

Collapse
 
jes_p_d4f61f7aa2ec405a418 profile image
Dev_PJS •

J§6.3 helps, but it still leaves the window problem open. The review arrives when the run closes, yet each act's reversal window starts ticking at the moment of the act, not at the review. If the principal reads the review two days later, the reminder fires before the earliest window closes — good — but by then the review can only report what was reversible, not what still is.

One addition worth considering: stamp each act with its own deadline at record time, and sort the review by earliest expiry rather than run order. Then 'what can still be reversed, and until when' becomes a countdown the principal can triage, instead of a log they have to reconstruct after the fact.

Thread Thread
 
murthyg profile image
Murthy G •

Good point, and half of it is now in the draft. Each entry already gets its own undoBy when it's recorded, and since #94 there's a reminder before every open window, not just the earliest, with escalation if nobody has looked. The half that's still missing is yours: the review should show what is reversible when it's read, sorted by earliest deadline, with closed entries marked as closed. I'll add that to J§6.3. Thanks.

Thread Thread
 
jes_p_d4f61f7aa2ec405a418 profile image
Dev_PJS •

The reminder-before-every-window change is the part I'd steal. A deadline that only surfaces at the earliest window trains you to watch one clock; per-window reminders make the queue itself the clock. One question the escalation raises: when nobody acts and it escalates, what does the record show afterwards — that the window lapsed, or that it lapsed after escalation? In my runs that distinction mattered later, when we audited why something irreversible had gone through. Lapsed-quietly and lapsed-despite-warning point at different fixes.

Collapse
 
alexcodebytes profile image
Oleksandr •

On question 3 (dependencies): inferring links from matching argument values is a reasonable first pass, but it will miss cases where the dependency is implicit. A safer approach might be to let tool servers optionally declare their inputs, so the Recorder can build the graph from those declarations rather than guessing. Start with the heuristic, but design the schema so explicit declarations can be added later.

Collapse
 
murthyg profile image
Murthy G •

Good point, and it's now in the draft: github.com/probita-ai/hatp/pull/68. Tool servers can declare effect.uses, and each link records its source (declared, host, inferred or default). Links inferred from matching values never replace the safe default on their own, for exactly the reason you gave. Thanks!

Collapse
 
alexcodebytes profile image
Oleksandr •

That's a really fast turnaround. The 'each link records its source (declared, host, inferred or default)' is a good design — it means you can always tell whether a dependency is guaranteed or just a guess. Does the 'inferred' source ever get promoted to 'declared' automatically, or does that require manual review?

Thread Thread
 
murthyg profile image
Murthy G •

Never automatically. Only the tool server's own effect.uses makes a link declared. A reviewer can confirm an inferred link, which is then recorded as confirmed (#61).

Thread Thread
 
alexcodebytes profile image
Oleksandr •

The 'never automatically' is the right call. It means the system can't accidentally promote a guess to a guarantee. A human has to look at it and say 'yes, this dependency is real' before it becomes trusted.

Thread Thread
 
murthyg profile image
Murthy G •

Exactly. A guess should never turn into a guarantee without someone signing off. Thanks for pushing on this!

Collapse
 
anp2network profile image
ANP2 Network •

I ran into a version of this in a different setting, and the thing that broke was not the bad entry. It was a good one going missing.

I run a public append-only signed event log, around 119,000 rows across 64 signing keys. Rows point at each other with a reference shaped ["e", <target id>, <role>]. That third element is mandatory in the read-side evaluator and validated nowhere on the intake path. Of 14,881 reference tags, five carried a role outside the declared vocabulary and 96 omitted the element entirely, so a 0.034% error rate, which turned out to be the wrong number to look at. The evaluator silently skips a row whose own reference lacks a declared role, then skips any row that names a skipped row. Four malformed rows took out two verdict rows that were byte-for-byte correct, with all three of their references carrying declared roles and naming their targets by ID. The public API reported those threads as timed_out and failed while returning thread_event_count: 4 in the same response. One had gone request to verdict in 22 seconds. The same intake code does enforce closed vocabularies elsewhere, rejecting an unknown category with a 400 and rejecting a reference that resolves to no existing row, so what is missing is that single element rather than strictness in general.

Mapping it onto v0.3: the first time a journal entry's pointer gets resolved is step 2's check, which can be 30 days after the entry was written. A signed, correctly chained, individually valid entry can turn out to be unreachable at the exact moment someone asks to undo it. Q5 reads to me as a narrower case of that, since authority is not the only thing on the undo path that goes unexamined until undo time. Resolving the pointer at Recorder intake, and refusing the journal entry there when undo.kind is outside the vocabulary or the resource does not resolve, buys the window back.

The Reversal Plan has two sections and I suspect the state space has three. "Cannot be undone" currently absorbs both declared irreversibility and anything that failed to resolve, which means the approver can sign off on a plan that quietly omits a compensable step, and the omission looks identical to the already-delivered email. My own log returned both halves of its contradiction in one response, every field present, with nothing to raise a flag on the combination. Would you give unresolved entries their own section, with a predicate that separates a failed check from declared irreversibility?

Collapse
 
murthyg profile image
Murthy G •

Great failure story, thank you. It's now in the draft (#71): the Recorder resolves references when it records each entry. A broken entry is kept and marked unresolved. It blocks only the entries that depend on it, never the valid ones after it. It also gets its own unresolved section in the Reversal Plan, separate from notReversible.

Collapse
 
ontomato profile image
ontomato •

undo gets hard the moment the agent touched more than one system.

from my experience the useful unit was not the chat turn, it was a typed intent with every side effect listed before commit: which record, which field, old value, new value. then undo is reverse those rows in order, not 'please take it back' in prose. when the welcome email and the crm win flag share one intent id, you can roll both. when they are two freestyle tool calls, you mostly get partial undos and a messier truth.

Collapse
 
murthyg profile image
Murthy G •

That's the planned half of HATP: a transaction lists every step in one plan before anything commits, and a human approves that plan. The Journal is for agents that don't plan, where you get the freestyle calls you describe. Effect previews (#62) would add the old and new value per field before commit, which is the row-level view you're describing.

Collapse
 
ontomato profile image
ontomato •

Separating the planned transaction path from freestyle journal calls is useful.

Effect previews with old and new values before commit are the row-level check I keep wanting on the unplanned path too.

Thread Thread
 
murthyg profile image
Murthy G •

Agreed. On the unplanned path, the preview has to come from the tool server's dry run of each call, since there's no plan to read it from. So the same effect object would describe the change before the call and after it. That's the shape planned in #62 for v0.3, so a preview and the receipt of what actually happened can be compared field by field.

Thread Thread
 
ontomato profile image
ontomato •

Yeah, putting the preview and the receipt in the same effect object is what makes a dry run checkable when there's no plan sitting around.

Field-by-field only holds if the object also marks which fields are allowed to change. A new id or a server timestamp will always miss otherwise. And the dry run has to be pinned to the same auth and row snapshot as the write, or the preview can look fine while the live call touches a different record.

Collapse
 
alexshev profile image
Alex Shev •

Undo is strongest when it is designed before the write, not improvised after an incident. Each action needs an inverse or compensation plan, the state it assumes, and a verification step after reversal. For actions without a real inverse, the agent should surface that irreversibility before execution—not after.

Collapse
 
murthyg profile image
Murthy G •

Agreed on all three, and that's the shape of the draft. Every operation declares its undo before any call (J§4). The state it assumes is recorded (effect.version, plus effect.before for UI undo).

Reversal is verified after (UNDO_UNVERIFIED).

Anything without a real inverse is none, so the policy makes a human approve before it runs (J§7).

Collapse
 
izgorodin profile image
Edward Izgorodin •

Question 3 has a version one level out. The reversal set (J§8.2) follows dependsOn, and dependsOn, as drafted, names only earlier entries of the same run. Between v13 and the undo, other runs may have read the deal: a forecast job that pulled D-881 as Won into a report, another agent that booked capacity against it, and the sales agent's own next run, which recalls "Globex signed, kickoff booked" from its memory. Undoing the stage puts the record back to Proposal, and every one of those readers still acts on Won.

A Recorder that those runs also pass through already holds the evidence to list them, since J§12 wants it to be the only path from an agent to its tool servers, and effect.uses records what a call read or relied on. Any entry in another run, recorded after e2, whose effect.uses names crm://acme/deal/D-881 or whose arguments carry D-881, read the value the undo is about to change. That list does not have to be reversible to be useful: a section next to notReversible, something like "read the changed value since", tells the approver whom to notify, the way sideEffects tells them that attendees get a cancellation notice.

Collapse
 
murthyg profile image
Murthy G •

Great catch: reversal only looked inside the run. It's now in the draft (#74). The Reversal Plan gets a readSince section listing entries from other runs that read the changed value after the agent's change, found through effect.uses or matching arguments. Nothing in that list is reversed; it tells the approver whom to notify, like sideEffects. One limit is stated explicitly: the Recorder only sees calls that pass through it, so an agent's own memory is outside its view.

Collapse
 
izgorodin profile image
Edward Izgorodin •

The limit holds for memory the agent keeps to itself, and it is narrower than it sounds. When the memory is a tool server the agent reaches through the Recorder, a recall is a call like any other: the next run asking its memory about Globex leaves an entry, and if the memory tool reports what it returned, effect.uses can name crm://acme/deal/D-881 and readSince picks it up next to the forecast job and the capacity booking. What stays outside is memory that never crosses the Recorder: a notes file the agent opens directly, a framework's in-process store, or context carried over in the conversation itself.

For that second kind the plan cannot list readers, but it can say that it cannot. A line next to readSince naming the agents whose memory does not pass through the Recorder tells the approver where the list is incomplete. For memory that does pass through it, the useful follow-up to an undo is often a correcting write through the same path rather than a reversal: a note that the stage went back to Proposal on that date, so the next recall finds the correction next to the old fact instead of only the old fact.

Thread Thread
 
murthyg profile image
Murthy G •

You're right, the limit is narrower than I wrote. If memory is a tool server behind the Recorder, a recall is an ordinary entry, and readSince picks it up through effect.uses with no change to the draft. I'll narrow J§8.2 to say so.

Your second kind needs one addition. The Recorder can't see that a reader is missing, so it can't work this out alone. The agent host or the organisation would have to declare whether an agent's memory passes through the Recorder, with "unknown" as the default. The plan can then say where readSince is incomplete instead of implying the list is whole. I'll add that next to readSince.

The correcting write fits J§8.7: the Reversal Record suggests it, and it runs as a new action with its own approval. It adds a fact rather than undoing one, so the old fact stays and the correction sits beside it. Thanks, this is a better answer than "outside its view".

Collapse
 
alexshev profile image
Alex Shev •

The distinction between compensation and erasure is the key one. I’d make the reversal receipt first-class too: each completed compensating step should record the observed version before and after, the authority that approved it, and any unavoidable residual effect such as a cancellation notice. Then a partial undo is auditable as its own change set rather than being presented as if the original run never happened.

Collapse
 
murthyg profile image
Murthy G •

Agreed, and most of this is in the draft. A reversal produces its own Reversal Record, appended to the run's record rather than editing it, with the signed approvals and the authority checked at approval time (J§8.4–8.5). Each step also carries the version it expects to find. What's missing is the other half you named: the version each compensation leaves behind, and the side effects that actually occurred rather than the planned ones. I'll add both to each step's receipt. Thanks, that makes a partial undo fully auditable.

Collapse
 
agentwareshq profile image
agentwares •

On question 5, I would recheck authority at undo time, not just at grant time. Undo is itself a new action with its own side effects — cancelling a meeting notifies people, restoring a deal stage can overwrite newer work — so the person approving it should hold the authority when the reversal actually runs, not only when the original run was approved. A role change in between should at least force a fresh approval, even if the undo window is still open.

On question 4, I think "act freely on what can be undone" is the right default only if the compensation is verified, not just declared. A tool server claiming an undo exists is different from the undo having been tested against the current version of the record. If the compensation has never been exercised, or the ifVersion check cannot run, I would treat it as "ask" rather than "allow" until it proves itself.

Collapse
 
murthyg profile image
Murthy G •

On question 5, agreed, and it's in the draft: authority is checked when the reversal is approved, not inherited from the original run (undoAuthority, J§8.4), so someone who has changed roles can't approve it. What's still open is whether a role change should also notify people whose runs can still be reversed (J§14).

On question 4, half of this is covered. If the version check can't run, because the server returns no effect.version, the operation is treated as unknown and goes to "ask" (J§7). The other half is a real gap: the draft trusts a declared compensate even if that undo has never been run. I'd like to add a policy value so an operation stays on "ask" until its undo has been exercised, for example by the conformance suite planned for v0.3 (#64). Thanks, "verified, not just declared" is the right test.