Files
open-multi-agent/docs/errors.md
T
Jack Chen 01b63594fb feat(core): add Black Forest Labs image adapter (#614)
* feat(core): add Black Forest Labs image adapter

Black Forest Labs (FLUX) generates asynchronously: a submit returns a polling_url, the task is polled until ready, and the image is downloaded from a result URL. BlackForestLabsImageAdapter runs that flow with fetch, sends input images as input_image, input_image_2, and so on, and maps a WIDTHxHEIGHT size to width and height.

A moderated task shows up only in the poll status. The adapter reports Request Moderated and Content Moderated as a non-retryable content_policy failure as soon as it sees them, so runImage moves to the next model instead of polling until the attempt deadline and retrying. Failed and lost tasks are retryable.

The API key goes only to the API origin and HTTPS hosts under bfl.ai, keyed requests refuse redirects, and the pre-signed image download carries no key. providerOptions follows the same rule as the other adapters: fields that carry the prompt or input images are rejected at construction, and width and height are defaults that request.size overrides.

Follow-up to #613.

* fix(core): report detected image media type and the BFL dimensions sent

The built-in image adapters declared a fixed media type, which was wrong whenever providerOptions chose another output format, for example output_format png on Black Forest Labs or jpeg on OpenAI. All four now report the type read from the returned bytes and fall back to the old default only when the bytes are not a recognized image.

The Black Forest Labs attempt parameters kept default width and height from providerOptions even when request.size replaced them in the request body. They now record the same width and height the request sent.

* fix(core): keep a submitted BFL task through transient poll failures

A transient failure while polling or downloading, such as a 429, a 5xx, or a network error, escaped the adapter as retryable, so runImage retried generate() and submitted another billable task while the first kept running. The Black Forest Labs adapter now retries those failures in place until the attempt deadline, honoring Retry-After. Non-retryable errors still throw at once, and so does a 4xx on the download, since an expired delivery link cannot recover without a new task.

The adapter contract now says what it meant: an adapter must not resubmit a generation internally, while retrying a status poll or download of an already-submitted task is allowed.

* fix(core): type rate-limited and timed-out image downloads

downloadImage turned every non-2xx status into api_error, so a 429 or 408 on a pre-signed result URL did not count as transient for the Black Forest Labs in-place retry, escaped to runImage, and resubmitted a billable task. A download 429 is now rate_limit with Retry-After and a 408 is timeout, so both are retried in place against the existing task.

* fix(core): scope BFL credentials and keep a submitted task final at the deadline

The Black Forest Labs adapter sent its key to any HTTPS host under bfl.ai. Behind a proxy that key belongs to the proxy, and a forwarded BFL polling URL would receive it. Sibling BFL hosts now qualify only when baseURL is itself a BFL host; otherwise the key goes to the configured origin alone.

A transient outage that lasted past the attempt deadline still led runImage to retry generate() and submit another billable task. Once a task is submitted, the adapter now reports a deadline as a non-retryable timeout, and runImage keeps a non-retryable adapter verdict at the deadline instead of replacing it with a retryable timeout, so the chain moves to the next model. A deadline before the submit completes stays retryable.

* fix(core): tell deadlines from cancellation and end BFL turns on permanent download failures

runImage now aborts an attempt's signal with a DOMException named TimeoutError when the deadline fires, the same convention as AbortSignal.timeout(), and with the caller's own reason on cancellation. The Black Forest Labs adapter turns only a deadline after the submit into a final timeout; a caller that cancels a direct generate() call gets its own abort reason back.

A permanent failure downloading a submitted task, such as an expired or missing result link, was still retryable, so runImage submitted and paid for another task. Once the task exists, a download failure that is not transient now ends that model's turn as non-retryable.

* fix(core): end a submitted BFL task's turn on any failure and keep its cost

Several failures after a Black Forest Labs task was submitted were still retryable: a Ready status without result.sample, an Error or Failed task, and a lost task. runImage retried them by submitting and paying for another task. Every failure after the submit now ends that model's turn as non-retryable.

The adapter's in-place retry of transient poll and download failures slept through any Retry-After, so a header above runImage's maxRetryAfterMs stalled the attempt until its deadline. runImage now passes its cap to adapters as ImageCallOptions.maxRetryAfterMs, and a longer wait ends the turn at once.

A failure after the submit also dropped the task ID and the cost BFL reported, because adapter params were only returned on success. ImageModelError now carries optional params, runImage copies them into the failed attempt record, and the BFL adapter attaches the task and its cost to every post-submit failure.

* fix(core): close the last BFL paths that resubmit a created task

Two failures after a successful Black Forest Labs submit still reached runImage as retryable and led it to submit and pay for another task: a submit response without polling_url, and a result download that answers 200 with bytes that are not an image. Both now end that model's turn as non-retryable invalid_output and carry the task ID and cost into the failed attempt record. The adapter checks the downloaded bytes itself instead of leaving it to runImage, whose retry of an invalid output would resubmit.
2026-09-24 03:59:11 +08:00

24 KiB

Errors

This page answers one question: when something goes wrong, where does it show up and can a retry help? It lists every error class OMA exports, who raises it, whether the framework treats it as terminal or retryable, and which surface the caller reads it from.

Three rules that decide where an error lands

1. Tool failures are values, not exceptions. ToolExecutor.execute() catches unknown tool names, Zod input-validation failures, gate denials, and anything the tool implementation throws, and returns a ToolResult with isError: true instead of rejecting. The class docstring states it directly, and every path funnels through one private errorResult() helper (packages/core/src/tool/executor.ts). The agent runner adds two more error results of its own: an ungranted tool name, and an executor call that still managed to throw (packages/core/src/agent/runner.ts). The model sees the message as a normal tool result and can adapt.

2. LLM and framework failures propagate through the runner. AgentRunner does not convert model-call failures into values. tracedChat() closes the span with a classified status and rethrows; only chatWithCallTimeout() substitutes one error, replacing a provider abort with LLMCallTimeoutError when OMA's own per-call deadline fired. The error keeps rising until it reaches Agent.executeRun(), whose catch block turns it into a failed AgentRunResult carrying status, errorInfo, and the original error object. So "propagates" means through the tool loop, not out to your await: a single agent.run() returns a failed result rather than rejecting. Input preparation is the exception, because Agent.run() calls prepareAgentRunInput() before the try.

3. Task failures cascade to dependents; independent tasks continue. TaskQueue.fail() marks the task 'failed', emits task:failed, and calls cascadeFailure(), which fires the same event for every transitively dependent task. skip() does the same through cascadeSkip(), treating a skipped upstream as permanently unsatisfiable. Neither touches tasks on unrelated branches, so a failure in one branch does not stop a sibling (packages/core/src/task/queue.ts).

Where a caller reads an error

Surface Shape Typical source
Thrown at your await The error object itself Configuration validation, input validation, a fatal dispatch failure, RoutingDeclarationRequiredError, DurableApprovalError
Run result fields result.status.code and result.errorInfo always; result.error only where a path preserved the live object Anything caught by Agent.executeRun() or converted by classifyRunFailure()
Tool result ToolResult.isError === true, message in data Tool execution, grants, gates
Progress and stream events OrchestratorEvent of type error, warning, or budget_exceeded; StreamEvent of type error or budget_exceeded Budget accounting, callback failures, recovery decisions

classifyRunFailure() (packages/core/src/observability/status.ts) is the single converter. It produces { status, errorInfo }, redacts and truncates the message (1,024 characters for errorInfo.message, 256 for status.message), copies a string or numeric .code off the error, and records retryable from isRetryableError(). Its special cases are:

  • TokenBudgetExceededError or CostBudgetExceededError becomes status.code: 'budget_exhausted', errorInfo.kind: 'budget'.
  • LLMCallTimeoutError or RoutingTimeoutError becomes status.code: 'timeout', errorInfo.kind: 'timeout'.
  • EgressPolicyError becomes status.code: 'rejected', errorInfo.kind: 'validation'.
  • A cancellation (.name === 'AbortError', or the OpenAI SDK's APIUserAbortError) becomes status.code: 'cancelled', errorInfo.kind: 'cancellation'.

Because errorInfo.code is copied from the error's own code field, it is the most stable thing to branch on across a serialization boundary. result.error holds the live error object, but only on the paths that keep one: it is undefined for app-level failures, and it collapses to {} if the result is JSON-serialized. Read status and errorInfo unless you specifically need the instance.

Retry classification

isRetryableError() (packages/core/src/errors.ts) decides whether another attempt is worth making. It is conservative: it returns true unless the error is provably terminal.

Terminal: InvalidTaskRequirementsError, TokenBudgetExceededError, CostBudgetExceededError, InvalidMessageError, StructuredOutputValidationError, UnsupportedToolCallError, EgressPolicyError, UnsupportedToolResultContentError, UnsupportedContentBlockError, JournalLineageError, RoutingProfilerFailedError, RoutingDeclarationRequiredError, any cancellation, and any 4xx status other than 408, 409, and 429.

Retryable: LLMCallTimeoutError, RoutingTimeoutError, errors carrying no numeric status (network blips), 408, 409, 429, and every 5xx.

ImageModelError is the exception to both lists: isRetryableError() returns its own retryable flag, which the adapter set when it classified the response.

executeWithRetry() (packages/core/src/orchestrator/retry.ts) consumes that classification at the task level. Retry is off by default (maxRetries: 0). It prefers result.errorInfo.retryable === false over re-classifying result.error, so a framework failure whose raw Error was stripped by a hook or a serialization seam still skips pointless attempts. Two cases bypass retry entirely: a 'suspended' status is a durable continuation boundary, and a DurableApprovalError is rethrown rather than retried.

Budget errors

TokenBudgetExceededError

code: 'TOKEN_BUDGET_EXCEEDED'. Carries agent, tokensUsed, budget.

The framework never throws this class; it constructs it as a value. applyBudgetAccounting() returns it as exceeded when cumulative tokens cross maxTokenBudget (packages/core/src/orchestrator/budget.ts), and Agent builds one whenever a run reports budgetExceeded so the result carries a classified status.

Caller sees: result.status.code === 'budget_exhausted', result.errorInfo.kind === 'budget', and result.budgetExceeded === true. emitBudgetExceeded() also fires an OrchestratorEvent of type budget_exceeded carrying the instance as data, and the streaming runner yields a StreamEvent of type budget_exceeded with the same payload.

result.error is not the place to look for it. The ordinary budget paths build the result from classifyRunFailure()'s status and errorInfo only, and Agent's post-hook ensureAgentOutcome() re-derives those two fields without attaching an instance. Exactly one path differs: Agent's structured-output corrective retry, where the retry attempt itself crosses the ceiling and the returned result carries error as well. Branch on status.code or errorInfo.code, not on error.

Terminal: a retry would re-cross the same ceiling.

CostBudgetExceededError

code: 'COST_BUDGET_EXCEEDED'. Carries agent, costUsed, budget.

Constructed only by applyBudgetAccounting(), and only when both maxCostBudget and estimateCost are configured. OMA ships no price table, so without estimateCost there is no cost to compare.

Visibility matches the token budget error, minus the exception: no path attaches a CostBudgetExceededError to result.error at all. Every consumer builds the failed result by spreading classifyRunFailure(), so it reaches callers only through status, errorInfo, budgetExceeded, and the budget_exceeded progress event. Terminal for the same reason.

A related failure is not an OMA error class: if estimateCost returns a non-finite or negative number, estimateIncrementalCost() throws a plain Error naming the agent, which then follows the ordinary failure path.

Timeout and routing errors

LLMCallTimeoutError

code: 'LLM_CALL_TIMEOUT'. Carries timeoutMs and optional agent.

Thrown by AgentRunner.chatWithCallTimeout() when AgentConfig.callTimeoutMs elapses for a single adapter.chat() request and the caller's own abortSignal did not fire. That last condition is what separates a stalled provider from a deliberate abort. It applies to every model call the runner owns, including summarize-based context compaction, so behavior no longer depends on each vendor SDK's default request timeout. It is distinct from AgentConfig.timeoutMs, which bounds the whole run.

Caller sees: a failed run result with status.code: 'timeout' and errorInfo.kind: 'timeout'. Retryable, so an orchestrated task with maxRetries set will try again.

RoutingTimeoutError

code: 'ROUTING_TIMEOUT'. Carries timeoutMs and stage: 'router' | 'profiler'.

stage: 'router' is rejected inside decideWithTimeout() (packages/core/src/orchestrator/execution-router.ts) when a custom ExecutionRouter exceeds executionRouting.timeoutMs. resolveExecutionRouting() catches it and falls back to the deterministic router, unless the caller aborted or failurePolicy: 'fail' is set, in which case it rethrows.

stage: 'profiler' is rejected by the timeout race in runSemanticProfiler() (packages/core/src/orchestrator/orchestrator.ts) when the hybrid TaskProfiler exceeds the same deadline. With the default failurePolicy: 'fallback', the assessment records outcome: 'fallback' with fallback_code: 'profiler-timeout' and the run continues on the deterministic decision. With 'fail', it is rethrown out of runTeam().

Retryable by classification, and mapped to status.code: 'timeout' when it reaches a result. See execution routing.

RoutingProfilerFailedError

code: 'ROUTING_PROFILER_FAILED'. Carries an optional cause.

Constructed in runSemanticProfiler() to wrap any non-timeout profiler failure, including a TaskProfileValidationError from LLMTaskProfiler. Terminal. It only reaches your await when executionRouting.failurePolicy === 'fail'; otherwise the run continues on the deterministic route and the fallback code (invalid-profile, profiler-unavailable, or profiler-error) is recorded on the routing span and assessment.

RoutingDeclarationRequiredError

code: 'ROUTING_DECLARATION_REQUIRED'. Carries reasons and an optional assessment.

Thrown by runTeam() when hybrid semantic routing returns recommendation: 'needs-declaration': inferred high-risk semantics require an explicit governance topology rather than a model-selected route. The run journal and trace root are closed with an error status first, then the error is thrown. Terminal, and it always reaches your await. Fix it by declaring governanceIntent with requiredRoles (see tool configuration) or by choosing an explicit mode.

Input and output validation errors

InvalidMessageError

code: 'INVALID_MESSAGE'. Message-only.

Raised from three places:

  • assertValidMessages() in packages/core/src/llm/validate.ts, for anything that is not a well-formed LLMMessage[].
  • prepareAgentRunInput() / prepareAgentPromptInput() in packages/core/src/agent/input.ts, for input that cannot be structured-cloned, or structured input aimed at a text-only external backend.
  • Agent's beforeRun plumbing, when a hook returns a non-string prompt, or when it rewrites messages for an agent whose backend accepts prompt rewrites only.

Where it surfaces depends on which one fired. Input preparation runs outside executeRun()'s try, so it rejects agent.run() and orchestrator.runAgent() at the caller's await. A beforeRun failure is caught and classified with errorInfo.kind: 'callback'. Terminal in both cases. See structured input.

StructuredOutputValidationError

code: 'STRUCTURED_OUTPUT_VALIDATION_FAILED'. Carries cause.

Constructed by Agent when outputSchema is set and the output still fails validation after the built-in single corrective retry. It is never thrown: the agent returns a failed result with status, errorInfo (classified with kind: 'validation'), and error. Terminal for orchestrator-level retries by design, because re-running the same prompt is not a transport recovery strategy.

InvalidTaskRequirementsError

code: 'INVALID_TASK_REQUIREMENTS'. Carries issues: readonly TaskRequirementIssue[].

Raised when a task has no eligible agent, or its explicit assignee does not satisfy the task's hard requires. Three call sites use it, and they surface differently:

  • Whole-plan validation in the coordinator path of runTeam(), after the plan is loaded into the queue. The error is converted by classifyRunFailure(error, { kind: 'validation' }) into a failed TeamRunResult, and an OrchestratorEvent of type error carries code and issues.
  • Whole-queue validation in the shared explicit-task path (runTasks(), runFromPlan(), restore(), and the declared-governance runTeam() topology). Here an error progress event fires and the error is then thrown, so it reaches the caller's await.
  • Per-task validation at dispatch time in executeQueue() (packages/core/src/orchestrator/task-execution.ts). Also thrown. The pipeline records it in dispatchErrors, stops admitting new tasks, lets in-flight work settle, skips the remainder, and then rethrows.

Issue codes are NO_ELIGIBLE_AGENT and ASSIGNEE_REQUIREMENTS_MISMATCH; see task scheduling. Terminal.

Provider capability errors

All three are raised by adapter code before or while mapping a request or response, and all three are terminal: the block and the adapter are both fixed for the attempt, so a retry re-runs the identical conversion.

UnsupportedToolCallError

code: 'UNSUPPORTED_TOOL_CALL'. Carries provider and toolType.

Thrown by the OpenAI-compatible response mapper (packages/core/src/llm/openai-common.ts) when a provider returns a tool-call type OMA cannot execute. OMA exposes JSON-schema function tools only; failing loudly keeps a provider's custom-tool response from being mistaken for a successful empty turn.

UnsupportedToolResultContentError

code: 'UNSUPPORTED_TOOL_RESULT_CONTENT'. Carries provider, contentType, and an optional detail suffix.

Thrown before an SDK request when a built-in adapter cannot faithfully map a model-visible tool-result part. Raised by the Anthropic, OpenAI-compatible, and Bedrock request mappers.

UnsupportedContentBlockError

code: 'UNSUPPORTED_CONTENT_BLOCK'. Carries provider, blockType, and an optional detail suffix.

Thrown before an SDK request when an adapter has no wire mapping for a whole model-visible content block. Raised by the Gemini, Bedrock, Anthropic, and AI SDK adapters. The class exists specifically so this case does not fall through isRetryableError()'s conservative default and spend the whole backoff ladder, plus a checkpoint rewrite per attempt, on a capability gap that cannot resolve itself. See structured input.

Image model errors

ImageModelError

code: 'IMAGE_MODEL_ERROR'. Carries type (timeout, rate_limit, content_policy, invalid_request, api_error, network, or invalid_output), retryable, and optional provider, status, retryAfterMs, providerCode, and params (task ID, cost, or other details of work the provider already accepted, copied into the failed attempt record).

Thrown by an ImageModelAdapter for one failed provider call. runImage() never lets it escape: it records the failure as an attempt and uses retryable to decide between retrying the same model and moving to the next one, then resolves with status: 'failed' if the chain runs out. You meet the class directly only when calling an adapter yourself or writing one. See image generation.

Egress policy errors

EgressPolicyError

Carries reason: 'invalid-policy' | 'denied' | 'unsupported' | 'unresolved-target', plus optional provider and origin. code is derived from reason: INVALID_EGRESS_POLICY, EGRESS_POLICY_DENIED, EGRESS_POLICY_UNSUPPORTED, EGRESS_POLICY_TARGET_UNRESOLVED.

Raised in packages/core/src/llm/egress.ts before a framework-owned LLM transport opens a request: on an invalid policy shape or allowlist entry, on a resolved origin outside the effective policy, on an adapter whose transport OMA cannot guard, and on a provider whose target cannot be resolved from configuration and environment.

Terminal, and never retried, because another attempt cannot widen a policy. classifyRunFailure() maps it to status.code: 'rejected' with errorInfo.kind: 'validation'. A direct createAdapter() call rejects with the same class. See egress policy.

Journal lineage errors

JournalLineageError

code: 'MISSING_CONTEXT_REPLACE', which deliberately differs from the class name. Carries messageIndex, blockIndex, and blockType.

Thrown by AgentRunner.describeRequestBlocks() before an adapter call, when the run journal has enforceLineage on and a model-visible block cannot name the journal event it came from. Enforcement is off by default. Failing at the request that would have hidden the gap, rather than at verification time, is the point. Terminal: a lineage gap is a property of the conversation, not the transport, so the same request would fail identically on every attempt. See run journal.

Store, CLI, and Run Viewer errors

These four are defined outside errors.ts and are not part of isRetryableError()'s classification.

TraceStoreError

Defined in packages/core/src/observability/store.ts. Exported from the package root and from @open-multi-agent/core/observability. Carries code: 'INVALID_ARGUMENT' | 'INVALID_CURSOR' | 'UNSUPPORTED_SCHEMA_VERSION' and an optional field.

The shared validation failure for every TraceStore implementation. Thrown synchronously from store methods (append validation, cursor parsing, schema checks) and reaches the caller's await on the store call. Losing telemetry must never roll back a durable run, so a store error is not converted into a run failure. See observability.

FileTraceStoreError

Defined in packages/core/src/observability/file-store.ts. Exported only from @open-multi-agent/core/observability/file, not from the package root. Carries code: FileTraceStoreErrorCode plus optional operation, path, lineNumber, and causeCode.

A payload-free, structured lifecycle or filesystem failure. Its code set covers path validation, open/read/write/sync/close, corrupt files, unsupported file format or trace schema, recovery, compaction and rename, and use after close. See observability.

RunViewerInputError

Defined in packages/core/src/dashboard/run-viewer-model.ts. Exported from the package root, together with the RunViewerInputErrorCode type. Carries code: 'MISSING_SOURCE' | 'RUN_ID_MISMATCH' | 'UNSUPPORTED_SCHEMA_VERSION'.

Thrown by buildRunViewerModel() (and therefore renderRunViewer()) when neither result nor run is supplied, when the two name different runs, or when the stored run's schema major does not match. See run viewer.

OmaValidationError

Defined in packages/core/src/cli/oma.ts and not exported. It is internal to the oma CLI and has no importable path. It marks bad flags, bad JSON shapes, and unloadable modules; the CLI's error mapper turns it into kind: 'validation' and exit code 2. FileTraceStoreError is mapped to exit code 2 as well, and DashboardCliError supplies its own exit code. See CLI exit codes.

Other exported error classes

Two more error classes ship on the public surface but belong to their own subsystems:

  • DurableApprovalError (packages/core/src/approval/durable.ts, exported from the package root with DurableApprovalErrorCode). Codes: APPROVAL_ATOMIC_STORE_REQUIRED, APPROVAL_CONFLICT, APPROVAL_INTEGRITY_ERROR, APPROVAL_NOT_FOUND, APPROVAL_STALE_DECISION, APPROVAL_VALIDATION_ERROR. executeWithRetry() rethrows it instead of retrying, so it escapes the task loop. See durable approvals.
  • TaskProfileValidationError (packages/core/src/orchestrator/task-profiler.ts, exported from the package root). Carries optional usage, model, and provider so a failed profile call is still accounted for. It is normally wrapped into RoutingProfilerFailedError before it leaves routing.

Quick reference

Class Import path Raised when Retryable Caller reads it at
InvalidTaskRequirementsError root A task has no eligible agent, or its assignee fails requires No Failed TeamRunResult from coordinator plan validation; thrown from the explicit-task path and from dispatch
TokenBudgetExceededError root Cumulative tokens cross maxTokenBudget No status.code: 'budget_exhausted', errorInfo.kind: 'budget', budgetExceeded, budget_exceeded progress and stream events
CostBudgetExceededError root Estimated cost crosses maxCostBudget (needs estimateCost) No Same, minus the stream event; never on result.error
LLMCallTimeoutError root One adapter.chat() exceeds callTimeoutMs Yes status.code: 'timeout' on the run result
RoutingTimeoutError root A router or the semantic profiler exceeds executionRouting.timeoutMs Yes Deterministic fallback by default; thrown when failurePolicy: 'fail'
RoutingProfilerFailedError root The profiler produced no valid task profile No Fallback by default; thrown when failurePolicy: 'fail'
RoutingDeclarationRequiredError root Hybrid routing infers high-risk semantics needing an explicit topology No Thrown from runTeam
InvalidMessageError root Malformed LLMMessage[], uncloneable input, or a disallowed beforeRun rewrite No Thrown from input preparation; a failed result with kind: 'callback' from beforeRun
StructuredOutputValidationError root outputSchema still unsatisfied after the corrective retry No Failed run result with kind: 'validation'
UnsupportedToolCallError root A provider returned a tool-call type OMA cannot execute No Failed run result, errorInfo.code: 'UNSUPPORTED_TOOL_CALL'
ImageModelError root An image adapter call failed; type says how Per retryable Attempt records and the failed runImage() result; thrown only from a direct adapter call
EgressPolicyError root Invalid policy, denied origin, unenforceable adapter, or unresolved target No status.code: 'rejected'; also rejects createAdapter()
UnsupportedToolResultContentError root An adapter cannot map a model-visible tool-result part No Failed run result
UnsupportedContentBlockError root An adapter has no wire mapping for a content block No Failed run result
JournalLineageError root enforceLineage is on and a model-visible block has no journal lineage No Failed run result, code: 'MISSING_CONTEXT_REPLACE'
DurableApprovalError root Approval persistence, integrity, or concurrency failure Rethrown, never retried Thrown out of the task loop
TaskProfileValidationError root LLMTaskProfiler output fails its schema No Usually wrapped as RoutingProfilerFailedError
TraceStoreError root, /observability Invalid store argument, cursor, or schema version n/a Thrown from the store call
RunViewerInputError root Missing, mismatched, or unsupported Run Viewer input n/a Thrown from buildRunViewerModel / renderRunViewer
FileTraceStoreError /observability/file FileTraceStore lifecycle or filesystem failure n/a Thrown from the store call; CLI exit code 2
OmaValidationError not exported Bad CLI flags, JSON shapes, or module loads n/a oma stderr and exit code 2