How AI agent tasks survive an app restart

What happens to an agent's turn when the app restarts mid-task: liveness clocks, an on-disk turn record, dev server cleanup and conversation recovery.

A Tesseract conversation with a five-step plan, every step ticked and the Approve & continue button below, on a cream background

When Tesseract restarts mid-turn, the turn is not lost: it was written to disk when it started, so the next start sends it again on the same framework with a note to continue, not start over. What the framework started ends with it, and a conversation the framework lost continues from Tesseract’s own copy.

That is the short version, and it is what the earlier what’s-new post describes from the outside. This post is the inside: how the app decides a turn is alive, what it writes down and when, what it does about questions, dev servers and background jobs, and how it handles 14 frameworks that each resume differently.

Why is a restart mid-turn hard for an agent app?

A chat turn with a model is a request and an answer. An agent turn is not. Claude Code, Codex, Gemini CLI or OpenCode can spend twenty minutes in one turn: reading files, running a build, starting a dev server, asking you something, spawning a subagent. All of that state lives in two places that a restart destroys at once: the framework’s process and the app’s memory of what it asked that process to do.

So the problem splits into four questions: is a turn alive or dead, what survives the process, what comes back and how, and what happens to everything the turn touched on the way.

How does Tesseract know a turn is still alive?

A turn is in flight from the moment it is sent until its result arrives. While it is in flight, silence has to be explained, or it counts as a hang. The watchdog asks, in order, whether any of these explains the quiet:

Silence is explained byHow long it may last
A /compact you typedno clock at all
The framework compacting on its own30 minutes from the last frame
A card waiting on you, or your Pauseno clock, a person answers when they answer
Background work the turn started, vouched for by whatever started itno clock while it is listed
The framework replying “Prompt is too long”30 seconds, then the overflow recovery runs, not a kill
Nothing20 minutes, then the turn is declared dead and can be retried

The first frame of a cold turn gets the same 20 minutes, because a cold start has more legitimate silence than a flowing stream, never less.

The important line is the fourth one. Three different producers start background work: the framework itself (a backgrounded shell command, a subagent, a workflow), Tesseract’s own tools running as jobs, and scheduled tasks. Each is the only authority for its own kind, and nothing else guesses. Deadlines on that work (10 minutes for a task or tool job, 15 for a subagent, 60 for a workflow) only apply to work nobody vouches for anymore, a ghost whose producer died without saying so. A job that runs for hours and says nothing is still a job.

Three more rules keep long turns alive:

  • Sleep and wake. The system’s sleep, resume, lid and network events reset every streaming clock and give each turn a full window again, so a turn that was answering when you closed the lid carries on when you open it.
  • Visible work. The host remembers what each task is when it starts and stamps that identity on every later frame, so a long workflow’s card shows its progress the whole way through.
  • Frameworks that speak the Agent Client Protocol (OpenCode, Qwen Code, Kimi, Hermes and others) run as one process answering JSON-RPC. Opening a session gets 2 minutes, loading one gets 4, and a turn with nothing at all from its framework for 6 minutes (no update, no tool call open, nothing waiting for you, not paused) ends that process. The turn carries on in a fresh process, up to three times.

What is written to disk while a turn runs?

Three records, each written as the turn happens, because there is no “just before the restart” moment to write them in.

The in-flight record. When a turn is sent, one small file per conversation is written: temp file, then rename, readable only by you. It holds what a re-send needs and nothing else. Its shape:

{
  "sessionId": "…",
  "engine": "codex",
  "startedAt": "…",
  "attempts": 0,
  "params": { "message": "…", "projectDir": "…", "model": "…", "effort": "…", "permissionMode": "…", "runnerConfigId": "…" }
}

It is removed when the turn ends: a result, a Stop, a teardown. It is deliberately not removed when the app itself shuts down. Whatever is still there at the next start is, by definition, a turn that was cut short.

The session record. Every framework’s events pass through one place on their way to your screen, and that place writes them to the conversation’s own file: what was asked, on which framework, model, mode and account, each tool call with its arguments, each result, the plan, notices and how the turn ended. Streamed text waits in a small side file, saved every 400 ms, and becomes one row when the message is complete. If the app stops mid-answer, the next process folds the side file in, so the half-written answer is kept too.

For Claude Code, this file sits beside the transcript the framework keeps itself. For frameworks that keep their history in their own stores (OpenCode’s database, Codex’s rollouts, Gemini’s snapshots), Tesseract records the conversation itself, from the same stream it renders, in the same shape. That copy matters later.

The task. Covered below, because it answers a different question.

The life of a turn across a restart: while it runs, an in-flight record and the session record are written and the watchdog needs a reason for silence; when the app stops, the framework and everything it started end while the records stay on disk; at the next start the turn is marked interrupted, sent again eight seconds later on the same framework, and resumed in the framework's own session or carried into a new one from Tesseract's copy

What happens in the first seconds after the app comes back?

The new process reads the in-flight records, oldest first, and for each one does five things.

  1. The turn is marked interrupted in the session record, so the conversation’s history says what happened instead of showing a turn that is forever running.

  2. The tab shows it as running from the first moment, not only when the re-send starts. If you type a message meanwhile, it is lined up behind the interrupted turn instead of racing it, which would have sent the interrupted turn twice.

  3. If you already carried the conversation on since the restart, the old turn is not sent again. It would run alongside yours.

  4. Eight seconds after start, it is sent again on the framework it ran on, with the model, effort, access and account it ran with. Two conversations cut off together, one on Claude Code and one on Codex, both pick up, each on its own framework, whatever the machine’s default is now. The message carries this note:

    This message is being re-sent because the app restarted while you were working on it.
    If the same request already appears above, do not start over — continue from where you
    left off, keeping what was already done.
    
  5. It is sent up to three times, so a turn never loops. After that, the conversation shows a “Stopped when the app closed” notice that says why, and your next message carries it on.

The setting that lets a run answer its own cards when you are away travels with every path that starts a turn, the restart re-send included.

How does “continue” stay part of the same task?

Re-sending the turn is half the job. The other half is what you type afterwards. When a run has a plan, Tesseract checks the work when the run ends, with an agent of its own, and sends its feedback back as the next turn. That check needs to know what the job is.

A job is what you are having done across however many messages it takes. Every message while it is open (a “continue”, a correction, an answer to the agent’s question) joins it, and only a check that finds it delivered closes it. The open job and its plan are written through to disk whenever they change, one file per conversation, kept for 30 days, and read back the first time a conversation is touched after a restart. A message belongs to the job from the moment its turn starts, not when it is checked, so a restart in the middle of a long turn, or in the middle of a question asked inside one, does not leave the next “continue” standing alone. It works the same on every framework.

What happens to a question the agent asked you?

A question to you is the one kind of silence with no clock.

Tesseract turns slow tool calls into background jobs after a short grace, so the chat is never held. The tools that put something to a person (a question, a form, a choice, a request for a secret, a plan to approve) are on one list that every framework’s job wrapper reads, and none of them is ever a background job. A question stays in the chat until you answer it. If you answer by typing instead of clicking, the waiting card is withdrawn with a note telling the model you replied in the chat, and your message goes in as the next thing it reads, so it continues from your answer rather than starting over.

If the app restarts while a question waits, the turn that asked it had not ended, so it is treated like any other interrupted turn. And because the job was recorded when that turn started, the “continue” or the answer you type afterwards joins the same task, not a new one. If you are away from every device for 20 minutes, cards are answered for you: a plan is approved and a question gets its recommended option, but a step the risk check stopped is never approved for someone who is not there.

What happens to dev servers the framework started?

Everything a framework starts goes when it goes, on Mac, Windows and Linux, so no dev server is left holding its port.

Every third-party program Tesseract runs (each framework’s CLI, and what it launches) is started under a small native supervisor, a reaper of about 1 to 3 MB that starts in under 5 ms. Its one job is that the family cannot outlive the app. Each operating system needs a different trick:

How the reaper keeps the familyWhat happens if the reaper itself is killed
LinuxIt becomes a child subreaper, so anything the tree orphans, a double-forked server included, is re-parented to it instead of init (prctl(2))The child has a parent-death signal set to SIGKILL
macOSIt follows the lineage with kqueue: a fork and exit event on every memberThe next sweep ends what it left
WindowsThe child goes into a Job Object that kills the whole job when its last handle closesThe OS ends the job when the reaper’s handle closes

A server that double-forks into its own session escapes both the process tree and the process group. So every process the child starts also inherits a tag in its environment, a chain of “process id and start time” links naming its reapers, innermost first. When the app starts, and once a minute after that, a sweep ends any tagged process whose reaper is gone. It never touches a process without a tag or one belonging to another user. The kill itself is a polite SIGTERM, up to 3 seconds, then SIGKILL until nothing is left. Because pids are recycled, the tag records start time as well as pid, and no process is ever chosen for a kill by its command line.

The reaper sleeps until the OS reports an exit, a fork or a signal.

What happens to background jobs that were running?

Each running job Tesseract starts for a conversation has a small record on disk. On the next start, any record whose process is gone marks a job the restart ended. The first time that conversation runs again, the agent is told once, in plain text, which jobs ended and to start them again if they are still needed.

This matters more than it looks. An agent that started a two-hour build knows exactly where it stands and carries on.

What if the framework lost the conversation?

Re-sending a turn works best when the framework can still open the conversation. Each framework signals a lost conversation in its own way, so Tesseract detects each one. These were measured on each installed framework, on the versions named:

FrameworkWhat “lost” looks likeWhat Tesseract does
Claude Code”No conversation found with session ID” on every message, for good, once its transcript is gone (its own cleanup removes old ones)Resumes from Tesseract’s fullest copy of the conversation
Codex 0.159.3A thread from an app-server that was replaced is not loaded and cannot be resumedOpens a new thread with everything said so far carried in
Hermes 0.21.5Loading a session it does not have answers {}, then every prompt ends as a refusal with no wordsAn empty load is read as “not found”
Gemini CLI 0.62.0Loading after a restart starts recording before reading, which wiped the saved chatNot loaded on versions before 0.64 (an unknown version counts as old); carried over from Tesseract’s copy
CodeBuddy, Cursor, OpenClawA load that replays nothing while Tesseract holds a transcriptRead as “not found”

Two cases need care. A Codex thread opened in the current app-server answers resume with an error until its first turn is written, yet it is loaded and takes turns, so Tesseract keeps it as the live thread it is. And on older Gemini CLI versions, Tesseract carries the chat over from its own copy, so the saved chat stays intact.

In every “lost” case the conversation continues in a fresh session behind the same conversation, carried from the record described above. You stay in the conversation you were in; the framework simply starts a new session underneath. Frameworks added as plugins report a lost session themselves, and get the same treatment.

Who brings the app back after it stops?

Each system brings the app back on its own.

  • Windows starts the app at sign-in, and a scheduled task runs a tiny watcher every two minutes that starts Tesseract if it is not running. It stands aside for five minutes while an update installs.
  • Linux runs it as a user service that restarts on failure.
  • macOS starts it at login.

Separately, a watchdog ends the app and starts it again if its main thread stops, and it is built not to fire for sleep or for a thread that is slow but working. For work that should carry on while you are away, put it on a machine that stays on, as in running agents on your own server. How updates choose a moment that does not cut a turn is its own story, in how Tesseract updates without stopping agents.

What numbers do we have, and how were they measured?

MeasurementValueHow
Reaperabout 1–3 MB, under 5 ms to startthe native binary
Re-send8 s after start, at most 3 attemptsthe shipped values

These are the shipped values.

Questions

Do I have to do anything after a restart? No: the interrupted turn is sent again eight seconds after the app starts. If you see “Stopped when the app closed”, send any message, “continue” is enough, and it joins the same task.

Will the agent redo work it already finished? It is told not to, and it sees its earlier tool calls and edits in the conversation. Edits already written to disk stay written. A background job that was running when the app stopped is reported to the agent, so it can start it again if it is still needed.

Does this work on every framework, or only Claude Code? The in-flight record, the re-send, the task that “continue” joins, and the process cleanup are the same on every framework. What differs is how each one loses a conversation, which is why each has its own detection in the table above.

What if I closed the window instead of quitting? Closing a view never stops the work, on any framework. The turn keeps running, and its questions follow you to your other devices.

Why not just resume the framework’s own session every time? Because frameworks lose sessions in ways that range from an error on every message to silently wiping the chat. Keeping Tesseract’s own record of every conversation is what makes “the framework forgot” recoverable.

Try it

Download Tesseract and run a long task on any of the frameworks it supports. Quit halfway; it picks up where it was.

Bài viết khác trên blog