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
- States
- Human actions
- Chatting with a stopped task
- Step outcomes
- Failure reasons
- Interruption is not failure
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_existsorbase_branch_missing, or aborted before it ever started, has no worktree, and opening a chat on it is refused with409 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 staysblocked, is not counted, and is retried by hand after its chat closes. The parent’scanceldoes to such a lane whatcancelalways 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
interruptedand 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_configmounts 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 answersunknownrather 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
checkexits 0. - command — the command exits 0, and any declared
checkexits 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 throughvincent 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 byteswhen 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 afor_each:over it cannot pick up a progress meter or aSwitched to branch …as an item. An attempt that recorded no stdout tail, which is any that ran before vincent kept one, falls back toresult_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, or a merge-back’s merge of a side task into its issue’s main worktree, conflicted. The worktree is left conflicted on purpose: resolve it, stage the files, and retry — the join commits your resolution and merges the rest. Cancelling or skipping a merge-back blocked here runs git merge --abort, so the issue’s next main task gets a clean worktree |
merge_source_missing |
A merge-back’s side task was deleted, or the side branch no longer exists in git, so there is nothing to merge. Skip or cancel the merge-back |
merge_target_missing |
A merge-back found the issue’s main branch gone at admission: every main task of the issue was archived and the branch the side task forked from was deleted too, so there is nothing to merge into. Skip or cancel the merge-back |
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 |
issue_branch_checked_out |
The task is one of an issue’s main tasks, and the issue’s main branch is checked out in the project’s own main checkout. An issue’s main tasks never run there, unlike a task adopting a branch. Switch that checkout to another branch, then retry |
repo_operation_in_progress |
The task is one of an issue’s main tasks, and the worktree it was to receive from the issue’s previous main task is partway through a merge, rebase, cherry-pick, revert or bisect. The previous task keeps the worktree. Finish or abort the operation there, then retry |
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:
- finalizes any step run still marked
runningasinterrupted, 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; - 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;
- 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
- Concepts — how a step runs.
- HTTP API — the action endpoints and the event stream.
- Using the TUI.