Errors & Retries
The failure contract
Section titled “The failure contract”Two kinds of failure, deliberately distinguished so you always know which one you are handling:
| Kind | Behaviour | Why |
|---|---|---|
| Bad argument | Throws synchronously | A programming error surfaces at the call site, never inside a promise chain. |
| Anything remote — auth, quota, transport, a 5xx | Rejects with a KiomonError |
These are conditions to handle, not bugs to fix. |
// Throws immediately — no request is made.kiomon.searchMemory({ query: " " });
// Rejects — the request was made and failed.await kiomon.searchMemory({ query: "vector index tradeoffs" });Everything the SDK validates locally is listed in the reference. Those failures are cheap: the SDK mirrors the server’s own validation, so a mistake costs zero round trips.
KiomonError
Section titled “KiomonError”import { KiomonError, isKiomonError } from "@kiomon/kiomon";
try { await kiomon.retrieveMemory({ intent: "plan", query: "q" });} catch (error) { if (!isKiomonError(error)) throw error; // not ours — do not swallow it
error.code; // "quota_exceeded" error.status; // 402 (0 when the request never reached the server) error.message; // human text from the API error.needsUpgrade; // true error.retryable; // false error.requestId; // from x-request-id, when the API sent one error.body; // the raw response body, for debugging — not for control flow}| Member | Type | Notes |
|---|---|---|
code |
KiomonErrorCode |
The stable identifier. Branch on this, never on message. |
status |
number |
HTTP status, or 0 if the request never left the client. |
needsUpgrade |
boolean |
true on a plan limit. Drives an upgrade prompt, not a retry. |
retryable |
boolean (getter) |
Whether retrying the same request could plausibly succeed. |
requestId |
string | undefined |
Include this when contacting support. |
body |
unknown |
Raw body. Useful in logs, unsafe for logic. |
| Code | Status | What it means | What to do |
|---|---|---|---|
bad_request |
400 | Malformed request | Fix the request. |
validation_failed |
400 | The shape was wrong (schema validation) | Fix the request. |
payload_too_large |
413 | Body over the limit | Send less; large documents are offloaded server-side. |
unauthorized |
401 | Missing, revoked or expired credential | Check apiKey / token, or reissue the key. |
account_pending_deletion |
401 | The account is mid-deletion | Not a key problem — reissuing a key will not help. |
forbidden |
403 | The credential lacks permission | Check the key’s workspace grant. |
insufficient_role |
403 | The role or grant is too low for this verb | A read-only grant cannot write. Widen the grant. |
connector_scope_denied |
403 | A delegated OAuth token reached outside the memory routes | Use a server-side API key for non-memory endpoints. |
not_found |
404 | Not available to you — see below | Treat as unavailable; do not retry. |
conflict |
409 | The request conflicts with current state | Re-read, then decide. |
duplicate |
409 | Already exists | Nothing to do; not retryable. |
idempotency_conflict |
409 | Same Idempotency-Key, different body |
A client bug. Use a new key per distinct request. |
idempotency_in_progress |
409 | Same key, first attempt still running | Wait for it to finish, then retry the same key. |
quota_exceeded |
402 | Plan quota reached · needsUpgrade: true |
Prompt for an upgrade. Do not retry. |
pro_required |
402 | Feature is Pro-only · needsUpgrade: true |
Prompt for an upgrade. Do not retry. |
rate_limited |
429 | Too many requests in the window · needsUpgrade: false |
Retryable — the SDK already does. |
service_unavailable |
5xx | Upstream or edge failure | Retryable. |
internal_error |
500 | The API failed unexpectedly | Retryable. |
invalid_request |
— | Raised by the SDK: a bad argument | Fix the code; thrown synchronously. |
timeout |
504 | Raised by the SDK: no response within timeoutMs |
Retryable. |
network_error |
— | Raised by the SDK: could not reach the API, or you aborted | Retryable for connection failures. |
unknown |
— | A code newer than this SDK version | Log it; do not crash. |
Retries
Section titled “Retries”Reads retry automatically; writes retry with the same idempotency key, which is what makes retrying them safe.
| Retried | Not retried |
|---|---|
rate_limited, internal_error, service_unavailable, timeout, network_error |
unauthorized, forbidden, insufficient_role, not_found, conflict, duplicate, idempotency_conflict, idempotency_in_progress, quota_exceeded, pro_required, and every 4xx argument error |
Backoff is exponential with jitter — 250 ms, 500 ms, 1 s, capped at 4 s, plus up to 100 ms of
jitter. If the error body carries a retry_after value in seconds (up to 30 s), that is respected
instead. maxRetries defaults to 2, settable per client or per call.
const kiomon = new Kiomon({ apiKey, maxRetries: 5 });await kiomon.searchMemory({ query: "x" }, { maxRetries: 0 }); // this call onlyIdempotent writes
Section titled “Idempotent writes”Every write carries an Idempotency-Key — generated per call, reused across retries — so a
replayed request cannot apply twice. The server stores the first response for 24 hours and returns it
verbatim to a retry, with an Idempotency-Replayed: true header.
// The SDK generates the key for you. Pass your own when a retry might come// from a *different* process — a queue worker, a cron job, a second server.await kiomon.reflectSession({ learnings }, { idempotencyKey: `session-${sessionId}` });Two consequences worth designing around:
- Keys are scoped to your user, so two tenants can use the same key string without colliding.
- Reusing a key for a different body is rejected with
idempotency_conflictrather than guessed at. Generate a key per logical write, not per HTTP attempt. - A
5xxis not cached: the server releases the key instead, so your retry is a genuine first attempt rather than a stored failure.
retrieveMemory and the other reads are naturally idempotent and do not send a key.