pleach
Operate

Error codes

Structured 1xxx–9xxx error codes the runtime emits — what each range means, how to surface them, and what the recovery hint tells you.

Every error the runtime emits carries a structured code field plus a recovery hint. Codes are organized into ranges by subsystem so a single switch statement can group them meaningfully without enumerating every value.

The same code that lands in an error stream event also lands on the thrown HarnessCodedError instance for synchronous code paths. For audit:* gate failures (a different surface — CI-time rather than runtime), see Audit gates.

Error shape

The user-facing throwable is HarnessCodedError:

import { HarnessCodedError, isHarnessCodedError } from "@pleach/core";

class HarnessCodedError extends Error {
  errorCode: HarnessErrorCode;        // e.g. "SESSION_NOT_FOUND"
  code:      number;                  // e.g. 2001
  message:   string;                  // "[2001] Session not found"
  recovery:  string;                  // human recovery instruction
  docs:      string;                  // anchor link
  context:   Record<string, unknown>; // ctor-supplied diagnostic context
}

In stream form:

{ type: "error", error: "Session not found", code: "PROVIDER_ERROR", codeNum: 5006 }

Stream code (string) vs codeNum (numeric). On the thrown HarnessCodedError, code is always the numeric catalog value (e.g. 2001). On the stream error event, code is a string discriminator a host may key on behaviorally (a graph fallback trigger, an exhaustion signal) — so it is not the numeric catalog code. The stream event also carries a dedicated numeric codeNum field (present when a catalog code is known at the emit site) that is the catalog value — use it for range dispatch (Math.floor(codeNum / 1000)) and numeric matching, and treat code as an opaque host-signal string. This keeps both values available without overloading the host trigger.

isHarnessCodedError(err) is the type-guard. getErrorInfo("SESSION_NOT_FOUND") and getErrorInfoByCode(2001) look up the catalog entry without constructing an instance. HarnessError (interface, exported from the same barrel) is a separate shape — it's the persisted row used by ErrorPropagator for centralized error tracking, not the throwable.

Typed subclasses

In addition to HarnessCodedError, several subsystems throw domain-specific Error subclasses you can catch by class:

SubclassThrown fromCatch when
TenantIdEmptyErrorruntime.tenant facetConstructing a runtime without tenantId in tenant-scoped mode
HarnessModuleLoaderUnregisteredErrorruntime/moduleLoaderRegistryA host adapter forgot to call setHarnessModuleLoader
ReplayDivergenceError / ReplayCacheMissError / ReplayUnknownEventError@pleach/replayDeterministic-replay parity broke
NotImplementedError@pleach/replay, @pleach/mcpA surface is declared but unwired
GatewayFamilyDeniedError / GatewayFamilyExhaustedError / GatewayTransportMissingError@pleach/gatewayMulti-key routing failed
ReservedNamespaceError / UnnamespacedIdError / DuplicateContributionIdErrorprompts/typesPlugin contributed a prompt id that collides with a reserved namespace

HarnessCodedError itself is instanceof Error, so the catch-all path still works.

Ranges

RangeCategoryTypical handling
1xxxTool errorsRetry once, then surface to the user with the tool name
2xxxSession errorsRe-create or re-load the session
3xxxSync errorsConflict UI; let the user pick
4xxxStorage errorsSurface as "could not save"; queue retry
5xxxProvider errorsFallback cascade; surface family-exhausted to the user
6xxxCheckpoint errorsSkip the checkpoint operation; log; continue
7xxxValidation errorsTreat as programmer error; fix the input shape
8xxxInterrupt / approval errorsTimeout, rejection, or cancel of an approval gate
9xxxAsync job errorsBackend dispatch / execution failures for long-running jobs
10xxxCaller-auth errorsThe runtime caller's own identity/credential (distinct from the provider key in 5xxx and the session in 2xxx)
11xxxRuntime / infra errorsCross-cutting: transport network, upstream service, cost budget, abort, control-flow

1xxx — Tool errors

CodeMeaningRecovery
1001Tool not foundCheck the tool registry; tool may have been disabled mid-session
1002Tool validation failedParameter validation failed; check arg types against the tool's schema
1003Tool execution failedInspect cause; one retry usually safe
1004Tool timeoutLengthen the timeout if the tool is legitimately slow; otherwise surface

Tool errors surface on the stream as tool.failed first; the generic error event fires only when the failure escapes the tool boundary (e.g. the registry itself errored).

2xxx — Session errors

CodeMeaningRecovery
2001Session not foundThe id may be stale or the session was deleted
2002Session conflictAnother writer modified the session; fetch the latest state and retry
2003Session expiredRe-create; carry over what you need into the new session
2004Session lockedAnother writer holds the lock; wait + retry, or surface "open elsewhere"

2004 is the one to handle carefully — it usually means the same session is open in another tab. The user has context the runtime doesn't.

3xxx — Sync errors

CodeMeaningRecovery
3001Sync network errorTransient transport failure; changes are queued locally and retried
3002Sync conflict unresolvedConflict reached a state the merger couldn't resolve; manual pick
3003Sync version mismatchLocal version vector is behind; pull the latest and retry

3002 is the canonical "two devices wrote to the same session and the merger declined to choose" case. The accompanying sync.conflict event carries the resolution the merger chose; 3002 fires when it declined. 3001 is a plain transport failure — retry once connectivity returns.

4xxx — Storage errors

CodeMeaningRecovery
4001Storage write failedQueue + retry with backoff; surface after N attempts
4002Storage read failedRetry once; fall back to last-known state
4003Storage quota exceededDelete old sessions or checkpoints to free up space

Writes route through the durable-flush pipeline, which already handles transient 4001s with retries. A 4001 on the stream means the durable-flush retries also failed.

5xxx — Provider errors

CodeMeaningRecovery
5001Provider not configuredSet up the provider credentials in environment variables
5002Provider auth failedCheck API-key validity; the key may have expired or been revoked
5003Provider rate limitedCascade; if every in-family rung is rate-limited, surface family-exhausted
5004Model not foundCheck the model id; it may have been deprecated
5005Context window exceededCompact context (context.summarized event) and retry once
5006Stream errorThe provider response stream was interrupted; retry the request
5007Provider timeoutThe provider took too long; retry with a smaller context
5008Provider cascade exhaustedEvery in-family rung failed; switch to a different model family or wait for capacity

A 5003 rarely escapes the cascade — the family-strict fallback walks every in-family rung first. When the consumer sees one as an error event, every rung has failed; the terminal signal is 5008 (cascade exhausted), carried by the typed ProviderFamilyExhaustedError / RetryFamilyExhaustedError. 5001 (not configured) and 5002 (auth failed) are terminal — no rung succeeds if credentials are missing or invalid. 5006 (stream error) and 5007 (timeout) are the transient mid-stream failures the retry loop attempts before giving up to 5008. The runtime emits family-exhausted separately so a UI can ask the user to pick a different family explicitly.

6xxx — Checkpoint errors

CodeMeaningRecovery
6001Checkpoint not foundRefresh the checkpoint list; id may be stale
6002Checkpoint corruptThe checkpoint envelope was corrupt or schema-incompatible; use an earlier one
6003Checkpoint restore failedState restoration failed; try a different checkpoint

Checkpoint errors never block the turn — the runtime skips the failing operation and continues. A 6001 on a manual runtime.checkpoints.rollback call is the one case where the consumer needs to surface the failure.

7xxx — Validation errors

CodeMeaningRecovery
7001Invalid inputProgrammer error; fix the call site
7002Missing required fieldProgrammer error; pass the required field
7003Type mismatch in inputProgrammer error; check value types against the schema

7xxx codes mean "the runtime was called incorrectly." They shouldn't appear in production once the integration is shaken out. Schema-version drift between the package and the database surfaces as 2006 (Session schema mismatch), not as a 7xxx — if you see 2006 in production, the schema bundle has advanced past what's applied; re-run npx pleach init --apply and apply the new migrations.

8xxx — Interrupt / approval errors

CodeMeaningRecovery
8001Interrupt approval timed outThe user did not respond in time; the tool call was cancelled
8002Tool call rejected by userThe user declined; surface the decline and offer a rephrase
8003Interrupt cancelledThe interrupt request was cancelled before a decision was made

The 8xxx range surfaces approval-gate outcomes, not bugs. Don't alert on 8002 — user rejections are normal flow. Distinguish in dashboards the same way the subagent terminalStatus discriminator does: 8001 (timeout) and 8003 (cancelled) are operational signals; 8002 is product feedback.

9xxx — Async job errors

CodeMeaningRecovery
9001Job not foundThe job may have expired or been deleted — check the job id
9002Job dispatch failedBackend service is unhealthy; retry the dispatch
9003Job execution failedCheck job logs for error details; the job may need to be retried
9004Job cancelledThe job was cancelled before completion; resubmit if the result is still needed

The 9xxx range surfaces failures from the long-running job substrate. A 9002 typically means the backend dispatcher (Modal, queue worker, etc.) is unreachable — retry with backoff. A 9003 is the job itself failing; the runtime can't recover, the caller decides whether to retry or surface. 9004 (cancelled) is not a failure — it's a user/operator action; don't alert on it.

10xxx — Caller-auth errors

CodeMeaningRecovery
10001Caller auth requiredSign in or supply a valid API credential before retrying
10002Caller credential invalidThe presented credential was rejected — check the key value; this is a credential problem, not an expired session, so do not force a full sign-out
10003Caller forbiddenAuthenticated but not authorized; request the required scope/role

The 10xxx range is the runtime caller's own identity — distinct from 5002 (the LLM provider key) and 2003 (the harness session). 10002 is the one to handle carefully: a rejected API key is a credential problem, not a session expiry, so the affordance is "fix your key", not "sign in again". A host maps its transport-level 401/403 reasons into these via a contributeErrorClassifiers plugin.

11xxx — Runtime / infra errors

CodeMeaningRecovery
11001Network errorTransient transport failure; retry
11002Service unavailableUpstream returned 5xx/unavailable; retry after a short delay
11003Cost budget exceededRaise the budget ceiling or reduce the work requested for the turn
11004AbortedThe request was cancelled (caller, interrupt, or signal); retry if unintended
11005Agent loop errorThe turn could not converge; inspect the last steps and retry, possibly simpler

The 11xxx range is cross-cutting infra not owned by a single subsystem. 11001/11002 are transient (retry); 11003 is a policy stop (budget); 11004 is usually intentional (user abort — don't alert); 11005 is the turn-level "couldn't make progress" catch-all.

Handling errors at the call site

for await (const event of runtime.executeMessage(sessionId, prompt)) {
  if (event.type === "error") {
    // Range-dispatch on the NUMERIC codeNum (the stream `code` is a string host
    // signal — see the callout above). codeNum is absent when no catalog code was
    // known at the emit site, so it falls through to the generic branch.
    const range = Math.floor((event.codeNum ?? 0) / 1000); // 2 for 2xxx, etc.
    switch (range) {
      case 2: return reload();
      case 3: return surfaceConflictUI();
      case 5: return showFamilyExhaustedDialog();
      case 8: return surfaceApprovalOutcome(event);
      case 9: return surfaceJobFailure(event);
      default: return showGenericError(event.error);
    }
  }
}

For thrown errors (storage construction failures, config errors):

import { isHarnessCodedError } from "@pleach/core";

try {
  const session = await runtime.createSession(config);
} catch (err) {
  if (isHarnessCodedError(err) && err.code === 7002) {
    // missing required field (VALIDATION_MISSING_REQUIRED) — fix the caller
  }
  throw err;
}

Catching a model-not-found error

A 5004 fires when the requested model id no longer resolves — usually because the model was deprecated or rotated out from under a pinned config. There is no dedicated safety-refusal code in the enum; a provider-side refusal surfaces through the ordinary provider-error path (5006 stream error / non-clean finishReason) after the family cascade gives up. Translate a 5004 into a user-readable message and offer a next step:

for await (const event of runtime.executeMessage(sessionId, prompt)) {
  if (event.type === "error" && event.codeNum === 5004) {
    showToast({
      title:  "That model is no longer available.",
      body:   "The requested model id could not be resolved — it may have been deprecated.",
      action: { label: "Pick another model", onClick: openModelPicker },
    });
    return;
  }
}

Transient vs terminal dispatcher

Not every code deserves a retry. Group 1xxx / 4xxx / parts of 5xxx as retryable; treat 2xxx / 6xxx / 7xxx as terminal for the current call site:

import type { HarnessCodedError } from "@pleach/core";

const TRANSIENT = new Set([1003, 1004, 4001, 4002, 5003, 5006, 5007, 9002]);
const TERMINAL  = new Set([2001, 2003, 5001, 5002, 6001, 7001, 7003, 8002]);

function dispatch(err: HarnessCodedError) {
  if (TRANSIENT.has(err.code)) return { retry: true, after: 500 };
  if (TERMINAL.has(err.code))  return { retry: false, surface: err.message };
  return { retry: false, surface: "Unexpected error", log: err };
}

The split tracks the recovery column in the range tables above — keep it in one place so call sites don't drift.

Where to go next

On this page