lazy documentationv0.90

Task State Machine and Crash Recovery

This page describes the statuses a lazy task moves through, which commands and events move it, and what lazy does when an agent crashes mid-turn.

How a turn ends

A task's status says where the task IS. It cannot say whether the agent thought it was finished: blocked reads exactly the same whether the agent delivered the whole task or simply ran out of budget halfway through. So an agent says which, and a turn ends in one of three ways.

Nothing asks the agent which of the three it was — lazy reads the ending off what the turn did.

The last two look the same in the status: blocked is blocked whether or not anything was raised. What differs is the record — a blocking item, or none — and lazy labels the task from that.

You can accept from any of the three. A human-facing task gets its walkthrough whichever way the turn ended, precisely so the decision you are being asked to make has something to go on.

The first two are exclusive. lazy_final refuses while a blocking item is open on the task, and names the item, because filing one is the agent having already chosen the other ending. The task's journal records which items parked it.

Work left uncommitted when the turn ends

A task is reviewed and merged from its commits. Anything an agent edits and never commits is in no diff, in no walkthrough, and in nothing lazy accept would carry — and it disappears with the worktree. The end of a turn is where this happens most: the checks that close a turn ask the agent to update docs or a changelog, and writing the file and forgetting the commit is an easy miss.

Three things make that visible rather than silent.

A declaration is about a SHA, and it can go stale

Because a final names the head it was made at, the head can move afterwards — a sync merge, a commit made while pairing, a subtask accepted into a parent. None of those cancel the declaration; every surface that shows a final instead says so:

declared final at 9f3c1a2b; head has since moved (a sync)

What DOES cancel it is an agent going back to work: a later agent work turn that produces commits clears the declaration, and whoever did that work declares again when they are finished. A turn that committed nothing, a sync, a review, a question and the automated follow-ups all leave it standing.

lazy show prints the declaration (or not declared) next to the task's status, lazy show --json carries it as final, and the web review page leads with it.

Task Status State Machine

Status Definitions

Lazy tasks can be in one of the following statuses:

A parent task is told when its subtask list changes

A task whose subtasks come and go gets a one-line comment each time, so its agent can see what happened on its next turn:

The quotes are deliberate: text somebody typed — the subtask's goal, the reason it was closed — is quoted so it reads as a quotation rather than as instructions to the parent's agent. Lazy's own wording, like reparented to <parent>, is not.

Like every other lazy comment, these never start a turn and never unblock the parent: they show up in its next notes. A top-level task has no parent task, so nothing is written for it, and neither is anything written to a parent that is already complete or abandoned.

Status Categories

Terminal statuses (task is finished, core fields frozen):

Blocked statuses (waiting for human action):

Active statuses (has active worktree that should not be merged into):

Valid State Transitions

backlog
  → working       lazy start (creates session, launches agent)
  → blocked       daemon (a backlog task whose branch already has commits)
  → abandoned     lazy close (task canceled before starting)

working
  → blocked       daemon (agent turn completes)
                  | daemon (the agent failed in a way retrying cannot fix —
                    see "Agent Failure Classification" below; NOT auto-resumed)
  → conflict      daemon (agent turn completes with file permission violations)
                  | lazy accept (a failed acceptance gate restores the status the task had before)
  → submitted     lazy accept (same restore, for a task that was submitted)
                  | daemon (a sync or review turn ends on a task that was
                    submitted — the turn restores the status it found; a sync
                    that failed in a way lazy will not retry restores it too,
                    while one it will retry goes to `interrupted` instead)
  → interrupted   daemon (the agent's container or process died without finishing the turn)
  → merging       daemon (the task's PR/MR was merged on the forge)
  NOTE: working cannot transition to pairing or abandoned — the agent is running.
  NOTE: accept flips the task to `working` while the acceptance gate runs and
        restores the status it actually had if the gate fails.

blocked
  → working       lazy unblock (human gives feedback)
  → conflict      any park that re-checks the label and finds pending
                  violations (see "conflict is derived" above)
  → pairing       lazy pair (human wants to work interactively)
  → submitted     lazy submit (opens a PR/MR for review)
  → merging       lazy accept (begins merge process)
  → abandoned     lazy reject (rejects the work) | lazy close (canceled without accept/reject)
  → backlog       daemon (a blocked task with no session is moved back to backlog)
  NOTE: blocked cannot go directly to complete — must go through merging first.

conflict
  → working       lazy unblock (feedback, like any blocked task) | lazy resume
                  | auto-resume / auto-delivery
  → blocked       any park that re-checks the label after every violation was
                  approved on the review page
  → pairing       lazy pair (human wants to work interactively on violations)
  → submitted     lazy submit
  → merging       lazy accept (once every pending violation is approved)
  → abandoned     lazy reject | lazy close

submitted
  → working       lazy unblock | lazy sync | lazy review
                  (each of these runs a turn; sync and review restore
                  `submitted` when the turn ends, an unblock does not — feedback
                  means the task is being worked on again)
  NOTE: `lazy resume` and `lazy ask` do NOT apply to a submitted task. Resume is
        for picking a task back up where its turn stopped (interrupted, blocked
        or conflict only); a review question runs only on a blocked or conflict
        task. Send feedback with `lazy unblock`, or run `lazy review`.
  → merging       lazy accept (merges the reviewed branch)
  → pairing       lazy pair (human wants to work interactively)
  → abandoned     lazy reject | lazy close
  → blocked       lazy itself, in one case only: the task was re-parented (its
                  parent was accepted or closed, or `lazy reparent`), and its
                  pull/merge request could not be moved to the new target, so
                  lazy closed it — the task no longer awaits a forge review
  NOTE: otherwise submitted does not go directly to blocked or conflict. It
        leaves through a turn (via `working`), an accept, a pairing session, or
        rejection — a finished sync or review puts `submitted` back rather than
        re-labelling the task, unless a protected-file decision is owed.

interrupted
  → working       auto-resume (daemon, circuit breaker allows) | lazy resume
                  | daemon (the agent's answer arrived after the task was
                    marked interrupted; it is recorded via working → blocked)
  → pairing       lazy pair (human investigates interactively)
  → merging       daemon (the task's PR/MR was merged on the forge)
  → abandoned     lazy reject | lazy close
  NOTE: interrupted cannot go to complete directly, and `lazy accept` does not
        take an interrupted task — resume or unblock it first.

pairing
  → blocked       pairing ends (the session exits) | daemon (the pairing process is gone)
  → conflict      same two paths, when the task still owes a decision on
                  file-permission violations

merging
  → complete      lazy accept (checks pass, merge succeeds) | daemon (merge completed)
  → blocked       lazy accept (checks fail) | daemon (PR/MR closed on the forge)
  → conflict      lazy accept (merge phase aborts on a task that was in conflict)
  → submitted     lazy accept (merge phase aborts on a task that was submitted)
  NOTE: merging cannot go to abandoned — merge either succeeds or fails back to
        the status the task held before the accept. A task in `conflict`
        (unresolved violations) or `submitted` (open PR awaiting review) is not
        `blocked`, so every abort restores the true prior status.
  NOTE: the merge is the accept's commit point. The moment it lands the task
        becomes `complete`; fast-forwarding the local branch, pushing the
        parent, tagging, re-parenting children and cleanup happen AFTER, and a
        failure there is reported as a failure (non-zero exit, the step named)
        and retried by the daemon — it never moves the task out of `complete`.
  NOTE: an accept that DIES (daemon restart, crash, kill) leaves the task in
        `merging`. The daemon RESUMES it, whether it died before or after its
        merge landed: a merge that already landed is recognised and not
        repeated. While that resume is pending, `lazy reject`, `close`, `submit`
        and `unblock` refuse (they would undo an accept that may already have
        merged); `lazy accept` resumes it immediately. After three failed
        automatic resumes the daemon stops, files a system message, and those
        commands work again to recover the task to a resting status — except
        when the work has already landed on the target: then they refuse and
        point you at `lazy accept`, which finishes the accept instead of
        undoing it. `lazy doctor` reports anything sitting in `merging`.

zombie (system-only)
  → complete      daemon (the task's branch is found merged)
  NOTE: any non-terminal → zombie (system actor only). Terminal statuses cannot go to zombie.

complete
  → blocked       lazy reopen --reason "..." (reopen accepted task)
  → backlog       lazy reopen (reopen accepted task with no session)

abandoned
  → backlog       lazy reopen (reopen rejected/closed task with no agent work)
  → blocked       lazy reopen (reopen rejected/closed task with agent work)

Actors (turn & transition provenance)

Every turn and status transition records an actor — who caused it. This is provenance only: it never changes what a command does, it records who did it.

There are five actors:

The actor records the channel, not who wrote the words. A command that arrives over MCP is builder; the same command over the CLI is human. When a builder relays a human's feedback through lazy_unblock, the turn is still builder — the actor records who submitted it, not who authored it. The content is stored as written either way.

Everything an operation writes carries its one actor: the turn, every status change it makes, and any comment it leaves — for commands that start a turn (start, unblock, ask, resume, stop, sync), for those that do not (reject, close, submit, reparent), for task creation and for journal entries. lazy show, the web dashboard and lazy_show label each human-side turn with its actor, so builder and supervisor turns are distinguishable from what a person typed.

An absent actor means the default. An agent's own reply never carries an actor (it is labelled by its role instead). On any other turn or status change, no actor means a CLI/human action. Commands that start a turn always record their actor explicitly, human included.

Text intake is sanitized at the boundary

Every prompt lazy sends an agent is passed on the agent's command line, where a raw NUL byte is illegal. So every place lazy takes in text — lazy unblock, comment and create, the $EDITOR and piped-stdin paths, every MCP tool argument, and auto-delivered PR comment and CI text — sanitizes it before saving it. Non-printable control characters (C0 except tab/LF/CR, DEL, and C1) are replaced with printable escapes: a NUL becomes the six characters \u0000.

For free-text arguments that become prose in a prompt (feedback, message, prompt, note, reason, question, goal_context), a short note is appended saying a substitution was made, so it is visible rather than silent. Strings that are not prose are escaped without that note: short single-line fields like a task goal, and file-path lists such as files and approved_files, where an appended paragraph would corrupt the path.

The policy is sanitize and deliver, never reject: rejecting would discard feedback at exactly the moment you finished typing it. Nothing is dropped — only re-encoded. Ordinary text passes through byte-for-byte.

Transition Triggers

The CLI commands below record the human actor; each has an MCP equivalent (lazy_start, lazy_unblock, …) that makes the same transition but records builder (or agent, when the caller is a task agent acting inside its own subtree).

Human Actions (CLI commands)

Agent Actions

System Actions (daemon)

The daemon keeps checking task state in the background and fixes up anything that has changed underneath it:

Only one turn runs at a time

A task never has two turns dispatched at once. If an unblock, an ask and an automatic sync arrive together, the first one to claim the task wins, and the others see the task is busy:

A turn is only dispatched from blocked, conflict, submitted or interrupted. A task in pairing or merging is never handed a new turn.

Command observability

Several CLI commands can run for seconds or minutes — starting a task (worktree creation, branch publish, container launch), unblocking with feedback, merging upstream into a task, asking its agent a question, accepting a merge, or closing/rejecting a task (stopping the agent, remote PR cleanup, worktree teardown). They narrate what they are doing, phase by phase.

How it looks on the terminal. Before work begins, the command prints a numbered plan of the phases it expects to run. As each phase starts and finishes, you get a line — on a TTY the open phase repaints with a live elapsed counter; in piped or CI output the lines are append-only. Optional phases that do not apply (for example publishing a branch for a linked task, or stopping an agent that is not running) are listed up front and then reported as skipped with a reason.

A long phase keeps saying so. Some phases do their work without anything to report as they go — fetching an upstream branch, creating a container. On a TTY the elapsed counter on the open line already shows that; in piped or CI output, where there is no line to repaint, the command adds a still running (…) line for the phase whenever roughly five seconds have passed with nothing printed. A phase that finishes quickly adds nothing. These lines follow the daemon's own liveness, so they stop when it stops — silence still means something is wrong.

Pre-flight is always a prelude. Every command runs validation first — task and session checks, runner availability, dirty-worktree guards — and narrates that as an unnumbered step. The numbered plan is announced only once pre-flight knows which path applies.

The MCP tools for the same operations relay the same phases as progress notifications.

Command Typical phases (after pre-flight)
lazy start Resolve integration base → create worktree → publish branch (when applicable) → launch agent
lazy unblock Prepare worktree → record feedback turn → launch agent; plus resolve file-permission violations when the task has any
lazy sync Check task branch on origin → fetch and resolve upstream → compare with upstream → prepare worktree → launch agent to merge
lazy reparent Repoint task to new parent → then the whole sync plan above, as one continuous checklist
lazy resume Prepare worktree → launch agent
lazy ask Prepare worktree → launch agent → wait for the agent's answer
lazy accept Branch-protection and merge gates → merge description → merge → finalize → clean up
lazy close Update task status → clean up worktree (when the task had a session); stop running agent first when applicable
lazy reject Same as close, plus close the remote PR/MR when one exists. The reason is recorded on the task; nothing is written to the PR

lazy submit is not in this table: it pushes the branch and marks the PR/MR ready for review, and launches no turn. It asks first: a protected target is a yes/no prompt (default No); an unprotected or unknown target requires typing the branch name or the task code. --yes skips that. MCP lazy_submit uses a confirmation_code instead — there is no --yes.

Inside the launch phase. Launching an agent is where a command can go quiet for the longest, because it may have to build the container image first. That interior is narrated under the launch phase itself, as indented continuation lines: which image is being resolved, whether a task-pinned or adopted image is in use, why a build is needed (no image for this lazy version yet, the Dockerfile or its inputs changed, or the image is older than the maximum age), the build's own output throttled to a readable rate, a still building (…) heartbeat while Docker is quiet, and finally whether the container was created or an already running one was reused. On a TTY these update the phase's line in place; piped, they append. The phase does not settle until the agent is actually launched, so the elapsed counter keeps running honestly throughout.

Narration is strictly observational — it never changes whether the command succeeds or fails, and a phase is only marked failed when the command itself fails. A sync that finds nothing to merge still settles its compare phase with "already up to date" and reports the remaining phases as skipped, rather than ending in silence; a sync whose fetch fails is not an error either — it queues the task for retry and says so on the upstream phase's own line.

lazy sync reconciles two things, in order

A task can be behind in two different ways, and sync handles both on the task's own branch:

  1. The task's own branch on origin. If a colleague has pushed commits to origin/<task-branch> — a review fix, a rebase of your work, a hand-written patch — those commits are merged into the worktree first. When the local branch is simply behind, this fast-forwards; when both sides have moved, it is a real merge, and any conflict is resolved by the task's own agent. The step is skipped, with a one-line reason on its own phase, when the remote driver is local, when lazy is offline, or when the branch is not on origin yet.
  2. The parent branch. Fetched and merged into the task branch.

Neither step ever touches, merges into, or pushes the parent branch — syncing a child does not reconcile its parent. Both merges are recorded on the task, so lazy show and the web review page name the ref and commit each one brought in ("Merged origin/lazy/my-task @ 4f2ab19", then "Merged main @ 9c1de07").

Sync itself never pushes. The reconciled branch reaches origin through the push that follows a turn, and through lazy accept.

Accept-specific behavior

Accept does not need a declaration. It works from any park where the turn ended normally — declared done, parked for a decision, or simply stopped — and requires only that every open blocking item is resolved in the same command. Deciding those items with the work in front of you IS the declaration.

The one park accept refuses is interrupted: a turn that was killed (watchdog, container death, lazy stop, a closed pairing session) has an unknown ending, because the agent never got to stop. Resume or unblock it first, or close it. Like every other acceptance precondition — branch sync, protection preflight, the review-issues gate — this is checked before anything is written, so a refusal leaves the task exactly as it was.

An accept can run for minutes — an opt-in acceptance gate, remote pushes, an LLM-written merge description, the merge itself. The announced plan is the plan that runs: the acceptance gate appears in it only when enabled, and a later lazy accept on a task already waiting on the forge shows a shorter plan (check remote state → finalize → clean up).

The merge description is an enhancement, never a gate. It is written by a one-shot agent run outside every git checkout, with write tools disabled and a ten-minute limit. If it fails or times out, the accept falls back to a plain list of the commits and proceeds. See conversation-import.md.

Status during an accept. The narration tells the caller who is watching what the accept is doing; the task's status tells everyone else (lazy list, lazy show, MCP):

Accept phase Status
Acceptance gate (opt-in) working
Merge phase (description, merge, finalize) merging
Aborted at any point the status held before the accept

No agent turn runs during the gate, so the task shows a bare working with no agent substate.

Working, but not alive

working is where the task is in its lifecycle; the part in brackets is what lazy can see running for it right now. working(not-alive) means the task is still recorded as mid-turn but nothing is running for it and no answer is waiting — the turn's process has died. It is a short-lived state: once the turn is at least 30 seconds old, the daemon's next check (every few seconds) either recovers a turn that had finished its work to blocked, or records the crash, moves the task to interrupted and resumes it automatically.

Two things that look similar are not that:

A task that reads working(not-alive) for more than a minute or so is a bug worth reporting. Include the liveness and session parts of lazy show <task> --json and the output of lazy daemon logs <short-id> --no-follow -n 20000, where <short-id> is the first 8 characters of the task's id in that JSON (daemon log lines name tasks that way).

Waiting on subtasks

An agent that splits its work into subtasks drives them with blocking lazy tools — lazy_wait (wait until a subtask finishes its turn) and lazy_ask (ask a subtask's agent a question and wait for the answer). While blocked in one of those calls the parent agent is doing nothing, and lazy shows that instead of a plain working(agent):

working(waiting on fix-foo (2m10s))

This appears in lazy list, lazy active, lazy status, lazy show, lazy watch, and the MCP substate field. Two concurrent waits list both labels; a larger fan-out is summarized (waiting on a, b +2).

Waiting outranks the turn-flavour substates (agent:answering, agent:reviewing) — those say what the turn is, waiting says what it is doing this second. A lazy-side phase (harness:<phase>) outranks waiting, and a dead run still reads not-alive. If the daemon is killed mid-wait, the marker is disregarded and the task falls back to working(agent).

Time spent waiting is recorded separately from the agent's own working time, so a turn that spent two hours blocked on a subtask does not bill those two hours to the agent in duration or cost reports. A failure to record a wait never affects the wait itself.

Agent-reported progress

The working substate answers who is active — agent, lazy, a wait, nothing. It cannot answer doing what. lazy_update_progress fills that gap: the agent posts a short line and every surface that shows the substate folds it in:

working(agent: running migration 3/7)

A progress post that cannot be recorded is simply lost; it never costs the agent its tool call.

Agent Failure Classification

Not every agent failure deserves the same response. A rate limit clears on its own; a revoked credential never does. So lazy sorts each failure into a class and acts on the class.

This is about credentials and conditions that go bad while a task runs. A daemon with no credential at all refuses to start in the first place.

The taxonomy

Class Examples Behaviour
fatal_auth 401/403, invalid API key, missing credential, credit balance exhausted Stop on the first failure
fatal_config unknown flag, invalid model, agent binary missing (exit 127) Stop on the first failure
transient_overload 429, 529, 503, "overloaded" Retry indefinitely, 5s→60s
transient_network ECONNRESET, ETIMEDOUT, socket hang up, fetch failed Retry indefinitely, 5s→60s
transient_unreachable ECONNREFUSED, ENOTFOUND Retry 5s→60s, bounded at 12 attempts (~9 min), then stop
unknown anything unrecognized Retry 15s→60s; the crash-loop detector still applies

A failure lazy does not recognize is classed unknown rather than guessed at — wrongly calling a failure fatal would block a task that would have recovered.

Each agent's own wording is recognized too (Claude Code's "Invalid API key · Please run /login", Cursor's "not logged in").

Retry pacing

Transient failures are retried at 5s → 10s → 20s → 40s → 60s (capped) — roughly 60 retries an hour.

The crash-loop detector (3 failures in a row, each under 10s) does not apply to transient classes: a 429 comes back in milliseconds, so three in a row would abort a turn that was about to succeed. It remains the bound for unknown.

transient_unreachable sits in between: a refused connection to a local proxy can heal if the proxy restarts, so it is retried generously, but not forever.

What "stop" means

When a failure is classed fatal (or transient_unreachable runs out of attempts), the turn ends and the task moves to blocked — not interrupted — with the class and reason recorded on it. Auto-resume only ever restarts interrupted tasks, so this is what keeps lazy from relaunching into a condition that will not clear, and puts the task in front of you with the reason attached.

A crash loop is different: its class is always unknown, recorded for diagnosis only. The task goes to interrupted and is auto-resumed, because many crash-loop causes are transient.

Conditions that never clear on their own are kept out of unknown. A Cursor plan quota spent or a spend limit reached is fatal_auth; Claude's usage limit reached is a 5-hour window that clears by itself and stays transient_overload. A Cursor quota message is only fatal when nothing in it says the limit clears soon: a rate-limit marker or a short reset horizon ("resets in 20 minutes") stays transient_overload, while a reset stated as a date ("when your monthly cycle ends on 9/19/2026") does not. When in doubt, lazy retries.

Seeing what is being retried

While a turn is retrying, lazy shows one line saying what and why — for example attempt 7 (transient_overload): API Error: 529 overloaded, with the error snippet truncated — in all of:

Watchdog kills

A no-progress watchdog kill has no error to classify — lazy killed the agent itself, after [agent] watchdog_output_timeout_ms (default 30 minutes; the timer resets on every completed step) passed with no forward progress. What happens next depends on one question:

Did the turn capture anything? Behaviour
Yes — a final result, or new commits Not retried. The work is already on disk, so relaunching would repeat it or hang the same way; the turn ends and you read what was captured
No — no result, no new commits Relaunched within the turn (5s → 10s backoff), at most 3 attempts. Each attempt costs a full no-progress window, so the bound is tight

If lazy cannot read git to check for commits, it treats the kill as having captured nothing and retries.

When a watchdog kill ends the turn, the task goes to interrupted with auto-resume, not blocked. The recorded turn and the interrupt reason say which guard fired, what its limit was, that keep-alive output does not count as progress, and whether lazy already relaunched.

Crash/Resume Lifecycle

When an agent crashes mid-turn, lazy detects it and tries to resume the task.

Detection

The daemon watches working tasks. A task whose agent container or process is gone without having finished its turn is marked interrupted. A task that became working within the last 30 seconds is left alone, so a container still starting up is not mistaken for a dead one.

What is recorded

The crash is recorded as a turn on the task (visible in lazy show), along with diagnostics: a reason (e.g. "OOM killed or SIGKILL (exit code 137)"), the exit code, the time, and the last lines of the container's logs. lazy show <task> displays them.

Every turn that dies leaves a visible record, even when the same failure repeats turn after turn: two identical fatal_auth failures on consecutive unblocks are recorded as two turns.

Circuit breaker

Each crash counts toward a limit of 3 consecutive interruptions. Once it is reached, lazy stops auto-resuming and the task stays interrupted until you run lazy resume or lazy unblock. An agent that crashes three times in a row without finishing a turn has something fundamentally wrong — out of memory, broken dependencies, an infinite loop — and needs a human to look.

The counter resets whenever a turn completes, and when you resume or unblock the task yourself. So only sustained, back-to-back crashes trip the breaker.

Auto-resume

Auto-resume continues the agent's existing conversation, so it keeps its full history — but the files on disk reflect wherever the crash left them, and the agent is told to verify before continuing. What else happens depends on whether the worktree has uncommitted changes:

The resume is recorded as a system turn, "Session interrupted and auto-resumed".

If the crash happened while lazy was merging upstream into the task and the merge failed, the task is not auto-resumed: it cannot make progress without that merge, and a human needs to look at the conflict.

Feedback is redelivered after a crash

Your feedback is saved before the agent is launched. If the turn then crashes before the agent finishes responding to it, the resume — automatic or lazy resume — delivers that feedback again verbatim, in place of the generic "you were interrupted" prompt. If several pieces of feedback went unanswered, the newest is reproduced and the prompt says how many others there are. A crash during the resume redelivers the same feedback again. Once an agent turn completes normally, all feedback before it counts as delivered and is never redelivered.

This applies to your initial task prompt, lazy unblock feedback, lazy ask questions, and PR comments and CI output delivered automatically. A comment made through lazy itself (lazy comment, the web UI, lazy_comment) is different: it never starts a turn, and reaches the agent with the next lazy unblock. lazy ask and lazy sync neither carry a comment nor use it up.

Three crash scenarios

Crash State after On auto-resume
Before any edits — agent read files, then crashed Worktree clean, no new commits Upstream merged; agent verifies and continues
Mid-edit — one file edited, another half-written Worktree has uncommitted changes No merge; agent reviews the changes, commits, fixes or discards them
After committing but before the turn finished Worktree clean, commits on the branch Upstream merged; agent finds its commits on the branch and continues. The commits are recorded on the task when a later turn finishes

A turn nobody read

If you unblock a task just as its agent finishes a turn, that finished turn is not lost: lazy still records it in the task's history, and remembers the agent session it ran in, so lazy pair resumes where the agent left off. A turn recovered this way is history only — it does not change the task's status or disturb the turn now running. If a turn's output genuinely cannot be recovered, the daemon log says so and names the task.

Manual recovery

If auto-resume fails or the circuit breaker fires: