Skip to content
titah

Reference

Troubleshooting

Symptoms, the actual cause, and the fix. Start with `titah doctor`.

titah doctor --probe

Almost every problem below is visible in that output before you go looking for it.

“The TUI will not start”

Cause: you are running from source without a build. Node runs .ts directly but not .tsx — JSX is not stripped.

npm run build
node dist/cli.js

Headless commands (run, serve, doctor) do run from source.

“Node version” errors

Titah needs Node ≥ 22.6 for node:sqlite.

node --version

“The model answers confidently about things I never said”

Cause: the context overflowed and the provider truncated it silently. This is the single most common cause of an agent that seems to have lost its mind.

Fix: declare contextWindow for the model.

"provider": { "ollama": { "models": { "qwen3:14b": { "contextWindow": 32768 } } } }

titah doctor lists every model missing one. ollama in particular truncates at num_ctx — 4096 by default — without a warning. See Context & compaction.

“Compaction fires on every single turn”

Cause: reserved is large relative to the window. On an 8k model the 8192 default would equal the whole window.

That is why reserved is capped at a quarter of the window. If it still feels aggressive, lower it explicitly:

"compaction": { "reserved": 1024 }

titah doctor reports every model where the cap is biting and the value actually in effect.

“Nothing happens in CI / the agent hangs and then does nothing”

Cause: with no client connected, any tool needing a permission dialog is auto-denied. That is deliberate — blocking would hang CI, auto-allowing would be a silent security hole.

Fix: be explicit.

titah run --auto "…"
{ "permission": { "bash": "allow", "allowlist": ["npm test", "git *"] } }

“edit keeps failing”

edit fails hard when oldString is missing or appears more than once, and writes nothing.

That is the feature. Give it more surrounding context so the match is unique, or use replaceAll if a multi-replacement is genuinely what you want. See Tools.

“@claude says the agent is unavailable”

The CLI is not on PATH. Titah detects external agents with which at startup and lists missing ones with the command that was not found rather than hiding them.

which claude opencode
titah doctor

“The second @claude starts a fresh conversation”

Check sessionMode for that adapter. generate means Titah owns the UUID (Claude Code’s way); discover means the id is read from the first call’s output (opencode’s way). The wrong one produces exactly this symptom.

Two things that are easy to get wrong: Claude Code rejects --output-format stream-json without --verbose, and passing the same --session-id twice is not how you resume.

“A skill in always does nothing”

An unresolved id is skipped silently at the point it happens — but /skills and titah doctor list every unresolved id together with where it was configured.

The usual cause is a bare name. Ids are always namespace:name; "project-analyzer" never resolves, "team:project-analyzer" does. See Skills.

“My opencode plugin does not load”

opencode plugins are JavaScript modules against opencode’s runtime API, not markdown. Titah reads opencode skills and cannot run its plugins. This is a supported-scope limit, not a bug.

“websearch returns nothing, or nonsense”

The default ddg backend scrapes HTML and can break without warning — titah doctor says so out loud. Move to a stable backend:

"search": { "backend": "brave", "apiKey": "${env:BRAVE_API_KEY}" }

“webfetch refuses a URL”

It accepts only http and https. file: in particular is refused, because it would sidestep the working-directory confinement every filesystem tool relies on.

Private addresses are not blocked — localhost works on purpose.

“undo reverted more than I expected”

You almost certainly ran it after a /tim turn whose coordinator wrote nothing itself. Undo walks back to the most recent message that actually took a snapshot, which can be an earlier turn, and reverts that too.

Read the worked example in Permissions & undo — there is no way to undo a /tim turn in isolation when its coordinator wrote nothing.

“A /tim sub-agent ran a shell command in Plan mode”

That was a real hole and it is fixed. A sub-agent’s permission is now derived from its parent and clamped by the stricter of the two on every axis, so a plan coordinator whose bash is "deny" cannot dispatch a sub-agent that runs one — even if that agent’s own block says "bash": "allow".

If you still see it, you are on a build from before that change. Check titah --version against the changelog, and see Sub-agents for how the ceiling is computed.

“bash is refused and the reason mentions a sandbox”

You set sandbox.bash: true on a platform with no sandbox available. It fails closed on purpose: someone who enabled it believes there is a fence, and running unfenced anyway would leave them believing it.

titah doctor    # says which of the three states you are in

Seatbelt on macOS, bubblewrap on Linux. Turn it off, or install the missing one. See Sandbox.

“A background turn did nothing but get denied”

titah run --bg detaches, so no client is attached — and with no client, any tool needing a dialog is auto-denied. A detached turn that will touch files needs --auto or an explicit allowlist:

titah run --bg --auto "…"

See Background turns.

“JSON.parse fails on --output-format json”

Nothing human is supposed to reach stdout in json modes, so this should not happen — if it does, check that you are not merging stderr into stdout (2>&1). Progress, notices and errors all go to stderr on purpose.

An exit code of 2 is not a parse problem: it means the turn succeeded and the answer did not match your --json-schema. Retrying will not help; the prompt or the schema has to change. See Structured output.

“titah stats shows tokens but no cost”

The model has no price declared. Titah ships no price table and never will — prices change by region and contract, and a stale table produces numbers that look official and are wrong.

"provider": { "9router": { "models": { "ant": { "price": { "input": 3, "output": 15 } } } } }

Per 1M tokens. Unpriced models are named separately rather than counted as free. See Cost & limits.

“The turn stopped before it finished”

It hit a limit, and it said so — check the notice. limits.turnTokens defaults to 5× the model’s declared contextWindow, and the step ceiling is 40 until you declare a token budget, at which point it becomes 200.

Raise the budget, split the work, or set limits.continueTurns to let a bounded turn be followed by another while the plan still has unchecked items. See Cost & limits.

“A hook is not firing”

Its match regex is probably invalid, which looks exactly like a pattern that matched nothing.

titah hooks list    # names invalid regexes explicitly

See Hooks.

“Editing my plugin file changes nothing”

Node caches modules by URL, so a plugin edit takes effect on the next Titah start, not mid-session. titah plugin list actually loads them and reports what each provides — reading the config alone cannot tell a working plugin from one that fails to import. See Plugins.

“A side panel is missing, or the side says ⚠ N failed”

⚠ N failed is not No extension. The first means the extensions are configured and were refused; the second means nothing is configured for that side. The footer carries the full sentence — ⚠ N side panels failed to load — run: titah extension list — for as long as the state holds, because the notice at load time is gone in four seconds.

Two commands answer it, and they see different things:

titah doctor              # the manifest and engines.titah, WITHOUT running any of it
titah extension list      # loaded for real — catches failures at import too

doctor never imports third-party code, so it cannot see a factory that throws or one that returns no render. That is what extension list is for.

A refused extension prints the reason:

✗ @titah/extension-git
    @titah/extension-git needs extension API ^9.0.0, but Titah 0.8.0 provides 0.5.0.

Both numbers are named on purpose — provides 0.5.0 on a 0.8.0 install reads like breakage otherwise. engines.titah is checked against the extension API version, which only moves when the contract itself changes, so a Titah minor bump no longer kills every installed extension. When a range genuinely does not match, the fix is the package, not titah upgrade:

titah extension update    # picks the newest published version this API accepts

See Extensions.

“A side panel folded itself”

Below panel.boxFloor rows per open box, boxes fold from the bottom up and a notice says how many. Config order is priority order, so the box you put at the top survives longest. A floor-folded box cannot be reopened with Ctrl+X Z — the same arithmetic would fold it again on the next render, so Titah says so instead of offering a key that appears not to work. Give the terminal more rows, or drop a box from that side.

“Cannot reach the account server”

The default is https://titah.dev. If you run your own titah-web, point at it:

export TITAH_ACCOUNT_SERVER=https://titah.internal

or { "account": { "server": "…" } } in the config — the environment wins. A token records which server issued it, so changing servers asks you to sign in again rather than reusing one that means nothing there. See Account & sign-in.

“The database is enormous”

It should not be — blobs go to disk, not into SQLite. Sweep everything at once:

titah sessions prune --older-than 30d

That deletes rows, orphaned tool-output/ blobs and orphaned snapshots together. Deleting rows alone just moves the growth into the directory next door.

“I cannot select text in the terminal”

While Titah tracks the mouse, the terminal stops using clicks to highlight text. Ctrl+X M turns tracking off; the footer says ✂ mouse off until you turn it back on. Most terminals also let you hold Shift while dragging.

“Shift+Enter does not insert a newline”

Most terminals cannot distinguish Shift+Enter from Enter. Use Ctrl+J.

Still stuck

Open an issue with the full output of titah doctor --probe: github.com/titah-dev/titah/issues.