Animated Octomind icon — purple pixel-art octopus

Yesterday one of our own agents checked whether a file existed, got its answer, and checked again. It did that 101 times, three seconds apart, until the turn ended. It was a specialist on GLM-5.3 Flash, and nothing was broken: every result sat in its context, and so did the supervisor's loop hint, 21 times. At the effort it ran on, the model reasoned on 3 of its 174 model calls. It kept going, and every round was another request we paid for.

It wasn't the first time. We had caught GLM-5.3 looping like this before, through Cloudflare and through Ollama. Octomind once had a hard stop for it, off by default, and we took it out in August in favour of hints. A hint is only advice.

Octomind 0.61.0 ends the turn when an agent repeats the same tool calls and gets the same results 10 rounds in a row. This post covers 0.59.1 through 0.61.0 since our last update. The open-source AI coding agent CLI update is available today.

Stop an AI doom loop before it runs up the bill

The supervisor already watched for this kind of LLM loop: when identical calls and results repeat for 3 tool rounds, it adds a hint to the conversation. That hint stays advisory, because repetition can be legitimate polling, so the model can ignore it and keep going.

0.61.0 adds one limit that is not advisory. When supervisor.max_identical_rounds tool rounds in a row repeat the same calls and get the same results, the turn ends. Octomind records that round's results, makes no further model request, and neither the verify-gate nor the unfinished-hand-back check re-runs the turn. The turn's final answer becomes a notice:

text
Stopped by the supervisor: the last 10 tool rounds repeated the same calls and got the same results, so this turn ended instead of spending more requests on it.

The notice names each repeated call and its result, up to 300 characters each, and ends with "Repeating it will not change the result; continuing needs a different approach." In an interactive terminal you also see · Supervisor: stopped — 10 identical tool rounds in a row.

"Identical" means the whole call: tool name, arguments, result, and error status. A round in which anything new comes back resets the count. When the stop ends a subagent's turn, the parent agent receives the notice as the subagent's reply, so it sees what repeated without opening the log. The trajectory is labelled failed for learning, and your next message starts the count again.

The limit lives in [supervisor], which is on by default:

toml
[supervisor]
enabled = true
max_identical_rounds = 10  # 0 turns the stop off

The stop needs enabled = true. If your agents legitimately poll something that keeps returning the same answer, raise the limit or set 0. The supervisor guide explains how the advisory detectors and this hard stop fit together.

Choose the git trailer on commits the agent writes

When the agent creates a git commit, 0.61.0 asks it to end the message with this git Co-Authored-By trailer, unless you have said otherwise about attribution:

text
Co-Authored-By: Octomind <[email protected]>

The trailer comes from the root key commit_attribution in config.toml. Set your own trailer there, or turn it off. Keep the key above the first [table] header, or TOML assigns it to that table:

toml
commit_attribution = ""

An empty value removes both the trailer and its instruction from the system prompt. This is an instruction to the model, not a git hook, and what you tell the agent about a commit's attribution always wins over it.

See provider retries instead of a silent wait

When a model provider call fails with a retryable error, such as a 429, a 5xx, or a dropped connection, Octomind waits and tries again. Until 0.60.0 that wait was silent, so a turn could look stuck. Now each retry prints one dimmed line on stderr before the wait starts:

text
↻ Model provider unavailable (<reason>) — retrying in <seconds>s, attempt <n>/<max>

The reason is short, such as HTTP 503, connection error, or request failed, and never a response body or a credential. Editors on ACP show the same line as agent thinking, and octomind workflow shows it in step progress. The number of retries and the backoff come from max_retries and retry_timeout in the model profile; the shipped [model] defaults are 1 and 30.

Keep API keys out of ps on macOS

Octomind sets its own process title, such as octomind-run <role> [<session>] or octomind-server. On macOS before 0.59.1, pgrep -fl — and ps for long titles — could read past that title into the environment strings stored right after it and print them, API keys included. The title now stays inside its own memory area.

If other accounts or tools on your Mac can list processes, update first, and consider rotating the keys that were in Octomind's environment.

For apps built on octomind server

A dropped connection no longer stops the turn (0.60.0). Before, a client that disconnected mid-turn cancelled it. Now the turn keeps running and the server holds its frames for the session's next bind. Send cancel when you do want it stopped. Until the turn ends, other message or command frames for that session get the busy error, which echoes their request_id since 0.59.1.

Resume a stream where you left off. Session acks advertise stream_resume_v1. Every frame a session produces carries a top-level stream_seq (1, 2, 3 … with no gaps), and a successful bind's ack reports the stream id and the highest stream_seq so far. After a reconnect, bind again with resume:

json
{
	"type": "session",
	"request_id": "bind-2",
	"session_id": "my-session",
	"resume": { "stream": "3f9a0c6e2b7d41e8a5c09d6f1b2e7a34", "after": 7 }
}

When the stream matches and every later frame is still held, the ack carries "resumed": true and the missed frames follow in order, byte-identical to the first sending. Then the stream continues live, including the rest of a running turn. The server holds the running turn and the one before it, up to 4096 frames or 8 MiB per stream.

Otherwise the ack says "resumed": false, nothing is replayed, and your client reloads its state from its own store. The stream id changes when the server restarts or the session is closed and loaded again. A bind with resume also takes the session's output over from any other connection.

Show retries in your UI. Each provider retry arrives as a retry frame with attempt, max_retries, delay_ms, reason, and message. It never ends a turn; the turn's cost or error frame still does.

Read a subagent's cost without its full log (0.61.0). Add "summary": true to a run_log request, and events holds only the run's latest usage event. Servers that predate the field ignore it and send the full log. Replay from disk now ends with a usage event carrying the session's totals, and its cost includes the run's own sub-delegations.

json
{
	"type": "run_log",
	"session_id": "my-session",
	"run_id": "tap-developer-cto-a3f1c2",
	"request_id": "log-1",
	"summary": true
}

Failed runs keep their error. A failed tap run's error now stays in its status snapshot and its replayed status event. A known run without an event log replays from its metadata instead of returning run not found.

The WebSocket server guide documents every frame and the replay rules.

The other fixes since 0.59.0

  • 0.59.1 caps MCP grants by the server's own filter. A role's allowed_tools can no longer expose a tool that the server's tools list hides. A role that grants only hidden tools gets nothing from that server.
  • 0.59.1 renews MCP OAuth tokens without the browser. A stored refresh token now renews an expired access token; you sign in again only when no refresh token is stored or the server rejects it.
  • 0.59.1 widens --sandbox for two paths. On Linux and macOS, ~/.npm is writable, so npx-launched MCP servers can write npm's cache. The run directory is writable too — the session inject socket and PID files for octomind send live there.
  • 0.59.1 keeps stdout clean for scripts. With --format jsonl, notices about finding or failing to load a session no longer print to stdout, and octomind workflow sends rendered markdown step output to stderr.
  • 0.59.1 answers every ACP request before exit. An editor that sends several requests and closes stdin right away still gets every reply.
  • 0.59.1 refuses webhook bodies over 10 MiB with 413, and the hook script does not run.
  • 0.59.1 fixes /done and /effort. /done no longer extracts lessons from the same transcript a second time at /exit, and setting /effort to the current level reports no change instead of re-saving the session.
  • 0.61.0 shapes background-job results like any tool result. A result that arrives later through the inbox is capped by mcp_response_tokens_threshold and condensed by the supervisor, as an on-time result would be.
  • 0.61.0 treats an empty OCTOMIND_DATA_DIR as unset, so Octomind uses the platform default data directory instead of the current directory.
  • 0.61.0 rejects unknown /context filters with an error that lists the valid ones: all, assistant, user, tool, and large. The system filter is gone.

Four of these fixes came from community contributors. Thank you.

Before you update

  • Commits the agent writes carry the Octomind trailer by default. Set commit_attribution = "" to keep your history as it was.
  • A WebSocket disconnect no longer cancels the running turn. Send cancel to stop one.
  • /context system is gone, and an unknown filter now returns an error instead of showing everything.
  • Your config moves from version 22 to 24. On first start, Octomind upgrades config.toml, keeps your values, adds commit_attribution and max_identical_rounds, and prints where it saved a backup. octomind config --upgrade runs the same migration on demand.

Update to Octomind 0.61.0

On macOS and Linux:

bash
curl -fsSL https://octomind.run/install.sh | bash -s -- --version 0.61.0
octomind --version

On Windows, in PowerShell:

powershell
$env:OCTOMIND_VERSION = "0.61.0"
irm https://raw.githubusercontent.com/muvon/octomind/master/install.ps1 | iex

Or run scoop update octomind. See the installation guide for other methods and the full changelog for every change since 0.59.0.

Then try it: open config.toml, find max_identical_rounds under [supervisor], and set it to the longest run of identical rounds you would still pay for.