Troubleshooting
The failures people actually hit, what each one means, and what to do.
- Start here:
vincent doctor - The daemon
- Agent CLIs
- Projects and worktrees
- Workflows
- Tasks that block
- The TUI
- Reading the log
- Reporting a bug
Start here: vincent doctor
When the symptom is “nothing is happening” and you do not yet know which section below you are in, run one command:
vincent doctor # 0 healthy · 1 problems found · 2 no daemon answered
It prints, in one pass: the config and data directories, whether config.yaml
parses and whether either is readable beyond its owner (with the chmod that
fixes it); the daemon’s status, pid, port, uptime and version; the daemon log’s
size and last lines; the database’s size on disk (including its WAL sidecar),
schema version, PRAGMA integrity_check, per-table row counts,
workflow-snapshot bytes and how far back its event history reaches; every agent
CLI with its path, version and
logged_in; whether the
GitHub integration can read issues, and
if not which piece is missing; whether the
skills vincent publishes are installed and
current for your agents; whether a newer vincent has been
released and whether the running daemon is
older than the binary you just ran; whether
scheduled backups are on, when the last
one succeeded and when the next is due; free disk, worktree count and bytes, and any
orphaned worktrees; and task counts by state — so “12 blocked” is visible
without opening the board.
It works with no daemon, which is the point: the daemon being down is one of
the answers. In that mode it exits 2, still prints everything it can read from
disk, and reports the database and task rows — and, with scheduled backups on,
how the last one went — as unknown — daemon not running rather than opening
SQLite behind the daemon’s back.
The bottom of the report is a PROBLEMS table — the closed set that makes it
exit 1: a config.yaml that does not parse, a daemon alive but unresponsive,
a failed integrity_check, a database written by a newer vincent, orphaned
worktrees, an unreconciled task, or — with backup.interval set — a scheduled
backup whose last attempt failed. A missing or logged-out agent CLI is printed
plainly and deliberately does not set the exit code, so doctor stays usable
in a script on a machine that only ever installs one of the three adapters.
An unreconciled task is one whose state and step runs contradict each
other — it is queued (or finished) while one of its step runs is still marked
running. A fan_out step’s own row does not count: a parent between rounds
is queued with that round’s row deliberately left open, which is normal
operation rather than a contradiction. Crash recovery finalizes the old attempt
before the task returns to the queue, so this means recovery could not
complete: the task is refused by admission and will sit at queued forever. Recovery runs at startup, so
restarting the daemon retries it; the daemon log says why it failed the first
time.
vincent doctor --json > doctor.json # the whole report, for a bug report
vincent doctor --fix # reclaim orphaned worktrees, compact the DB
vincent doctor --fix --force # also remove orphans that have local changes
--fix needs a running daemon — every repair is a write, and only the daemon
writes. It reclaims orphaned directories (an entry under a data root that no
task row claims — the same set vincent gc
reclaims, run by the same code) and runs a real VACUUM. Two refusals are by
design and are reported rather than hidden: an orphan with local changes is
skipped until you pass --force, and the VACUUM is skipped while any task is
in flight, because it would stall that task mid-step.
Vincent does not run git worktree prune in your repositories. After doctor
removes an orphan, the repo it came from may still carry a stale registration;
git -C <repo> worktree prune clears it.
The daemon
Every subcommand exits 2
Exit 2 means nothing answered. Start one:
vincent daemon start
vincent daemon status # 0 healthy, 1 not running, 2 unresponsive
Subcommands never auto-start a daemon by design — only the TUI does. If
daemon status says unresponsive (exit 2 with a daemon present), the process
is alive but not serving; vincent daemon stop --force clears it.
“A daemon is already running”
Single-instance is enforced with a lock file in the data directory that releases
automatically when the process dies. If you get this after a hard kill, check
whether the daemon really is running (vincent daemon status) before assuming a
stale lock — an orphaned lock is rare, an orphaned daemon is not.
The TUI starts its own daemon even though the service is installed
Almost always: the service and your shell resolved different data
directories, so the TUI looked for daemon.json where the service did not
write one.
- On Windows, this is the signature of a pre-Scheduled-Task install that ran as LocalSystem. See Running at login → upgrading.
- Otherwise, check whether
VINCENT_DATA_DIRis set in one context and not the other, and reinstall the service from the shell you actually use.
The daemon starts and immediately exits
Read the log — vincent daemon logs, which reads {data_dir}/logs/daemon.log
from disk and so still answers here. An invalid config.yaml at startup is fatal
and says which key and line. (An invalid config on hot reload is not fatal —
the last good config keeps running and the rejection is logged.)
Crash recovery is fatal too. If it cannot close a previous run’s open step
runs, the daemon refuses to start — startup failed: crash recovery, with the
underlying error beside it, a storage failure nearly every time. Nothing is
half-done: each task is reconciled whole or left exactly as it was found, so
starting the daemon again simply retries it.
Agent CLIs
An agent CLI is not found
vincent agents and the TUI’s daemon view
list what the daemon resolved.
If your shell finds the binary and the daemon does not, it is a PATH problem —
in this order:
- Service-installed daemons capture
PATHat install time on macOS and Linux. Install a CLI after that, and the service cannot see it. Fix:vincent service installagain, from a shell wherewhich claudeworks. -
Point at it directly and skip
PATHentirely:agents: claude: { path: "/opt/homebrew/bin/claude" } - Cursor: vincent resolves
cursor-agent, nevercursor—cursoris the editor launcher. If onlycursoris on yourPATH,cursor-agentis not installed.
A task that runs in a container
uses the CLI in the image, not yours, so none of the above applies to it:
agents.*.path is ignored there, and agent_unavailable means the image has
no claude, codex or cursor-agent on its own PATH. Check with
docker run --rm <image> /bin/sh -c 'command -v claude'. On macOS, a
containerized claude step that is not logged in while your own claude is has hit
the Keychain gap — claude’s login does not reach the container; see
Agents in a container.
Detection is cached by binary identity (path + mtime + version), so upgrading a
CLI is picked up automatically. Force a re-probe with R in the new-task view
or GET /v1/agents?refresh=true.
The adapter is found but every run fails
Suspect authentication. An installed-but-unauthenticated CLI probes as perfectly
healthy and then fails at the first API call. vincent doctor prints the answer
per adapter under AGENTS, re-probing each time:
AGENTS
codex found 0.147.0 auth NOT LOGGED IN /opt/homebrew/bin/codex
Run the CLI by hand once and log in (codex login, cursor-agent login,
claude auth login or claude interactively), then run doctor again. All three
answer this cheaply, so vincent asks them. A claude older than 2.1.41 has no
auth status command, so it shows unknown rather than claiming fine. ok
means the CLI found credentials configured, not that they were checked.
restricted_unsupported
The step asked for permission_mode: restricted and the adapter cannot restrict
on this platform. Today that is cursor on Windows: its sandbox requires macOS
or Linux.
Most of the time you will not see this reason at all, because task creation
refuses it first: creating a task whose restricted step resolves to such an
adapter answers 400 naming the step and the agent. Seeing the block reason
means the task and its daemon parted company after creation — a data directory
carried to Windows, or a workflow edited after the task was queued.
The step fails rather than running unrestricted, which is deliberate — a
restricted mode that quietly isn’t restricted is worse than none. Use claude or
codex for that step on Windows, or drop the step to full-auto knowingly.
mcp_unsupported
The step was to be wired to vincent’s own MCP server — the default — and the adapter cannot carry one for a single run.
All three shipped adapters can, so you will not meet this: claude takes
--mcp-config, codex takes -c mcp_servers.… overrides, and cursor gets a
.cursor/mcp.json written into the task worktree. None of them checks your CLI
build for MCP support. The reason is reserved for a future adapter with no
per-run MCP surface.
The step fails rather than starting an agent that silently has no vincent
tools, because a prompt written against those tools would otherwise burn a run
producing work premised on a channel that was never there. If the step does not
need them, set mcp.wire_steps: false —
it is hot-reloaded — and retry.
“untested version” beside an adapter
Not a fault, and nothing to fix. Vincent pins the CLI builds its parsers were
captured against; anything else reads untested, which is the normal state a few
weeks after any vincent release because agent CLIs ship far more often than
vincent does. It changes nothing: no run is refused, and vincent doctor does
not exit 1 over it.
It is worth knowing when something else is already wrong — a stream that parses
oddly, a flag the adapter stopped finding — because it tells you whether you are
on ground vincent has walked. incompatible version is different: that is a
build vincent knows breaks. No such build has been observed for any adapter, so
that list currently ships empty.
A workflow is listed but cannot be selected — platform_unsupported
The workflow declares a platforms: list this host is not in
(schema). vincent workflow ls
shows it with status unsupported and the platforms it needs, the TUI’s
workflow view says “not on this platform”, and the new-task picker refuses it —
creating the task would only produce a run that fails at the first cat or
Get-ChildItem.
Three honest fixes, in order of preference:
- run the task on a host the workflow names;
- widen the list, if the workflow really is portable —
[posix, windows]is the same as removing the key; - write a host-specific copy under the project scope, which shadows the global one by name.
The block reason platform_unsupported on an existing task means the task was
created elsewhere and its data directory has since moved between machines. The
snapshot is fine; it is the host that changed.
An agent cannot be picked for a workflow — input_unsupported
The workflow has a step declaring
on_input: require: it needs an
agent that can stop mid-run and ask you something. Codex and cursor never can —
neither CLI has a control channel — and claude can only inside the CLI version
family vincent has verified the protocol against.
Where you meet it:
- The new-task picker greys the agent out, and
POST /v1/tasksanswers400naming the step. Pick an agent whose row invincent agentscarries nono mid-run inputnote — or whoseinput_verdictinvincent agents --json(orGET /v1/agents) issupported. - The workflow fails to validate when a step pins
agent: codexoragent: cursoroutright. Change the pin, or droprequireback towaitif the questions are optional after all. - A task blocks with
input_unsupportedwhen the answer changed after the task was created — almost always a claude upgrade past the verified family. Checkvincent agentsfor theno mid-run inputnote beside itsBUILDverdict; the fix is on the machine, not in the workflow, andretryrefuses until it is done rather than reproducing the block. - A task in a container blocks
with
input_unsupportedwhen the claude in its image is outside the verified family. Task creation andretryjudge your host’s claude, so neither catches it; only the step checks the image’s, withclaude --versioninside the container, andvincent agentsdoes not show that one. The fix is the image, and a retry blocks again until it is.
An agent that is not installed never triggers any of this: an unknown
verdict is not a refusal, and you get agent_unavailable at run time instead if
it is still missing.
Every cursor tool call is blocked on Windows
If cursor steps run, produce no edits, and report blocked tool calls, check
what started the daemon. A daemon parented by Git Bash carries MSYSTEM,
and cursor-agent imports Claude Code’s hooks and composes them as PowerShell
while executing them with bash — so every hook errors, and an erroring hook
blocks the call. It is a Cursor interop bug on Windows, not a vincent one.
unset MSYSTEM in the launching shell does not help: the MSYS runtime
re-injects it into every child. Vincent sets the child’s environment block
directly, so this does:
environment:
unset: [MSYSTEM]
A daemon started by vincent service install (a Scheduled Task in your logon
session) never had MSYSTEM in the first place.
The agent received C:/Program Files/Git/review instead of /review
You sent a skill invocation from Git Bash — vincent chat send 12 /review
— and the agent got your Git install root with the skill’s name on the end.
The MSYS2 runtime converts any argument that looks like a POSIX path into a
Windows one before vincent starts, and says nothing when it does; vincent
sends what it was given, and cannot tell it was ever /review. A
/plugin:skill name is left alone, which is why only some invocations break.
Either fix works:
MSYS_NO_PATHCONV=1 vincent chat send 12 '/review the auth change'
printf '%s' '/review the auth change' | vincent chat send 12 --message-file -
--message-file takes the message out of argv entirely, so it works in every
shell — and it is the fix for the other silent rewrite too, a $name inside
double quotes that bash, zsh and pwsh expand. See
quoting a skill invocation.
A restricted codex step cannot commit
In a linked worktree the real git directory lives under the main repository,
outside --sandbox workspace-write. Move the commit into a command step,
which runs outside the agent’s sandbox.
Projects and worktrees
project add refuses: no default branch
Vincent detects the default branch from origin/HEAD, then a local main, then
master, then the current branch. A detached or unborn HEAD hits none of them:
vincent project add /path/to/repo --default-branch main
worktree_dirty
The task’s worktree has uncommitted changes at a point where vincent needs it clean — most often when archiving. Archiving removes the worktree, so uncommitted work would be lost; it refuses unless you force it.
Inspect the worktree first (its path is on the task detail view), commit or discard, then archive again.
branch_exists / worktree_path_occupied
The branch this task would use, or its worktree directory, already exists from an earlier run. Vincent never deletes a branch that carries a commit, so leftovers accumulate if you re-create tasks with the same title — or if a branch template has no discriminator in it, in which case the second task for the same input collides every time. (Archiving cleans up only the branches that received no commit at all, which is precisely the set that never collides with anything you would miss.)
Most collisions are caught at creation with a 400, but the check at creation is
racy by nature, so this block is the authority.
Two ways out. Give the task a different name and let it re-admit, which keeps its history and transcripts:
curl -X POST .../v1/tasks/7/retry -H 'Content-Type: application/json' \
-d '{"branch_override":"feat/second-attempt"}'
Or delete the leftover branch yourself, then retry:
vincent task ls --archived # find the branches vincent made
git worktree list
A related failure is a ref hierarchy conflict: feat/foo cannot be created
while feat/foo/bar exists, because git stores refs as a path hierarchy. The
message names the branch that is in the way.
worktree_path_occupied has a second cause with a different fix: a crash between
creating the worktree and recording its path leaves the directory on disk while
the task claims nothing, so every later admission finds it occupied. Run
vincent gc — it reclaims exactly the
directories no task claims — then retry the task. vincent gc --dry-run shows
you the path first, and --force is needed when git cannot judge the directory
because the repository behind it is gone.
base_branch_missing / project_path_missing
The base branch was deleted, or the repository moved. Re-point the project
(vincent project edit <id> --path /new/path, add --default-branch if the
stored one is gone, or edit it in the projects view) or set the task’s base
branch to one that exists.
pull_fetch_failed / pull_branch_diverged / pull_branch_checked_out
These three belong to a task created from a pull request
(--github-pull),
whose branch is the pull request’s head rather than one vincent cut. All three
happen at admission, before any step runs.
pull_fetch_failed means the head could not be fetched. Unlike the base-branch
fetch — which degrades quietly, because the local base is always a valid answer
— there is nothing to fall back to here: the fetched commit is where the branch
has to be. Check the network and your GitHub credential (vincent doctor), then
retry.
pull_branch_diverged means you already have a local branch of that name and it
carries commits the pull request’s head does not. Vincent never reset --hards
it — your unpushed commits are still there. Merge or rebase yours onto the head,
or delete the branch if it is stale, then retry. A branch merely behind the
head is fast-forwarded and never blocks.
pull_branch_checked_out means that branch is already checked out in another
worktree — another vincent task’s, or your own main checkout. Git cannot put one
branch in two worktrees. Switch that checkout to another branch, then retry.
Within vincent a second task for the same head branch is refused earlier, with a
400 at creation.
branch_override is refused on such a task, so the branch_exists escape
hatch above does not apply: renaming its branch would detach it from the pull
request it was created for, and every later commit would go somewhere that pull
request never sees.
adopt_branch_missing / adopt_branch_diverged / adopt_branch_checked_out
These three belong to a task or chat created on a branch that already exists
(--existing-branch, existing_branch), which runs on that branch instead of
cutting one.
adopt_branch_missing means the branch is gone. Creation checks that it is
there, but a task can sit queued while you delete it; recreate the branch, or
retry with branch_override pointing at one that exists.
adopt_branch_diverged means the branch and its own upstream have each moved.
Nothing was moved — your commits and the remote’s are both still there. Merge or
rebase, then retry. A branch merely behind its upstream is fast-forwarded, and
one ahead is left exactly where it is, because those commits are yours to
push.
adopt_branch_checked_out means the 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 deliberately not this case: a task adopting
a branch you have checked out runs in that checkout, alongside you.
The detail view’s worktree row — worktree_path on the task over the API —
is the project path itself when that happens.
Two things follow from running there. Vincent removes nothing when such a task
is archived — the directory is yours — so worktree_dirty never applies to it.
And the diff tab shows the working directory’s diff, so uncommitted work you
already had when the task started reads as part of the task’s diff.
Workflows
A workflow file does not show up
vincent workflow lsshows built-in and global scopes only. Add--project <id>to include that repository’s.vincent/workflows/.- The daemon reloads on save; a file that fails to parse is reported as
invalid and the previously loaded version keeps running. The workflows view
shows validation status per entry, and
vincent workflow validate <file>tells you exactly what is wrong. - Check the scope: a project file shadows a global file of the same name.
template_error
A template referenced something that does not exist. Rendering uses
missingkey=error on purpose, so this fails the step before any process
starts rather than writing a silent hole into a prompt.
The usual cause is an optional task field read directly. Read it defensively:
{{with index .Task.Fields "ticket"}}Ticket: {{.}}{{end}}
The other cause is .Steps.<id> for a step that has not completed — .Steps
holds completed steps only.
condition_error
A step’s if: guard did not produce a verdict. Guards must render to exactly
true or false; anything else is this error, including the two spellings a
broken guard usually produces:
- an empty string or
<no value>— the guard is reading something that is not there.{{ .Steps.probe.ExitCode }}for a step that has not run yet does this; - a value that is merely truthy —
yes,1,0, a number. Loose truthiness is deliberately not accepted, because it would treat the two cases above as decisions.
Write comparisons, not values:
if: '{{ ne (index .Steps "probe").ExitCode 0 }}' # ✅ renders true or false
if: '{{ index .Steps "probe" }}' # ❌ renders a struct
This is the one block reason vincent does not retry. A guard is evaluated before the step becomes an attempt, so there is no attempt to retry, and re-rendering the same template against the same context cannot answer differently. Fix the workflow file and retry the task — the guard is re-evaluated from scratch, as it is on every pass.
vincent workflow validate <file> catches a guard that does not parse.
vincent workflow render <file> goes one step further and executes it against
a preview context, so a guard that references a field nothing supplies shows up
there — it prints what each guard renders to, and warns when that is neither
true nor false. Neither can tell you the guard renders the wrong answer
for a particular task: that needs the task’s own context, which only exists at
run time.
loop_limit
A loop step has more iterations ahead of it than max_iterations allows, so
the task blocked instead of doing some of them. Two ways to get here:
- a
for_each:list longer than the ceiling. It blocks before the first iteration and names the count, so nothing has run yet; - a
count:the ceiling moved under —loop.max_iterationswas lowered while the task was queued or paused. The ceiling is read per loop, so a config reload reaches a task already in flight.
It blocks rather than truncating the list or advancing past the loop, because a
loop that ran out of iterations did not do what it was looping for, and
advancing would hand every later guard a .Steps saying the work is finished.
condition (the step type) is what a workflow uses to stop and succeed;
running out of tries is not that.
Three fixes, in the order worth trying:
- filter the list at its source.
git diff --name-only … | grep -v _test.gois usually what you wanted anyway, and it keeps the filtering where the output already is; - raise the ceiling for this loop with
max_iterations:on the step; - raise it for the machine with
loop.max_iterationsinconfig.yaml. The default of 10 is low on purpose: ten iterations of a three-step body is thirty agent runs.
A loop ran fewer iterations than I expected after a retry
Working as intended. A loop’s position is derived from its step rows on every
admission, so a retry resumes at the body step that failed, in the iteration
it failed in — body steps that already succeeded in that iteration are not run
again, and earlier iterations are not repeated. The detail view groups the rows
by iteration, folded, with the latest one open, which is where to check what
actually ran.
If you wanted the whole loop again, the loop is not the unit to retry — retry
the task from an earlier step, or skip the loop and start over.
A workflow finished having run only half its steps
Almost certainly working as intended. Two things end a run early:
- a
conditionstep whose guard was false. The task isdone, and the detail view shows astoppedrow where it ended; - a chain of
if:guards that all evaluated false, each leaving askippedrow with reasoncondition; - a
breakinside a loop, which ends the loop successfully and moves on — astoppedrow on the break step, and the loop’s own iterations stopping short of itscount:.
Both are visible in the task’s step list, which is the place to look: the board shows a finished task as finished, and does not try to say how much of the workflow it walked.
If it was not intended, the guard is the thing to read. .Steps holds only
steps the workflow has moved past, so a guard naming a later step, or naming a
step that was itself skipped, evaluates against something you did not mean.
shell_unavailable
The shell a command step asked for is not installed. On Windows vincent uses
pwsh -NoProfile -Command and falls back to powershell; a step pinned to
shell: sh on a machine with no POSIX shell fails here. Pin a shell that exists,
or write the command so the platform default can run it.
container_image_unavailable
The task’s container.image is not on this machine and could not be pulled —
a typo in the tag, a private registry the daemon is not logged in to, or no
network. The task blocks before a worktree or a branch is created and
without spending a retry, so nothing has to be cleaned up: fix the image or
the login, then retry the task.
The check is deliberately not made when the task is created. Pulling a multi-gigabyte image inside the create request would run it against the API’s timeouts, and refusing anything not already on disk would reject every first run on a fresh machine.
Pull it by hand to see the runtime’s own error:
docker pull ghcr.io/acme/dev:latest
container_unavailable
The container runtime answered when the task was created and does not now — the desktop app quit, the socket was revoked, the binary was uninstalled — or it refused to create the container. Start the runtime and retry.
A step the container was going to run is never moved to the host because
the runtime failed — the task blocks instead. A step that is not
contained after asking to be contained has inverted the choice the workflow
made, which is the same rule that makes an agent refuse an unsupported
restricted mode rather than quietly running full-auto.
Validation warns about a model
A value in no catalog at all passes with a warning rather than an error: the CLI is the final authority on what your account can run. A value found in a different adapter’s catalog is an error — that is a claude effort reaching a codex step, and it is a real mistake.
Tasks that block
A step that exhausts its retries blocks the task and waits for a human. Nothing
is ever silently abandoned. From blocked you can retry, edit-and-retry, repair
with a one-off agent, skip the step, or cancel the task.
Repair is the one that can change files: it runs a throwaway agent, prompted by
you and handed the blocked step’s failure context, in the task’s existing
worktree and branch. It always returns the task to blocked at the same step
with the same reason, so you look at what it did before deciding to retry. See
repairing a blocked task.
The block reason names what happened:
| Reason | Meaning |
|---|---|
check_failed |
The step ran but its check command exited non-zero |
nonzero_exit |
A command step exited non-zero |
agent_error |
The agent’s own 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 (see below) |
usage_limit |
The agent’s usage quota for the window is spent — not a failure. By default the task waits and re-runs itself; with usage_limit_auto_continue turned down it blocks here instead (see below) |
retry_backoff |
Not a block reason: it is what a task shows while a step’s retry_backoff paces the next attempt (see below). The failure itself keeps its own reason |
timeout |
The attempt exceeded its timeout and was killed |
input_timeout |
A mid-run question went unanswered past input_timeout |
template_error |
A template failed to render (see above) |
condition_error |
A step’s if: guard rendered nothing usable (see below) |
loop_limit |
A loop has more iterations to run than it is allowed (see below) |
restricted_unsupported |
The adapter cannot restrict on this platform; refused at task creation, so this is a workflow or machine that changed afterwards |
mcp_unsupported |
The adapter cannot be given vincent’s own MCP server for this step (see below) |
platform_unsupported |
The workflow’s platforms: list does not admit this host |
input_unsupported |
A step needs an agent that can answer questions mid-run, and this one cannot (see below) |
transcript_limit |
The attempt’s transcript hit transcript_max_bytes |
cost_limit |
The task has spent past max_task_cost_usd (see below) |
tree_cost_limit |
The task’s fan-out tree has spent past max_tree_cost_usd (see below) |
transcript_io_error |
The attempt’s transcript could not be written or closed (see below) |
agent_protocol_error |
Vincent could not read the agent’s stream to the end (see below) |
rejected |
You rejected a manual gate |
shell_unavailable |
The requested shell is not installed |
interrupted |
The daemon stopped mid-step — not a failure; the step re-runs and does not consume a retry |
invalid_snapshot |
The task’s stored workflow snapshot is unusable |
check_failed is the common and healthy one: it means a check caught something
the agent claimed was done. Read the step’s transcript, then E to edit the
prompt and retry with better instructions.
usage_limit — do nothing, unless you asked to be told
The agent CLI stopped because your account’s usage quota for the current window is spent. By default vincent treats this as a wait, not a failure:
- the attempt is recorded
interruptedand consumes no retry; - the task goes back to
queuedand gives up its concurrency slot, so other work carries on; - the row shows the time it will resume —
queued → 14:20on the board, andqueued · usage limit → 14:20in the detail header; - when that time comes the step re-runs by itself. There is nothing to press;
- every other task on that agent waits too. When one reaches an agent step while
the window is still shut, it goes back to
queuedwith the same reason and resume time without starting the agent, so its timeline shows no attempt.
The resume time is the reset the CLI reported. When it reported none, vincent
waits usage_limit_recheck_interval
(default 15 minutes) and tries again, repeating until the window reopens. If you
know your plan’s window, set that knob to match it.
If you would rather not wait, cancel the task. Pausing and resuming drops the wait too, but while vincent’s recorded window for that agent is still shut the task goes straight back to waiting on it, without starting the agent.
If it is blocked here instead of waiting, someone turned
usage_limit_auto_continue
down from its default. never blocks every quota stop; reported_only blocks
the ones where the CLI named no reset time and vincent would have been guessing
at the window. Nothing is lost: the attempt is still interrupted, no retry was
consumed, and the task is still on the step it stopped at — press r once the
window reopens and the step re-runs with a full budget. The board’s agent badge
still names the window, so you can see what you are waiting for.
The reason to turn it down: vincent recognizes a spent quota from the CLI’s wording, and a stop it recognized wrongly would otherwise re-queue itself and spend money on the next attempt with nobody watching. If you have seen a task that keeps re-running against a quota that is plainly not spent, that is the switch.
The daemon remembers which adapter it was. A human action drops the task’s
own wait, but the observation is per adapter and outlives it: the board header
badges that agent (claude ⏳14:20), the daemon view names the reset beside its
path and version, and the new-task form warns before you queue more work against
the same window. → means the CLI stated that time; ≈ means vincent estimated
it from the recheck interval. It clears itself the next time a step on that
adapter succeeds — so if the badge is still up, nothing has proved otherwise yet.
That is the observation path, and it is what an adapter with no reported
reading is rendered from. Where the adapter reports one — codex on request,
claude through its status line — the reading wins and the daemon view spells it
out window by window instead. See
Agents › How much quota is left.
Only the claude adapter recognizes usage-limit wording today. On codex and
cursor a quota stop still surfaces as agent_error or nonzero_exit, because
their wordings have not been captured from a real run and vincent will not guess
at one: a wrong guess would park a genuinely failed task forever.
retry_backoff — also do nothing, but for a different reason
A task showing queued · retry backoff → 14:20 in the detail header (and
queued → 14:20 on the board) is not walled by a quota. Its
step failed, it has retries left, and the workflow asked for a pause between
attempts with retry_backoff.
Like a quota hold it gives up its concurrency slot and comes back on its own.
The difference is the cost, and it is the thing to read the row for:
usage_limit |
retry_backoff |
|
|---|---|---|
| The attempt | interrupted |
failed, with the reason it actually failed with |
| Retry budget | untouched | one spent |
| What ends the wait | the quota window reopening | the configured duration elapsing |
| If it keeps happening | it waits again, indefinitely — unless usage_limit_auto_continue says to block |
the budget runs out and the task blocks with the step’s own reason |
So a task that keeps reappearing on retry_backoff is a task on its way to
being blocked — the transcripts of the attempts already made are what say why.
A quota hold is not: it will sit there until the window returns.
If you would rather not wait out a paced retry, any human action drops it — cancel, or pause and resume.
agent_unauthenticated
The agent CLI is installed and runs, but is not logged in. This one does
block — waiting cannot fix it. Log in with the CLI’s own command
(claude auth login or claude interactively, codex login,
cursor-agent login), then r to retry the task.
vincent doctor catches most of these before a task is ever created: it reports
logged_in for claude, codex and cursor, re-probed on every run. What it cannot
catch is credentials that are configured but no longer valid — an expired API
key still reads as logged in — and a claude older than 2.1.41, which has no
auth status command; there the first sign is still a failed step.
transcript_limit
One attempt produced more output than transcript_max_bytes (default 512MB).
Vincent kills the process rather than filling your disk, and writes the tripping
annotation into the file so it records why it ends. Usually this means an agent
or command is in a loop; fix that rather than raising the cap.
cost_limit — raise the cap and retry
The task’s cost so far — every attempt of every step, retries included — has
passed max_task_cost_usd.
Vincent stopped it rather than letting it keep spending.
It is a policy stop, not a broken step. The step run that just finished keeps
its own state and reason — if it succeeded, it still reads succeeded — it
consumed no retry, and a retry that was already due did not run.
What to do:
- Look at what the money went on. The detail view shows cost per attempt, and a task that spent more than you expected has usually been looping — an agent redoing the same work every turn, or a step retrying into the same wall.
- If the work is worth it, raise
max_task_cost_usdinconfig.yamland pressrto retry. The file is hot-reloaded, so no restart is needed. If the task was created with its own, lowermax_task_cost_usd, that is the wall it hit: it is fixed at creation, so raising config’s does not move it. - If it is not, cancel the task, or skip the step and let the rest run.
Retrying without raising the cap makes exactly one more attempt, then blocks here again. That is deliberate — the check runs after an attempt finishes, so a retry always buys one attempt of progress, and pressing retry is your decision to spend it. Nothing is lost either way, but nothing is free either.
Three things that surprise people:
- You overshoot by up to one attempt. An agent reports what it spent when it
finishes, so the attempt that crosses the line has already run.
agent_timeoutis what bounds a single run. - The cap counts one task. Every lane of a
fan_outstep is its own task with its own budget, so a tree can spend a multiple of it. To cap the tree as a whole, setmax_tree_cost_usd; it blocks withtree_cost_limit, below. - The total is the task’s whole life and never resets. A repair run and a
follow-up run are step runs of that task, so their cost counts too — a
finished task already over the cap blocks here again on the first attempt of
its next follow-up.
skipabandons that follow-up and returns the task todone.
If a runaway task on codex or cursor sailed past the cap, that is expected: neither CLI reports cost, so the cap cannot see them. See Agents.
tree_cost_limit — raise the tree cap and retry the parent
The spend of a whole fan-out tree — the root task and every lane below it at
any depth, every attempt of every step, archived lanes included — has passed
max_tree_cost_usd. The
task showing this reason is the one whose attempt crossed the line. That is
usually a lane. It can also be the parent, when its own step before the fan-out
or after the join, or an agent resolving a merge conflict, did the crossing.
Like cost_limit, it is a policy stop, not a broken step: the step run keeps
its own state and reason, and it consumed no retry.
A parent waiting on its lanes never blocks this way, because it runs nothing
while it waits. It stays awaiting_children, and its join stays open because a
lane is blocked. The parent’s row reads awaiting_children (×N …), and
GET /v1/tasks/{id} on it carries children.blocked with the lane ids and
children.cost_usd with what the lanes have spent. The TUI’s Task Details
adds the parent’s own spend to that and shows it as tree cost.
What to do:
- Look at what the money went on. Open the parent and walk its lanes (
l), or readchildren.cost_usdand each lane’s attempts. A tree usually overspends because one lane looped, or because it has more lanes than the budget was sized for. - If the work is worth it, raise
max_tree_cost_usdinconfig.yaml(it is hot-reloaded), then pressron the parent, or runvincent task retry <parent-id>. One retry re-admits every blocked lane beneath it, at any depth. If the parent itself is the blocked task, the same retry resumes it and re-admits anything blocked below it. You can also retry a single lane. - If it is not, cancel the parent. The cancel reaches every unfinished lane, and their branches and worktrees are kept.
Retrying without raising the cap costs one attempt per blocked lane, every time. The check runs after each attempt, so every re-admitted lane makes one attempt and blocks here again. A retry on the parent re-admits all of them, so on a tree with eight blocked lanes each press buys eight attempts.
Three more things that surprise people:
- You overshoot by more than one attempt. Each task checks the total at its own boundary, so when the tree crosses the line every lane still running finishes its attempt, and a lane still queued makes one attempt when it starts. Each of them blocks after that attempt. The overshoot is up to one attempt per task still working in the tree.
cost_limitwins a tie. When one attempt takes a task past both its ownmax_task_cost_usdand the tree cap, the reason iscost_limit. Raising the tree cap would not have cleared that one.- The total never resets. A follow-up run on a finished parent, and any lanes it spawns, add to the same tree. A tree already over the cap blocks again on the first attempt of its next follow-up.
The cap sees only what agents reported. Codex and cursor report no cost, so their lanes add nothing to the total and a tree that mixes them with claude lanes has spent more than the figure says.
transcript_io_error — check the disk
Vincent could not write, encode or close the attempt’s transcript, so the record
of the run is incomplete. The step fails rather than reporting a success it
cannot evidence: the usual cause is a full disk or a data directory whose
permissions changed under the daemon. df the data directory (vincent doctor
prints it), fix the cause, and retry — a new attempt writes a new file, so this
clears on its own once there is room.
It is not what a very long line of output produces. Those are captured in
partial pieces and the step succeeds normally; only transcript_limit fails a
step for size.
agent_protocol_error — vincent’s reader, not the CLI’s fault
Vincent could not read the agent’s event stream to the end, so its transcript is
missing lines the CLI wrote. The reason is named separately from agent_error
on purpose: the agent may have completed its work perfectly, and there is
usually nothing to fix in the CLI. Retry the task; if it repeats on the same
step, the transcript up to the cut is the thing to attach to a bug report.
A task is stuck in awaiting_input
It is waiting on you. It is pinned to the top of the board with a badge; press
enter on the row to open the answer form. If nobody will ever be there, set
on_input: deny in the workflow so vincent answers immediately and the run stays
unattended.
Note that awaiting_input keeps its concurrency slot — the agent process is
alive mid-step. A forgotten question does occupy a slot until input_timeout
(default 24h) expires.
A task is stuck in queued
It is waiting for a scheduler slot. Check the caps: max_parallel_tasks
globally (default 3) and the per-project cap. Raise the priority
(vincent task add --priority, PATCH /v1/tasks/{id}, or + in the new-task
form) to move it up the queue.
The TUI
I cannot select text with the mouse
The TUI owns mouse events while the mouse is on. Press M to toggle it off, or
hold shift while dragging — most terminals treat that as “bypass the
application”.
esc does not quit
By design: esc closes one layer (popup → screen → filter). q quits.
The screen looks wrong after a resize
Re-render with any key. If a pane is genuinely unusable, the terminal is likely too small for the TUI layout — the board alone needs a reasonable width to show state and step columns.
Reading the log
vincent daemon logs # the last 500 lines
vincent daemon logs -n 50 # fewer
vincent daemon logs -f # keep printing as it is written
{data_dir}/logs/daemon.log
Rotated and size-capped. vincent daemon logs and the TUI’s daemon view both
read it from disk rather than through the API, so both still work when the
daemon is the thing that died — which is when you want the log.
Turn up detail in config.yaml:
log_level: debug
debug: true # record each step's resolved settings and full argv in its transcript
debug: true is the one to reach for when a step “ran with the wrong settings”:
it writes the resolved agent, model, effort, permission mode, working directory
and the exact argv into the step’s transcript. It is off by default because argv
can carry a prompt and transcripts get pasted into issues.
Reporting a bug
Include:
vincent doctor --json— one paste covering paths, daemon, log tail, database, agents, storage and task counts. Read it first: it carries your directory paths and the tail of the daemon logvincent versionoutput, for the build details doctor does not carry- your OS and terminal
- the relevant slice of the daemon log (
vincent daemon logs -n 200, or{data_dir}/logs/daemon.log) - the workflow YAML, if a step misbehaved
- the step’s transcript if you can share it (
vincent task transcript <id> --step RUN, orvincent chat transcript <id> --turn Nfor a chat) — read it first, it contains the prompt and everything the agent did
Open an issue. Security issues go through private advisories instead — see SECURITY.md.