Skip to content

Troubleshooting

What a problem looks like, and what to do about it — grouped by where you were when it happened.

Starting a flow

the flow drives 2 agents, 1 given

The flow declares more agents than -a named, or fewer. Give one -a per agent, in the order the flow takes them:

sh
hmz exec -f rlar -a claude/claude-opus-4-8:high -a claude/claude-opus-4-8:high "fix the build"

Ask a flow how many it wants without running it:

python
from hmz.flows import drives

print(drives("rlar"))   # ('actor', 'reviewer')

A Person place does not count. Nobody chooses what the person runs.

<flow>: no flow to read: a flow is a directory with an __init__.py in it

-f named something that is not there, or a directory with no flow in it. humanize looks for a name in .humanize/flows, then ~/.humanize/flows, then among the flows humanize ships and every flowverse fetched here. A name no place answers to is taken as a path — flows/mine and flows/mine.py both. See where flows live.

the official flowverse has not been fetched yet

The name is right; the download has not happened. /flow fetches whatever has never been fetched as it opens. Press r on it in /flowverses to fetch it again — or, from a script with no terminal to press it at, Hmz().verses.fetch("official").

nothing in it is marked @flow(), and it holds …

The file holds several flows, and none of them is under its own name. Say which one you want with a colon: -f humanize1:gen-plan.

nothing in it is marked @flow()

Nothing in the file says which of its functions is a flow. A function called run is not a flow because it is called that. Mark it:

python
from hmz.flows import flow

@flow
def run(agents: tuple[Agent], task: str) -> None:
    ...

a flow is a function marked @flow() taking (agents, task), whose agents are annotated …

The agents parameter is annotated with a type that does not say how many agents there are. tuple[Agent, ...] means any number, which is no answer.

python
def run(agents: tuple[Agent], task: str) -> None:          # one agent
def run(agents: tuple[Agent, Agent], task: str) -> None:  # two
def run(agents: Agents, task: str) -> None:                     # a NamedTuple of them

(Each is marked @flow, which is what makes it a flow at all.)

the flow's agents cannot be read here (…)

The annotation names something that exists only for a type checker:

python
if TYPE_CHECKING:                     # ← this is the problem
    from hmz.flows import Agent

Import it at runtime instead. The count has to be readable where the flow runs, not only where pyright looks.

bad agent 'claude:high': expected [NAME=]CLI[@PROVIDER]/MODEL:EFFORT

An -a is missing a part. The CLI, the model and the effort are all three required:

sh
-a claude/claude-opus-4-8:high

The CLI is read from the front and the effort from after the last colon. A model with slashes in it, such as kimi/kimi-code/k3:high, is fine.

bad agent 'cli=claude': cli= is gone

The written-out cli=…,model=…,effort=… form no longer exists, and neither do provider=, service_tier= or config.KEY=. = on the line means something else now — it names which of the flow's agents this one is:

sh
-a claude@deepseek/claude-opus-4-8:high        # the account is @, not provider=
-a reviewer=codex/gpt-5.6-sol:max              # = names the place it fills

A latency tier and a backend-native override are still an agent's to carry. They are set where the agent is made — from the SDK, or by the flow that declares it — rather than on the line that names one.

bad agent '…': permission is the flow's to say

What an agent may do, and whether it may search the web, are not settings of -a: the flow declares them where it declares the place, so that they mean the same thing whichever CLI fills it. Take the key off the line and write it into the flow:

python
class Agents(NamedTuple):
    reviewer: Annotated[Agent, AgentDefaults(permission="read-only")]

See Permissions and Writing a flow.

The agent starts and immediately fails

Run the backend's own command line by hand first. humanize passes model and effort through untouched. So a model your account cannot run fails the same way it would anywhere:

sh
claude --help
codex --version

The turn failed and I want to know which of my problems it is

Every failed turn now ends with the kind of failure it was and what to do about it, in brackets after whatever the CLI said:

Command '['claude', …]' returned non-zero exit status 1. 429 rate limit exceeded
(throttled: this account has spent its quota; another one, or a wait, is what answers it)

The kinds are throttled, refused, retired, contended, dropped, killed and missing, and each of them gets a different answer before the turn is given up on. A failure with no kind in brackets is one nothing recognised, which is tried again exactly as a failed turn always was.

(refused: that account needs signing in again)

The credential, not the request: a 401, a 403, or a login that has expired. Nothing is tried again under it — it would be refused a minute later too — and the turn carries on under the next account of that backend. Sign the one it left back in:

sh
claude auth login

Or, for an account humanize keeps rather than the one this machine is signed into, open /providers, put the cursor on it and press enter: sign in again runs that backend's own way in under this account's paths.

(retired: the model is gone or was never this account's; another place is what answers it)

The CLI or its service says there is no such model. No account of that backend answers it, they are all offered the same catalogue, so the turn goes straight to the next place. Ask the backend what it actually runs, and give the agent one of those: in the agent's own setup sheet, open the model row and press r, which asks that CLI again as that account and keeps what it said. From a script, Hmz().accounts.ask(cli, provider) is the same question.

(missing: npm i -g @anthropic-ai/claude-code)

There was nothing to run. The CLI is not installed, or would not start. The line in brackets is the one that installs it; curl https://cursor.com/install -fsS | bash for Cursor, pip install 'deepseek-harness-sdk' for DeepSeek Harness. humanize looks on PATH first and then where an installer would have put one — see what you have.

(contended: two turns of it are sharing one database)

opencode keeps its sessions in one SQLite database shared across workspaces, so two turns of it at once can lose a race and be told database is locked before either has spoken to a provider. The turn is taken again three times, a second apart, which nearly always clears it. If it does not, run fewer opencode agents at once.

codex: this machine will not run an agent at bypass, so it runs at auto

Not a failure: a note, said once per agent. This Codex was given requirements by somebody else — an enterprise policy that arrives with the account, or a requirements.toml on a machine whose platform packages Codex — forbidding the danger-full-access sandbox that bypass is. Codex refuses such a call outright, so humanize asks again a rung down, at auto: the same freedom, with Codex asking before it reaches past the workspace and humanize granting what it asks. Ask for the agent at permission=auto to say it yourself and skip the note. What the machine allows is its own to say:

sh
cat /etc/codex/requirements.toml

In the interface

no coding agent is installed here

No backend was found, on your PATH or in the directories an installer puts one in. humanize drives the CLI you already have; it holds no API key and talks to no model provider itself.

sh
command -v claude codex kimi pi opencode mimo zcode

no choosing a flow while a flow is running: ctrl+c twice stops it first

Or no switching flow while a flow is running. Choosing a flow means running it, which means stopping whatever was running — and humanize says so rather than doing it behind your back. Press ctrl+c twice first, or type /stop, which is the same stop asked once.

a flow is already running

This has the same cause, from a /flow that named a path.

say on or off, not 'yes'

/details and /afk flip when you give them nothing. When you tell them which, they take exactly on or off.

no such command: /foo

Type / to see the list. hmz internal anchor is deliberately not a command here: it is not a thing to do to a flow that is running. /epics is where the runs of this directory are: going into one is where it is exported, trace and all.

A line I typed did not reach the agent

Look at whether it is still pinned above the prompt. A line sits there, rather than in the transcript, until somebody has actually taken it — the next turn if none was open, or the running turn saying the words are in front of it. A line to a running flow is never dropped: one that nothing ever took is written down as never sent rather than left looking like it went. It reaches whichever agent has a turn open, not whichever was named last.

Several lines typed in a row go one at a time. The ones behind the first sit pinned for a turn or two before their own answer comes back. That is deliberate: handed over together, they would be run together and answered once.

If the agent is anchored and is Claude, it hears you between turns rather than during one. An anchored Claude ends its process with each turn, so that its work reaches the target before the turn says it landed. See Remote execution.

The screen is unreadable in my terminal

The interface uses only your terminal's own 16 colours and never asks what they are. So this is usually a theme with too little contrast between two of them. NO_COLOR=1 hmz drops to no colour at all.

The token count sits still, then jumps

It should not: the cost readout tails the logs the backends write as they go rather than waiting for a turn to end. If it does sit still, the backend's home is somewhere humanize is not looking. Check CLAUDE_CONFIG_DIR, CODEX_HOME, KIMI_CODE_HOME. ZCode has no variable of its own, so its home is ~/.zcode under whatever HOME says.

Driving agents from Python

RuntimeError: session has not run a turn yet

session.id is the backend's id, and the backend has not named the session yet. Use session.named instead. It answers None if you need it before a turn has landed.

NotImplementedError: … cannot be talked to mid-turn

That backend takes a turn's whole prompt up front. So there is nowhere for a later word to go. See what each backend can do.

RuntimeError: no turn is running to be talked to

You called interject on a backend that can be talked to, but no process is up to hear it. Open the session with a turn first.

the watchdog stopped this turn: … has said nothing for …

The CLI was still running and had stopped saying anything, so the turn was ended rather than waited on. It is a failed turn like any other: the conversation is untouched, and the retries and fallbacks written down for that place take it against the same session. Nothing has to be done unless it keeps happening.

If it keeps happening, the run says which rung it got to and what it saw — is idle, is stopped, is gone, is still working — on the stream you are watching the agent on. is gone means the CLI exited without ending the turn, which is a crash to look for in that CLI's own log; is stopped means something suspended it; is idle means it was waiting on something that never came back.

A healthy turn was killed for thinking too long

The default window is a quarter of an hour of complete silence — no reasoning, no tool call, no line of protocol — which no ordinary turn reaches. If yours does, say so:

sh
HUMANIZE_WATCHDOG=3600 hmz exec -f rlar -a claude/claude-opus-5:high "…"

HUMANIZE_WATCHDOG=0 turns the watchdog off entirely. See when a CLI stops answering.

NotImplementedError: … cannot be interrupted: …

That backend takes a turn's whole prompt up front and runs it as one command, so there is nothing listening to be told to stop. Nothing is wrong: whatever asked goes on to what it does about a turn that cannot be asked — the watchdog puts the process down instead.

NotImplementedError: … has no goal feature

pursue is the backend's own goal feature, not a prompt asking for one. suppress=True does not catch this, deliberately. Asking for a feature that is not there is a flow to correct.

My loop never ends after I stop it

Your loop is catching the stop. Stopped is not a CalledProcessError, and suppress=True does not catch it. But a bare except Exception in your flow will. Let it propagate.

A turn raises subprocess.CalledProcessError and I want the loop to continue

python
agent(task, suppress=True)

Whatever the turn was actually run through, a failed turn raises this one type. So a flow catches turns rather than transports.

Collecting a trace

cannot parse time: …

--start and --end take anything dateparser understands. Quote it: --start "3 days ago".

session id cannot be empty

A --session with an empty entry. Usually a trailing comma.

0 sessions, 0 slices

Nothing matched. In order of likelihood:

  1. The backend's home is elsewhere. Check CLAUDE_CONFIG_DIR, CODEX_HOME, KIMI_CODE_HOME. A home that does not exist is skipped silently.
  2. The run being traced opened nothing. A trace is of a run and holds that run's own sessions. So a run that died before its first turn is a trace of nothing. /epics says how many sessions each run opened, and --epic names another.
  3. You are tracing the wrong directory. Runs are kept per workspace. Without a <workspace>, the last run of this one is what is traced.
  4. Nothing here was ever run by a flow. Then there is no run to trace, and /epics has nothing to offer. A trace of loose sessions is Hmz().epics.trace(), or Hmz().epics.trace(sessions=["<id>"]) for named ones — there is no way in that asks for one.
  5. The time window excludes it. Drop the start and end you passed.

Two agents show up as one

They ran at the same configuration, and nothing said they were two. A trace reads that off the run it is a trace of, which is the epic /epics gathered it from. So a trace asked for by Hmz().epics.trace(sessions=…) is of no run and has nothing to read it off. If you drive agents by hand, pass agents={a.id: a.opened for a in …}. See what counts as one agent.

Remote execution

humanize has a register map for aarch64, x86_64; this host reports …

The half that runs beside the agent needs an architecture-specific register map, and there is one for those two. The target may be any architecture, so a machine this refuses can still be anchored to from one that it does not.

humanize intercepts syscalls with a Linux seccomp filter and a ptrace supervisor …

Said on a Mac, or on anything else that is not Linux. Run the agent inside a Linux virtual machine — Docker Desktop, colima and lima each give you one, on Intel Macs and on Apple silicon alike. Serving a target from a Mac needs none of this and is unaffected.

unsupported target '…'

expected ssh://HOST, docker://CONTAINER, tcp://HOST:PORT or local[:PATH]

Those four, and nothing else. See Targets.

refusing to listen on a non-loopback address without --token

An open port is equivalent to a shell on that machine. Give --token a real secret, or prefer ssh:// or docker://. Those need no open port at all.

The target cannot be reached

Ask it what it is. This runs nothing there:

sh
hmz internal anchor --check --target ssh://build-box

It bootstraps the target half, opens the channel and reads the workspace back — the whole path, without starting an agent. --log-level debug says more, on stderr, the one stream a session never speaks the protocol on.

The target refuses the mirror directory

The mirror is authoritative: anything in it the target does not have is deleted. So humanize refuses a mirror directory holding unrelated files, or one last used against a different target. Point --shadow somewhere empty, or pass --force if you are sure.

the target speaks protocol …

The two halves are different versions. The bundle is cached on the target by digest. A stale one is replaced by a new connection. So this usually means two different humanize installations are both driving that target.

The agent can no longer reach its model provider

--net remote sends the agent's own connections to the target. Leave it at local, the default, or keep the provider local with --net-allow api.anthropic.com:443.

A command ran against stale files

Only file contents cross. A permission change made through an already-open descriptor never reaches the target. Ownership, device nodes and extended attributes never leave the mirror. The full list is What is not guaranteed.

Containers

could not start a container of python:3.12: …

Whatever docker said is attached. The usual causes are no daemon to reach, an image that is not pulled, and an image with no shell in it.

could not install humanize in …: container … is not running

The container stopped as soon as it was started, which is what an image holding no Python the target half can use does. What it said on the way out is attached: the places an interpreter was looked for. Any Python 3.12 or newer answers, on the image's PATH or at one of those paths.

no directory to give the container

The workspace is not there. humanize refuses it rather than mounting it into being. Docker would create it for you, owned by root, inside directories you own.

Containers left behind after a flow was killed

They are labelled with the uid that started them:

sh
docker rm -f $(docker ps -q --filter label=humanize=$(id -u))

That cannot reach past you on a machine several people share.

Still stuck

  • --log-level debug on hmz internal anchor, both ends.
  • The SPECs under specs/ say what it is supposed to do, normatively. specs/coganchor.md is the one worth reading when a remote session behaves strangely.
  • Architecture says which layer to look in.
  • Ask in issues.

Released under the Apache-2.0 licence.