DEV Community

Cover image for I thought persistent AI agents needed more autonomy until Codex taught me the value of a hard stop
Lars Winstand
Lars Winstand

Posted on • Originally published at standardcompute.com

I thought persistent AI agents needed more autonomy until Codex taught me the value of a hard stop

I used to think the main job in agent ops was helping the model keep going.

Longer runs. More autonomy. Fewer human checkpoints.

Then I started reading failure reports from people running agents in production, and the pattern was obvious: the expensive part usually wasn’t that the agent stopped.

It was that it didn’t stop soon enough.

If your agent can call tools, edit files, write to GitHub, hit webhooks, or mutate production state, interruption is not a nice-to-have. It’s a runtime feature you need to design up front.

That clicked for me while reading a Reddit thread about interrupting an agent turn with Codex in OpenClaw. At first glance it sounds like a niche UX question. It isn’t. Once an agent is in the middle of a turn and side effects are already happening, “just let it finish” becomes a bad operating model.

The real failure mode is partial execution

One of the clearest examples came from an OpenClaw complaint thread after release 2026.7.1 broke installs.

A user saw this warning:

Agent couldn't generate a response. Note: some tool actions may have already been executed — please verify before retrying.

That line is the whole problem.

Not because it’s dramatic. Because it’s normal.

This is what agent failure looks like in the real world:

  • the model gets partway through a turn
  • some tools already fired
  • state is now half-mutated
  • retrying may duplicate side effects
  • nobody is fully sure what happened

A lot of agent discussion still treats interruption like a UI feature.

I think that’s backwards.

Interruption is an ops primitive.

If your agent can do any of these, you need a stop strategy:

  • send email
  • post to Slack or Discord
  • merge code
  • edit files
  • create Jira tickets
  • trigger n8n webhooks
  • kick off Make scenarios
  • write to databases

The question is not “how autonomous is this agent?”

The question is: where can I stop it before it gets expensive or irreversible?

OpenAI cancellation only works if you planned for it

OpenAI’s Responses API has a real cancellation path for long-running work.

That’s useful.

But there’s a catch: you can only cancel responses created with background=true.

So interruption is not something you bolt on later. Either the run was designed to be interruptible, or it wasn’t.

Start the run in background mode

curl https://api.openai.com/v1/responses \
  -H "Content-Type: application/json" \
  -H "Authorization: Bearer $OPENAI_API_KEY" \
  -d '{
    "model": "gpt-5.6",
    "input": "Write a very long novel about otters in space.",
    "background": true
  }'
Enter fullscreen mode Exit fullscreen mode

Then poll for status while the response is still running.

curl https://api.openai.com/v1/responses/$RESPONSE_ID \
  -H "Authorization: Bearer $OPENAI_API_KEY"
Enter fullscreen mode Exit fullscreen mode

If the turn is stuck, looping, or taking too long, cancel it.

curl -X POST https://api.openai.com/v1/responses/$RESPONSE_ID/cancel \
  -H "Authorization: Bearer $OPENAI_API_KEY"
Enter fullscreen mode Exit fullscreen mode

Minimal polling example in Python

import os
import time
import requests

API_KEY = os.environ["OPENAI_API_KEY"]
HEADERS = {"Authorization": f"Bearer {API_KEY}", "Content-Type": "application/json"}

create_resp = requests.post(
    "https://api.openai.com/v1/responses",
    headers=HEADERS,
    json={
        "model": "gpt-5.6",
        "input": "Run a long multi-step task",
        "background": True,
    },
)
create_resp.raise_for_status()
response_id = create_resp.json()["id"]

for _ in range(30):
    r = requests.get(f"https://api.openai.com/v1/responses/{response_id}", headers=HEADERS)
    r.raise_for_status()
    data = r.json()
    status = data.get("status")
    print("status:", status)

    if status in {"completed", "failed", "cancelled"}:
        break

    if status == "in_progress":
        # your own timeout / anomaly checks go here
        pass

    time.sleep(2)
else:
    cancel = requests.post(
        f"https://api.openai.com/v1/responses/{response_id}/cancel",
        headers=HEADERS,
    )
    cancel.raise_for_status()
    print("response cancelled")
Enter fullscreen mode Exit fullscreen mode

A lot of agent runners still assume every turn should be synchronous.

That’s how teams end up with a process that waits forever and no clean way to interrupt it.

Cancellation does not undo side effects

This is the part people gloss over.

Stopping the model does not roll back what already happened.

If the agent already:

  • posted to Slack
  • created a Jira issue
  • wrote a file
  • hit a webhook
  • updated a record

then cancelling the response won’t reverse any of that.

You still need:

  • idempotent tools
  • verification before retry
  • compensating actions
  • audit logs
  • state inspection

Cancellation stops the bleeding. It does not clean up the blood.

LangGraph gets interruption more right than most frameworks

LangGraph’s interrupt model is much closer to what production systems need.

Instead of hoping you can kill a turn at the right moment, you define exact pause points in the graph.

from langgraph.types import interrupt

def approval_node(state):
    approved = interrupt("Do you approve this action?")
    return {"approved": approved}
Enter fullscreen mode Exit fullscreen mode

Later, you resume the graph with the same thread_id.

from langgraph.types import Command

graph.invoke(
    Command(resume=True),
    config={"configurable": {"thread_id": "ticket-123"}}
)
Enter fullscreen mode Exit fullscreen mode

That’s a much better model than blind retries.

You’re not guessing where execution stopped. You know where it paused.

Durable state is the difference between a demo and a system

This is where a lot of agent demos cheat.

Pause/resume looks great with in-memory state until the process restarts.

If you want recoverable interrupts, you need a real checkpointer.

For LangGraph, that usually means something like:

  • PostgresSaver
  • SqliteSaver

Not an in-memory checkpointer that disappears on restart.

A practical sketch looks like this:

from langgraph.checkpoint.sqlite import SqliteSaver

checkpointer = SqliteSaver.from_conn_string("state.db")

graph = builder.compile(checkpointer=checkpointer)

result = graph.invoke(
    {"task": "prepare deployment plan"},
    config={"configurable": {"thread_id": "deploy-42"}}
)
Enter fullscreen mode Exit fullscreen mode

That gives you a real recovery path if the worker crashes or the process gets redeployed.

If your agent can pause but can’t resume deterministically after restart, you don’t have interruption. You have theater.

Where I’d actually put interrupt points

Not every workflow needs human approval.

If you’re summarizing logs, classifying tickets, extracting fields from PDFs, or routing support messages, too many approval gates just kill throughput.

But there are clear spots where interruption is worth the friction.

1. Before irreversible side effects

Put a pause before:

  • sending email
  • merging PRs
  • deleting records
  • posting externally
  • writing to production systems

2. When a turn exceeds its normal runtime

If GPT-5, Claude Opus, or Grok is still chewing far beyond the expected envelope, something is probably wrong.

Use time budgets.

MAX_RUNTIME_SECONDS = 90
Enter fullscreen mode Exit fullscreen mode

If the run crosses the threshold, route it to review or cancel it.

3. When tool output is inconsistent

Examples:

  • malformed JSON
  • repeated retries
  • empty payloads
  • contradictory tool results
  • missing required fields

That’s a strong signal to pause before the agent compounds the mistake.

4. When the agent starts looping

This one shows up constantly in the wild.

An agent keeps retrying the same pattern, keeps calling the same tool, or keeps asking for a format it never gets.

That’s not autonomy.

That’s a missing stop condition.

5. Before cost and queue pressure snowball

Long-running turns plus retries plus tool churn create the same old problem teams hate about usage-based AI billing: you can feel the meter running while the system does something dumb.

That’s one reason predictable compute matters so much for agent workloads. If you’re running automations all day in n8n, Make, Zapier, OpenClaw, or custom workers, per-token billing turns every bad loop into a finance problem too.

A flat monthly model is a much better fit for agent ops because it lets you focus on runtime controls instead of watching spend dashboards all day.

Quick comparison: what each approach is actually good at

Approach What it’s good at
OpenAI Responses background mode Async execution, polling, and cancellation for long-running model work when you start with background=true
LangGraph interrupts Exact pause points, durable resume behavior, and human-in-the-loop control when state is persisted correctly
OpenClaw/Codex-style free-running turns Exposing the real production failure mode: mid-turn errors, partial tool execution, and the need for verification and recovery logic

My opinionated version:

  • OpenAI Responses is good for cancellable long-running model calls.
  • LangGraph is better when you need precise pause/resume semantics.
  • Free-running agent loops are where teams learn that interruption without recovery is only half a design.

If I had to compress this into one rule, it would be this:

Never give an agent a side effect you can’t inspect, pause, or repair.

A practical pattern for safer agents

If I were building a persistent agent today, I’d use a pattern like this:

  1. Start risky runs asynchronously.
  2. Persist state in Postgres or SQLite.
  3. Add explicit interrupt points before irreversible actions.
  4. Add runtime limits per step and per run.
  5. Make tool calls idempotent where possible.
  6. Log every side effect with enough metadata to verify or compensate later.
  7. Route suspicious runs to review instead of retrying blindly.

In pseudocode:

def run_agent(task):
    state = load_state(task.thread_id)

    for step in plan(task, state):
        if step.is_irreversible:
            request_approval(step)
            return "paused"

        result = execute_step(step)
        persist_result(task.thread_id, result)

        if looks_inconsistent(result):
            flag_for_review(task.thread_id)
            return "paused"

        if runtime_exceeded(task.thread_id):
            cancel_or_pause(task.thread_id)
            return "stopped"

    return "completed"
Enter fullscreen mode Exit fullscreen mode

That is less flashy than “fully autonomous software engineer.”

It is also much closer to what survives production.

The underrated skill in agent ops

People love posting screenshots of agents doing 14-step workflows unattended.

What they don’t post is step 9:

  • one API timed out
  • one tool already fired
  • the model lost the thread
  • the queue is backing up
  • now someone has to decide whether to resume, retry, compensate, or kill the run

That’s the actual job.

Not just prompting.

Not just picking between GPT-5, Claude Opus, Grok, Qwen, or Llama.

The underrated skill is knowing when to interrupt a turn instead of letting it finish.

Once you accept that, a lot of architecture decisions get easier:

  • make risky runs async from the start
  • add stop conditions early
  • persist state somewhere real
  • design tools for retries
  • build compensating actions for partial execution

That’s not anti-autonomy.

That’s how you make autonomy survivable.

And if you’re running agents at scale, it’s also why pricing model matters. Teams building automations on top of OpenAI-compatible APIs often want the control surface of modern models without the constant cost anxiety from per-token billing. That’s exactly why services like Standard Compute are interesting: same OpenAI-compatible workflow, but with flat monthly pricing that fits always-on agents much better.

The smartest agent is not the one that never stops.

It’s the one you can stop on purpose.

Top comments (0)