#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Family {
Compile,
Runtime,
TxRevert,
Backend,
Core,
}
pub const FAMILIES: [Family; 5] = [
Family::Compile,
Family::Runtime,
Family::TxRevert,
Family::Backend,
Family::Core,
];
impl Family {
pub fn of(code: u16) -> Option<Family> {
match code {
1..=999 => Some(Family::Compile),
1000..=1999 => Some(Family::Runtime),
2000..=2999 => Some(Family::TxRevert),
3000..=3999 => Some(Family::Backend),
4000..=4999 => Some(Family::Core),
_ => None,
}
}
pub fn label(self) -> &'static str {
match self {
Family::Compile => "compile",
Family::Runtime => "runtime",
Family::TxRevert => "tx-revert",
Family::Backend => "backend",
Family::Core => "core",
}
}
}
#[derive(Debug, Clone, Copy)]
pub struct ErrorCode {
pub code: u16,
pub family: Family,
pub meaning: &'static str,
pub hint: &'static str,
}
impl ErrorCode {
pub fn label(&self) -> String {
format!("LH{:04}", self.code)
}
}
pub fn fmt_label(code: u16) -> String {
format!("LH{code:04}")
}
pub const UNEXPECTED_BYTE: u16 = 1;
pub const UNTERMINATED_STRING: u16 = 2;
pub const UNKNOWN_ESCAPE: u16 = 3;
pub const BAD_CHAR_LITERAL: u16 = 4;
pub const BAD_NUMBER: u16 = 5;
pub const UNEXPECTED_TOKEN: u16 = 100;
pub const EXPECTED_ITEM: u16 = 101;
pub const EXPECTED_TYPE: u16 = 102;
pub const EXPECTED_EXPRESSION: u16 = 103;
pub const EXPECTED_PATTERN: u16 = 104;
pub const MISSING_SEMICOLON: u16 = 105;
pub const INVALID_ASSIGN_TARGET: u16 = 106;
pub const NESTING_TOO_DEEP: u16 = 107;
pub const UNKNOWN_TYPE: u16 = 200;
pub const UNDEFINED_VARIABLE: u16 = 201;
pub const UNKNOWN_FUNCTION: u16 = 202;
pub const ARITY_MISMATCH: u16 = 203;
pub const TYPE_MISMATCH: u16 = 204;
pub const NOT_MUTABLE: u16 = 205;
pub const BAD_FIELD_ACCESS: u16 = 206;
pub const BAD_INDEX: u16 = 207;
pub const BAD_CAST: u16 = 208;
pub const UNKNOWN_STRUCT: u16 = 209;
pub const UNSUPPORTED_FEATURE: u16 = 300;
pub const UNKNOWN_HOST_IMPORT: u16 = 301;
pub const NO_ENTRY: u16 = 302;
pub const OVERSIZE: u16 = 303;
pub const FRAME_TIMEOUT: u16 = 1001;
pub const WASM_TRAP: u16 = 1002;
pub const INSTANTIATE_FAILED: u16 = 1003;
pub const NO_ENTRY_RUNTIME: u16 = 1004;
pub const TX_NOT_DUE: u16 = 2001;
pub const TX_STALE_NEXT_RUN: u16 = 2002;
pub const TX_SPEND_EXCEEDS_BUDGET: u16 = 2003;
pub const TX_NOT_SCHEDULER: u16 = 2004;
pub const TX_NOT_JOB_OWNER: u16 = 2005;
pub const TX_UNKNOWN_JOB: u16 = 2006;
pub const TX_JOB_NOT_ACTIVE: u16 = 2007;
pub const TX_JOB_NOT_PAUSED: u16 = 2008;
pub const TX_UNREGISTERED_TARGET: u16 = 2009;
pub const TX_ZERO_INTERVAL: u16 = 2010;
pub const TX_ZERO_RUNS: u16 = 2011;
pub const TX_CODE_TAKEN: u16 = 2012;
pub const TX_BAD_TTL: u16 = 2013;
pub const TX_ESCROW_CAP_EXCEEDED: u16 = 2014;
pub const TX_UNKNOWN_INVITE: u16 = 2015;
pub const TX_NOT_OPEN: u16 = 2016;
pub const TX_EXPIRED: u16 = 2017;
pub const TX_NOT_YET_EXPIRED: u16 = 2018;
pub const TX_ZERO_BUDGET: u16 = 2019;
pub const TX_ZERO_AMOUNT: u16 = 2020;
pub const TX_NOT_CONFIGURED: u16 = 2021;
pub const TX_REASON_STRING: u16 = 2022;
pub const TX_PANIC: u16 = 2023;
pub const TX_INSUFFICIENT_CREDITS: u16 = 2024;
pub const BACKEND_RATE_LIMIT: u16 = 3001;
pub const BACKEND_AUTH: u16 = 3002;
pub const BACKEND_CREDITS: u16 = 3003;
pub const BACKEND_TIMEOUT: u16 = 3004;
pub const BACKEND_EMPTY: u16 = 3005;
pub const BACKEND_SERVER: u16 = 3006;
pub const BACKEND_NETWORK: u16 = 3007;
pub const BACKEND_STALE_AUTH: u16 = 3008;
pub const CORE_IO: u16 = 4001;
pub const CORE_JSON: u16 = 4002;
pub const CORE_HTTP: u16 = 4003;
pub const CORE_CLOSED: u16 = 4004;
pub const CORE_NOT_STARTED: u16 = 4005;
pub const CORE_ALREADY_STARTED: u16 = 4006;
pub const CORE_CONFIG: u16 = 4007;
pub const CORE_TOOL_NOT_FOUND: u16 = 4008;
pub const CORE_TOOL_FAILED: u16 = 4009;
pub const CORE_POLICY_DENIED: u16 = 4010;
pub const CORE_TIMEOUT: u16 = 4011;
pub const CORE_OTHER: u16 = 4012;
pub const REGISTRY: &[ErrorCode] = &[
ec(UNEXPECTED_BYTE, Family::Compile, "unexpected byte in source",
"remove the stray character; rustlite only accepts ASCII Rust-subset source"),
ec(UNTERMINATED_STRING, Family::Compile, "unterminated string literal",
"add the closing \" on the same line (strings can't span newlines)"),
ec(UNKNOWN_ESCAPE, Family::Compile, "unknown string/char escape",
"use a supported escape: \\n \\t \\\\ \\\" \\0"),
ec(BAD_CHAR_LITERAL, Family::Compile, "malformed char literal",
"a 'x' char is exactly one byte; use a \"string\" for text"),
ec(BAD_NUMBER, Family::Compile, "malformed numeric literal",
"check the digits/suffix; hex is 0xFF, floats need a fractional digit"),
ec(UNEXPECTED_TOKEN, Family::Compile, "unexpected token",
"the grammar expected a different token here — read the [start..end] span"),
ec(EXPECTED_ITEM, Family::Compile, "expected a top-level item",
"only fn/struct/enum/const are allowed at the top level"),
ec(EXPECTED_TYPE, Family::Compile, "expected a type",
"supply a known type (i32/i64/f32/f64/bool or a declared struct/enum)"),
ec(EXPECTED_EXPRESSION, Family::Compile, "expected an expression",
"an expression is required here; check for a dangling operator"),
ec(EXPECTED_PATTERN, Family::Compile, "expected a pattern",
"a match arm / let needs a pattern (binding, literal, path, or range)"),
ec(MISSING_SEMICOLON, Family::Compile, "missing ';' after a statement",
"terminate the statement with ';' (or close the block with '}')"),
ec(INVALID_ASSIGN_TARGET, Family::Compile, "invalid assignment target",
"assign to a variable, struct field, or arr[i]; non-places (5 = 9) and indexed writes through struct fields (s.arr[i] = v) are unsupported"),
ec(NESTING_TOO_DEEP, Family::Compile, "nesting too deep",
"flatten deeply-nested expressions/blocks; the parser caps recursion depth"),
ec(UNKNOWN_TYPE, Family::Compile, "unknown type name",
"declare the struct/enum, or use a primitive (i32/i64/f32/f64/bool)"),
ec(UNDEFINED_VARIABLE, Family::Compile, "undefined variable",
"declare it with let before use, or fix the spelling"),
ec(UNKNOWN_FUNCTION, Family::Compile, "unknown function",
"define the fn, or use a valid host fn (host::display::*, host::net::*, …)"),
ec(ARITY_MISMATCH, Family::Compile, "wrong number of arguments",
"match the function's parameter count exactly"),
ec(TYPE_MISMATCH, Family::Compile, "type mismatch",
"convert with an `as` cast or fix the operand types so they agree"),
ec(NOT_MUTABLE, Family::Compile, "assignment to a non-mut binding",
"declare it `let mut` to reassign"),
ec(BAD_FIELD_ACCESS, Family::Compile, "field access on a non-struct / missing field",
"access a real field of a struct value"),
ec(BAD_INDEX, Family::Compile, "invalid index expression",
"index an array with an i32; only arrays of i32 are indexable"),
ec(BAD_CAST, Family::Compile, "invalid `as` cast",
"`as` only converts between numbers (i32/i64/f32/f64)"),
ec(UNKNOWN_STRUCT, Family::Compile, "unknown struct in a literal",
"declare the struct before constructing it"),
ec(UNSUPPORTED_FEATURE, Family::Compile, "unsupported language feature",
"rustlite lacks traits/generics/references/heap types (Vec/String/Box)/globals"),
ec(UNKNOWN_HOST_IMPORT, Family::Compile, "unknown host import",
"use a registered host fn — check the host::display / host::net / host::audio names + arity"),
ec(NO_ENTRY, Family::Compile, "no frame/render entry export",
"add `fn frame(t: i32)` (animated) or `fn render()` (one-shot) — the loader calls one of these"),
ec(OVERSIZE, Family::Compile, "cartridge exceeds the publish size cap",
"shrink the cartridge below the on-chain publish cap before publishing"),
ec(FRAME_TIMEOUT, Family::Runtime, "cartridge hung (watchdog terminated it)",
"a frame() ran too long / looped unbounded — bound your loops; reload to retry"),
ec(WASM_TRAP, Family::Runtime, "cartridge trapped during a frame",
"a wasm trap (unreachable / out-of-bounds) — check array indices + arithmetic"),
ec(INSTANTIATE_FAILED, Family::Runtime, "cartridge failed to instantiate",
"the wasm module is invalid/incompatible — recompile with compile_rustlite"),
ec(NO_ENTRY_RUNTIME, Family::Runtime, "cartridge exports neither frame nor render",
"export `fn frame(t: i32)` or `fn render()` so the engine has an entry to call"),
ec(TX_NOT_DUE, Family::TxRevert, "NotDue — job not due yet",
"the scheduler only fires on the interval; check `localharness jobs`"),
ec(TX_STALE_NEXT_RUN, Family::TxRevert, "StaleNextRun — run already fired",
"the on-chain clock already advanced; nothing to do"),
ec(TX_SPEND_EXCEEDS_BUDGET, Family::TxRevert, "SpendExceedsBudget — over the job budget",
"top up the job or it will be marked exhausted"),
ec(TX_NOT_SCHEDULER, Family::TxRevert, "NotScheduler — scheduler-only call",
"only the scheduler worker can record a run; not a user action"),
ec(TX_NOT_JOB_OWNER, Family::TxRevert, "NotJobOwner — you don't own this job",
"use the right `--as` identity; check `localharness jobs`"),
ec(TX_UNKNOWN_JOB, Family::TxRevert, "UnknownJob — no job with that id",
"list yours with `localharness jobs` (the id is the #N)"),
ec(TX_JOB_NOT_ACTIVE, Family::TxRevert, "JobNotActive — already cancelled/exhausted",
"nothing to cancel; see `localharness jobs`"),
ec(TX_JOB_NOT_PAUSED, Family::TxRevert, "JobNotPaused — can't resume a running job",
"only a paused job can be resumed"),
ec(TX_UNREGISTERED_TARGET, Family::TxRevert, "UnregisteredTarget — target isn't an agent",
"confirm it exists first (`localharness whoami <target>`)"),
ec(TX_ZERO_INTERVAL, Family::TxRevert, "ZeroInterval — interval below the 60s minimum",
"use `--every 60s` or more"),
ec(TX_ZERO_RUNS, Family::TxRevert, "ZeroRuns — max-runs must be >= 1",
"drop `--runs 0`"),
ec(TX_CODE_TAKEN, Family::TxRevert, "CodeTaken — invite code already exists",
"generate a fresh code (`invite create` makes a new one each time)"),
ec(TX_BAD_TTL, Family::TxRevert, "BadTtl — TTL outside 1h..90d",
"use e.g. `--ttl 7d`"),
ec(TX_ESCROW_CAP_EXCEEDED, Family::TxRevert, "EscrowCapExceeded — past the per-funder cap",
"reclaim an expired invite or use a smaller amount"),
ec(TX_UNKNOWN_INVITE, Family::TxRevert, "UnknownInvite — no invite for that code",
"double-check you copied the full code (incl. the inv- prefix)"),
ec(TX_NOT_OPEN, Family::TxRevert, "NotOpen — invite already accepted/reclaimed",
"it's spent; ask for a fresh invite"),
ec(TX_EXPIRED, Family::TxRevert, "Expired — invite past its TTL",
"it can only be reclaimed by its funder now (`invite reclaim <code>`)"),
ec(TX_NOT_YET_EXPIRED, Family::TxRevert, "NotYetExpired — reclaim only after the TTL",
"until then it can still be accepted"),
ec(TX_ZERO_BUDGET, Family::TxRevert, "ZeroBudget — budget must be > 0",
"supply a positive budget"),
ec(TX_ZERO_AMOUNT, Family::TxRevert, "ZeroAmount — amount must be > 0",
"supply a positive amount"),
ec(TX_NOT_CONFIGURED, Family::TxRevert, "NotConfigured — credits token unset",
"a platform-side misconfiguration; report it via `localharness feedback`"),
ec(TX_REASON_STRING, Family::TxRevert, "Error(string) — reverted with a reason",
"the decoded reason is shown inline; an escrow/balance reason means you need more $LH"),
ec(TX_PANIC, Family::TxRevert, "Panic — internal assertion failed",
"a platform bug, not your input; please `localharness feedback` it"),
ec(TX_INSUFFICIENT_CREDITS, Family::TxRevert, "InsufficientCredits — chat-meter credits locked or short",
"fiat-minted $LH is locked for spending on inference, not withdraw/transfer; check_balances shows the withdrawable amount + unlock time"),
ec(BACKEND_RATE_LIMIT, Family::Backend, "model provider rate-limited / over quota",
"the platform's model provider is throttled or over its spend cap — wait a moment and retry; not a problem with your account"),
ec(BACKEND_AUTH, Family::Backend, "model API key rejected",
"check the Gemini/model API key (BYOK); on the platform path this is a server-side key issue to report"),
ec(BACKEND_CREDITS, Family::Backend, "out of platform credits ($LH)",
"redeem a code or top up — this signing address has no active session / no $LH"),
ec(BACKEND_TIMEOUT, Family::Backend, "the model request timed out",
"the backend didn't respond in time — retry; if it persists the provider may be degraded"),
ec(BACKEND_EMPTY, Family::Backend, "empty or truncated model response",
"the model returned nothing usable — retry; shortening the input can help"),
ec(BACKEND_SERVER, Family::Backend, "model backend error (5xx)",
"the provider returned a server error — transient; retry shortly"),
ec(BACKEND_NETWORK, Family::Backend, "network / transport failure",
"couldn't reach the backend or proxy — check connectivity and retry"),
ec(BACKEND_STALE_AUTH, Family::Backend, "request auth went stale (device clock skew)",
"your device clock is off by more than ~5 minutes — sync it and retry"),
ec(CORE_IO, Family::Core, "I/O error",
"an OS-level read/write failed — check paths and permissions"),
ec(CORE_JSON, Family::Core, "JSON (de)serialization error",
"malformed or unexpected JSON — verify the payload shape"),
ec(CORE_HTTP, Family::Core, "HTTP transport error",
"the request failed at the transport layer — retry; check the endpoint"),
ec(CORE_CLOSED, Family::Core, "connection closed unexpectedly",
"the stream/connection dropped — restart the operation"),
ec(CORE_NOT_STARTED, Family::Core, "agent not started",
"call start() before using the agent"),
ec(CORE_ALREADY_STARTED, Family::Core, "agent already started",
"start() was called more than once — reuse the running agent"),
ec(CORE_CONFIG, Family::Core, "invalid configuration",
"fix the configuration value named in the message"),
ec(CORE_TOOL_NOT_FOUND, Family::Core, "tool not found",
"no tool is registered under that name — register it or fix the name"),
ec(CORE_TOOL_FAILED, Family::Core, "tool execution failed",
"the tool returned an error — see the inline message for the cause"),
ec(CORE_POLICY_DENIED, Family::Core, "policy denied the operation",
"a policy blocked this action — adjust the request or the policy"),
ec(CORE_TIMEOUT, Family::Core, "operation timed out",
"the operation exceeded its deadline — raise the timeout or retry"),
ec(CORE_OTHER, Family::Core, "unspecified error",
"a catch-all error — see the inline message for details"),
];
const fn ec(code: u16, family: Family, meaning: &'static str, hint: &'static str) -> ErrorCode {
ErrorCode { code, family, meaning, hint }
}
pub fn lookup(code: u16) -> Option<&'static ErrorCode> {
REGISTRY.iter().find(|e| e.code == code)
}
pub fn runtime_phase(code: u16) -> &'static str {
match code {
INSTANTIATE_FAILED | NO_ENTRY_RUNTIME => "instantiate",
_ => "run",
}
}
pub fn describe(code: u16) -> String {
match lookup(code) {
Some(e) => format!("{}: {}", e.label(), e.meaning),
None => fmt_label(code),
}
}
pub fn compact_index() -> String {
let mut out = String::new();
for fam in FAMILIES {
out.push_str(fam.label());
out.push_str(":\n");
for e in REGISTRY.iter().filter(|e| e.family == fam) {
out.push_str(&format!(" {} {}\n", e.label(), e.meaning));
}
}
out.trim_end().to_string()
}
pub fn classify(s: &str) -> Option<u16> {
let l = s.to_lowercase();
if l.contains("stale or future timestamp") || l.contains("clock") {
return Some(BACKEND_STALE_AUTH);
}
if l.contains("429")
|| l.contains("rate limit")
|| l.contains("rate-limit")
|| l.contains("resource_exhausted")
|| l.contains("spending cap")
|| l.contains("spend cap")
|| l.contains("too many requests")
|| l.contains("quota")
|| l.contains("overloaded")
{
return Some(BACKEND_RATE_LIMIT);
}
if l.contains("401")
|| l.contains("403")
|| l.contains("api key")
|| l.contains("api_key")
|| l.contains("permission_denied")
|| l.contains("unauthenticated")
|| l.contains("unauthorized")
{
return Some(BACKEND_AUTH);
}
if l.contains("402")
|| l.contains("payment required")
|| l.contains("no $lh")
|| l.contains("no credit")
|| l.contains("insufficient")
|| l.contains("no active session")
{
return Some(BACKEND_CREDITS);
}
if l.contains("timed out") || l.contains("timeout") || l.contains("deadline") {
return Some(BACKEND_TIMEOUT);
}
if l.contains("empty response") || l.contains("truncated") || l.contains("no response") {
return Some(BACKEND_EMPTY);
}
if l.contains("500")
|| l.contains("502")
|| l.contains("503")
|| l.contains("504")
|| l.contains("internal server")
{
return Some(BACKEND_SERVER);
}
if l.contains("network")
|| l.contains("connection")
|| l.contains("failed to fetch")
|| l.contains("dns")
{
return Some(BACKEND_NETWORK);
}
None
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn codes_are_unique_and_in_family_range() {
let mut seen = std::collections::HashSet::new();
for e in REGISTRY {
assert!(seen.insert(e.code), "duplicate code LH{:04}", e.code);
assert_eq!(
Family::of(e.code),
Some(e.family),
"LH{:04} family {:?} doesn't match its numeric range",
e.code,
e.family
);
assert!(!e.meaning.is_empty() && !e.hint.is_empty(), "LH{:04} blank text", e.code);
}
}
#[test]
fn label_is_zero_padded() {
assert_eq!(fmt_label(1), "LH0001");
assert_eq!(fmt_label(204), "LH0204");
assert_eq!(fmt_label(2001), "LH2001");
assert_eq!(lookup(TYPE_MISMATCH).unwrap().label(), "LH0204");
}
#[test]
fn runtime_phase_maps_every_lh1xxx_code() {
assert_eq!(runtime_phase(INSTANTIATE_FAILED), "instantiate");
assert_eq!(runtime_phase(NO_ENTRY_RUNTIME), "instantiate");
assert_eq!(runtime_phase(WASM_TRAP), "run");
assert_eq!(runtime_phase(FRAME_TIMEOUT), "run");
for e in REGISTRY.iter().filter(|e| e.family == Family::Runtime) {
assert!(matches!(runtime_phase(e.code), "instantiate" | "run"));
}
}
#[test]
fn describe_falls_back_for_unknown() {
assert_eq!(describe(TYPE_MISMATCH), "LH0204: type mismatch");
assert_eq!(describe(9999), "LH9999");
}
#[test]
fn index_doc_lists_every_code() {
let doc = std::fs::read_to_string(concat!(
env!("CARGO_MANIFEST_DIR"),
"/docs/error-codes.md"
))
.expect("docs/error-codes.md must exist");
for e in REGISTRY {
let label = e.label();
assert!(
doc.contains(&label),
"docs/error-codes.md is missing {label} ({})",
e.meaning
);
}
}
#[test]
fn compact_index_covers_all_families() {
let idx = compact_index();
for fam in FAMILIES {
assert!(idx.contains(&format!("{}:", fam.label())), "missing family {}", fam.label());
}
assert!(idx.contains("LH0204"));
assert!(idx.contains("LH1001"));
assert!(idx.contains("LH2003"));
assert!(idx.contains("LH3001"));
assert!(idx.contains("LH4001"));
}
#[test]
fn classify_maps_common_backend_errors() {
assert_eq!(classify("gemini HTTP 429 Too Many Requests"), Some(BACKEND_RATE_LIMIT));
assert_eq!(classify("status: RESOURCE_EXHAUSTED, spending cap"), Some(BACKEND_RATE_LIMIT));
assert_eq!(classify("exceeded your quota"), Some(BACKEND_RATE_LIMIT));
assert_eq!(classify("the model is overloaded"), Some(BACKEND_RATE_LIMIT));
assert_eq!(classify("HTTP 401 Unauthorized: bad API key"), Some(BACKEND_AUTH));
assert_eq!(classify("PERMISSION_DENIED"), Some(BACKEND_AUTH));
assert_eq!(classify("402 Payment Required: no $LH"), Some(BACKEND_CREDITS));
assert_eq!(classify("the request timed out"), Some(BACKEND_TIMEOUT));
assert_eq!(classify("empty response from model"), Some(BACKEND_EMPTY));
assert_eq!(classify("HTTP 503 internal server error"), Some(BACKEND_SERVER));
assert_eq!(classify("failed to fetch: network down"), Some(BACKEND_NETWORK));
assert_eq!(classify("stale or future timestamp"), Some(BACKEND_STALE_AUTH));
assert_eq!(classify("a perfectly ordinary message"), None);
}
#[test]
fn classify_prefers_rate_limit_over_credits() {
assert_eq!(
classify("429 RESOURCE_EXHAUSTED: project exceeded its monthly spending cap"),
Some(BACKEND_RATE_LIMIT)
);
}
}