A nanobot run that looks like an infinite loop is a bounded one: in the shipped v0.3.5 source every agent turn is capped by agents.defaults.maxToolIterations, whose default is 200, and a maintainer states on the record that this is not a literally infinite loop. The token usage is the real problem, because a run that reaches that ceiling logs 201 assistant turns and resends a growing prompt each time. Sorting out which of those you have is a four-question diagnosis, and the fix that has evidence behind it is not a configuration value.
Start by deleting the obvious lead. The upstream report is issue #5781, whose title points at dream.maxIterations, and PR #5782 is titled "fix(dream): enforce configured iteration limit". That key does not exist in v0.3.5, and the PR was closed unmerged on September 16, 2026. A tutorial built on it configures nothing.
One disambiguation, because the query surfaces the wrong tracker. This page is about HKUDS/nanobot, MIT, installed as the PyPI package nanobot-ai — 0.3.5, Python 3.11 or newer, uploaded September 15, 2026. obot-platform/nanobot is a different Go project whose README declares maintenance mode with issues disabled; its issue numbers are not evidence here. And pip install nanobot, with no suffix, installs an unrelated robot navigation package.
What was reported, and against which version
Dream is nanobot's scheduled memory-consolidation job, run by the gateway as a cron job named dream. It is not an embedding or vector-index job: Kunavo serves no embedding model, and Dream does not need one, because nanobot's durable memory is plain files under the workspace and a Dream run is ordinary chat traffic.
Issue #5781 was filed on September 15, 2026 by BrianMwangi21 against nanobot v0.3.0, on Python 3.12 with a reasoning model reached through OpenRouter. The symptom: the Dream job alternating read_file on memory/history.jsonl with read_file on one SKILL.md, dozens of times, while the reasoning trace re-deliberated where to file a single small fact. Seven runs logged over about 26 hours ranged from roughly 25 minutes to roughly 111 minutes; the ones near 200 tool calls were hitting the global cap, and a session checkpoint showed runtime_checkpoint.iteration: 164, phase tools_completed, on the same read_file call.
Three scope limits belong with those numbers. They are one user's self-reported gateway logs, on one model, on v0.3.0 — no maintainer reproduced them, and nobody has re-tested on v0.3.5. The issue is labelled enhancement and priority: p2, not bug, and it is still open. And this shape has surfaced before: issue #3073, a near-identical read_file loop on history.jsonl from April 12, 2026, was closed not planned. None of that supports treating nanobot as generally expensive; it supports checking whether your own install is doing this.
The deprecated key, and the boundary that is actually live
The whole confusion is a version gap, and the source settles it. Read at the two release tags on September 21, 2026:
| Config key | In v0.3.0 | In v0.3.5 | What to do now |
|---|---|---|---|
dream.maxIterations | Default 15, marked # Deprecated: no longer used | Removed from the schema | Do not write it; it configures nothing |
dream.maxBatchSize | Default 20, same deprecation comment | Removed | Do not write it |
dream.annotateLineAges | Default true, same deprecation comment | Removed | Do not write it |
dream.enabled | Default true | Default true | Editable in the WebUI runtime settings |
dream.intervalH | Default 2 | Default 2 | Edit config.json; not a WebUI leaf |
dream.cron | Null; legacy override | Null; legacy override | Takes precedence over intervalH when set |
dream.modelOverride | Declared, commented pending implementation | Implemented | Preset names only, never a raw model id |
agents.defaults.maxToolIterations | 200 | 200 | The only cap, and it is process-wide |
Two things make that table easy to get wrong. The 200 is doubly true — it is the value in the reporter's config and the shipped default at nanobot/config/schema.py line 129, identical at the v0.3.5 tag and on main, so a reader who never set it still has 200. And it is undocumented: a fetch of nanobot's own 0.3.5 configuration reference on September 21, 2026 returned 488,608 bytes of HTML with zero occurrences of maxToolIterations, and the repository's docs/configuration.md at that tag has none either. The number is provable from source and from a maintainer comment, not from a documentation page — so treat any tutorial quoting a different figure with suspicion.
modelOverride is the one remedy with a version gate. In v0.3.0 the field existed with the comment pending implementation and no resolving code at all; PR #5107, merged July 27, 2026, implemented it for v0.3.5. The official 0.3.5 memory page states that it selects a named entry from model_presets for Dream, accepts preset names only, and that raw model identifiers are not supported. That page documents three Dream keys and mentions no iteration limit anywhere. Here is the complete current surface, with the providers block it leans on covered on the nanobot setup page:
{
"modelPresets": {
"dream-cheap": {
"provider": "kunavo",
"model": "claude-haiku-4-5",
"maxTokens": 8192
}
},
"agents": {
"defaults": {
"maxToolIterations": 200,
"dream": {
"enabled": true,
"intervalH": 2,
"modelOverride": "dream-cheap"
}
}
}
}Note what is not in that file: any way to bound Dream alone. AgentLoop is constructed once with max_iterations from the process defaults, and both the Dream cron path and the manual /dream path call process_direct(...) with no iteration argument, as do subagents. Lowering the cap lowers it for chat, Dream, heartbeat and subagents together.
Diagnose in this order
Answer these four before changing anything, because three of them are free and the fourth tells you whether the first three matter. The log strings are the literal text in v0.3.5; the braces are runtime values.
| Question | Where to look | What the answer means |
|---|---|---|
| 1. Did the run stop on its own, or hit the ceiling? | Gateway log: Max iterations (200) reached, from agent/loop.py | Present means a capped run, and roughly 201 assistant turns. Absent means the run ended before the ceiling — either the model converged, or the run failed outright and logged Dream cron job failed |
| 2. Did the Dream cursor advance? | Three distinct lines in cli/gateway_runtime.py: Dream cron job completed, cursor advanced to …; … completed with no memory changes; cursor advanced to …; Dream cron job did not complete (…); cursor remains at … | The third line is the v0.3.5 fail-safe working: an incomplete run leaves the batch to be retried. It also means the same batch comes back next tick |
| 3. Is the same tool with the same arguments repeating? | The Tool call: lines between those two markers | Identical tool and arguments over and over is model non-convergence, which is what upstream concluded. The only repeat guard in v0.3.5 matches web_fetch and web_search; a repeating read_file is not caught |
| 4. What did it cost? | llm_usage.sqlite3 in the config directory, aggregated by date and source; on the gateway side, per key and per day in usage | Dream is tagged dream. The heartbeat is tagged cron, so filtering for "heartbeat" finds nothing |
You do not have to wait two hours for the cron tick to reproduce it. The /dream command runs the same job on demand and reports the same distinction in chat: Dream completed in Ns. or Dream completed in Ns; no memory changes. on success, against Dream did not complete after Ns (reason); memory cursor was not advanced. when it does not finish. One other v0.3.5 change matters while you are hunting for evidence: session JSONL files moved under the configuration directory's sessions/<workspace-id>/ tree, so the v0.3.0 paths quoted in the issue thread are not where your checkpoint lives.
Five levers, and what each one is actually backed by
| Lever | Evidence behind it | Wins when | What it costs you |
|---|---|---|---|
| Upgrade v0.3.0 to v0.3.5 | Commit 4e2640f, in v0.3.5 and not v0.3.0, makes the cursor advance only when the stop reason is completed | Your symptom is skipped memory rather than spend | It does not shorten a non-converging loop, and no one has re-tested #5781 on 0.3.5 |
| Change the model for Dream | The only remedy with a before-and-after: in the reporter's audit a batch that one model spent 91 minutes on without finishing was completed by the next run on a different model in about a minute with 6 tool calls, same prompt, same history, same tools | Chat quality must stay on your expensive model | Requires v0.3.5 and a defined preset; on v0.3.0 the field resolves to nothing |
Lower maxToolIterations | Editable in the WebUI runtime settings, minimum 1, per webui/settings_runtime.py | You want a worst-case ceiling while you diagnose | One knob for chat, Dream, heartbeat and subagents; and a capped run does not advance the cursor, so the same batch is retried next tick |
| Slow or disable Dream | intervalH, or dream.enabled in the WebUI; merged PR #5407 retires the persisted job when disabled | Consolidation is not worth its cost on your workload | You lose memory consolidation, which is the feature |
| Route so the resend is cheaper | In v0.3.5 exactly two provider specs set supports_prompt_caching: anthropic and openrouter; the field defaults to false | You accept that long runs happen and want them to cost less | Depends on which protocol path you configured, and on verifying the returned usage yourself |
The reporter's two model ids were deepseek/deepseek-v4-flash-0731 and openai/gpt-5.6-luna as he wrote them via OpenRouter; those ids and their prices were not checked here, so read the comparison as evidence that the model decides convergence, not as a recommendation of either one. Upstream reached the same conclusion: announcing the closure of PR #5782 on issue #5781, chengyongru wrote that a fixed Dream iteration cap does not address the underlying model-dependent convergence problem and may cause otherwise viable runs to be retried repeatedly.
What a capped run costs: illustrative arithmetic
These are illustrative token arithmetic, not a measured task cost and not a bill ceiling. nanobot publishes no per-run token figure for Dream, so every input here is an assumption you should replace with your own measurement. Assume one run that reaches the cap at 201 assistant turns — the count in the reporter's audit — each resending a prompt held flat at 25,000 tokens, which is his characterisation of his own workspace, not a published figure. That is 5.03M input tokens. Output tokens are excluded, and a real prompt grows with each tool result, so this understates a real run in two directions at once. Rates are live Kunavo catalog prices.
| Model | Input per 1M | Cache read per 1M | One capped run, no cache | Same run, resends read from cache |
|---|---|---|---|---|
| Claude Haiku 4.5 | $0.40 | $0.04 | $2.01 | $0.21 |
| GPT-5.6 Terra | $0.70 | $0.07 | $3.52 | $0.37 |
| Claude Sonnet 5 | $2.00 | $0.20 | $10.05 | $1.05 |
The last column assumes a best case that nothing here tested: the first send at the plain input rate and all 200 resends served as cache reads. A cache write is billed at its own rate, which on some models sits above the plain input rate; a cache entry expires; and a prompt that grows re-writes rather than re-reads — so treat the gap between the last two columns as the size of the prize, not as a quote. The point is only that on a long repetitive run the resend, not the model, is where the money goes.
Whether cache markers reach the wire at all is decided in your nanobot config, and it is visible in the shipped source. An invented provider key under providers is treated as a plain OpenAI-compatible provider and leaves supports_prompt_caching at its default of false, so nanobot's client sends no cache_control markers on that path even for a Claude-shaped model id. Keeping the preset on the built-in anthropic provider and overriding providers.anthropic.apiBase keeps them on. That is a statement about what nanobot's client sends, not about what any endpoint does on its own side, and Kunavo has not runtime-tested nanobot. Read one real call's returned usage before budgeting a recurring job as cached — the caching doc shows what a working cache looks like and the base URL reference gives both endpoint conventions.
Kunavo's catalog amount is a billing floor rather than a cap: when the upstream reports its charge, the bill is the greater of catalog cost and upstream cost times the applicable markup. The minimum top-up is $10 in prepaid credit — a funding minimum, not a task fee or a subscription. See billing details, and cost optimization for the measurement method that replaces the assumptions above.
Verify the fix on one run
Change one thing, then run /dream once rather than waiting for the schedule, and check the three markers in order: no Max iterations (200) reached warning, a cursor advanced to … line rather than cursor remains at …, and a plausible tool-call count in between. Then read the charge your own account recorded for that window, per key and per day, in usage; nanobot's store counts tokens, not money. If you are configuring the endpoint for the first time, the quickstart and the nanobot setup page cover both protocol paths, and creating a Kunavo account is the step before funding a key. Comparing runtimes instead? nanobot vs OpenClaw puts the two background cadences side by side, and the agent API directory indexes clients by wire protocol.
FAQ
Is nanobot really stuck in an infinite loop?
No, and a maintainer says so on the record. Commenting on issue #5324 on August 10, 2026, chengyongru wrote that nanobot's agent runner is bounded by agents.defaults.maxToolIterations, 200 by default, so this is not a literally infinite loop — adding that a long bounded loop can still cause very high cumulative token usage, so the practical impact is real. The shipped v0.3.5 source agrees: max_tool_iterations defaults to 200 in nanobot/config/schema.py, and agent/loop.py logs the warning "Max iterations (200) reached" when a run hits it. What people call an infinite loop is a run that reaches that ceiling, which in one reporter's logs meant 201 assistant turns re-reading the same two files.
Why does setting dream.maxIterations do nothing?
Because the field no longer exists. In nanobot v0.3.0 the Dream config carried max_iterations, max_batch_size and annotate_line_ages, each marked in the source with the comment "Deprecated: no longer used"; in the v0.3.5 tag all three are gone, and the class defines only enabled, intervalH, cron and modelOverride. Maintainer chengyongru stated on September 15, 2026 that dream.maxIterations was intentionally marked deprecated and unused when Dream was moved onto the normal agent loop, and has since been removed from main. The pull request that would have restored it, #5782, was closed unmerged the next day. Writing that key into a v0.3.5 config writes a key the schema does not define.
How do I limit tokens for just the nanobot Dream job?
Not as a cumulative budget, in nanobot v0.3.5. Nothing in the shipped code totals a run's tokens and stops it when a figure is reached, and agents.defaults.maxToolIterations is the only iteration cap — it is process-wide, applying to ordinary chat turns, Dream, the heartbeat and subagents at the same time. Two things you can scope to Dream alone through agents.defaults.dream.modelOverride: the model, and that preset's own per-call limits, because ModelPresetConfig carries maxTokens and contextWindowTokens and dream_runtime() resolves the named preset into the runtime the Dream run executes under. Those bound each individual call, not the run's total, so 200 calls under a low maxTokens is still 200 calls. The schedule is scopeable too, through intervalH. A cumulative budget is what upstream has declined to add for now: on issue #5781 on September 16, 2026, the day he closed PR #5782, chengyongru wrote that a token or resource budget for background tasks needs a more detailed design covering the budget unit and scope, termination semantics, retry and cursor behavior, observability, and interaction with different models, and that they do not plan to move this forward for now.
How do I stop a nanobot Dream run that is already going?
Not with /stop, on a reading of the v0.3.5 source. /stop is documented as cancelling the active agent turn for this chat and is implemented as a cancel over tasks registered under that chat's session key, while a Dream run is created under its own ephemeral key of the form dream:YYYYMMDD-HHMMSS. That is a code-path reading, not a tested result — /stop was not executed against a live Dream run here. The levers that are documented are /restart, switching agents.defaults.dream.enabled off, or stopping the gateway process. Merged PR #5407 in v0.3.5 is what makes disabling actually retire the persisted system job rather than leave it scheduled.
How do I see how many tokens nanobot's background jobs used?
Read the local usage store, not a slash command. nanobot v0.3.5 records every model call with a source field typed as user, api, cron, dream or system, writes them to llm_usage.sqlite3 in the configuration directory — ~/.nanobot by default — with input, output, cache-read and cache-write token columns, and aggregates them grouped by date and source. Two traps. There is no /insights or /cost command: both pull requests proposing one, #3735 and #3921, were closed unmerged, and the v0.3.5 builtin list is /new, /compact, /stop, /restart, /status, /model, /history, /goal, /trigger, /dream, /dream-log, /dream-restore, /dream-prompt, /evaluator-prompt, /skill, /help and /pairing. And the labels are asymmetric: Dream spend is tagged dream, but heartbeat spend is tagged cron, because the heartbeat session key is literally "heartbeat". Those are token counts, not money, so reconcile against your provider's own ledger.
Does upgrading to nanobot v0.3.5 fix the loop?
Nobody has said that it does, and this page will not either. Issue #5781 was filed against v0.3.0 and is still open, labelled enhancement and priority p2; the reporter never re-tested after upgrading and no maintainer reproduced it. What v0.3.5 does fix is narrower and worth having anyway: commit 4e2640f stops an incomplete run advancing the Dream cursor and silently skipping history, merged PR #5442 makes an incomplete run report why it did not complete, and merged PR #5325 makes edit_file return "Error: new_text must be different from old_text." instead of reporting success on a no-op edit. That last one addresses the read-then-edit loop of issue #5324, not the read-only loop of #5781. No general repeated-identical-tool-call guard ships in v0.3.5 either. The one repeat guard in the shipped source, repeated_external_lookup_error in nanobot/utils/runtime.py, blocks an identical web_fetch or web_search after two attempts and matches no other tool name, so a repeated read_file runs on to the iteration cap. The five pull requests proposing a general guard — #3077, #4522, #5344, #3701 and #4154 — are all unmerged.
Checked September 21, 2026: the GitHub API for HKUDS/nanobot and for issues #5781, #5324 and #3073 and pull requests #5782, #5107, #5325, #5442, #5407, #3077, #4522, #5344, #3701, #4154, #3735, #3921 and #4622; the PyPI record for nanobot-ai 0.3.5; nanobot's 0.3.5 memory and configuration documentation; and the shipped source at the v0.3.5 tag for every default, log string and code path quoted above, compared against v0.3.0 where the two differ. Kunavo has not installed or run nanobot, no behaviour here is Kunavo-tested, and the loop of issue #5781 has not been re-tested on v0.3.5 by anyone. Token rates come from the live catalog and every dollar figure is illustrative arithmetic at the stated assumptions.