What happens after you press Stop?
A Stop button that ends in one word hides what actually happened. A small synthetic demo of a return summary: one confirmed write, one unknown, one never sent, the actions each one allows, and why a timeout must never turn unknown into failed.
On this page
- Where the question came from
- ”Stopped” answers one question out of three
- A small synthetic demo: three steps, three outcomes
- What each outcome lets the user do
- Unknown has to stay unknown
- Reverse is not undo, and compensation is a new action
- An approval covers one exact request
- What I didn’t check
- Seven checks before you ship a Stop button
At a glance
- A Stop button that ends in the single word "Stopped" is telling the truth about one thing, your own orchestrator, and nothing about writes that were already in flight.
- In a three-step synthetic demo, Stop left one write confirmed with a receipt, one unknown with no receipt and no cancel acknowledgment, and one never sent. Each needs different words and different buttons.
- The state model has no "failed" state. timeout_tick() is a deliberate no-op, and a test calls it five times on an unknown write and checks that it's still unknown.
- Reverse is allowed only against a confirmed effect marked reversible. Anything else is rejected with a reason, and undoing an irreversible effect is filed as compensation: a new action with its own approval.
- An approval covers one exact request. Changing the version, target or amount invalidates it, and the five state tests check each of those separately plus a no-change control.
Say you asked an agent to update product descriptions for a batch of SKUs across three tools: a storefront editor, a catalog sync service, and a local draft queue it hadn’t reached yet. Partway through, you press Stop.
This is a hypothetical. There’s no real client, storefront or catalog service here. What I care about is what the screen says to you a few seconds later.
Most Stop buttons resolve to one word: Stopped. That word is accurate about exactly one thing, the local orchestrator. It isn’t scheduling new steps. It says nothing about the rest. One of your three writes already landed and the other side sent back a receipt. One is in a state nobody can speak to: not confirmed, not canceled, just unanswered. The third never left your machine. “Stopped” puts all three under the same label, and the person who comes back to that screen has to guess which is which before they decide what to do.
Where the question came from
This started in Medium threads, not in a product. Under Sanjay Dey’s article on AI agent interface design, which covers delegation, autonomy levels, activity feeds, approvals, stop and undo, and a return summary, I replied:
The stop control deserves its own return experience. If an agent is updating those product descriptions across several tools, clicking Stop may prevent the next write while an earlier request is still completing. I would show stopping, stopped, and effects still being reconciled as separate states, with the final log identifying which changes actually landed.
The product-description scenario above is from that reply. Sanjay answered in the same thread that he agreed with explicit feedback about stopped work, ongoing work and reversibility, and that realistic scenario testing matters. That’s agreement with a design idea. It isn’t evidence that anyone tested it, and nothing below is his.
Two other threads fed in. Under a Towards AI piece on Redis for AI agents, I’d argued that an unknown outcome should survive recovery as a pending action. Ray Hu pushed back: keeping the operation ID doesn’t tell you what happened downstream, and without idempotency or a status lookup you need a real reconciliation path. He was right, and that exchange shaped the section on unknown below. And Riya Jawandhiya’s article on billing approval microcopy led to a thread about which click actually commits the user, which shaped the section on approvals.
None of these authors reviewed this piece or endorsed its conclusions.
”Stopped” answers one question out of three
When you stop a multi-step task, there are three separate questions, with separate answers:
- Did local execution stop? This is about your orchestrator. It isn’t scheduling new steps anymore.
- Was the external request canceled? This is about the other system acknowledging that it won’t carry out, or stopped carrying out, what you asked.
- Was the external effect confirmed? Also about the other system, but a different claim: the thing already happened.
A local “stopped” settles the first one only. The storefront editor might have applied your change before the stop reached it. The catalog service might never have heard about the cancel, might still be writing, or might have failed without saying so. And the draft queue might hold work that’s still yours to keep or drop.
The return screen’s job is to answer all three, per step, and to be honest when it can’t answer the third yet.
A small synthetic demo: three steps, three outcomes
To make this concrete, I had a small demo built and then reviewed its behavior against the outcomes I wanted. It’s a Python state model, three authored fixtures, a runner and a static page. No network calls, no real tools, no real customers. Expected outcomes were written by hand into expected.json from the plan before the runner was first run, and the runner compares against them.
The three fixtures are one per outcome:
- SKU-4471, storefront tool: confirmed. The storefront acknowledged the write as applied before Stop finished locally, with a change receipt. Marked reversible.
- SKU-9102, catalog sync tool: unknown. The write was in flight. The tool’s status endpoint didn’t answer before the connection closed. No receipt, no cancel acknowledgment.
- SKU-2208, local draft: never sent. Stop arrived before the write left the draft stage.
Fixtures matched
3 of 3
results.json against expected.json, one recorded run
State tests pass
5 of 5
python3 -m unittest test_state_machine.py
Failed states
0
EXTERNAL_STATES has none, and a test asserts it
Mobile width checked
390px
scrollWidth equals 390, no horizontal scroll
The page reads the trace the run produced and renders one card per step instead of one word. Each card says what happened in plain words, what evidence it rests on, what the next evidence step is, and which actions are open.
The live demo page has all three cards and links to every source file: the state model, fixtures, tests, trace and results.
What each outcome lets the user do
Once local and external state are shown separately, “try again” stops being one button. The actions depend on the outcome:
- Confirmed: inspect. If the other side says it happened, you can look at what it says happened. That’s a read, not a decision.
- Confirmed: reverse, if reversible. A true undo only makes sense once you know, with evidence, what happened and that it can be cleanly reversed.
- Confirmed but irreversible: compensate. If the effect can’t be reversed, “undo” would be a lie. What the user wants is a new action with its own consequences and its own approval.
- Unknown: wait and reconcile. Unknown isn’t a synonym for failed. Nobody has told you anything yet. The honest move is to get evidence, not to guess.
- Canceled: inspect what ran, then resume if still wanted. The cancel acknowledgment tells you the other side stopped. It doesn’t tell you nothing happened before it did.
- Never sent: discard or resume. Nothing external exists to undo. Resuming is still a choice, not a default.
Sanjay’s article separates true undo from compensation. Here’s what the return screen adds to that: the user can choose recovery against the effects that were observed, not against the original plan. The plan said “update three SKUs.” What was observed was one confirmed, one unknown and one draft. Recovery has to start from the second list.
Unknown has to stay unknown
Unknown is the hardest outcome to get right, and the one products most often cheat on. A disabled retry button with no explanation looks like the product is hiding something. A retry button that quietly re-enables after thirty seconds is worse. It treats elapsed time as an answer.
Ray’s pushback is the core of it. My reply to him said:
UNKNOWN is a guard against acting on a false conclusion, not a reconciliation mechanism. The contract needs to say what external evidence can establish success or failure, who can resolve conflicting evidence, and when retry becomes permissible.
In the demo’s state model, that shows up as a function that does nothing on purpose. timeout_tick() represents time passing with no new evidence. It logs that a tick happened and leaves the external state alone. There’s no “failed” state for unknown to fall into. The only ways out are a receipt (confirmed) or an acknowledgment (canceled) from the other side. One test calls timeout_tick() five times on an unknown write, checks it’s still unknown, and then checks that “failed” isn’t in the list of states at all.
The card’s wording matters as much as the state. “We don’t know if this landed” is a complete, honest sentence. “Failed” would be a guess, and “Retry” would bet the user’s catalog on that guess. If the write did land, retrying it is how you get a duplicate. If the other system has no status lookup at all, the screen should say that plainly and keep the step unresolved. It shouldn’t invent a resolution.
Reverse is not undo, and compensation is a new action
reverse() in the model accepts exactly one case: a confirmed effect marked reversible. Everything else is rejected with a reason, not silently ignored:
- Against pending or unknown, the message says there’s nothing settled to reverse yet and repeats the next evidence step.
- Against confirmed but not reversible, it says this can’t be a true reversal, and that removing the effect would be compensation: a new action with its own approval and consequences.
The test checks all three rejections, that the irreversible case’s message actually says “compensation”, and that the one allowed case goes through. It sounds pedantic, but the distinction changes what the user is agreeing to. Reversing a description edit puts the old text back. Compensating for a sent email means sending another email, and the recipient sees both. A screen that labels both “Undo” is making a promise it can’t keep for the second one.
An approval covers one exact request
The last thread is Riya’s. Her article is about billing confirmations that feel like traps, and the thread under it turned on a narrower point. In my first reply, I said the handoff needs copy that separates continuing to review from approving the charge, and that “if those details change after the redirect, the difference should be visible.” She replied suggesting the confirmation show the previous and new plan price. I agreed and added that the comparison should stay separate from the amount due now.
I’m not writing about billing here. What carries over to Stop is the shape: an approval is valid for the exact thing it was given for. In the demo, an approval token records version, target and amount. change_request() invalidates it if any of the three changes, and never re-approves on its own. The test checks version, target and amount separately, plus a control case with no change that must stay valid.
That matters after Stop because the world moved while the agent was paused. If the user approved “description v3 for SKU-4471” and comes back to find v4 pending, resuming shouldn’t ride on the v3 approval.
What I didn’t check
- No usability test. Nobody used this screen. Whether people read four outcome states correctly under pressure is an open question, and a test that interrupts an agent mid-task and asks what’s still in flight would be the way to answer it.
- The demo doesn’t render everything the diagram proposes. The state map says a disabled retry should show its reason on screen. The demo’s unknown card simply offers no retry button. The canceled outcome exists in the state model but none of the three fixtures uses it.
- Reversal is recorded as “canceled”. In
state_machine.py, a successfulreverse()sets the external state to canceled. Those are different facts (the effect happened and was undone, versus it was stopped before it happened), and a real model should give reversal its own state. - Stop is modeled per step. The model calls
stop()on each task. A real orchestrator stops once and then has to find out what each in-flight step did, which is where most of the difficulty lives. - No real tools, no concurrency. Single process, no network, no concurrent writers, no real status endpoints or webhooks.
- The duplicate check was bounded. Before drafting, the recent Medium stories and drafts were checked for overlap with my earlier retry and changed-goal pieces. It was a bounded pass, not an exhaustive search of every story.
One problem made it all the way to the draft and was caught in review. The first draft quoted my own Medium replies as if Sanjay, Ray and Riya had written them. The source file stored each thread as one record with one text field, and that field held my reply, not theirs. Every quote now links to the page where those words actually appear. Where I describe what someone else said, I paraphrase and link. The lesson isn’t new: a source record that doesn’t say whose words it holds will get misread.
Seven checks before you ship a Stop button
- Show local state and external outcome as separate things. “Stopped” describes the orchestrator only.
- Give every step that was in flight its own outcome: confirmed, unknown, canceled or never sent.
- Make evidence the only way out of unknown, or an explicit human decision that’s recorded as one. Never a timer.
- When retry is disabled, say why and name the next evidence step.
- Allow reverse only against a confirmed, reversible effect. Treat everything else as compensation, with its own approval.
- Re-check approvals at the moment of action. A change in version, target, amount or evidence means ask again.
- If the other system can’t be asked what happened, say so on screen and leave the step unresolved.
If you’re designing how your own agent product behaves when someone presses Stop, here’s how I work.
AI-assisted, human-reviewed.
How this was made: the fixtures and expected outcomes were written before the runner was first run. Everything runs on the Python standard library, and the page is rendered from the run’s trace. Every source file is linked from the demo page.