ADR-0014: Error Model and Observability
ADR-0014: Error Model and Observability
Status: Accepted (2026-07-02)
Spec: docs/superpowers/specs/2026-07-02-error-handling-observability-design.md
Guide: docs/OBSERVABILITY.md
Context
Agents are the primary consumers of vinxi, over HTTP syscalls and CLI
commands. Every failure must tell an agent, in machine-readable form: what
happened, why (full cause chain), and how to fix it. The original
VinxiError was a 9-variant enum carrying only a message string; causes were
flattened into text at each adapter call site, there were no stable codes, no
retry semantics, no trace correlation, and no metrics.
Decisions
-
Struct + compile-time catalog over enriched enum or anyhow classification.
VinxiErroris a struct (code,message,context,causeframes,suggestions, retry override) backed by an exhaustiveErrorCodeenum whosecatalog()returns a completeCatalogEntry(title, description, common causes, curated suggestions, retryability, category). Fix suggestions are fundamentally a catalog problem — they must be authored once per failure mode, not improvised at ~40 call sites. A completeness test iterates all codes; shipping an undocumented error is impossible. Codes are append-only and stable. -
Classification at the typed boundary. Third-party errors are classified where they are still typed (
classify_sqlx,classify_io).with_causewalkssource()into serializableCauseFrames and intentionally does not retain the original object — downcasting after capture is impossible, which forces classification to stay at the capture site instead of degrading into string matching downstream. Consequence: nested frames carry layer label"cause"(Rust cannot name concrete types behinddyn Error); the first frame carries the real type name. -
RFC 9457 with same-server dereferenceable
typeURIs. HTTP errors areapplication/problem+json;typeis/sys/errors/{CODE}, which the server itself serves — agents self-serve documentation. One canonical wire object (WireError) is shared by HTTP bodies, CLI stderr JSON, and worker logs; HTTP addsstatus/instance/request_id. -
No unstructured exits.
anyhowis removed from all runtime paths, including binary startup. Config/bind failures use cataloged codes (VINXI_CONFIG_MISSING,VINXI_CONFIG_INVALID,VINXI_BIND_FAILED);main()returns sysexits-aligned exit codes derived from the error category; a panic hook emits one structuredVINXI_PANICevent with a backtrace. -
Log-once rule. Errors are logged exactly once, at the boundary (HTTP handler, CLI main, worker loop, service startup). Inner layers enrich the error and return it.
-
OTel-ready, stdout-only telemetry.
vinxi-observabilityowns a custom JSONFormatEvent(per-lineservice.*resource fields, span-scope field merging, OTel semantic-convention names) and W3CTraceContextparse/generate. No OTLP exporter or backend containers yet; adding one is a subscriber-layer swap. tower-http’sTraceLayerwas replaced by our own middleware, which owns the request span, the completion log line, and thetraceparent/x-trace-idresponse headers. -
Trace correlation across async boundaries. The HTTP boundary overrides
OperationContext.trace_id; the kernel persists it onto enqueued jobs (process.job_request.trace_id, migration 000008).error_jsoncolumn exists for job-failure post-mortems and is written when the job-claim loop lands. -
Prometheus pull over push.
metricsfacade +metrics-exporter-prometheus;/metricson server and worker. RED metrics per syscall, port-op histograms viatimed_port_op, build-info gauges.vinxi_jobs_*metrics land with the job loop.
Consequences
- Every new error code costs a catalog entry up front (enforced by test); in exchange, agents get uniform what/why/how-to-fix everywhere.
- The wire contract (
WireError, codes, exit codes, status mapping) is a public API; changes are breaking and must be versioned deliberately. - Kernel-core now depends on
vinxi-observability(tracing + metrics facades only) — consistent with the “kernel depends on foundations and ports, never on vendors” rule. - Latent pedantic clippy violations were fixed workspace-wide as part of this work (lint-group priority bug had masked them).