Session failure extension
September 14, 2026 · View on GitHub
This document defines the provider-neutral experimental sessionFailure extension implemented by
claude-agent-acp. Warnings and errors are durable transcript entries shown in order beside user,
agent, and tool messages. They are not assistant-authored text and not ephemeral chat banners.
Capability negotiation
The extension is opt-in. A client advertises it in initialize:
{
"clientCapabilities": {
"_meta": {
"jetbrains": {
"air": {
"version": 1,
"capabilities": ["sessionFailure"]
}
}
}
}
}
The adapter enables the extension only when version is a finite integer greater than or equal to
1 and capabilities contains sessionFailure. Otherwise it preserves the legacy ACP behavior.
Wire record
The record is placed under _meta.jetbrains.air.sessionFailure:
{
"_meta": {
"jetbrains": {
"air": {
"version": 1,
"sessionFailure": {
"id": "prompt-uuid:error",
"revision": 1,
"category": "limit",
"severity": "error",
"title": "You've hit your individual spend limit · run /usage-credits to ask your admin for a higher limit",
"actions": []
}
}
}
}
}
| Field | Required | Type | Meaning |
|---|---|---|---|
id | yes | non-empty string | Stable identity of one logical incident. |
revision | yes | positive integer | Monotonically increasing version of that incident. |
category | yes | category enum | Broad machine-readable visual group. |
severity | yes | warning or error | Inline warning or error presentation. |
title | yes | string | Complete normal user-facing presentation. |
details | no | string | Long explanation used only when required text is too large for title. |
actions | yes | ordered string array | Recovery actions recommended by the adapter. |
There are no phase, source, safeMessage, retryable, retryAfterMs, turnId, retry-counter,
or provider-code fields. Retry progress belongs in title when Claude supplies it. Transcript order
and persistence time come from the receiving ACP event.
Identity, revisions, and history
id identifies one occurrence, not an error type.
- The first record for an incident creates one transcript entry at the current stream position.
- The same
idwith a higherrevisionupdates that entry in place without moving it. - The same or a lower revision is idempotently ignored by the client.
- A later independent occurrence uses a new
id, even when its category and text are identical. - Consecutive updates for the same notice may reuse one id and increment its revision.
- Different ids are never deduplicated by comparing title or category.
Live turn failures use <turnId>:error. Session-scoped incidents use an adapter-session epoch plus an
incident sequence. Notices have their own :notice: namespace. Replayed usage-limit failures recover
the persisted user-message UUID used as the live turn id, so the same occurrence keeps the same id.
Malformed history without a preceding user message falls back to
<sessionId>:history-error:<messageUuid>.
Resolved records remain in transcript history. Recovery removes only the adapter's internal active state; it does not publish a clear tombstone and does not delete or rewrite the historical entry. A producer may publish a higher revision only when it has a real new user-facing status to show.
Delivery surfaces
A turn-terminal failure is attached to the successful ACP PromptResponse in _meta; the response
uses stopReason: end_turn. Session-scoped, replay-restored, warning, and background incidents use a
session_info_update. The schema is identical on both carriers.
A warning does not end a turn. An error record does not independently invent ACP lifecycle state; the prompt response or transport lifecycle remains authoritative. If no capable client negotiated the extension, existing JSON-RPC errors and transcript text remain unchanged.
Categories
Categories are deliberately broad and drive only iconography and generic accessibility labels.
| Category | Covers |
|---|---|
connection | Lost transport, stopped or shutting-down worker, unavailable runtime. |
access | Authentication required, rejected credentials, denied access. |
limit | Rate, quota, token, context, turn, or configured budget limit. |
request | Invalid input, unsupported model or operation, rejected request. |
service | Provider overload, provider failure, adapter/internal failure. |
unknown | A warning or failure that does not fit another group. |
Claude SDK conditions map to these groups:
| Claude condition | Category |
|---|---|
authentication_failed, oauth_org_not_allowed, synthetic login message | access |
verification_required, cloud_credential_error | access (no login action) |
billing_error, rate_limit, max_output_tokens, usage/spend limit, budget/turn limit | limit |
invalid_request, model_not_found | request |
overloaded, server_error, unknown provider error, adapter internal error | service |
| API retry without an HTTP response, query transport loss, worker shutdown | connection |
| model fallback notice | unknown with severity: warning |
Unknown SDK error kinds degrade to service; they never become success. Category does not determine
message text or client behavior beyond presentation.
Severity
warningmeans the operation may still succeed and normally has no actions while recovery is in progress. It does not terminate the turn. SDKapi_retryprogress reuses the active turn failure id; a later terminal failure updates that record with a higher revision.errormeans the operation cannot continue without user action or another request.
Severity is always explicit.
Title and details
title is the complete normal presentation: what happened, retry progress when present, and a short
next step. The adapter must not replace a real Claude message with category-specific canned wording.
For Claude SDK failures the title is copied from the user-facing text Claude already emitted:
- top-level assistant error text, including synthetic usage-limit and login messages;
- otherwise the terminal SDK result or error text;
- the exact model-fallback notice for warning records.
Adapter-authored fallback text is allowed only when no Claude user-facing message exists, such as a query iterator throwing, worker-shutdown EOF, or an internal missing-result invariant. Raw thrown exceptions, stack traces, transport URLs, tokens, headers, environment values, and private paths are never used as titles.
details is reserved exclusively for required explanations too large to fit reasonably in title.
If the complete message fits in title, details must be omitted. It is not a place for status text,
retry counters, provider payloads, or diagnostics.
Actions
Version 1 defines only:
| Action | Meaning |
|---|---|
retry | Retry the operation associated with this incident. |
login | Start authentication. |
new_session | Start a fresh agent session/runtime. |
The adapter supplies action ordering. The client filters actions it cannot safely execute and ignores unknown or duplicate values. The client must not infer actions from category.
Current Claude policies are:
- authentication:
login; - provider overload/error and recoverable internal error:
retry; - transport loss or worker shutdown:
new_session; - context or configured session-budget exhaustion:
new_session; - account quota and invalid request: no action.
Internal recovery
Recovery policy is internal adapter state and is not serialized:
- restored quota remains active until a real model answer;
- authentication remains active until successful
auth_status; - transport loss and worker shutdown remain active until runtime replacement;
- other turn-scoped failures stop being active at a later confirmed attempt boundary;
- notices are not cleared merely by generic success.
Recovery never removes the transcript record. Publishing another incident is not evidence that an older one recovered.
History replay
session/load scans top-level assistant history for SDK synthetic usage-limit messages. It recognizes
only <synthetic> messages beginning with SDK-owned stable prefixes, not arbitrary model prose. Every
recognized occurrence is replayed as a typed record at its original transcript position. The latest
matching message stays active until a later real-model answer proves recovery; older and recovered
records remain historical typed entries.
For a capable client, replay suppresses duplicate assistant prose and restores typed errors using the
exact stored Claude text as title. Legacy clients receive the original transcript and no typed record.
Verification
The executable contract lives primarily in src/tests/acp-agent.test.ts, under usage-limit failure replay, stop reason propagation, and model refusal fallback handling.
npm run build
npm run check
npm run test:run