Troubleshooting¶
Diagnose the failures you're most likely to hit, by symptom.
Run /doctor first. It checks this install for the usual causes — which
config.yaml is actually loaded, whether the provider's credentials resolve and
its endpoint answers, whether rg/git/bash are on PATH, how many MCP tools
came up and whether every declared server connected, and whether an enabled
feature is missing its dependency. Most of the sections below start with a check
it already performed for you (details).
| Symptom | Go to |
|---|---|
| Startup hangs, or tools are missing entirely | The assistant starts but has no tools |
grep_search returns ripgrep (rg) not installed |
Content search doesn't work |
| A model error on the first prompt | The model fails to load |
| Searches find nothing in indexed documents | RAG or episodic memory returns nothing |
fallback embeddings in the logs |
Semantic search quality is degraded |
Connection was closed before we received a valid response |
A turn dies on a dropped connection |
No first token for Ns on a long conversation |
A turn dies on a dropped connection |
| A write or edit is refused | A file write is refused |
ImportError before the prompt appears |
Import errors on startup |
The assistant starts but has no tools¶
The MCP server runs as a subprocess (python -m mnemoai.server.server). If it
fails to start, the client still comes up — but the model has nothing to call.
- Run with
LOG_LEVEL=DEBUG mnemoaiand look for the server's stderr. - Check the MCP transport log at
~/.mnemoai/logs/mcp.log, which captures the subprocess's own output across runs. - Confirm the interpreter running
mnemoaican import the package:python -c "import mnemoai; print(mnemoai.__file__)". A mismatch here is the usual cause when the app was installed into one environment and launched from another. - Run
/mcpin the app to list the servers that did connect.
If an external MCP server from ~/.mnemoai/mcp.json is the one failing, the
built-in tools still load — external server failures are skipped rather than
fatal. See External MCP servers.
Content search doesn't work¶
ripgrep (rg) not installed. Install with: brew install ripgrep (macOS) or apt install ripgrep (Linux)
grep_search requires ripgrep and has no fallback. Install it, then verify
with rg --version. Filename search (glob_search) is unaffected — it uses the
Python standard library.
The model fails to load¶
Check MODEL_ID.TYPE and MODEL_ID.NAME in config.yaml first (/model edits
both), then the provider's own prerequisite:
| Provider | Verify with |
|---|---|
ollama |
ollama serve is running, and ollama pull <model> has been run |
mlx |
The server is up on HOST/PORT (/doctor probes it), and NAME matches a name it serves — curl http://127.0.0.1:8000/v1/models lists them |
bedrock / mantle |
aws sts get-caller-identity, plus region and model access |
openai |
OPENAI_API_KEY is set (via the config ENV: block or the shell) |
anthropic |
ANTHROPIC_API_KEY is set |
sagemaker |
The endpoint name is correct and InService in the configured region |
The answer contains a tool call instead of making one¶
A reply with a literal <tool_call>{"name": "fs_read", …}</tool_call> (or a bare
JSON blob describing a tool) means the model asked for a tool correctly and the
server never turned that into a tool call, so the agent saw plain text. It is a
server-side parser setting, not a Mnemo AI config key.
On mlx-openai-server, set tool_call_parser on the model entry — hermes for
Qwen3 Instruct, qwen3_coder for the Coder line — and reasoning_parser for a
thinking model. Ollama handles this through the model's own template, so if a
custom Modelfile drops the tool section the same symptom appears.
RAG or episodic memory returns nothing¶
Both are off unless enabled, and both need a working embedding model.
- Confirm the feature is on:
ENABLE_RAG: trueorENABLE_EPISODIC_MEMORY: true./featuresshows the current state. - Confirm
RAG.EMBED_MODEL_IDpoints at a model that is actually reachable. - Ask the assistant to run
list_documents— if it reports none, nothing was ingested. Only large documents are offloaded to RAG; a small file is returned inline and never indexed. - Episodic recall is additionally filtered: results below the retrieval threshold are dropped, and injection is skipped for very short follow-up prompts. See Memory & learning.
Semantic search quality is degraded¶
The configured embedding model was unreachable, so deterministic SHA256 vectors
were used instead. They keep the app running but carry no semantic meaning —
retrieval will look random. Fix the embedding model rather than tuning
thresholds: for Ollama, ollama pull qwen3-embedding:0.6b.
A tool times out after 300 seconds¶
The number is LLM.MCP_CALL_TIMEOUT, and the tool named in the message is
usually the victim, not the cause — especially when its real work takes
milliseconds. The MCP server is one subprocess with one event loop, so a tool
whose body blocks that loop holds up every other agent's call behind it until
their timeouts fire. Expect it in bursts, while several agents are working in
parallel.
- If the failing call is a genuinely long one — a slow build under
execute_bash,wait_for_taskon a long background job — raiseLLM.MCP_CALL_TIMEOUT. That is the case the message's own hint is about. - If it is a fast tool that timed out anyway, something else was blocking the
server. Check the log for a long-running tool that started before it (see
Read the logs); a tool added locally is the first suspect —
write it as a plain
defunless its body really awaits, so it is offloaded to a worker thread (python -m pytest tests/unit/test_thread_offload.pyfails on anasync defthat never awaits, helpers included). glob_searchbounds itself at 30 seconds and returns what it found withtimed_outset, so a slow filename search reports a partial result rather than hanging. Seeing that instead means the pattern is aimed at too large a tree: narrow it, pointpathat a subdirectory, or usefindviaexecute_bash.
529 overloaded_error / Router classification failed¶
WARNING — Stream connection failed (Error code: 529 … 'overloaded_error');
retrying turn on a fresh connection in 1.0s (attempt 1/6)
WARNING — Router classification failed (Error code: 529 …); binding the full toolset
WARNING — Task decomposition failed: Error code: 529 …; using single subtask
A 529 (or a 503/overloaded) is the provider saying it is busy, not a
misconfiguration. It is transient and retried automatically — the turn on a fresh
connection, and the smaller internal calls (routing, decomposition, each
compaction summary batch) up to three attempts each, with jittered backoff and
the provider's own retry-after honored when it sends one.
- Nothing to do if the retry succeeds; the lines are informational.
- The two
WARNINGlines above mean the retries were exhausted and the app proceeded on its fallback: every tool was bound instead of a routed subset, or the request ran as one task instead of a decomposed one. The answer is still produced — resend the message if the result looks coarser than usual. - A burst of them means genuine provider load.
LLM.MAX_RETRIES,LLM.RETRY_DELAYandLLM.RETRY_BACKOFFgovern how long the app waits; raisingRETRY_DELAYhelps more than raisingMAX_RETRIESwhen the overload lasts more than a few seconds. - Concurrency multiplies it: orchestrator waves,
spawn_agentsub-agents and the compaction summary all call the provider at once. LowerLLM.SUBAGENT_MAX_CONCURRENCYif overloads cluster around parallel work.
A turn dies on a dropped connection¶
WARNING — Stream connection failed (No stream data for 120s (connection likely dropped));
retrying turn on a fresh connection in 1.2s (attempt 1/6)
ERROR — Model request failed: Connection was closed before we received a valid
response from endpoint URL: ".../converse-stream".
● The model request failed with an error I can't recover from automatically.
Both lines are retried automatically as of 1.12.3. If you see this on an older
version, two separate causes were at work — and the giveaway for each is how large
the context was (the footer's meter, or the [Context: N tokens] line printed with
the turn on those versions):
- A genuinely dropped socket. botocore words this "Connection was closed", which the transient-error classifier didn't recognize, so the most retryable failure there is got no retry at all and ended the turn. Fixed by matching botocore's own phrasings.
- A long prompt mistaken for a dead stream.
LLM.STREAM_IDLE_TIMEOUT(default 120s) used to police the wait for the first token as well as the gaps between chunks. That first wait is the model reading your whole prompt before it answers — minutes on a large conversation, not seconds — so the watchdog killed healthy turns, and each retry re-sent the same prompt and re-paid the same wait. The first-token window is now derived fromLLM.REQUEST_TIMEOUT(default 600s) instead, whileSTREAM_IDLE_TIMEOUTstill guards the running stream.
If it persists after upgrading, the context itself is the problem — a turn large
enough to exceed REQUEST_TIMEOUT before the first token can't be rescued by
retrying, only by sending less:
- Run
/contextto see what the next turn pays for, and/compactto summarize the conversation now. - Lower
MAX_CONVERSATION_TOKENS. It sets the compaction trigger too (80% of it by default), so a value matching the model's full window — e.g.1000000— means compaction effectively never runs and every turn re-sends the whole history. A ceiling well below the model's limit keeps turns fast; the model's context window is a hard maximum, not a target. /branchto continue from an earlier point without the accumulated history.
A turn ends with "I couldn't compact it further"¶
Compacted: summarized 1209 older messages, kept 2 recent.
WARNING — Context overflow: prompt is too long: 3155357 tokens > 1000000 maximum;
compacted and retrying
WARNING — Context overflow: prompt is too long: 3157792 tokens > 1000000 maximum;
could not compact
● The conversation grew past the model's context window and I couldn't compact it
further. Use /clear to start fresh, or /compact <focus>.
Fixed in 1.12.4. The tell is the arithmetic: the second overflow is a few thousand tokens above the first, and it lands after a compaction that reduced history to two messages — so the message is wrong, there was plenty left to compact. Three separate defects lined up, and on older versions all three are worth knowing about:
- The compaction didn't reach the rest of the turn. A running turn re-reads the history it started with, so only the immediate retry saw the smaller prompt; the next model call re-sent the original one, and by then compaction genuinely had nothing left to give.
- The turn then undid its own compaction. Everything summarized away was
appended back when the turn ended — and written to the session transcript as
this turn's work — so the following turn compacted the same history again
(
1209 older messages, then1165). - The pre-flight check was skipped entirely. It uses the context size the
provider reported for the previous turn, and
/branch,/loadand--resumereplace history wholesale. Since the transcript keeps every message compaction had summarized away, restoring it makes history much larger while the leftover count still describes the small version — so the check passed and the first turn went straight to a provider-side overflow.
Defect 3 is why this shows up right after a /branch, /load or --resume of a
long conversation. On an older version, /compact immediately after restoring one
avoids it; on 1.12.4 the next turn measures the history that is actually there, and
since 1.12.6 restoring rebuilds the compacted state rather than re-inflating it
(see below).
A resumed session reports an impossible context size¶
Twelve million tokens into a one-million-token window is not a display glitch — the history really was that large, and the next message would have overflowed. Fixed in 1.12.5: each resume used to re-save every tool result wrapped inside a copy of itself with all the quotes escaped again, so the same results roughly doubled on every resume while adding no content. File reads in the reported session had reached 1.07M characters each, and ~90% of the 21M-character transcript was backslashes.
Upgrading is the whole fix — there is nothing to delete or migrate. Opening an
affected conversation (--resume, --continue or /load) unwraps the nested
copies as it reads and re-saves it clean; the reported session dropped from 21M to
2.1M characters with no content lost. To see the effect, run /context after
resuming.
One related note if the number is large but plausible: MAX_TOOL_RESULT_CHARS
caps a tool result when it is first produced, not when it is restored, and it
derives from MAX_CONVERSATION_TOKENS — at 1000000 a single result may be
400,000 characters. If the number is instead much larger than it was when you
closed the session, see the next section.
A resumed session reports a much larger context than when I closed it¶
Fixed in 1.12.6. The transcript holds every turn's full text — deliberately,
because nothing you said should ever be lost from disk — but restoring it used to
replay all of that text, which brought back everything a previous /compact had
summarized away. So the context came back at its pre-compaction size (here 5×
larger, past the model's window), the first message after resuming had to summarize
the whole conversation again, and the summary you had already paid for was
discarded. A compaction now records what replaced that history, and a restore
rebuilds the state the session ended in, for --resume, /load and /branch
alike. The conversation still replays on screen in full — what came back is visible
in the footer's context meter, which reads about what it did when you closed the
session.
Nothing needs migrating, but note what a checkpoint is and isn't:
- It is written when a compaction happens, so a session that never compacted is
unaffected, and a session compacted on an older version has no checkpoint to
restore — the first
/compactafter upgrading creates one. /saverecords the active summary too, so a/loadof a compacted conversation no longer resumes mid-thread with the earlier history missing. Files saved before 1.12.6 load exactly as they did.- A
/branchbefore the compaction point deliberately forks the raw history — that is the point of rewinding to there. - A compaction that only trimmed old tool-result bodies (the cheap pass that runs before any summarizing) is checkpointed as well, but only since 1.12.7 — on 1.12.6 that case still came back at full size, and with no summary involved nothing on screen hinted that anything had been reclaimed.
On an older version, /compact immediately after resuming is the workaround; it
costs a summarization but brings the context back down before the first real turn.
A file write is refused¶
Three different guards produce three different messages:
| Message contains | Cause | What to do |
|---|---|---|
read it first |
The file was never read this session | Read the file, then retry the write |
changed on disk since you last read it |
The file changed after it was read | Re-read it so the edit is based on current content |
protected system directory |
The target is under /etc, /usr, /System, … |
Write somewhere in your home, project, or temp directory |
This is the read-before-write gate and the path policy. See Safety & permissions.
Import errors on startup¶
Some dependencies are heavy and platform-sensitive.
faiss-cpuon Apple Silicon:pip install faiss-cpu --no-cache-dirchromadb/crawl4ai: check their platform-specific instructions; both are only needed if RAG or the web crawler is enabled- If you installed from a checkout, re-run
pip install -e ".[dev]"— the runtime extras alone don't cover the test and docs tooling
Permission errors¶
Ensure the app home is writable: ~/.mnemoai/ holds config, prompts, plans,
tasks, logs, skills, agents, and all per-profile state. Override the location
with MNEMOAI_HOME if the default isn't writable.
Read the logs¶
Two destinations, with different jobs.
On screen, a problem is one line in the app's own shape — no timestamp,
logger name or level word, and never a stack trace (the terminal is a
conversation, and a trace there buries the answer above it). The mark carries
the severity: ✗ an error, ! a warning, · anything lower.
The pointer at the end appears only when something was left out — a traceback, or a message too long for one line.
A warning raised by a dependency reads the same way, even though the standard library would print four lines for it (path, line number, category, message, then the offending source line):
Where it came from is recorded in the log file with it, and a warning repeated during the session is written there without taking another line on screen.
On disk, ~/.mnemoai/logs/mnemoai.log has the whole record — traceback,
thread name, and the surrounding INFO lifecycle lines — for both the app's own
errors and anything a library or the standard library logged. Start here for any
"it went wrong and I couldn't see why". The file rotates at 2 MB (two
generations kept) and every file under logs/ is deleted after
LOG_MAX_AGE_DAYS days (default 7; 0 keeps them forever). /doctor prints
the path and the retention it's using.
The screen threshold is WARNING by default:
LOG_LEVEL=DEBUG mnemoai # everything, tracebacks included, on screen too
LOG_LEVEL=INFO mnemoai # lifecycle events
mnemoai # warnings and errors only, one line each
The MCP subprocess writes separately to ~/.mnemoai/logs/mcp.log, which is the
right place to look when startup fails before the prompt appears.
See also¶
- Configuration: every config key, with defaults
- Safety & permissions: what is blocked, and what asks first
- The
~/.mnemoaidirectory: where state lives on disk