Troubleshooting
Each entry below is a symptom you can actually observe, the cause behind it, and
what to do. They are all failures this stack has real evidence for, recorded in
the compose files, the configuration files or the proxy’s own code. If your
problem is not here, the boot log of crab-shell-proxy is almost always the right
first place to look — it names missing variables on purpose.
A ganglion agent stops answering, and the proxy logs a failed image pull
Symptom. Chatting with an agent that runs the ganglion harness fails. The
proxy’s log carries a pull error naming the image, typically
zombie-crab/crab-ganglion:dev. Other agents are unaffected. It often starts
right after a docker system prune, or on a server that was deployed by pulling
images rather than building them.
Cause. EnsureImage in crab/crab-shell-proxy/internal/docker/client.go has
a fast path: it asks the daemon for the image locally and, if it is there, returns
without contacting any registry. That is what lets a locally built tag work at
all. It does not stop there when the image is missing — it falls through to
POST /images/create, a real registry pull, which 404s on a tag no registry has.
The default CRAB_GANGLION_IMAGE is zombie-crab/crab-ganglion:dev, a name that
exists only on the machine that built it. A prune removes unused images, this one
goes, and the agent is dead until the tag comes back.
Fix. Rebuild the tag. The ganglion-image service in docker-compose.yaml
exists for exactly this: it is a build-only service that produces the image, runs
/bin/true and exits, and crab-shell-proxy waits for its completion. picoclaw-image
does the same for the other harness, so both are recovered by the same command:
docker compose up -d --build
On a server, prefer the durable fix: set CRAB_GANGLION_IMAGE in your .env to a
published immutable reference — crab/crab-ganglion-harness’s release workflow
publishes ghcr.io/lepistabioinformatics/crab-ganglion:sha-<short-sha> — and pull
it. See Deployment for why that reference must be immutable
rather than a moving tag.
Nothing comes up at all: the gateway never becomes healthy
Symptom. docker compose up -d returns, but mycelium-gateway sits waiting
and chat-webapp never starts. Nothing is reachable.
Cause. mycelium-gateway declares depends_on: crab-shell-proxy: condition: service_healthy, and chat-webapp in turn depends on the gateway being healthy,
so a proxy that exits at boot takes the whole stack with it.
The proxy exits fatally for a small, specific set of reasons. Its validate()
rejects a configuration with no hostDataRoot, no network, an agent with no
serviceName or no template, or an agent naming a harness it does not
orchestrate. Separately, a picoclaw agent whose bearer token cannot be
resolved from the environment is fatal — deliberately, because silently dropping
one would lock a member out with no boot-time signal.
Fix.
docker compose logs crab-shell-proxy
The failure names what is missing. Set it in .env and bring the stack up again.
One agent’s routes answer 404 and everything else works
Symptom. A single agent behaves as though it does not exist — its routes return 404 — while the other agents are fine and the proxy is healthy.
Cause. A ganglion agent removes itself at load instead of taking the proxy
down, and this is the designed behaviour rather than a bug. One config file can
describe several deployments, and an agent that reaches a host with no key for it
degrades to “that agent does not exist” rather than “the proxy will not boot”,
which would take every other agent down too. There are three reasons it can
happen: CRAB_GANGLION_IMAGE is unset, the agent’s model apiKeyEnv variable is
unset, or its bearer token cannot be resolved.
Nothing is silently downgraded. The proxy prints one line per disabled agent at
boot: agent "<key>" disabled: <reason> — its routes will answer 404, where the
reason names the setting, for example that ganglionImage (or
CRAB_GANGLION_IMAGE) is unset and has no default on purpose.
Fix. Read the boot log for disabled, set the variable it names, and restart
the proxy.
An agent that declares no
harness:key is a ganglion agent today (DefaultHarnessincrab/crab-shell-proxy/internal/config/config.go), so it is subject to all three checks. Declare the harness explicitly on every agent — a config upgrade should not change a runtime by omission.
Creating a scheduled task answers 501
Symptom. The Tasks panel, or a direct API call, refuses to create, change,
disable or delete a scheduled task with a 501 Not Implemented and a message like:
creating scheduled tasks over this API is not available on the picoclaw
harness (agent beta): its agent creates them itself
Cause. crab/crab-shell-proxy/internal/httpapi/cron_write.go refuses every
cron write route for an agent whose harness is not the ganglion. On picoclaw the
harness owns the job store and holds the live schedule in memory, so a toggle in
the panel could disagree with the timers actually running. On the ganglion the
proxy owns the schedule, above the container, so it can serve the writes honestly.
The read routes are served on both harnesses, which is why you can still see a
picoclaw agent’s tasks and their run history.
Fix. On a picoclaw agent, ask the agent in chat to create, change or remove the task; it owns them. If you want the panel’s write controls, use an agent on the ganglion harness. See Scheduled tasks.
The general mechanism is
requireHarnessFeatureininternal/httpapi/harness_gate.go: a feature a harness cannot serve answers 501 naming the harness, rather than quietly succeeding. That rule exists because a withdrawn harness once let projects be created, stored, listed and reported active while changing nothing about the agent that answered. As the tables in that file stand today it reserves nothing from either shipped harness, so the cron write above is the 501 you will actually meet.
The gateway answers 400 "Request path does not match any service"
Symptom. A request through mycelium-gateway is rejected before the proxy is
ever reached, with that exact text.
Cause. Mycelium routes by the first path segment, matched against a service
key in its TOML config, and then matches the rest of the path against that
service’s [[<agent>.path]] blocks. A path with no matching block is refused
here. This is what happens when a proxy route exists but the gateway config was
not extended to allow it — the /v1/cron/* read routes are the usual example, and
all the profiles under deploy/ already carry a block per agent for them.
Fix. Add the matching [[<agent>.path]] block to the gateway config your mode
mounts (deploy/standalone/config.standalone.toml or deploy/prod/config.base.toml)
and recreate the gateway.
The same error text appears for an unrelated reason: Mycelium’s own
/healthroute only handlesGETand rejectsHEADwith this message. If you are probing it with something that sendsHEAD—wget --spider, for instance — the 400 is about the method, not about routing. Both healthchecks indocker-compose.yamluse a plainGETfor this reason.
Grafana is empty and every container is healthy
Symptom. The dashboards load, the panels render, and there is no data. No container is unhealthy, no log shows an error, and harness-sphere’s own log looks busy.
Cause. The -f chain was omitted on some command. Compose applies an overlay
only when you name it, so docker compose up -d, restart, or even
up -d harness-sphere without both files reverts every service the overlay
overrides back to docker-compose.yaml. For harness-sphere that means the
exporter goes back to stdout and its config bind-mount disappears, so it prints
metrics into its own logs instead of sending them. Nothing anywhere reports this.
Fix. Bring it up naming both files, and then remove the footgun by putting
this in the .env at the repository root, after which a bare docker compose up -d
is correct:
COMPOSE_FILE=docker-compose.yaml:docker-compose.observability.yaml
See Observability.
The agent says it saved a file, and the Files panel does not list it
Symptom. The agent reports writing a document, and nothing appears in the member’s Files panel. The file really was written — it is simply somewhere the interface does not look.
Cause. public/ inside a workspace is the only directory a member’s interface
lists, and public/attachments/ is where an agent is told to write deliverables.
A managed memory file (FILE_DELIVERY.md) carries that rule into every workspace
and is read on every turn, precisely because a file written outside public/ is
invisible to the member no matter how the deployment is configured.
Fix. Ask the agent to move or re-save the file under public/attachments/,
naming the path. Do not write to uploads/: that is the directory’s former name,
kept in the code only so a one-time migration can recognise a workspace predating
the rename.
A related surprise: the paperclip notice the proxy appends when a file is delivered is stream-only and is never persisted. After a page reload, the only account of a delivered file is whatever the model itself wrote in its reply, which is why the managed rule also tells it to name the path out loud.
The Map and Entities tabs stay empty
Symptom. The knowledge graph never records anything. No error is shown and nothing is logged about it.
Cause. CRAB_MCP_TOKEN_SECRET is empty. That value signs the bearer token a
spawned agent presents back to the proxy’s own MCP endpoint. Unset is a supported
and deliberate state: /v1/mcp is not registered, no MCP server block is written
into any workspace, and everything else behaves normally. A deployment that forgot
the secret must get no memory rather than an unauthenticated endpoint reachable by
every container on the network.
Fix. Generate one with openssl rand -hex 32, set it in .env, and recreate
the proxy. Both .env.example files spell this out.
harness-sphere cannot attribute instances, or /v1/instances 404s
Symptom. The watcher reports the host, itself and the three service probes,
but nothing per tenant. A direct request to GET /v1/instances on the proxy
returns 404.
Cause. CRAB_TELEMETRY_TOKEN is empty, so the route is not registered at
all — absent, not 401. This endpoint discloses the deployment’s whole tenant,
subscription and user topology, so a deployment that has not opted in grows no new
surface.
Fix. Generate a fresh secret with openssl rand -hex 32 and set it. Do not
reuse an agent’s bearer token for this: an agent token gates chatting as any
member of any tenant, and the reasoning is in Observability.
rm -rf data/... fails with Permission denied
Symptom. Wiping the on-disk state during a reset fails.
Cause. crab-shell-proxy creates the tenant tree as root, 0700, so the
directories are not yours.
Fix. Use sudo for that one command, as the reset sequence in
Deployment does. This is also why harness-sphere runs as
root with a read-only bind: nothing else could traverse the tree.
No sign-in e-mail arrives in standalone
Symptom. You request a magic link, or the Staff bootstrap code, and no e-mail ever comes.
Cause. Standalone has no real SMTP. It ships a stub transport that writes the message to the log instead of sending it — a deliberate property of the mode.
Fix. Read it out of the gateway’s log. Sign-in links land in the same place.
docker compose logs mycelium-gateway | grep -i bootstrap
Nobody can sign in to a fresh prod deployment
Symptom. The prod stack is up, the gateway is running, and authentication does not work.
Cause. Mycelium’s Postgres adapter has no embedded migrations, unlike its SQLite one, so a freshly created database has no schema.
Fix. Run the one-time, two-step schema application described in Database and migrations. Both steps are required at the release this repository pins; the second is not optional.
Where to go next
If the failure is about how a deployment is assembled, Deployment is the fuller account. If it is about what the watcher does or does not show, Observability covers that surface. For the behaviour of the two harnesses themselves, see Harnesses.