Skip to content

Errors

Error semantics for all /api/* endpoints. Grounded in hub/src/web/routes/guards.ts and the individual route files under hub/src/web/routes/; reference consumer web/src/api/client.ts (ApiError, parseErrorCode).

Body shape

Error responses are JSON:

json
{ "error": "Session is inactive", "code": "session_inactive" }
  • error — human-readable message. Never match on it: consumers i18n it and the hub may reword it (this rule is stated in guards.ts itself).
  • code — optional stable machine-readable discriminator. Clients branch on (status, code).
  • issues — present on some 400s: Zod validation details (parsed.error.issues or .flatten() output). Useful for logging, not for branching.
  • A few endpoints add context fields (e.g. 413 export adds count/limit; 422 reopen adds missing[]).

When no code is present, branch on status alone and treat the failure generically. The web reference falls back to using error as a pseudo-code when code is absent (parseErrorCode) — acceptable for logging, not for logic.

Status × code table

StatuscodeWhere (source)Meaning / client action
400All routes with Zod bodies ({error: 'Invalid body'}, some with issues)Client bug — fix the request; do not retry
400Flavor gates (sessions.ts: wrong-flavor model/mode endpoints)Hide the control for this flavor
400scratchlist_attachment_invalid, scratchlist_entry_empty, attachment-limit codes from validateScratchlistAttachmentsForWritesessions.ts scratchlist routesSurface validation message
401Middleware (middleware/auth.ts) and POST /api/auth (routes/auth.ts)See Auth → 401 bodies; middleware 401 → silent re-auth once, /api/auth 401 → re-pair
403guards.ts (Session access denied, Machine access denied); owner-only routes (usage.ts, storage.ts, hubSettings.ts, voice.ts)Namespace mismatch / not hub owner — hide the surface, don't retry
403access_deniedRPC-flow results mapped in sessions.ts (resume/reopen/cursor-chat-store)Same as above
404guards.ts (Session not found, Machine not found); permissions.ts (Request not found); scratchlist entry/attachment; events.ts visibility (Subscription not found)Stale reference — refresh the parent list
404session_not_found, machine_not_foundCoded variants from resume/reopen/restart-runner result mappingSame
409session_inactiveguards.ts requireSession(requireActive) — send/steer/abort/approve/deny/config on an inactive sessionOffer Reopen (the web router does exactly this on this code)
409scratchlist_at_capsessions.ts scratchlist create (200-entry cap)Show cap notice; do not retry
409scratchlist_attachment_in_usesessions.ts attachment delete while still referencedDetach from entry first
409resume_unavailablesessions.ts resume/reopen result mappingSession can't be resumed (e.g. unsupported state)
409metadata_conflictsessions.ts reopen result mappingRefetch session, retry once at most
409runner_upgrade_requiredmachines.ts Agent availabilityUpgrade and restart the runner; disable session creation
409control_mode_not_applicablesessions.ts switchConcurrent clients do not use ownership switching; hide takeover controls
409— (version conflict)sessions.ts PATCH rename/summary, machines.ts PATCH rename — message mentions version/concurrently; no codeConcurrent edit — refetch and reapply
409sessions.ts delete-while-active, archive of plain inactive row, fork/rewind refusals, remote-only config on terminal-controlled sessions (controlledByUser)Surface message; refresh session state
413sessions.ts upload (> 50 MB decoded), export too large ({error, count, limit}); voice.ts transcription (Audio file too large, 25 MB audio / ~26 MB body)Reduce payload
413scratchlist_attachment_too_largesessions.ts scratchlist uploadReduce attachment
422sessions.ts reopen with incomplete metadata ({error, missing[]}); title-suggestion pass-through (TitleSuggestionError, statuses 422/429/502/503)Not reopenable / feature unavailable
429Title suggestion (provider rate limit)Back off
500Catch-all in most routes ({error: message})Log; generic failure UI
502Title suggestion upstream failure; restart-runner unknown errorRetry later
503guards.ts requireSyncEngine{error: 'Not connected'} — hub subsystems not up (startup/shutdown window); also Telegram-disabled on /api/auth initData pathRetry with backoff
503no_machine_onlineresume/reopen/spawn-flow result mapping (sessions.ts)The machine that owns the session is offline — tell the user to start the runner
503machine_offlinemachines.ts restart-runnerSame
503rpc_target_missingmachines.ts pi/codex model catalogs (RPC_TARGET_MISSING_ERROR_CODE in shared/src/rpcMethods.ts — RPC handler unregistered or socket disconnected)CLI-side target gone — treat as offline

RPC-wrapped endpoints

Many endpoints do not answer from hub state — the hub relays the request over Socket.IO to the session's CLI process (or the machine's runner) and forwards the result: git/file/directory/search, generated images, uploads, model catalogs, Agent availability, slash-commands, skills, spawn, list-directory, paths/exists. (The mode/model/effort config endpoints are RPC-backed too, but map apply-failures to 409 with a message.) Their failure modes differ from plain endpoints:

  1. CLI reachable, command failed → HTTP 200 with {success: false, error} (e.g. runRpc in hub/src/web/routes/git.ts catches RPC errors, including the 30 s RPC timeout, and returns them as a JSON envelope). Clients must check the success field on every RPC-shaped response; HTTP 200 alone means nothing.
  2. CLI offline / handler missing → depends on the route: the model-catalog routes in machines.ts map RpcTargetMissingError to 503 rpc_target_missing; git.ts-style routes fold it into the 200 {success: false} envelope; resume/reopen surface 503 no_machine_online.
  3. Hub subsystems not up503 Not connected from requireSyncEngine (brief startup/shutdown window).

Explicit rpc_target_missing / no_machine_online failures mean the execution host or handler is unavailable. Do not infer that from success: false alone: a reachable CLI can report a command, path, permission, or validation failure. Preserve that error for display; HTTP success is not operation success.

One route qualifies that rule. GET /api/machines/:id/agy-models keeps serving the last catalog the machine got out of agy models while the CLI re-checks in the background, so it can answer success: true and carry an error: the list is usable, and error says why it may be stale (typically the machine's agy sign-in has lapsed). Render it beside the catalog rather than instead of it, and offer ?refresh=true as the way to ask again — a plain repeat is answered from the same cache.

When that background re-check lands a different listing, the machine says so over the existing event stream rather than making clients ask: machine-agy-models-updated (see SSE) carries the machineId and nothing else. Refetch that machine's route on it — the answer comes from the machine's cache, so it costs no agy run and produces no further event. It is emitted whenever the re-check changes what this route would answer — a different listing, or a sign-in warning that appeared or cleared — and not when it changes neither, so a failed re-check that raises a warning does announce. The machine's very first listing is never announced: whoever triggered it is already awaiting it. Requests during that window are answered from the machine's cache and do not launch agy.

Retry guidance

ClassRetry?
400 / 403 / 404 / 409 / 413 / 422No (fix input, refresh state, or hide surface)
401 (middleware)Once, after silent re-auth (Auth)
429 / 502 / 503Reads: retry with backoff. Mutations: follow the endpoint's recovery contract; do not replay a write whose outcome is unknown
200 {success: false}Inspect the error and endpoint contract; manual retry only when safe. This envelope can also represent a missing RPC target or timeout

Released under the AGPL-3.0 License. · Privacy Policy