Troubleshooting
Every error the Cephable Automate HTTP Server returns and what to do about it — connection refused, 401, 404, 409, timeouts, port moves, secure storage, model selection, and runs that fail or stall.
Quick diagnosis
Start here. One authenticated /health call answers most questions:
curl -sS -i http://127.0.0.1:4317/health -H "Authorization: Bearer $CEPHABLE_AUTOMATE_KEY"
| What you see | What it means | Go to |
|---|---|---|
| Connection refused / no route to host | Nothing is listening there | Cannot connect |
401 Unauthorized |
A server is there, the key is wrong | 401 |
200 with "service": "cephable-agent" |
The server is healthy — the problem is in the request or the run | 409 · Run failures |
200 from something that is not Cephable |
Another program owns the port | Finding the port |
Cannot connect
Work through these in order:
- Is Cephable running? The server lives inside the desktop app process. Closing Cephable — or the app being quit to the tray and then killed — stops it. There is no background service.
- Is the extension enabled? Open Extensions → Cephable features → Build & Extend → Automate HTTP Server. The detail view must say Running.
- Is the account licensed? The extension is gated behind the
enableAutomateHttpServerfeature flag and requires Cephable Professional. Without it the card shows an upgrade prompt and the server will not start. Signing out, or a license change, stops the server automatically. - Did the port move? See below.
- Are you using
localhost? On some systemslocalhostresolves to::1first and the server only listens on IPv4. Use the literal127.0.0.1. - Are you calling from a browser page? You cannot. There are no CORS headers and no
OPTIONShandler, so afetchfrom anhttp(s)://origin fails preflight. Call from Node, Electron's main process, a native app, or your own backend. - Is a firewall or endpoint-security product intercepting loopback? Some enterprise agents do. Test with
curlfrom the same machine to separate your client from the network path.
Finding the port
Cephable prefers 4317. When that port is taken — most often by a second Cephable instance or a stale process — it binds the next free port in a twelve-port window (4317–4328) and the extension detail view shows a notice:
Port 4317 was already in use on this device, so the server started on port 4318 instead. Use the endpoint above in your tools, not the default.
If all twelve are taken, the server fails to start with an error naming the range.
Read the endpoint, don't assume it. The detail view's Endpoint field is authoritative. For an integration that has to survive a move, sweep the same window and confirm you found Cephable rather than some other local service:
async function discoverEndpoint(token) {
for (let offset = 0; offset < 12; offset += 1) {
const candidate = `http://127.0.0.1:${4317 + offset}`;
try {
const response = await fetch(`${candidate}/health`, {
headers: { authorization: `Bearer ${token}` },
});
// A 401 still proves a server is listening here.
if (response.status === 401) return { endpoint: candidate, authorized: false };
if (!response.ok) continue;
const body = await response.json();
if (body.service === 'cephable-agent') return { endpoint: candidate, authorized: true };
} catch {
// Nothing listening here; keep sweeping.
}
}
return null;
}
Checking service === "cephable-agent" matters: OpenTelemetry collectors also default to 4317, and pointing an agent client at one produces baffling errors.
To force a port — in a lab, CI rig, or kiosk — launch Cephable with --port:
Cephable.exe --port 4321
Cephable is single-instance, so relaunching with a new --port rebinds the running app's server. See Launch options.
401 Unauthorized
{ "error": { "message": "Unauthorized", "type": "authentication_error" } }
Causes, most common first:
- The key was regenerated. Rotation is immediate with no grace period. Copy the new key from the extension detail view.
- Header format. It must be exactly
Authorization: Bearer <key>. A bare key,Basic, orX-Api-Keyall fail. - Whitespace. A trailing newline from a copy-paste or a
$(cat keyfile)shell substitution changes the token. Trim it. - Wrong machine or profile. The key is per-device and per-user-data-directory; it does not sync.
- You are hitting a different server. Another service on the port will reject your key too. Confirm with the discovery sweep above.
A 401 on /health is expected behavior, not a misconfiguration — there are no anonymous routes.
404 Not Found
{ "error": { "message": "Not found", "type": "not_found" } }
- Wrong method. The server returns
404, not405.GET /v1/runsandPOST /healthare both "not found". - Wrong path. Only these exist:
/health,/v1/models,/v1/automate/models,/v1/automate/models/select,/v1/automate/cancel,/v1/runs,/v1/chat/completions. - An OpenAI route that is not implemented.
/v1/completions,/v1/embeddings,/v1/responses, and per-completion GET routes do not exist. - A doubled
/v1. If your client is configured with a base URL ofhttp://127.0.0.1:4317/v1, it will append/v1/chat/completionsitself — check for/v1/v1/…in the request line.
409 Conflict
{ "error": { "message": "The on-device assistant is already running or preparing (executing-progress)", "type": "conflict" } }
The app has one inference slot, so exactly one run happens at a time. This includes runs the user started in the AI Workflows panel — your integration is sharing the assistant with a person — and a run parked awaiting caller-executed tool results, which is still mid-agent-loop. Check awaitingToolResults on /health to tell the two apart.
Three ways to handle it:
Wait for readiness (the polite default):
async function waitUntilReady(endpoint, token, timeoutMs = 300_000) {
const deadline = Date.now() + timeoutMs;
while (Date.now() < deadline) {
const health = await getJson(endpoint, token, '/health');
if (!health.busy && ['idle', 'terminated'].includes(health.workflowStatus)) return health;
await new Promise((r) => setTimeout(r, 1000));
}
throw new Error('The assistant did not become available');
}
Take the slot — only when your integration legitimately owns the machine:
curl -sS http://127.0.0.1:4317/v1/automate/cancel \
-H "Authorization: Bearer $KEY" -H "Content-Type: application/json" -d '{ "force": true }'
Fail fast and tell the user Cephable is busy. Often the best answer in an interactive tool.
Do not retry immediately in a tight loop, and make sure your HTTP client has retries disabled — an automatic retry either gets another 409 or, worse, starts a duplicate run.
/v1/automate/models/select returns 409 for the same reason. /health, /v1/models, /v1/automate/models, and /v1/automate/cancel are always available.
400 Bad Request
Everything the server rejects before or during a run, in one envelope:
| Message | Fix |
|---|---|
prompt must be a non-empty string |
Send a non-blank prompt |
timeoutMs must be at least 1000 |
Raise it, or omit for the 15-minute default |
answerContract must be a string / continuation must be a boolean / include must be an object / include.steps must be a boolean |
Type error in the request body |
messages must be an array |
Chat-completions request without messages |
The latest user message must contain string content |
Your client sent content parts or an image. Only plain string content is supported |
Request body exceeds 1 MiB |
Put large inputs in a file and have the run read it, rather than inlining them |
Automate run timed out after Nms |
See timeouts |
Timed out initializing the on-device assistant |
The worker or llama.cpp server did not come up within 15 minutes. Check that a model is downloaded, and look at the app's own status |
No catalog model matches the requested name/family/size |
Check spelling against /v1/automate/models |
Model selector is ambiguous; matches: … |
Add modelName, or narrow with sizeCode |
Model is not compatible with this device: … |
availableForDevice is false — pick a smaller model |
Model is not marked as tool-capable: … |
Automate requires tool calling; pick another model |
Model is not downloaded. Select and download it in Cephable first: … |
Download it in the app; there is no download endpoint |
Cannot switch models while workflow status is … |
Wait for idle, or cancel first |
Selected model did not become active; expected …, got … |
The switch did not take. Cancel with force: true, then select again |
mcpServers[0].name: Use lowercase letters only. (and similar) |
An inline MCP server failed validation — the message names the field. Names are lowercase letters, numbers and hyphens |
mcpServers[0].name collides with an MCP server already configured in Cephable |
Rename the inline server, or select the configured one with selectedMcpServerIds |
clientTools[0].name must contain only letters, numbers, underscores and hyphens |
Tool names follow the OpenAI function-name rules |
clientTools[0].name must not start with "mcp__" |
That prefix is reserved for MCP tools |
results[0].id is required |
A resume body's results entries each need the id from the toolCalls entry |
| Malformed JSON | A JSON parse error surfaces here too — check Content-Type and the body |
The extension will not enable
This server cannot be enabled because secure operating-system storage is unavailable.
The access key is only persisted if the OS can encrypt it.
- Linux is the usual cause: the
basic_textfallback backend is explicitly rejected because it would store the key in plaintext. Install and unlock a real keyring (GNOME Keyring, KWallet), then try again. - Headless, container, or remote sessions often have no keyring available.
- Windows / macOS should work by default; a failure here points at a broken user profile or a security product blocking DPAPI/Keychain.
A Cephable Professional license is required
The account does not have the enableAutomateHttpServer feature flag. The server also stops on its own if the license changes or the user signs out.
Other startup failures are reported in the detail view's error line — for example, all twelve candidate ports being in use.
A run failed
An HTTP 500 with schemaVersion: 1 is not a server fault. The run happened and did not succeed, and the body is a full record:
if (body.schemaVersion === 1 && body.status !== 'completed') {
console.error(body.status, body.errorCode);
const failed = (body.steps ?? []).filter((s) => s.status === 'failed');
console.error(failed.map((s) => `${s.toolName}: ${s.resultSummary}`));
}
errorCode |
What to try |
|---|---|
MODEL_LOAD_FAILED |
Confirm the model is downloaded and the device has free RAM. Check /health backend for a CPU fallback |
CONTEXT_LIMIT_EXCEEDED |
The task outgrew contextSize. Split it, narrow the scope, drop continuation, or use a machine with more RAM |
TOOL_TIMEOUT |
A tool call hung — commonly a web fetch or an MCP server. Narrow the prompt or remove that MCP server from the run |
PLANNING_TIMEOUT |
The model could not form a plan. Simplify the prompt, or raise thinkingLevel |
EXECUTION_TIMEOUT / EXECUTION_FAILED |
Read the last failed step's toolArgs and resultSummary |
SCHEMA_INVALID |
The model produced a malformed tool call. A larger model, or a simpler task, usually fixes it |
SERVER_LOST |
The llama.cpp server died mid-run. Retry; if it repeats, check the app logs for a GPU crash and backend degradation |
DISABLED_BY_POLICY |
An organization policy blocks this workflow or tool |
UNSUPPORTED_DEVICE / UNSUPPORTED_OS_VERSION |
The device or OS cannot run AI Workflows |
CANCELED |
You, the user's Stop button, or a timeout stopped it |
status: "canceled" with no errorCode is usually the user pressing Stop in the panel, or a cancel from another client. status: "terminated" means the worker was killed — the next run will cold start.
A run times out
On timeout the server cancels the run and returns 400, not a run record — so you lose the steps and usage for that attempt.
If you need diagnostics on slow runs, invert the control: set a generous timeoutMs and enforce your own deadline with POST /v1/automate/cancel. A cancelled run settles as a complete record with status: "canceled".
Also check:
- Your client's timeout is above
timeoutMs. Many HTTP clients and SDKs default well below a realistic agent run. A client-side abort does not stop the run — it keeps going inside Cephable and blocks the next one until it finishes. - A cold start is included. The first run after app launch, or after a model switch, loads the GGUF first.
durationMscovers that. backend.cpuFallback. If GPU acceleration failed, everything is an order of magnitude slower./healthtells you.
A resume returns 404
{ "error": { "message": "No suspended run matches that resume token. It may have timed out or been cancelled.", "type": "not_found" } }
A run parked on caller-executed tools waits two minutes per round for results, then cancels itself — otherwise a caller that crashed mid-tool would hold the user's assistant until the run's own timeoutMs. Causes:
- Your tool took longer than two minutes. Post an
errorresult quickly and let the agent adapt, rather than blocking; or do the slow work behind an inline MCP server, where there is no per-round limit. - The run's overall
timeoutMsexpired. It is armed once at run start, not re-armed per round, so a long tool loop can exhaust it. - Something cancelled the run — your own
POST /v1/automate/cancel, the user's Stop button, or the server stopping. - A stale token from an earlier run. Tokens are per-run; a new run mints a new one.
GET /health tells you whether anything is parked: awaitingToolResults. On the OpenAI route, the same situation is not an error — echoed tool_call_ids that match no live run fall through and start a fresh run, so watch for an unexpectedly restarted task rather than a 404.
A run hangs, or my client disconnected
If your process died mid-run, the run is still going. Recover with:
curl -sS http://127.0.0.1:4317/v1/automate/cancel \
-H "Authorization: Bearer $KEY" -H "Content-Type: application/json" -d '{ "force": true }'
/v1/automate/cancel is safe to call unconditionally — it reports mode: "none" when there was nothing to stop — so a client can call it on startup to guarantee a clean slate.
Two strengths: the default asks the agent loop to unwind (worker stays warm, only lands between steps), and force: true kills the worker outright (always works, cold start next time). If a default cancel does not land within a few seconds, escalate to force.
A run asks a question and then cancels
The agent called request_user_input, the question had a required field, and your request did not supply it in hitlAnswers — so the server cancelled rather than hanging forever.
Fix it one of two ways:
- Write prompts that do not need to ask. State the recipient, tone, filename, and constraints up front. Best for unattended runs.
- Supply the answers. Run the task once, read the
request_user_inputstep instepsto learn the field ids, and pass them:
{ "prompt": "…", "hitlAnswers": { "recipient": "team@example.com", "tone": "concise" } }
Keys are field ids; values are a string or an array of strings for multi-select.
A run refuses to delete, move, or run a command
Expected. Destructive tools are rejected unless the request sets allowDestructiveTools: true, and the agent is told why, so it usually explains or adapts. Read Security model before enabling it — with run_command in that set, it grants arbitrary local execution through the agent.
A run cannot see the files I expect
restrictToWorkspace: trueconfines it to the workspace folder from/health. Stage inputs there or drop the flag.- The user's own workspace restriction wins. If the user configured one in Cephable, your run is confined to it and
restrictToWorkspaceis ignored. It can only narrow, never widen. - Paths are resolved fuzzily. Check each step's
producedFilePath— the resolved absolute path — rather than the path you passed in the prompt.
Still stuck?
Collect this before asking for help:
- The full
/healthresponse (safe to share — no secrets in it). - The run record with
include: { "trace": false, "events": false }, andstatus,errorCode, and the failed step. appVersion,platform,architecture,backend, andcontextSizefrom/health.- The exact request body, with the key redacted.