Skip to content

Troubleshooting and errors

The technical edges an integrator meets, stated as they are: what a request is refused for and with which status, what an ingest or a retry costs, and the smaller rules a reply follows. The short version, for anyone deciding whether Limma fits, is what it will not do.

No ranking judgement

mode="explore" orders facts by a retrieval score — token overlap with your question across four substrate layers that score on incomparable scales (coverage.retrieval_order names the order used). It is not a judgement of relevance and it carries no denominator: nothing in a ranked list licenses and there are no others. Its effect on retrieval quality is measured but unestablished as an improvement — the field descriptions on /v1/public/facts say so in numbers. Use explore to discover addresses; act on counts from mode="enumerate" (see a count).

No free-text slots

A numeric fact has three coordinates — entity, attribute, period — and a value. A relation has typed slots and is addressed with relation_query — {relation, where: [[slot, op, value]], active_in, entity_id} — beside coordinate (one address per request; naming both is a 422). What it refuses, by design: contains, like and regex are a 422, because a substring match has no closed set to count over, and there is no way to file prose as an addressed value. A slot value is a vocabulary term, expanded before the query runs (relations.expansion prints every resolution), or — on an axis with no vocabulary — an exact spelling, with the completeness licence withheld as narrowed_by: vocabulary_unbound:<slot>. The ops and value rules are on relations; a pack cannot declare a free-text slot at all. A condition no slot can hold is not searched as a substring: it is sent as a criterion and READ from each candidate's documents, with a quote per condition — criteria.

Prose becomes rows only for relations a pack declares

Prose reaches you two ways. On the facts lane, as clauses (kind: "semantic") and passage windows (kind: "passage"): text with a byte-exact span and, for a clause, a clause_type. They carry no value and no entity / attribute / period; states_values / stated_by join a clause to the numbers it prints, and nothing more.

On the relation lane, as rows: what a sentence states — "Anna was Vice President at Tesla from 2019" as a row with a subject, a role slot and a date range — is read from a document only when all three hold: the workspace's pack declares that relation; the document was ingested as being about ONE entity (a sentence never mints a subject — document kinds); and the deployment has a model. Such a row is extracted, with a span receipt. A document nothing could read declares no coverage, so its subject reads not_recorded, never none. How often a reader recovers a stated role correctly is not measured in these pages.

A condition NO relation holds is not a row at all. It is a criterion: read per search, from each candidate's documents, under a budget.

No brief compile without a model

limma_search compiles a plain-language brief into constraints with one model call. A deployment with no model (EPISTEM_DISABLE_LLM=1, or no provider key) answers a brief with 503 {"type": "compile_unavailable", "reason": "compile requires a model; pass constraints"} — there is no rule-based fallback, because a guessed compile would answer a different question. The constraints path runs no model and works everywhere (search).

Two further prose facts: semantic extraction runs only when an embedder is configured (a bare EPISTEM_DISABLE_EMBEDDER=1 deployment stores no clauses), and the clause label is not scored by retrieval — canonical_clause_type is delivered but does not move a clause up the ranking.

No single-digit receipts on labelled rows

The receipt rules that locate a value beside its own label require the printed token to carry at least two digits (attestation_method: short_token otherwise). A printed 0, 3 or 8 on a labelled row is therefore never receipted by those rules, however exact the stored span — defect #141, filed and open. A one-digit value inside a clause already proven unique in the document is receipted (the span is the corroboration); the floor applies to the label rules only.

No $0 ingest of a document

Extraction of a document runs a model: with EPISTEM_DISABLE_LLM=1 an ingest completes with zero facts. limma_ingest over MCP takes text only; a PDF goes through the document upload route before the agent session. Measured on 2026-09-10 (from demo/RUNBOOK.md, Kraft Heinz 2019 10-K, 152 pages):

step time cost notes
ingest (Opus 4.6, structurer on) 25 min $15.36 490 model calls, 799K in / 864K out tokens; status partial (274/277 regions)
agent question, substrate-only (Opus 5) 39 s $4.01 4 tool calls; cost is the 100-fact tool result — the prompt now caps limit
same question, PDF in context, no Limma 36 s $2.16 430K input tokens
limma_ask with synthesis on 34 s ~$0.30 est. 9 model calls; tier sourced, value-only
ingest PepsiCo 2021 10-K, 549 pages 28 min ≈ $30 est. 960 model calls; partial (737/740 regions), 1,049 claims; dollars estimated from Kraft's per-call average — no token accounting captured
agent question, compact + cached (Opus 5) 33–50 s $0.29–0.60 7–10 tool calls; six questions measured 2026-09-10, all correct
cross-filing question, two 10-Ks in the store 33 s $0.36 7 tool calls; the cost did not grow with the second filing

A retry costs a whole extraction

A re-send of content already read to the end (processing_status complete or partial) is a status read: it runs nothing. retry: true on limma_ingest, the form field retry=true on the upload doors (POST /v1/workspaces/{id}/documents, POST /api/workspaces/{id}/sources — the doors that carry a PDF or an image), and the operator's POST /api/workspaces/{id}/re-extract each read the document AGAIN: one new extraction, at the cost of the ingest rows above. So a retry is bounded:

  • It is a write. A read-only (viewer) key is refused 403 — as it is on ingest whatever it sends, a plain re-send included: a viewer reads a document's status with a GET. A key that may write re-sends as a status read.
  • Every retry counts against the ingest verb's rate limit — the verify tier, 60 calls a minute per key by default (EPISTEM_RATE_VERIFY_RPM) — on every door, and the re-extract counts once per call however many documents it re-reads.
  • While an ingest of the document is queued or running, a retry queues nothing (ingest_reason job_queued / job_running): one extraction per document at a time.
  • A retry that fails leaves the document as it was — its facts still delivered — and records the failure on retry_failed; the retry is not repeated for you.
  • A retry reads a PDF's pages exactly as its first read did: the document's text, and so every receipt into it, stays byte for byte as it was. The way its pages were turned into text is stored with the document's parse, and a document read before 2026-10-18 is re-read the old way, straight across the page. Only a first read uses the current reader, which reads two columns of text column by column and keeps a table's columns apart.

Nothing else caps retry spend: an agent that retries in a loop pays for an extraction per accepted call.

The verbs an agent calls — limma_facts, limma_region, limma_verify, limma_generations, limma_diff, limma_correct, limma_corrections, limma_apply_pack, and limma_search with constraints — run no model; limma_ask and limma_plan compile with one, and so does limma_search with a brief. limma_search with criteria READS documents with a model — one call per candidate document it has not read before, and a second for a would-be match — under the budget the request states, which refuses a plan above it whole (criteria). No latency or cost of the brief compile is measured in these pages yet. Estimates are marked as such in the table; nothing else is.

Other edges you will meet

  • limma_connect gates by deployment configuration, and both gates are off until an operator sets them: a network URL's host must be on EPISTEM_CONNECT_ALLOWED_HOSTS (none configured — the default — and the verb is disabled); a file-backed URL (sqlite:///) is refused unless the file's directory is listed in EPISTEM_CONNECT_ALLOWED_SQLITE_DIRS (none configured — the default — and every sqlite URL is refused, with the reason naming the variable). The workspace store itself is refused under any listing. A document-store export file named by its path on the API host is gated the same way by EPISTEM_CONNECT_ALLOWED_EXPORT_DIRS (the connect path); one UPLOADED (POST /v1/public/upload_export, then document_source.export_id) needs no listing, is held to EPISTEM_CONNECT_MAX_EXPORT_BYTES (512 MiB unless set; 413 over it), is deleted by the pull that loads it, and its rows' receipts cannot be re-read (row_unreadable) (upload it, then connect its id).
  • One export is pulled by one limma_connect call at a time. A second call for the same table or collection waits, and past the deployment's wait is refused 409 pull_in_flight having written nothing — call again when the first has answered. 503 store_write_failed wrote nothing either, and neither did 503 store_unavailable (any verb: the store had no database connection free). None is a fault in your request (those are 422): call again.
  • A text the store cannot hold is never stored: a slot value or an entity id longer than 2,000 bytes of UTF-8, and any string holding the character U+0000 or broken Unicode. A document export sets such a value aside and names it (quarantined[]); a relation_mapping pull of a table is refused naming the row and column; and a relation question (relation_query, a search constraint) that names a value holding U+0000 or broken Unicode is a 422 — no row can hold it.
  • A criterion is ONE per search, and a read returns inside the deployment's request wall: a niche over many candidates comes back partial and takes several calls, resumed by the criterion's address. It finds what a person's documents STATE; a condition written in words the reader does not recognise leaves that person undetermined — listed, with the reason — never none (criteria).
  • scope on /v1/public/facts is reserved and not honoured; narrow with coordinate.source_id instead.
  • No verb answers as of a document generation: every count is over the current generation of each document (item 1.8 pins generations). limma_search's as_of is a month the relation rows are tested at, not a generation. See supersession.
  • A numeric fact's per-axis provenance grades are stored, not delivered, and a numeric fact carries no confidence; relation rows carry provenance_kind and confidence. See provenance.
  • A relation entity has a display name only where a document load recorded one (name_path: read for every record that names its subject, and kept per person — with or without a registered document): limma_search names a person by it, and answers name: null for anyone else — a record loaded from a SQL table (a relation_mapping states no name), a load whose mapping has no name_path, a person whose documents or loads record two different names. A name is never derived from the id.
  • limma_search groups conditions by relation, so every condition on one relation is a condition on ONE row: "a Sales role in Japan" is expressible, "a Sales role, and separately some role in Japan" is not (search).
  • contested: false is a finding only about contested_scope (same_address, judged_pairs, both, or none — then contested_checked is false). See contested.
  • bound is decided on the attribute axis alone (clause_type for prose): an entity or period the store spells differently is a bound, empty address — none at this spelling, not not recorded. Read the spellings off the closed set at {"attribute": ...} before acting on exist: 0. See None versus Not recorded.