Errors and rate limits

~5 min read

Errors and rate limits

Two envelopes

Errors come back in one of two shapes, depending on where they happen.

1. Protocol / auth failures — malformed JSON, an unknown method, bad params — use the JSON-RPC error envelope, which carries a numeric code:

{
  "jsonrpc": "2.0",
  "id": 1,
  "error": {"code": -32602, "message": "Invalid params"}
}

2. A failure inside a tool call comes back as a normal tools/call result with isError: true and a stable, machine-dispatchable kind (no numeric code), plus any extra structured fields alongside it:

{
  "content": [{"type": "text", "text": "File is being edited by the user; try again later"}],
  "isError": true,
  "kind": "target-is-being-edited",
  "retry_after_ms": 2000,
  "since": "2026-05-29T17:00:00Z"
}

Dispatch on code for protocol errors and on kind for tool errors. message is for humans, never for branching.

Authentication & authorisation

A missing, malformed, unknown, revoked, or expired token is rejected at the transport layer with HTTP 401 (Missing or invalid API key). The server does not distinguish the reason, so there’s no per-reason kind — just the 401. Refresh / rotate the token; none of these are transient.

Authorisation failures (a valid token that lacks access) come back as tool-result errors carrying a kind:

kind Description
insufficient-scope Tool requires more scope (e.g. write) than the token has
folder-not-allowed A path argument is outside the token’s folder allowlist

Tool-result error kinds

Returned in the tools/call result envelope (isError: true, kind: …). These do not carry a JSON-RPC code — dispatch on kind. This is the complete set:

kind Raised when Extra data
args-invalid Tool arguments don’t match the schema — a required field is missing, the wrong type, or an empty/non-array where one is required
path-invalid A path is malformed: absolute, contains .., or escapes the vault (path-traversal attempt)
note-not-found path doesn’t resolve to a note
note-exists write_note with mode: create, but the file already exists
not-markdown path isn’t a Markdown file
confirmation-required The op needs confirm: true (e.g. delete_note)
target-not-found A patch_section / apply_edits target (heading, block id, or frontmatter key) didn’t match
edit-not-unique An apply_edits find matched zero or multiple times — the whole call rolled back matches (Int)
target-is-being-edited A write was blocked by the editor soft-lock (the user has the note open) retry_after_ms, since
immutable-path The target is under an immutable prefix (e.g. raw sources/) — create or append a new file instead
forbidden A folder-scoped key called a tool that writes outside its allowlist with no path argument to gate on — e.g. ingest_url / ingest_file, which always write to the sources/ layer
proposal-stale A queued proposal’s target drifted (content changed, or the path now exists) and can no longer apply cleanly
unsupported The tool name isn’t recognised, or it’s a folder op a propose-only key can’t use in Suggestions mode
internal An unhandled server error — please file a bug, with the message

…plus the two authorisation kinds above (insufficient-scope, folder-not-allowed).

Note

The kind set is deliberately small and stable. A target mismatch — heading, block id, frontmatter key, or a directive line — surfaces uniformly as target-not-found; a path that’s absolute, contains .., or escapes the vault surfaces as path-invalid. Branch on these classes, not on finer distinctions the wire doesn’t make.

Standard JSON-RPC errors

Protocol-level failures use the JSON-RPC error envelope with these codes:

code Meaning
-32700 Parse error (request wasn’t JSON)
-32600 Invalid request (missing jsonrpc or method)
-32601 Method not found
-32602 Invalid params

An unhandled server-side error surfaces as the internal tool-result kind (above) rather than a protocol -32603.

Rate limits

The paid AI tools — the ones that spend on the user’s LLM/embeddings key (semantic_search, answer, related_notes, find_duplicates, reindex_embeddings, topic_map, eval, lint_semantic, wiki_maintain, suggest_tags, ingest_url, ingest_file) — share a generous per-key sliding-window limit (60 calls/minute per identity). It exists as a runaway-loop backstop, not a throughput cap: a normal interactive agent never trips it. Exceeding it returns the rate-limited tool-result kind with a retry_after_ms hint. All other tools are unlimited — the server runs locally and your machine is the limit.

Retry policy guidance

Error class Strategy
Auth (401, insufficient-scope, folder-not-allowed) Don’t retry. Surface to the user.
Not found (note-not-found, target-not-found) Don’t retry as-is. Re-list / re-read to rediscover, then re-issue.
Validation (args-invalid, path-invalid, not-markdown, confirmation-required, immutable-path) Don’t retry. Fix the call.
edit-not-unique Re-read the note, rebuild the find anchors, retry (up to ~3×).
proposal-stale Re-read the target, then re-issue the proposal.
target-is-being-edited Retry after retry_after_ms — the user is editing; back off, up to ~10×.
rate-limited Back off retry_after_ms, then retry. You’re in a loop if you see this twice — slow down.
internal Retry once. If it persists, report it with the full message.