# 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.

Source: https://developers.cephable.com/docs/automate-http-server/troubleshooting

---
## Quick diagnosis

Start here. One authenticated `/health` call answers most questions:

```bash
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](#cannot-connect) |
| `401 Unauthorized` | A server is there, the key is wrong | [401](#401-unauthorized) |
| `200` with `"service": "cephable-agent"` | The server is healthy — the problem is in the request or the run | [409](#409-conflict) · [Run failures](#a-run-failed) |
| `200` from something that is not Cephable | Another program owns the port | [Finding the port](#finding-the-port) |

---

## Cannot connect

Work through these in order:

1. **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.
2. **Is the extension enabled?** Open **Extensions → Cephable features → Build & Extend → Automate HTTP Server**. The detail view must say **Running**.
3. **Is the account licensed?** The extension is gated behind the `enableAutomateHttpServer` feature 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.
4. **Did the port move?** See below.
5. **Are you using `localhost`?** On some systems `localhost` resolves to `::1` first and the server only listens on IPv4. Use the literal `127.0.0.1`.
6. **Are you calling from a browser page?** You cannot. There are no CORS headers and no `OPTIONS` handler, so a `fetch` from an `http(s)://` origin fails preflight. Call from Node, Electron's main process, a native app, or your own backend.
7. **Is a firewall or endpoint-security product intercepting loopback?** Some enterprise agents do. Test with `curl` from 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:

```javascript
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`:

```powershell
Cephable.exe --port 4321
```

Cephable is single-instance, so relaunching with a new `--port` rebinds the running app's server. See [Launch options](/docs/automate-http-server/models).

---

## `401 Unauthorized`

```jsonc
{ "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`, or `X-Api-Key` all 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`

```jsonc
{ "error": { "message": "Not found", "type": "not_found" } }
```

- **Wrong method.** The server returns `404`, not `405`. `GET /v1/runs` and `POST /health` are 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 of `http://127.0.0.1:4317/v1`, it will append `/v1/chat/completions` itself — check for `/v1/v1/…` in the request line.

---

## `409 Conflict`

```jsonc
{ "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):

```javascript
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:

```bash
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](#a-run-times-out) |
| `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_text` fallback 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:

```javascript
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. `durationMs` covers that.
- **`backend.cpuFallback`.** If GPU acceleration failed, everything is an order of magnitude slower. `/health` tells you.

---

## A resume returns `404`

```jsonc
{ "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 `error` result 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 `timeoutMs` expired.** 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_id`s 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:

```bash
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_input` step in `steps` to learn the field ids, and pass them:

```jsonc
{ "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](/docs/automate-http-server/security) 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: true`** confines 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 `restrictToWorkspace` is 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:

1. The full `/health` response (safe to share — no secrets in it).
2. The run record with `include: { "trace": false, "events": false }`, and `status`, `errorCode`, and the failed step.
3. `appVersion`, `platform`, `architecture`, `backend`, and `contextSize` from `/health`.
4. The exact request body, **with the key redacted**.
