Errors
Match on the machine code, never on the status alone and never on a message string. Codes are stable; statuses are shared by unrelated conditions and messages are not part of the contract.
The error body
Section titled “The error body”Almost every error is one field:
{ "error": "cloud_run_in_progress" }Seven codes add a second field:
{ "error": "insufficient_scope", "requiredScope": "wamp.cloud.turns:submit" }{ "error": "cloud_rate_limit_exceeded", "retryAfterSeconds": 3 }{ "error": "invalid_request", "issues": [{ "path": ["model"], "message": "model must be omitted for a runtime-managed agent" }] }{ "error": "invalid_path_parameter", "parameter": "sessionId" }{ "error": "unsupported_media_type", "mediaType": "application/x-www-form-urlencoded" }{ "error": "method_not_allowed", "allow": ["GET", "HEAD", "OPTIONS", "PUT"] }{ "error": "cloud_interaction_conflict", "outcome": { "interactionId": "toolu_01K2mWpR7yLd4bQx9sTfNvHe", "choice": "deny", "status": "resolved", "resolution": "denied" } }outcome is present only when another approval decision already won. A closed
or expired approval with no answer returns the same conflict code without it.
Which field a 400 carries, and why there are three codes
Section titled “Which field a 400 carries, and why there are three codes”The three ways a request can be wrong before it reaches any resource have three different fixes, so they have three codes. Branch on the code, not on which field happens to be present.
| Code | What is wrong | Second field | Where you fix it |
|---|---|---|---|
malformed_request_body |
The bytes were not JSON | — | Your serializer |
invalid_path_parameter |
A path segment is not a valid id | parameter |
The URL you built |
invalid_request |
A field of the body or the query was refused | issues |
The payload |
issues has one entry per refused field. path is the field path with the
outermost segment first — ["initialTurn", "id"] for a nested field, and an
integer segment is an array index. message is a short reason meant for a human
to read; it is not part of the contract, so branch on the code and the path.
Those two keys are the whole entry, and the WAMP Account API sends the same two
under the same field name.
A path segment is refused on its own and first: if the id in the URL and the
body are both wrong you get invalid_path_parameter, because a body cannot be
judged against a resource that is not addressable. Fix the URL and send again to
see what the body says.
Treat the body as extensible: new fields may appear, so read the ones you need
rather than validating the object shape. Every /v1 failure puts a machine code
in error — including the 413 the body parser answers, which is
{ "error": "request_entity_too_large" }. The only answers without a code are
ones no WAMP server wrote: a 502, 503 or 504 from the proxy in front of the
API, with an HTML body or none. Releases and answers with no code
covers it.
Read the delay from the header, not the body
Section titled “Read the delay from the header, not the body”Five domain codes — cloud_workspace_starting, cloud_session_finalizing,
cloud_run_in_progress, cloud_agent_catalog_unavailable and
cloud_extension_catalog_unavailable — carry a delay as
a Retry-After header only. It is always 2. There is no
retryAfterSeconds in their bodies.
503 service_unavailable, the answer while the API restarts or while its
database cannot keep up, also carries Retry-After: 2 as a header only.
429 cloud_rate_limit_exceeded carries the delay both as a Retry-After
header and as a retryAfterSeconds body field.
So read the header if you want one code path for all of them. A client that only looks at the body will silently fall back to its own default on every code where the server actually told it what to do.
Transport, authentication and validation
Section titled “Transport, authentication and validation”| Status | Code | Cause | Retry? |
|---|---|---|---|
400 |
invalid_request |
A body or query parameter failed validation — including an unknown property or an unknown query parameter. issues names each refused field |
No. Fix the request |
400 |
invalid_path_parameter |
A path segment is not a valid id. parameter names it |
No. Fix the URL |
400 |
malformed_request_body |
The request body was not parseable JSON, so no field was ever read | No. Fix your serializer |
400 |
invalid_input |
A malformed grantId path parameter, or a value that cleared a route’s character bound and then failed the byte bound behind it — a publication body or a merge commitMessage over 65 536 UTF-8 bytes |
No. Fix the request |
400 |
environment_invalid |
An environment document, secret name or value, cache path, extension, archive or operation body failed validation; issues[].path names the field. An archive refusal says which ingest check failed; an extension names a catalog slug that is not listed or an archive the environment does not own |
No. Fix the request |
400 |
audio_file_required |
A transcription request omitted its multipart file part |
No. Attach one audio file |
400 |
invalid_audio_duration |
durationMs is missing, fractional, below 100 ms, or above 10 minutes |
No. Fix the request |
400 |
invalid_audio_upload |
The multipart body is malformed or contains more than one file or field | No. Fix the request |
400 |
cloud_repository_branch_not_found |
source.baseBranch does not resolve in the granted repository. The caller’s own input, which is why it is a 400: no change of authority makes a branch that does not exist exist |
No. Send a branch that is there, or omit baseBranch for the default |
400 |
invalid_organization |
A malformed or repeated X-Wamp-Organization header |
No |
400 |
organization_header_not_allowed |
X-Wamp-Organization sent together with a bearer |
No. Remove the header |
401 |
bearer_credential_required |
No Authorization: Bearer header on a /v1 path |
No |
401 |
invalid_or_expired_credential |
Token expired, revoked, wrong audience, or introspection rejected it | Mint a new token, retry once |
403 |
cloud_access_denied |
The credential lacks wamp.cloud.access |
No. The installation is not consented for Cloud |
403 |
environment_forbidden |
The actor cannot read or manage organization environments, or an installation cannot select one | No. Obtain the required capability |
403 |
insufficient_scope |
The credential lacks the operation’s capability, named in requiredScope |
No. Re-consent is required |
403 |
installation_principal_required |
An installation-only operation was called with a human credential: repository grants, a GitHub-sourced Session create, runtime release probes, or GET /v1/sessions/stream |
No |
403 |
cloud_repository_grant_required |
Nobody has granted this repository to this installation, the grant was revoked, or the grantId + repositoryId pair does not match |
No. An administrator configures access; GET /v1/repositories returns the exact usable pairs |
403 |
cloud_github_connection_required |
A browser user chose the personal-repository path but their own GitHub connection is missing or expired | No. That person reconnects GitHub; organization grants do not depend on personal OAuth |
403 |
cloud_repository_unavailable |
The grant is intact and the repository is no longer reachable through the GitHub App installation — removed from it, deleted, or renamed | No. Somebody restores it on the GitHub side |
403 |
cloud_repository_permission_denied |
The App reaches the repository and GitHub refuses the operation: a missing App repository permission, or an organization policy such as unsatisfied SAML SSO | No. One action on the GitHub side |
403 |
cloud_compute_not_configured |
The organization has no compute it may run on, or withdrew the compute it owns | No. Ask an administrator to add compute |
409 |
cloud_compute_outdated |
The organization’s own machine is connected and refuses this release’s sandbox image — it is still holding an older one | No. Ask an administrator to update that machine’s images |
404 |
unknown_endpoint |
The path is not part of this API. Nothing was looked up, which is what separates it from the 404s below |
No. Fix the URL |
405 |
method_not_allowed |
The path exists and does not answer this method. Allow and the allow field name the ones it does — Sessions are created with PUT, and there is no POST /v1/sessions |
No. Use a listed method |
406 |
representation_not_acceptable |
Accept did not match on GET /v1/openapi.json or GET /v1/docs |
No. Fix Accept |
415 |
unsupported_media_type |
The body carried a media type this API does not parse, or none at all. Bodies are read under application/json and RFC 6839 application/*+json only; mediaType echoes what you sent. Nothing of the body was read, so no field of it is named |
No. Fix your Content-Type |
415 |
unsupported_audio_format |
The transcription file is not FLAC, MP3, MP4/M4A, Ogg, WAV, or WebM audio | No. Encode to a supported format |
413 |
request_entity_too_large |
JSON body over 1 MB, or an environment archive over 32 MiB | No. Send less |
413 |
audio_too_large |
A transcription file is over 20 MiB | No. Record a shorter note |
422 |
invalid_audio |
The declared audio format is supported but the provider cannot decode the bytes | No. Re-encode or record again |
422 |
empty_transcript |
The audio decoded successfully but contained no transcribable speech | No. Record speech and retry |
429 |
cloud_rate_limit_exceeded |
Your authority’s token bucket is empty | Yes, after Retry-After |
429 |
cloud_auth_rate_limit_exceeded |
The per-IP pre-authentication bucket is empty | Yes, after Retry-After |
500 |
internal_error |
Unexpected server fault | Cautiously, with backoff |
503 |
identity_service_unavailable |
Credential introspection is down. Retry-After: 2 |
Yes |
503 |
event_stream_unavailable |
The Event wake listener has not proven delivery or the process has reached its 256-stream cap. Retry-After: 2 |
Yes, after Retry-After |
503 |
cloud_rate_limit_unavailable |
The rate limiter faulted and fails closed. Retry-After: 1 |
Yes |
503 |
cloud_extension_catalog_unavailable |
An environment write adds a catalog extension, and the extension catalog could not be reached to check it. Writes that add no catalog item are unaffected. Retry-After: 2 |
Yes, after Retry-After |
503 |
cloud_agent_catalog_unavailable |
Agent/model capability discovery could not answer: it is down, or the bounded number of calls this server may have outstanding against it is spent — a wide concurrent fan-out reaches the second before it spends its own rate budget. Retry-After: 2 |
Yes, after Retry-After; alert the operator if persistent |
503 |
transcription_unavailable |
The speech-to-text provider or Account relay is unavailable | Yes, with backoff |
504 |
transcription_timeout |
Transcription did not complete within the bounded request window | Yes, once; record a shorter note if persistent |
The five repository-authority codes above name five different parties’ next
action, and they are newer than the collapsed code they replace. Deployments
roll independently, so an older one answers 403 cloud_repository_grant_required
for every case in the family — including a source.baseBranch that does not
resolve. If you receive that code where the table promises a finer one, confirm
the branch exists and that GET /v1/repositories still lists the exact
grant/repository pair, which is the distinction the single code cannot draw.
Which build is answering you
names the deployment you are on.
401 versus 403 is the distinction to encode: 401 means this credential
is no good, so re-mint and retry once. 403 means the installation is not
permitted, and no amount of retrying will change it — surface it to whoever can
grant the capability.
Not found
Section titled “Not found”| Status | Code | Cause |
|---|---|---|
404 |
cloud_session_not_found |
No such session, or it belongs to another tenant |
404 |
cloud_turn_not_found |
No such turn in this session |
404 |
cloud_run_not_found |
No such run in this session |
404 |
cloud_artifact_not_found |
No such artifact, or it is not publicly visible |
404 |
cloud_publication_not_found |
No such publication |
404 |
cloud_publication_merge_not_found |
No such merge under that publication |
404 |
cloud_session_access_not_found |
GET …/execution on a session you cannot see, or that does not exist |
404 |
environment_not_found |
No environment with this id or name belongs to this organization, or the requested revision does not exist |
404 is also what you get for a resource that exists but belongs to a different
organization — the API does not distinguish “absent” from “not yours”, because
doing so would leak the existence of other tenants’ data. Two practical
consequences: a 404 right after a successful PUT usually means an id case or
typo mismatch, and a 404 on a session you are sure exists usually means the
token belongs to a different installation.
Conflicts
Section titled “Conflicts”409 covers two very different situations, and the code tells you which.
Idempotency conflicts — your bug
Section titled “Idempotency conflicts — your bug”These mean the same caller-owned id was reused for different content. They are never transient and retrying is pointless.
| Code | Operation | Trigger |
|---|---|---|
cloud_session_conflict |
Session create or an environment write with If-Match |
Same Session id with a different normalized create command (including environment selection), or an environment revision mismatch |
cloud_turn_conflict |
PUT …/turns/{turnId} |
Same id, different message or replyTo |
cloud_publication_conflict |
PUT …/publications/{publicationId} |
Same id, a different stored request hash |
cloud_interaction_conflict |
PUT …/turns/{turnId} with replyTo, or PUT …/interactions/{interactionId}/decision |
The interaction is already resolved or expired, or another approval answer won; a decision conflict includes the winning outcome when one exists |
The rule that keeps you out of this: an id identifies one exact command. To send
different content, mint a new id. Replaying an identical body is always safe and
returns 200 (or 202 with created: false for turns).
An oversized publication body or merge commitMessage is not one of these.
Either field is checked twice — 65 536 characters at the route, then 65 536 UTF-8
bytes behind it — and both refusals are a 400: invalid_request on the
character bound, invalid_input on the byte bound. The request is malformed, not
in conflict with a stored one, so there is no conflict to resolve and no new id
to mint.
cloud_publication_merge_conflict is the exception and it is not always your
bug, so it gets its own row:
| Code | Operation | Triggers |
|---|---|---|
cloud_publication_merge_conflict |
PUT …/merges/{mergeId} |
(a) the same merge id with a different stored request hash — your bug, permanent; (b) a different, brand-new merge id while another merge for the same publication is still admitted, attempted, waiting or succeeded — transient in case (b) except for succeeded; (c) the first-party /cloud request’s expectedHeadSha does not match the locked publication head — merge_head_mismatch, which admits no merge |
You cannot tell these apart from the public code alone. GET …/publications/{publicationId}/merges
pages every merge for that publication, so you can see whether one is already
live and, if so, resume polling that one instead of minting another. For a
first-party head mismatch, refresh and review the publication before retrying.
A publication admits at most one live merge and at most one successful merge ever.
Busy conflicts — wait and retry
Section titled “Busy conflicts — wait and retry”| Code | Meaning | Retry-After |
|---|---|---|
cloud_run_in_progress |
A non-terminal run blocks this action, or a fork source changed during capture | 2 |
cloud_session_finalizing |
The previous run is being closed out and its transcript captured | 2 |
cloud_publication_in_progress |
A publication for this session is not terminal yet | — |
cloud_agent_locked |
Session PATCH would change runtime or model after a Turn exists. Select the agent on a new Turn instead |
— |
cloud_publication_merge_not_ready |
A when_ready merge is still waiting on checks or protection |
— |
Only the first two carry a delay. For the rest, back off on your own schedule —
a few seconds is right for cloud_publication_in_progress; a when_ready merge
is a background wait measured in minutes, so poll it slowly.
Concurrent session commands
Section titled “Concurrent session commands”Each command checks the Session’s durable state when it commits. A Turn submitted
after Archive receives cloud_session_not_found; Archive following a Turn with
an active Run receives cloud_run_in_progress. Publish after Archive also
receives cloud_session_not_found, while Archive following publication admission
receives cloud_publication_in_progress. A merge submitted before its
Publication succeeds receives cloud_publication_merge_not_ready. Changing an
agent after Turn admission receives cloud_agent_locked; submitting a Turn
against an agent changed since preflight receives cloud_session_conflict.
Re-read the Session or Publication before retrying a state conflict.
State conflicts — act, then retry
Section titled “State conflicts — act, then retry”| Code | Meaning | What to do |
|---|---|---|
cloud_conversation_busy |
A plain turn was submitted while an interaction is open, or another command holds the session’s conversation | Answer the open interaction with replyTo. Never retry the plain turn |
cloud_turn_unresolved |
A turn id already exists but its run does not — an inconsistent replay, not an interaction problem | Re-PUT the identical turn once; if it persists, mint a new turn id |
cloud_agent_configuration_invalid (400) |
The agent selection cannot run: an unknown runtime, or a native Turn switched from a foreign runtime without a model |
Use an advertised runtime and supply a catalog model for a native Turn that cannot inherit one |
cloud_agent_model_unavailable (400) |
The explicit model is not one this organization may spend on, or a new native Session omitted model and no agent slot is available |
Send a model from models.items in GET /v1/capabilities, or configure model access; exact Session replays keep their stored model |
cloud_workspace_expired |
The sandbox lease has ended | Read continuation on the session to see whether work can resume |
cloud_workspace_expiring |
The lease is too close to its deadline to start this work | Retry shortly; a new lease will be taken |
cloud_resume_unavailable |
The session cannot be continued from its last checkpoint | Start a new session |
cloud_turn_execution_lost |
The execution backing this turn disappeared | Re-submit as a new turn id |
cloud_turn_limit_reached (409) |
The session reached its durable 1,000-turn boundary | Start a new session; exact retries of already accepted turn ids still work |
environment_archived (409) |
The selected environment is archived | Unarchive it or choose a live environment |
environment_name_conflict (409) |
Another live environment has this name, ignoring case | Rename it or archive the other environment |
quota_exceeded (409) |
The organization already has 200 live environments, or its environment archives would exceed 1 GiB | Archive an environment before creating or unarchiving; remove archives from documents and let retention collect them |
cloud_turn_limit_reached is a per-session state boundary, so archiving another
session cannot change it.
Upstream and infrastructure
Section titled “Upstream and infrastructure”| Status | Code | Meaning | Retry? |
|---|---|---|---|
502 |
cloud_turn_start_failed |
The turn was admitted but starting execution failed | Yes — re-PUT the same turn id |
502 |
cloud_environment_setup_failed |
The pinned environment setup failed or timed out; the apply event has a redacted output tail | No. Fix the environment, then submit a new Turn |
502 |
cloud_run_control_failed |
A control operation on the run (such as cancel) failed downstream | Yes, with backoff |
503 |
cloud_workspace_starting |
The sandbox is being provisioned. Retry-After: 2 |
Yes |
503 |
cloud_workspace_unavailable |
No sandbox could be provisioned | Yes, with backoff |
503 |
cloud_runtime_outdated |
The requested runtime is too old for this session | No. Create a new session |
503 |
cloud_runtime_account_unavailable |
No configured account can currently admit this runtime | Yes if discovery advertises temporary unavailability; otherwise connect an account |
503 |
service_unavailable |
The API is restarting for a WAMP release, no API server is up behind the edge yet, or the database could not serve the request in time (transient contention under load). Retry-After: 2 |
Yes, after Retry-After. See Releases |
A durable Run that exhausts bounded dispatch attempts reports the terminal Event
code runtime_account_unavailable (without the cloud_ HTTP prefix). It is not
still queued: reconnect an account, then submit a new caller-owned Turn.
The 502 codes are the ambiguous ones: the command may or may not have taken
effect. This is exactly what caller-owned ids are for — re-send the identical
PUT and the response tells you which world you are in.
Releases and answers with no code
Section titled “Releases and answers with no code”A WAMP release restarts the API. For a few seconds, requests are answered with
503 service_unavailable and Retry-After: 2 — by the stopping server, or by
the edge while no server is up. Wait Retry-After and send the identical request
again. A request that was already running when the restart began can be cut off,
so treat a PUT like any ambiguous failure: re-send the same id, never a new
one.
The same coded answer arrives outside a release when the database could not
serve a request in time — a transaction that waited too long behind other work
on the same Session, a write conflict, or no free connection. Handle it exactly
like the restart: wait Retry-After and send the identical request.
More rarely the proxy in front of the API fails on its own and answers 502,
503 or 504 with no JSON error field — an HTML page or an empty body. The
missing code is the signal: no WAMP server answered, and the request may or may
not have reached one. Retry an idempotent request — a GET, or a PUT that
carries your own id — with capped exponential backoff and jitter for up to about
30 seconds, then surface the failure. Do not resend a POST such as run
cancellation on that answer; read the resource first.
Repository and GitHub errors
Section titled “Repository and GitHub errors”GET /v1/sessions/{sessionId}/repository/review can fail for repository-level
reasons. The codes below are the one family on this page with no cloud_ prefix,
and they arrive in the same { "error": "<code>" } body as every other /v1
failure.
Workspace conditions
Section titled “Workspace conditions”| Status | Code | Meaning |
|---|---|---|
409 |
no_changes |
The agent changed nothing. There is nothing to publish |
409 |
not_repository |
The session has no repository checkout |
409 |
repository_conflict |
The checkout is not in a state that can be reviewed |
409 |
review_changed |
The tree moved while the review was being computed |
409 |
branch_conflict |
The working branch conflicts with what is on the remote |
413 |
review_too_large |
The diff exceeds the review budget |
404 |
not_found |
The workspace or repository is gone |
502 |
checkout_failed |
The base revision could not be checked out into the sandbox |
502 |
review_failed |
The diff could not be computed |
502 |
publish_failed |
The reviewed tree could not be committed or pushed |
502 |
workspace_lost |
The sandbox backing the checkout disappeared mid-operation |
no_changes is the common one and it is not really an error — it is the honest
answer when an agent decided nothing needed changing. Handle it as a normal
outcome in your product.
branch_conflict is the one row on this table you should not write an HTTP
handler for. A push that conflicts is discovered after the publication is
admitted, so the PUT answers 2xx and the conflict arrives as
publication.lastError.code with the publication terminal in failed — exactly
like the merge-phase codes below. A client that treats the 2xx as “pushed”
reports a conflicting push as a success. Branch on the publication’s terminal
status, never on the status of the request that started it. See
Push directly to a branch.
What GitHub said
Section titled “What GitHub said”Everything the provider answers is mapped to its own code rather than collapsed into “GitHub is down”, because the four cases need four different reactions.
| Status | Code | Meaning | Retry? |
|---|---|---|---|
404 |
repository_not_found |
GitHub answered 404 or 410 — deleted, renamed, or never visible to this grant | No |
409 |
pull_request_changed |
The pull-request head moved after the publication recorded it | No. Re-review, republish, then merge the new head |
409 |
pull_request_closed |
Someone closed the pull request | No. There is nothing to merge |
409 |
merge_not_ready |
Checks pending, a review outstanding, or the pull request is still a draft | Yes with mode: "when_ready"; for a draft, republish with draft: false |
409 |
merge_not_allowed |
GitHub refused the merge for this actor or strategy — commonly branch protection, or a strategy the repository disallows | No. Change strategy or ask an administrator |
403 |
repository_read_only |
The GitHub App installation cannot write the repository — its contents permission is read-only, or the repository is not in the installation |
No. Ask an administrator |
403 |
github_request_refused |
A permanent 4xx refusal: an organization policy, an unsatisfied SAML SSO authorization, a missing App permission, a rejected payload | No. Retrying is pure waste |
429 |
github_rate_limited |
GitHub is throttling. The response carries the wait GitHub itself asked for in Retry-After |
Yes, after Retry-After |
401 |
not_connected |
No GitHub connection backs this grant | No. An administrator must connect one |
401 |
authorization_expired |
The connection’s authorization lapsed | No. An administrator must re-authorize |
503 |
not_configured |
GitHub integration is not configured on this deployment | Yes, but alert the operator |
502 |
github_unavailable |
GitHub was unreachable or returned 5xx | Yes, with backoff |
502 |
invalid_state / oauth_failed / account_already_connected / invalid_return_url |
Connection-flow faults. These belong to the browser authorization flow and should not reach a /v1 caller; if one does, it is a server-side fault |
No. Report it |
github_rate_limited is the one worth special-casing. It is the only code in
this table whose Retry-After comes from the provider rather than from WAMP, and
hammering through it is what deepens a secondary rate limit. Honour the header.
The four merge-phase codes — pull_request_changed, pull_request_closed,
merge_not_ready and merge_not_allowed — never reach you as an HTTP status.
Publishing and merging are asynchronous, and the merge reconciler catches all four
and records them, so they arrive only inside a 200 body: as
merge.lastError.code, and as the errorCode of
wamp.publication.merge_failed. Their 409 is what the code means — the
observed pull request’s own state, not an upstream fault — and it is what a
synchronous answer would carry if one ever existed. Branch on the code; do not
write a 502 handler expecting these. See
Failure codes inside successful responses.
Failure codes inside successful responses
Section titled “Failure codes inside successful responses”Not every failure is an HTTP error. Long-running commands report their own
outcome in a 200 body:
| Field | Where |
|---|---|
run.lastError.code |
GET …/runs/{runId} |
publication.lastError.code |
GET …/publications/{publicationId} |
merge.lastError.code |
GET …/merges/{mergeId} |
data.errorCode |
wamp.publication.failed, wamp.publication.merge_failed events |
These are diagnostic strings drawn from several error families, and the set is open — treat an unrecognized value as a permanent failure of that command and report it verbatim rather than guessing.
A publication or merge can also surface any code from Repository and GitHub errors above, or any of the HTTP codes on this page, because the failure is recorded with whatever code raised it. Beyond those, each command has its own family.
publication.lastError.code — the publish state machine’s own codes:
| Code | Meaning |
|---|---|
publication_failed |
The fallback when no more specific code applies |
publication_not_found |
The publication vanished between admission and execution |
publication_in_progress |
Another publication for this session is not terminal |
publication_idempotency_conflict |
The stored request hash for this id does not match |
publication_session_archived |
The session was archived before the publish ran |
publication_workspace_changed |
The reviewed tree moved before the commit was made |
publication_authority_lost |
The credential’s authority was revoked mid-command |
publication_repository_grant_required |
The session’s repository grant is missing, revoked, or does not reach this repository |
publication_claim_lost |
The worker’s claim on the command expired and another took over |
publication_state_conflict |
The publication was not in a state this step could advance |
publication_invariant_violation |
A durable invariant failed. Report it |
merge.lastError.code — the merge state machine’s own codes:
| Code | Meaning |
|---|---|
merge_failed |
The fallback when no more specific code applies |
merge_not_found |
The merge vanished between admission and execution |
merge_already_admitted |
Another merge for this publication is already live |
merge_idempotency_conflict |
The stored request hash for this id does not match |
merge_publication_not_ready |
The publication has not reached succeeded |
merge_session_archived |
The session was archived, so commands are refused |
merge_authority_lost |
The credential’s authority was revoked mid-command |
merge_claim_lost |
The worker’s claim expired and another took over |
merge_state_conflict |
The merge was not in a state this step could advance |
Repository-grant failures — the grant service’s own codes. Both publications and merges spread this family in, because the grant behind the session is re-checked at every attempt:
| Code | Meaning |
|---|---|
installation_not_found |
The GitHub App installation the grant belongs to is not connected to the organization |
grant_not_found |
The grant backing the session is missing or revoked |
branch_not_found |
The branch the session pinned does not resolve in the repository. Reaches a /v1 caller as 400 cloud_repository_branch_not_found |
grant_idempotency_conflict |
The same grant id was requested with different content |
active_grant_exists |
An active grant already exists for that GitHub App installation |
github_connection_required |
No GitHub connection is available to back the grant |
repository_permission_denied |
GitHub refused the write: the person creating the grant lacks push on the repository, the App installation can no longer write it, or an organization policy such as unsatisfied SAML SSO blocks it |
run.lastError.code values are catalogued with the events that carry them, in
Events.
Note that lastError can be present on a non-failed resource: a waiting
merge carries lastError.code: "merge_not_ready" while it is still retrying.
Read status first, lastError second.
A retry policy that matches the server
Section titled “A retry policy that matches the server”const RETRY_AFTER_HEADER = new Set([ 'cloud_workspace_starting', // 503 'cloud_session_finalizing', // 409 'cloud_run_in_progress', // 409 'cloud_rate_limit_exceeded', // 429 'cloud_auth_rate_limit_exceeded', 'identity_service_unavailable', 'cloud_rate_limit_unavailable', 'service_unavailable', // 503, a release restart or database contention // Not a rate limit and not an outage: the bounded set of calls this server may // have in flight against its own identity and catalog dependency is spent. 'cloud_agent_catalog_unavailable', // 503 // An environment write that adds a catalog extension could not reach the catalog. 'cloud_extension_catalog_unavailable', // 503 // The wait here is GitHub's own, not ours. Ignoring it deepens the limit. 'github_rate_limited', // 429, repository/review]);
const BACKOFF = new Set([ 'cloud_workspace_unavailable', 'cloud_runtime_account_unavailable', 'cloud_publication_in_progress', 'cloud_run_control_failed', 'github_unavailable', 'internal_error',]);
// Not retryable at all: the agent is waiting on you, and nothing times it out.const ANSWER_THE_INTERACTION = new Set(['cloud_conversation_busy']);
// The proxy in front of the API answered: no WAMP server wrote this response.const EDGE_FAILURE = new Set([502, 503, 504]);
/** * @param {number} status * @param {string | undefined} code the body's `error`; undefined when the body is not JSON or has none * @param {string | null} retryAfterHeader * @param {boolean} idempotent a GET, or a PUT that carries your own id */function plan(status, code, retryAfterHeader, idempotent) { if (status === 401) return { action: 'remint-once' }; if (code === undefined) { // Back off for up to about 30 seconds, then surface it. return EDGE_FAILURE.has(status) && idempotent ? { action: 'retry-backoff' } : { action: 'fail' }; } if (ANSWER_THE_INTERACTION.has(code)) return { action: 'answer-interaction' }; if (RETRY_AFTER_HEADER.has(code)) { return { action: 'retry', delayMs: (Number(retryAfterHeader) || 2) * 1000 }; } if (BACKOFF.has(code)) return { action: 'retry-backoff' }; // 502 on an idempotent PUT: re-send the SAME id. if (status === 502) return { action: 'resend-same-id' }; return { action: 'fail' };}Everything not in those sets is terminal. In particular: never retry a 400, a
403, a 404, or a 410 — each of them means the request itself was wrong, and
a retry loop on them is how an integration turns a bug into a rate limit.
Give the whole loop a budget measured in time, not attempts: three attempts two seconds apart can end inside one restart. The source App SDK keeps retrying a read, or a request that carries your own id, until it has failed for 20 seconds and at least three times.
Two conflicts are the exceptions to “never retry a …_conflict”:
cloud_publication_merge_conflict clears once the merge holding that
publication ends failed — but never after one has succeeded, because a
publication may land at most once, ever — and cloud_conversation_busy clears
when you answer the open interaction, never on its own.
Related
Section titled “Related”- API conventions — idempotent
PUTand what “the same body” means. - Limits — the caps behind
429and413. - Follow a run live — handling
409while a run is in flight. - the API reference — the error object in the machine contract.