// docs/reference/task-lifecycle.md docs online

Task lifecycle

Every task moves through one state machine, defined in exactly one place in the source. Both the API (which returns 409 on an invalid action) and the execution engine consult it, so “what may happen next” has a single definition — and every task representation carries available_actions, so no client ever restates it.

This page is about tasks. A chat has its own, much smaller vocabulary — idle, running, awaiting_input, archived, handed_off, closed — kept deliberately separate so no task query or board legend has to decide whether it means chats too. It is documented with the chat routes. The one place the two meet is chatting with a stopped task, below.


The diagram

                 create
                   │
                   ▼
              ┌────────┐   slot free    ┌─────────┐  input request  ┌────────────────┐
   ┌─────────►│ queued ├───────────────►│ running │◄───────────────►│ awaiting_input │
   │          └────────┘  (scheduler)   └──┬──┬──┬┘    answer       └────────────────┘
   │   approve ▲   ▲ retry/skip            │  │  │
   │           │   │                       │  │  │ all steps
   │      ┌────┴───┴─┐   manual step       │  │  │ succeeded
   │      │ awaiting │◄────────────────────┘  │  ▼
   │      │  _gate   │                        │ ┌──────┐
   │      └────┬─────┘      step failed,      │ │ done │
   │           │ reject     retries exhausted ▼ └──┬───┘
   │           └─────────►┌─────────┐              │
   │                      │ blocked │              │ archive
   │ resume               └──┬──────┘              ▼
┌──┴─────┐  pause            │ abort         ┌──────────┐
│ paused │◄───────┐          ▼               │ archived │
└────────┘ (from  │      ┌─────────┐ archive └──────────┘
           queued/│      │ aborted ├──────────────▲
           running┘      └─────────┘

Tasks are queued immediately on creation, unless the create asked for paused — vincent task add --paused, or the new-task form’s start row — which puts the task straight into paused, an edge the diagram leaves off. It waits there, with no worktree and no agent started, until you resume it. There is no separate draft state.

done and aborted have one more edge out of them that the diagram leaves off to stay readable: follow_up re-queues either of them to run more work in the task’s existing worktree, and returns it to the state it came from. See human actions.

States

State Meaning Holds a slot?
queued Ready to run; waiting for scheduler admission — or, when queued_reason is set, for something else (see below) no
running A step process is executing, or about to yes
awaiting_gate Stopped at a manual step, waiting for approval no
awaiting_input The running agent asked a question; its process is alive and idle on stdin yes
awaiting_children A fan_out step’s lanes are running as child tasks; this task owns no process no
blocked A step failed and retries are exhausted; waiting for a human no
paused You asked it to hold; takes effect at the next step boundary. Or it was created held, and has not run yet no
done Every step succeeded. Worktree and branch retained for inspection. follow_up runs more work in them; archive tears them down no
aborted You cancelled, or rejected terminally. Worktree and branch retained, and open to follow_up on the same terms as done no
archived Terminal. Worktree removed, record kept — until a human discards it with vincent task delete, which is not an action on this page’s list and is the only thing that removes the row. A deleted task can be brought back from a backup with vincent task import. The branch is kept unless it has no commits past its base, in which case it is deleted (delete_empty_branch_on_archive) no

Three of these are worth dwelling on.

queued covers two different waits. Usually it means “waiting for a free slot”. But a task can also be queued waiting on a clock rather than on the queue, and it says so through two fields: queued_reason and admit_not_before, the instant vincent will try again. Either way it holds no slot while it waits, keeps its place in the queue, and needs nothing from you — when the wait is over the step runs on its own. The board shows the resume time on the row (queued → 14:20) and the detail header names the reason. Cancelling, pausing or otherwise acting on the task drops the wait immediately — a human action always means go.

Two reasons produce that wait, and they are worth telling apart:

queued_reason What it means
usage_limit The agent’s usage quota for the window is spent. The attempt is recorded interrupted and costs no retry; the wait ends at the reset the CLI named, or after usage_limit_recheck_interval when it named none. Whether there is a wait at all is usage_limit_auto_continue — under never, and under reported_only when the CLI named no reset, the task blocks instead. In the modes that wait, a task that never hit the limit can carry it too: it reached an agent step on an adapter whose window vincent saw close, and waits out that reset with no attempt recorded and no process started
retry_backoff The step failed and its next attempt is being paced by retry_backoff. The attempt is recorded failed with its own reason and does consume a retry; the wait is the configured duration. When the budget runs out the task blocks with the step’s own reason, with no wait first

retry_backoff is a queued_reason only, and is never a step’s failure reason either — the step’s row keeps whatever actually failed. usage_limit is a queued_reason in the default mode and a block_reason in the modes that do not wait; it names the same condition either way.

awaiting_children holds no slot, and offers cancel and retry. A fan-out parent waiting on its lanes owns no process, so keeping a slot would starve the queue for hours — and it is what makes fan-out deadlock-free at any depth, since a parent releases its slot before its children need one. It is not a reuse of awaiting_gate because approve, reject and skip would all be meaningless while children are still running: the states differ in what a human can do about them, which is the only thing the lifecycle is for.

The parent resumes on its own once every lane has settled — finished or ended — or, under a fan-out’s schedule: eager, as soon as a further lane settles, so such a parent parks and resumes more than once. A lane that is blocked, at a gate, or paused holds the join open until you deal with it; the children rollup on GET /v1/tasks/{id} (and the TUI’s awaiting_children (×2 ●1) row, where × counts blocked lanes) is where you see that. For blocked lanes, dealing with them is one retry on the parent: it re-admits every blocked lane beneath it, at any depth, without touching the parent’s own row, and the join closes itself when they finish. Fix the cause first — the retry re-runs each lane’s step exactly as it was.

awaiting_input keeps its concurrency slot. The agent process is alive mid-step, idle on its stdin; killing or re-queueing it would lose the very session the answer belongs to. So a forgotten question does occupy a slot, until input_timeout (default 24h) expires.

blocked is a resting state, not a failure. Nothing is silently abandoned: a step that exhausts its retries stops and waits for a person, and the task keeps its worktree, its branch and its transcripts while it does.

Repair when the worktree is what is wrong. retry re-runs an unchanged step, edit + retry can rewrite only that step’s own prompt or command, and skip advances past an unsatisfied check — none of them changes a file. When the fix is in the worktree, repair runs one throwaway agent there: you write what it should do, the daemon hands it the task’s context and the blocked step’s failure (its rendered prompt or command, the reason and exit codes, the tail of the failed attempt’s transcript and where to read the rest), and it works on the task’s own branch in the task’s own worktree.

It decides nothing. However the repair agent exits, the task comes back to blocked at the same step with the same reason, so you read the diff and then choose — retry, repair again, skip or cancel. It is recorded as a step run of its own under the reserved step id __repair at the blocked step’s index, which is why the timeline shows it as its own entry and why it does not eat the blocked step’s retries: after any number of repairs, a retry gets exactly the attempts it would have had with none.

Repair is offered from blocked whatever the reason. A task blocked before its worktree existed re-blocks on the same reason without starting an agent, and one blocked because its agent CLI is missing has its repair fail the same way — both honest outcomes rather than a hidden filter.

A finished task is not finished with until you archive it. done and aborted both keep the worktree, the branch and the commits, and follow_up is how you do one more piece of work in them without leaving vincent. You supply one of three things — an agent prompt, a shell command, or the name of a workflow from the registry — and the daemon runs it on the task’s own branch, in the task’s own worktree, recorded in the task’s own ledger with a step run, a transcript, events and cost accounting.

It decides nothing about the verdict: a done task comes back done and an aborted one comes back aborted, whatever the run did. That is deliberate — a command that exits 0 should not be able to reverse an abort you made on purpose. Follow-ups are repeatable, and each is a round: round 1’s rows sit one past the workflow’s last step index, round 2’s one past that, so the timeline shows ↳ follow-up 1, ↳ follow-up 2 rather than pretending the workflow grew.

A follow-up step that fails blocks the task at the follow-up’s own index. From there retry re-runs the follow-up where it stopped, repair runs an ad-hoc agent against that failure, skip abandons the follow-up and puts the task back where it came from, and cancel aborts. Edit-and-retry is refused there: an override rewrites a step in the task’s snapshot, and a follow-up is deliberately not in it.

One edge worth knowing about: because a follow-up’s process is live while it runs, cancel during one aborts the task — so done → aborted is reachable, which it was not before follow-ups existed.

Human actions

Action Valid from Effect
cancel queued, running, awaiting_input, awaiting_gate, awaiting_children, blocked, paused Kills any running process — graceful termination, then a kill after 10s — and moves to aborted. From awaiting_children it cascades to every unfinished lane, whose branches and worktrees survive
pause queued, running From running, finishes the current step then holds. The request is persisted, so it survives a daemon crash; any other action clears it
resume paused → queued
retry blocked, awaiting_children Re-runs the failed step as a fresh attempt with the retry counter reset → queued, or → paused when it is held. From awaiting_children it re-admits every blocked descendant instead and leaves the parent parked; the response says how many, and a held retry is refused there
edit + retry blocked Overrides the step’s prompt or command in this task’s snapshot only, then retries. The override is recorded on the step run. A parked fan-out parent refuses it: its fan_out step has no text to edit, so edit the blocked lane instead
repair blocked Runs one ad-hoc agent, prompted by you, in the task’s existing worktree and branch → queued, and back to blocked at the same step with the same reason when it exits. It does not consume the blocked step’s retry budget
skip blocked, awaiting_gate Marks the step skipped and advances → queued. A step skipped this way carries no skip_reason, which is how it stays distinguishable from one an if: guard skipped
answer awaiting_input Delivers the answer into the live agent session → running, and the step clock resumes
approve awaiting_gate Gate approved, advance → queued
reject awaiting_gate Gate rejected → blocked, from which you can edit-and-retry an earlier step, skip, or abort
set priority queued, paused Reorders scheduler admission
archive done, aborted Removes the worktree → archived, then deletes the branch only if it has no commits past its base. Refuses on a dirty worktree unless forced — uncommitted work would be lost, and a refusal never reaches the branch
follow_up done, aborted Runs one more piece of work — an agent prompt, a shell command or a registry workflow — in the task’s existing worktree and branch → queued (→ paused when held), and back to the state it came from when it ends. Repeatable; it never changes the task’s verdict and never spends the workflow’s retry budgets
chat blocked, awaiting_gate, done, aborted Opens a chat with an agent in the task’s existing worktree and branch. The task does not move, and it is locked — every other action but cancel is refused — until you close the chat

In the TUI these are the action bar keys (a, x, r, R, E, s, p, c, A, F, T); over the API they are POST /v1/tasks/{id}/{action}; from the CLI, vincent task <action> <id> — one subcommand each, spelled in kebab-case (follow_up is vincent task follow-up). set priority is the exception: it is a PATCH, and has no subcommand.

An action the current state does not allow returns 409 with details.state set to the state actually found — so a client branches on a value rather than parsing prose.

An action the state does allow never fails on a race. Each one is a compare-and-swap on the state the request read, and the task can move under it — the scheduler admitting a queued task is the usual way. When it does, the action is re-applied once from the state actually found, provided the table above allows it from there: cancel on a task the scheduler admits at that instant aborts the now-running task rather than reporting a conflict, and pause becomes the deferred kind that holds at the next step boundary.

Chatting with a stopped task

repair is one prompt and one run. When the fix needs a conversation — look at the failure, try something, check, adjust — open a chat on the task instead. It is offered from blocked, awaiting_gate, done and aborted, and it is an ordinary chat with one difference: it works in the task’s own worktree and on its branch, so the agent starts from the files exactly as the task left them and its edits are the task’s edits.

Its first message is preceded by what the task already knows, put together by the daemon: the title, description and fields, and then what the stop is about — for blocked, the same failure context a repair gets, including the tail of the failed attempt’s transcript and where to read the rest; for awaiting_gate, the gate’s instructions; for done and aborted, the last step’s summary and the abort reason. What you type follows it as written.

While the chat is open, the task is locked. Nothing moves it, and no action but cancel is accepted — retry, skip, approve, follow_up, archive and the rest are all refused with 409 task_locked_by_chat, naming the chat. A retry can never race the conversation, even between two of its turns. available_actions shows only cancel, where cancel is legal, or nothing; the task also carries open_chat_id, which is how a client finds the conversation. The task’s state, step, block reason and retry budget are exactly as they were when you close the chat. PATCH /v1/tasks/{id} and the pull-request actions are not actions on this page, and still work.

When you are done, close the chat. That ends the conversation and lifts the lock; it never touches the worktree or the branch, so you read the diff and then choose what the task does next — retry, skip, approve, archive. There is no “close and retry” in one step, on purpose. cancel on the locked task is the other way out: it stops the chat’s live turn, closes the chat and aborts the task, together.

A task can have at most one open chat. After you close it, the next stop can have a new one, and the closed ones stay listed on the task as its history.

A few edges:

  • No worktree, no chat. A task blocked on branch_exists or base_branch_missing, or aborted before it ever started, has no worktree, and opening a chat on it is refused with 409 task_has_no_worktree — vincent does not create a worktree for a conversation. A git operation in progress is not a refusal: a half-finished rebase is a fine thing to talk about.
  • A fan-out lane with an open chat is skipped by the parent’s cascading retry: it stays blocked, is not counted, and is retried by hand after its chat closes. The parent’s cancel does to such a lane what cancel always does to a locked task.
  • Cost stays on the chat. Its turns are the chat’s, not step runs, so they do not count toward max_task_cost_usd.
  • A daemon restart during one of its turns marks that turn interrupted and does not re-run it, like any chat’s. The chat stays open, so the task stays locked.
  • A task that runs in a container has its chat’s turns run inside that container too, with the container’s own home — so the agent configuration mount_agent_config mounts is the one it reads. Its skill list is the container’s as well, obtained by the image’s CLI inside it; if the container is gone, that list answers unknown rather than falling back to your host.

Open one with vincent task chat or POST /v1/tasks/{id}/chat, and close it with vincent chat close or POST /v1/chats/{id}/close — see the API for the details.

Step outcomes

Each attempt of each step is a step run row, and every attempt is kept — that is what the TUI’s timeline lists.

Outcome Meaning
succeeded Exit 0, a terminal result, and any check passed
failed A retryable failure; see the reasons below
interrupted The daemon stopped mid-step. Does not consume a retry
skipped You skipped it, or its if: guard was false. skip_reason is condition in the second case and empty in the first
stopped A condition step whose guard was false: the run ended here, deliberately, and the task is done
approved / rejected A manual gate’s two outcomes

A step succeeds only when all of its criteria hold:

  • agent — the process exits 0, and the event stream produced a terminal result rather than an error, and any declared check exits 0.
  • command — the command exits 0, and any declared check exits 0.
  • manual — a person approves.

On a retryable failure the step re-runs, up to max_retries (default 1, so two attempts). For agent steps the previous failure is appended to the retried prompt as a structured block, so the agent is told exactly what went wrong rather than asked to guess.

The re-run is immediate unless the step sets retry_backoff, in which case the task waits that long in queued — holding no slot — and the step re-runs when the wait is over. The budget is unchanged either way: the backoff decides when an attempt happens, never whether.

What a step says about itself

Beside the outcome, a step run carries two pieces of text that are not vincent’s words:

  • status_message — a line the running step wrote about itself through vincent status. While the step runs it is the live answer to “what is this doing”; the last value it set stays on the finished attempt. Empty unless the workflow asked for it, and empty always for the step types that run no process (manual, parallel, fan_out, condition, loop, break).
  • result_summary — the agent’s final result text, or the last 200 lines of a command step’s output: both its stdout and its stderr, because a step that failed with a stderr-only diagnostic must not summarize as blank. Capped at 4096 bytes, keeping the end — where a failure’s error is — cut at a line boundary where possible and marked … N earlier bytes when anything was dropped. Recorded on every attempt, and what the board, the detail view and the repair prompt show. A later step’s {{ .Steps.<id>.Result }} reads it for an agent step, and for a command step reads a narrower record of the same attempt — the 200-line tail of its stdout alone, so that a for_each: over it cannot pick up a progress meter or a Switched to branch … as an item. An attempt that recorded no stdout tail, which is any that ran before vincent kept one, falls back to result_summary.

Neither is a failure reason, and no surface renders either as one. The reasons below are a closed set vincent authors; these two are whatever the step produced, possibly long before it stopped.

Failure reasons

The reason is recorded on the step run and on the task when it blocks. The vocabulary is shared across the engine and the worktree layer: a reason means the same thing wherever it originated.

Reason Meaning
check_failed The check command exited non-zero
nonzero_exit A command step exited non-zero
agent_error The agent’s event stream reported an error
agent_unavailable The adapter’s CLI could not be resolved or started
agent_unauthenticated The agent CLI is installed but not logged in. Retries as usual, then blocks — log in and retry
usage_limit The agent’s usage quota for the window is spent. Not a failure: no retry is consumed. In the default mode the task waits queued until the window reopens and this is never a block_reason; under usage_limit_auto_continue never, and under reported_only when the CLI named no reset, it blocks instead — at the step it was on, cursor unmoved, attempt still interrupted, so a retry once the window reopens re-runs the step with a full budget
retry_backoff Not a failure reason at all, and never appears on a step run — it is the queued_reason of a task waiting out a step’s retry_backoff between two attempts. The attempt that triggered it keeps its own reason
timeout The attempt exceeded its timeout and was killed
input_timeout A mid-run question went unanswered past input_timeout
input_protocol_error A control message the adapter could not parse — it fails, it never hangs
template_error A template failed to render, before any process started
condition_error A step’s if: guard failed to render, or rendered something that is neither true nor false. The one reason that is not retried — a guard is evaluated before the step becomes an attempt, and re-rendering it cannot answer differently. Fix the workflow and retry
fan_out_invalid A derived fan_out list is not a lane list: an item that is not a JSON object (the message names the line), a rendered lane id that is not a slug, two items rendering to the same id, or a needs: edge to a lane the step does not declare, or a cycle among them. Nothing is spawned. Every one of these is a load-time error for a declared lanes: list; a derived list has no ids until it is rendered, which is why it blocks here instead. Fix the producing step’s output, or the lane template
fan_out_limit A derived fan_out list is too long: past the step’s max_lanes:, or past fan_out.max_tasks for the whole tree. Nothing is spawned. Raise max_lanes: on the step or fan_out.max_tasks in config, or narrow the list at its source. fan_out.max_depth is unaffected — it counts nesting, and a dynamic width does not nest
loop_limit A loop step cannot run within max_iterations: a for_each list longer than the ceiling, or a count: the ceiling moved under. It blocks rather than truncating or advancing — running out of tries is not a decision the workflow made, and advancing would tell every later guard the work is finished. Raise max_iterations: on the step or loop.max_iterations in config, or narrow the list at its source
restricted_unsupported The adapter cannot restrict on this platform (cursor on Windows). Task creation refuses these, so reaching it means the workflow or the machine changed after the task was queued
mcp_unsupported The adapter cannot be given vincent’s MCP server for this step. The step fails rather than running an agent that silently has no vincent tools. Set mcp.wire_steps: false if the step does not need them
transcript_limit The attempt hit transcript_max_bytes
cost_limit The task has spent past its cap: max_task_cost_usd, or the task’s own max_task_cost_usd when it was created with a lower one. Not a step failure: the finished step run keeps its own state and reason, no retry is consumed, and a retry that was due does not run. Checked after each attempt, so expect to overshoot by up to one. Raise the cap — config’s is hot-reloaded — and retry; retrying without raising it buys exactly one more attempt. A task’s own cap is fixed at creation, so when it is the lower one, raising config’s does not move the wall. Inert on agents that report no cost
tree_cost_limit The task’s whole fan-out tree — the root task and every lane below it at any depth, archived ones included — has spent past max_tree_cost_usd. The task that blocks is the one whose attempt crossed the line: usually a lane, sometimes the parent’s own step before the fan-out or after the join. Not a step failure, exactly as cost_limit. Every task still working in the tree finishes one attempt and then blocks too, so expect to overshoot by up to one attempt per such task. Raise the cap and retry the parent, which re-admits every blocked lane at once, or retry one lane; without raising it each re-admitted lane buys one attempt and blocks again. When both caps are passed at once the reason is cost_limit. Counts only what agents reported
transcript_io_error The attempt’s transcript could not be written, encoded or closed — a full disk, a revoked permission, a short write. The step fails rather than reporting a success over a record that is missing the run it describes. Retries as usual (a new attempt writes a new file), then blocks. Not what an over-long line produces: those are captured in partial pieces
agent_protocol_error Vincent could not read the agent’s stream to the end, so the transcript is missing lines the CLI wrote. Not agent_error — the CLI may have behaved perfectly; the reader that failed is vincent’s
shell_unavailable The requested shell is not installed
container_image_unavailable The task’s container.image is not on this machine and could not be pulled. Blocks at admission, before a worktree, a branch or a retry is spent — the image check is deliberately not made when the task is created
container_unavailable The container runtime answered when the task was created and does not now, or refused to create the container. A step the container was going to run is never moved to the host because of it — the task blocks instead
rejected You rejected a manual gate
canceled You cancelled the task
invalid_snapshot The task’s stored workflow snapshot is unusable
platform_unsupported The workflow is restricted to platforms this host is not (platforms:). Only reachable for a task created on another OS
input_unsupported The step declares on_input: require and its agent cannot take mid-run input. Refused at task creation, so only reachable when the agent changed underneath the task — or when the task runs in a container: creation judges your host’s CLI, the step the claude in the image
merge_conflict A fan-out lane’s merge conflicted. The worktree is left conflicted on purpose: resolve it, stage the files, and retry — the join commits your resolution and merges the rest
lane_failed A fan-out lane was cancelled or ended without finishing. Nothing of that round is merged — a partial round looks exactly like a complete one downstream. With needs: the step runs in rounds, and rounds that already merged stay on the branch: they were there before this one failed, and the task is blocked, not done, so nothing downstream consumes it. No further lane is spawned; lanes already in flight are left to finish. A lane list without needs: is one round, so for it this is still “nothing is merged”. Under schedule: eager there are no rounds: nothing new is merged by the admission that noticed the failure, and lanes merged by earlier ones stay. Fix the lane (it is an ordinary task), then retry the parent
interrupted The daemon stopped mid-step — not a failure
internal_error A bug. Please report it

One reason is a step run’s skip_reason rather than a failure:

Reason Meaning
condition The step’s if: guard rendered false, so the step was skipped. Not a failure — the task carries on with the next step

The worktree layer adds its own, raised while vincent prepares the task’s branch and worktree:

Reason Meaning
project_path_missing The project’s repository is not at its recorded path — it moved or was deleted
base_branch_missing The branch the task was to start from does not exist
branch_exists The branch this task would create already exists, usually from an earlier run
branch_name_invalid The branch name the task would use is not one git accepts
worktree_dirty The task’s worktree has uncommitted changes where vincent needs it clean
worktree_missing The task’s worktree is no longer on disk
worktree_path_occupied Something already occupies the directory the task’s worktree would use
git_error A git command vincent ran for the task failed; the message carries git’s own output

A branch_exists block is recoverable without losing the task: POST /v1/tasks/{id}/retry accepts a branch_override that renames the branch and re-admits it. See Troubleshooting.

Three more belong to a task created from a pull request (--github-pull), whose branch is the pull request’s head rather than one vincent cut:

Reason What happened
pull_fetch_failed The pull request’s head could not be fetched. There is nothing to fall back to — the fetched commit is where the branch has to be — so unlike the base-branch fetch this blocks rather than degrading quietly
pull_branch_diverged You have a local branch of that name carrying commits the pull request’s head does not. Vincent will not discard them; merge or rebase yours, or delete it if it is stale, then retry
pull_branch_checked_out That branch is already checked out in another worktree — vincent’s or your own main checkout. Git cannot put one branch in two worktrees. Switch that checkout to another branch, then retry
adopt_branch_missing The task was created on an existing branch (existing_branch) and that branch is gone. Recreate it, or retry the task with branch_override pointing at a branch that is there
adopt_branch_diverged The branch you asked the task to run on and its own upstream have each moved. Nothing was moved: merge or rebase, then retry
adopt_branch_checked_out That branch is checked out in one of vincent’s own worktrees, so another task or chat is working on it. Wait for that one and retry. Your own main checkout is not this case — a task adopting a branch you have checked out runs in that checkout

branch_override is refused on such a task: renaming its branch would detach it from the pull request it was created for.

Interruption is not failure

Vincent is crash-first: every transition is persisted before it is acted on. When the daemon starts, recovery:

  1. finalizes any step run still marked running as interrupted, keeping whatever status message that step had set — “what was it doing when the machine went down” is exactly what you want off that row;
  2. kills verified orphan processes — the recorded PID must still exist and still hold the very process the run spawned, checked against a platform-native identity recorded at spawn (start ticks plus boot id on Linux, the kernel fork stamp on macOS, the creation time on Windows, each paired with the PID, because the operating system’s stamp is a clock tick wide and processes started inside one share it). That is the guard against PID reuse killing an innocent process: anything vincent cannot prove is the same process is left running. A run recorded before vincent kept that identity — by an earlier version, or on a machine where the read is unavailable — falls back to the older check, a start time within five seconds of the recorded one;
  3. re-queues the owning task and re-runs the interrupted step as a fresh attempt that does not consume a retry.

Tasks found in awaiting_input are treated identically: the pending request is discarded with the process, and the fresh session may re-ask.

This is safe by construction because every agent step is a fresh session operating on a worktree whose committed state survives. Workflow authors help by having agents commit incrementally.

A graceful stop (vincent daemon stop, SIGTERM, a service stop) takes the same path: a daemon.shutting_down event, admission stops, running processes get 15 seconds before being killed, and their runs are marked interrupted. The API — SSE streams included — stays up through the grace period, so clients watch the wind-down live.


See also