Skip to content

Errors

A failure arrives as { ok: false, error } from tenon.intents.send, or as a structured envelope from the CLI. There are three layers, and telling them apart is most of debugging.

json
{
  "source": "control",
  "code": "intent_not_found",
  "message": "intent 'ui.toast.v1' is not callable by the CLI principal"
}

source is control or intent. Control failures are a closed set; intent failures keep the kernel's own structured diagnostics rather than being flattened into a handful of CLI-only cases.

An intent failure can also carry details, retryable, retryAfterMilliseconds, outcome, requestID and providerID.

Control-plane codes

The complete set. If your source is control, it is one of these:

CodeMeans
unsupported_versionwire version mismatch — your client is older or newer than the app
malformed_jsonthe request did not decode
payload_too_largeover the bound
unknown_actionnot one of the five actions
invalid_paramsthe action's own parameters were wrong
intent_not_foundsee below — this one is not what it looks like
not_readythe app is not serving yet
busyat capacity; refused rather than queued
internal_errora defect — report it

intent_not_found is deliberately ambiguous

It means either "no such intent" or "you may not call it". You cannot tell which, and that is the design:

sh
$ tenon-cli intent describe ui.toast.v1
{"code":"intent_not_found","message":"intent 'ui.toast.v1' is not callable by the CLI principal"}

$ tenon-cli intent describe totally.made.up.v1
{"code":"intent_not_found","message":"intent 'totally.made.up.v1' is not callable by the CLI principal"}

A caller that may not use a contract does not get to learn whether it exists. Check with tenon-cli intent list — if it is not in your projection, you cannot call it, whatever the reason.

busy is a cap doing its job

The app holds a fixed complement of in-flight requests and refuses past it. The alternative to an immediate answer is an unbounded queue of work waiting behind whatever is slow, which is not a better outcome — it is the same failure, delayed and harder to attribute.

Back off and retry; retryAfterMilliseconds tells you how long.

Domain errors

Each contract declares its own, and each intent reference page lists them under Errors it can return. Every core domain error is namespaced dev.tenon.core.*:

CodeTypically means
agent-handoff-unresolveda cross-agent resume with no transcriptPath
agent-unavailablethat agent is not installed on this machine
agent-question-capacitytoo many questions already pending
agent-question-pane-closedthe pane went away before it was answered
agent-question-pendingone is already waiting on that pane
close-refusedthe target refused to close
content-not-textasked for text from something that is not
content-unavailablethe content could not be produced
cursor-invalidateda paged read's cursor is stale — restart the walk
external-open-failedthe OS declined to open it
filesystem-failedthe underlying filesystem operation failed
invalid-urlmalformed URL
layout-unavailablethe layout operation could not be applied
pane-not-found · tab-not-found · workspace-not-foundthe scoped target is gone
path-already-exists · path-not-foundfilesystem preconditions
process-launch-failedthe command could not start
process-output-unavailableoutput could not be collected
process-timed-outthe command exceeded its own timeout
terminal-unavailableno terminal to act on
user-cancelleda person said no — an answer, not a fault
workspace-unavailablethe workspace is not usable right now

user-cancelled is a result

Treat it as a branch, not an exception. Someone was asked and declined; retrying or logging an error is the wrong response to a decision.

Lifecycle failures

On top of domain errors, any intent can fail at the boundary before its provider ever runs — an undeclared use, a missing capability, an unauthorized scope, an unconfirmed policy operation, an ineligible provider, an admission refusal, or a deadline.

Two of those look like bugs and are not:

A policy-confirmed operation expiring. CLI and agent callers receive no standing consent. An unattended confirmation: policy operation expires rather than silently escalating. That is the fail-closed rule working — do not route around it by raising the timeout.

A deadline exceeded. The deadline covers admission, confirmation, provider execution and settlement. Raising it can diagnose slow work; it is not a way to hold a request open. Multiple values over time is a resource.

A timeout is not always an error

terminal.wait.v1 settles normally with met: false when its condition is not reached in time:

json
{"paneID":"UUID","condition":"command-finished","met":true}

"It did not finish in 30 seconds" and "something went wrong" are different facts. Check met, not just ok.

Handling failure

js
const result = await call.send(name, input)
if (!result.ok) {
  switch (result.error.code) {
    case "dev.tenon.core.user-cancelled":
      return                                  // a decision, not a fault
    case "dev.tenon.core.terminal-unavailable":
      return fallback()
    case "busy":
      return retryAfter(result.error.retryAfterMilliseconds)
    default:
      tenon.log("unexpected", result.error.code, result.error.message)
  }
}

send does not throw for a failed intent. If you are not checking ok, you are not handling failure.