Errors, limits and replays
A failed tool call is not a transport error: the result is a normal envelope with ok: false, isError: true on the MCP result, and an error object:
{ "type": "rate_limited", "message": "Too many requests this minute for this account.", "recoverable": true, "retryable": true, "data": { "fix_hint": "Pace calls to under the per-minute allowance.", "retry_after_ms": 2000 } }recoverable— the caller can do something about it (fix an argument, sign in, wait).falsemeans report it and stop.retryable— the same call may succeed if repeated after a short wait. Never loop: honourdata.retry_after_mswhen present, otherwise back off once.datacarries the specifics:paramandfix_hintfor input errors,did_you_meanfor a mistyped argument or tool,reason,retry_after_ms,reset_at,used/allowancefor quota,deadline_msandcausefor deadlines. The upstream query is never echoed back.suggested_tool_calls[]on an input error is the corrected call — apply it once.
error.type vocabulary
The vocabulary is closed; every tool description enumerates it and hosaka://capabilities publishes it with a retryable flag per code. "varies" means the value depends on the situation and the row explains how.
error.type | recoverable | retryable | What it means and what to do |
|---|---|---|---|
invalid_argument | yes | no | an argument failed validation; data.param names it, data.fix_hint says how to fix it. Fix and call again |
one_of_required | yes | no | the tool needs one of several alternative arguments and got none (for example resolve_artist without q, url, cluster_id, discogs_id or mbid) |
range_too_large | yes | no | limit or a range exceeds the tool's maximum; data names the maximum. Lower it |
placeholder_detected | yes | no | an argument looks like a placeholder (<cluster_id>, TODO, string). Resolve the real value first |
unauthorized | varies | no | no or invalid token on a signed-in tool — recoverable: start the sign-in from _meta["mcp/www_authenticate"]. Not recoverable when the signed-in account is inactive |
insufficient_scope | reserved | reserved | not issued today; every signed-in tool uses the same scopes |
quota_exceeded | no | no | the account's allowance for the period is used up; data.reset_at (UTC) says when it resets, data.used/data.allowance the figures. Relay that; nothing is purchased inside the assistant |
anonymous_capacity_exhausted | yes | yes | anonymous capacity is used up or paused; sign in for a personal allowance, or retry later |
rate_limited | yes | yes | too many calls per minute, too many concurrent calls, or too many anonymous calls from one caller; slow down, honour retry_after_ms |
linked_capacity_exhausted | yes | yes | hosaka's shared capacity for signed-in accounts is exhausted for now; retry in a few minutes |
request_in_progress | yes | yes | the identical request is already running (a concurrent duplicate); retry after data.retry_after_ms |
upstream_degraded | yes | yes | the data service is in a degraded window (see below); retry after 30 s or follow next_actions |
upstream_error | varies | varies | the data service answered with an error or a deadline passed. Retryable on a 5xx, an upstream rate limit or a deadline (data.cause); not retryable on a record that is not available. Not recoverable if the service refused hosaka's credentials — report it |
metering_unavailable | yes | yes | hosaka could not meter the call so it was not served (fail-closed); retry shortly |
tool_disabled | yes | varies | the tool is switched off by hosaka for now; data.reason is a short code and data.fix_hint says whether to retry later or sign in |
idempotency_conflict | reserved | reserved | not issued today |
internal | no | no | an unexpected server error; report meta.request_id |
Cursors (search_catalog)
A cursor is opaque, bound to the signed-in account and to the exact q + filters, and checked before any upstream call. Three things produce invalid_argument with data.param: "cursor": a value this server did not issue (edited, truncated, invented), a cursor replayed from another account, or a cursor carried to a different query. A fourth comes from the data service itself when it no longer accepts a cursor this server did issue (expired). The fix is always the one in data.fix_hint: omit cursor and start the same search again. A cursor longer than 1024 characters fails input validation. Cursors are never issued to anonymous callers (search_catalog requires a signed-in account). A walk can also end early when the data service hands back a cursor this server cannot carry: next_cursor is null and pagination.walk_ended_early explains it (more rows may exist; a narrower query is a new walk).
Deadlines and data.cause
Each upstream call and the tool as a whole run against deadlines. When one passes the error is upstream_error with data.cause:
data.cause | Meaning |
|---|---|
call_deadline | one upstream call exceeded its per-call deadline (typically a cold dossier); the tool answered honestly instead of waiting |
tool_deadline | the whole tool exceeded its deadline (data.deadline_ms) |
client_abort | the client cancelled the request |
A deadline-failed call is still metered (meta.units counts the upstream calls made). Retrying a call_deadline after a few seconds often succeeds because the upstream cache is then warm.
The degraded window (upstream_degraded)
If the data service fails repeatedly, hosaka stops calling it for 30 s and answers upstream_degraded immediately, with degraded.recommended_action: "retry_after_30s". Since Sprint 9 the window is also kept per data family: repeated deadlines on one family (for example artist dossiers) open the window for that family only, so resolve_artist keeps working while get_artist is degraded, and a get_artist include whose family is degraded comes back as present: false, reason: "upstream_degraded" with a next_actions entry rather than failing the whole card. Since Sprint 10 a family window also needs several distinct callers: one caller's deadlines can contribute only a minority of the window, so a single client cannot degrade a family for everyone else — if you alone see repeated deadlines, slow your own calls rather than expecting the window to open.
A 404 from the data service for a key you passed is not an error: find_similar_artists, get_upcoming_gigs, get_label and the aura lookup answer ok: true with present: false and reason: "not_found_upstream" plus a next_actions entry (resolve the name again); meta.units is charged as usual.
Budgets a client can see
- Anonymous use is limited and shared; the server tells you where you stand through
meta.anon_budget_bucket(high,medium,low) and answersanonymous_capacity_exhaustedwhen it is used up. The fix is always the same: sign in. Thresholds are not published. - Signed-in accounts have a monthly allowance.
meta.quota(used,allowance,warning_80,reset_at) is on every signed-in response,get_usagereturns the same figures on demand (it costs no units), andquota_exceededis the hard stop. There is also a per-minute pace and a concurrency cap per account (rate_limited). - Units are what is metered: most tools cost 1 unit per upstream call made (
get_artistadds 1 per include,resolve_artistadds 1 for the same-name probe);get_profileandget_usagecost 0. The per-tool figure is inhosaka://capabilities. - Tokens: soft target 1 500, hard cap 4 000 per response — see envelope.md.
find_similar_artistsacceptslimitup to 25 andfind_tracksup to 25; above the maximum the answer isrange_too_large.
Idempotency and replays
Identical calls (same account, same tool, same arguments, same view) made within the idempotency window are answered from the stored result with meta.replay: true and are not metered again. A duplicate that arrives while the first is still running waits briefly and then answers request_in_progress with retry_after_ms. If the replay store cannot be reached the call is still served, with meta.idempotency: "unavailable" to say that no replay protection applied. There is no client-supplied idempotency key: the server derives it from the call.
Provenance
Vendored from ono-sendai docs/agents/errors-and-limits.md at commit a447f2b on 2026-10-13. ono-sendai is the source of truth; this page is regenerated from the pinned commit and a test fails if the two disagree. The other pages in this set: connect, tools, envelope, errors and limits, data posture — and the agents landing.