Liminalis
Engineering

The parts a reader who builds this for a living would want to check.

Retrieval on prose is easy to make look good in a demo and hard to make good in general. This page is the specifics: what the stack does, what it measured, what it cost — and the experiments that were built, measured, and then deleted because the evidence didn't support them.

§ 01 retrieval
Corpus shapeprose — pronouns, implicit referents, few proper nouns
§ 01  The stack

Hybrid recall, cross-encoder reranking, contextual embeddings.

Content-defined chunking
Cut points chosen by a rolling hash of sentence content — the same idea backup and deduplication systems use — constrained to sentence ends and paragraph breaks, between roughly 400 and 1,800 characters. Passages are diffed by hash, so editing one paragraph of a long chapter costs about one passage's worth of work. Every passage also emits overlapping sentence windows, and consecutive passages are chained so a graph walk can follow narrative continuity.
Deterministic pins
Always-include documents, forced documents, @-mentions, and any key named in the query resolved through the reverse index. Two access barriers — the GM-only wall and the lens barrier — strip pins before the bundle is built.
Active-memory recall loop
Up to four cycles per turn: multi-signal vector search, seed the entities named in the query, then seed the entities named in what was recalled, so the next cycle's graph walk pulls their documents in. A per-turn decay keeps a long conversation from silting up. Entity hits are never rendered directly — they're kept separate so entity hubs can't crowd out renderable passages. The graph this walks over is §02.
Lexical fusion
A per-project BM25 index over passages plus a synthetic title entry per document, fused with the dense ranking by reciprocal rank. The fused pool is what feeds the reranker — so the cross-encoder gets to see lexical-only matches too. Tokenization splits camel case and underscores, so a filename like 14.9__What_The_Map_Omits matches natural phrasing.
Reranking
A cross-encoder scoring passages, not whole documents, taking forty candidates down to eight — measured as the optimum; twelve cost about 6,500 extra tokens for zero recall gain. Pins are never reordered or dropped, and any failure leaves the original ordering. Fail-soft by contract.
Contextual embeddings
Each chunk embedded aware of its surrounding document rather than in isolation — which matters enormously for prose full of pronouns and implicit referents. The proof case: the passage “She finally told him the truth”, containing no proper nouns whatsoever, scores essentially level with the chunk that names the character, against the query “Who did Wren confess to?”
Rendering
Documents under the whole-document threshold render entire. Larger ones render the matched passages expanded outward and merged, snapped to document edges. Never a head-slice.
The invariant that came out of a real bug
Never head-slice a retrieved document — show what matched.

A chronicle feature divided its character budget across member documents, giving each about two thousand characters from the top. The fact it needed sat at character 4,194. A head slice is the worst available failure shape precisely because it is invisible: the caller believes it has read the document. Silent truncation is how you get confidently wrong answers.

§ 02 resonance
ModelACT-R base-level activation over a Hebbian association graph
Not benchmarked— and §04 explains why it structurally can't be
§ 02  The cognitive layer

Underneath the retrieval stack is a memory model, not an index.

Everything in §01 is machinery for answering “what best matches this query?” — and it is stateless. Ask the same question of the same corpus a thousand times and you get the same ranking a thousand times. That is correct behaviour for a search engine, and it is the wrong behaviour for something that is supposed to know your material.

So retrieval sits on top of a second thing: a cognitive memory model, ported from an earlier conversational-agent system and reused here almost verbatim. It is the layer that gives recall intuition — the ability to surface something that shares no keyword and no obvious embedding neighbourhood with your question, because in this project, over time, those two things have come to be connected.

One substrate for everything

There is no separate passage store, entity store, fact store and summary store. A document passage, an extracted fact, an entity, a generated summary and a conversation turn are all the same kind of object — a chunk — in one graph, and therefore all subject to the same machinery: activation, decay, association, spreading, search.

Activation (ACT-R)
Every chunk carries an activation level derived from the classic ACT-R base-level equation — the log of a recency-weighted sum of its past activations. Frequency and recency, in one number. Material you have engaged with often and lately is structurally easier to reach, rather than being boosted by a heuristic bolted on after the ranking.
Typed, directional associations
Edges are weighted independently in each direction and carry a relation type, so the graph knows the difference between “A responds to B” and “B was responded to by A”. Each edge has its own activation history.
Hebbian strengthening
Weights strengthen when both endpoints are active together, and every spread step strengthens the edge that carried it. Use reinforces connection. The graph is not a static extraction artifact — it is reshaped by how the project is actually worked in.
Decay and pruning
Association weights decay when not reinforced, and edges below a floor are removed. The graph forgets on purpose. Without that, years of extraction produce a dense mesh where everything is weakly connected to everything and no signal survives. Chunk types decay at different rates — structural and identity material more slowly than episodic.
Resting activation gates reach
A contribution arriving at a chunk is multiplied by how warm that chunk already is. The same query, over two projects with identical documents but different histories, returns different material — because the graph has learned what matters here.
The ontology is in the graph
Semantic type nodes are themselves chunks, connected to their instances. Activation therefore flows instance → category → sibling instances. Thinking about one character faintly warms the others; thinking about a concept warms its instances.

How a resonance pass runs

The distinctive part isn't spreading activation on its own — it's that the query is split into several independent passes, and agreement between them is the signal.

Resonance pass — buckets, spread, convergence depth 2 · hop decay 0.25
BUCKETS — ONE PER QUERY, ONE PER NAMED ENTITY INDEPENDENT SPREAD, DEPTH 2 CONVERGENCE query entity · Wren entity · the compass reached by 1 bucket ranked within its own quota REACHED BY 2+ BUCKETS promoted to the shared bucket final ranking quota-allocated AGREEMENT BETWEEN INDEPENDENT PASSES IS THE SIGNAL — NOT ACCUMULATED SCORE FROM ONE STRONG HUB DIMINISHING RETURNS ON REPEATED CONTRIBUTIONS · TYPE WEIGHTS FAVOUR PASSAGES AND FACTS OVER BARE ENTITY NODES
The query gets a bucket. Every entity named in it gets its own bucket. Each runs an independent spreading pass over the project's association graph, and the result cap is allocated across buckets by quota with guaranteed slots — so one dominant thread cannot starve the others.

Convergence beats accumulation

Material reached from two or more independent directions is promoted into a shared bucket and ranked above material that merely scored highly in one. A hub connected to everything doesn't win by racking up contributions — diminishing returns are applied to repeated arrivals at the same target.

Hubs aren't answers

Type weights rank a passage or an extracted fact above a bare entity node at equal resonance. Entity nodes are how you travel through the graph; they are rarely what you wanted to read.

The loop feeds itself

Entities named in the recalled passages are seeded back in, so the next cycle's walk pulls their documents. Recall discovers what to look for next rather than being told up front.

The engineering problem this created

Textbook ACT-R computes base-level activation by summing a power-law term over every stored activation event for a chunk. That is fine for a conversational agent with thousands of memories. It is untenable for a corpus of tens of thousands of documents, because it demands an unbounded per-chunk event log that grows forever and has to be replayed on every recalculation.

So Liminalis runs a modified activation model: a single decayed-strength accumulator per chunk — a leaky integrator — that reproduces the same ACT-R shape (frequency, recency, and a configurable decay rate) in O(1) state with no event log at all. The conversational agent keeps the exact textbook model; the document platform swaps in the accumulator through dependency injection. Same cognitive semantics, corpus-scale cost.

Isolation

Every project has its own graph

Resonance spreads only over the association graph of the project it was asked about, seeded only from that project's own vectors, and ingestion never creates edges that cross projects. Recall isolation is physical, not a filter applied to shared results.

Honest status

This is the part with no number attached

Say so plainly: the 44-query benchmark in §04 cannot price this layer. That bench scores a fixed corpus with an effectively cold graph, and the entire value of resonance is history-dependent — it is supposed to get better in a specific project as that project is worked in. A single-shot, stateless benchmark is structurally blind to it.

Two orchestrations exist over the same substrate: a per-chat active-memory loop (activate → promote → decay → iterate, the current default) and the full multi-bucket resonance pass, kept behind a configuration flag for A/B comparison. Designing an evaluation that can actually distinguish them is open work, and until it exists no performance claim is made for either.

§ 03 context & cache
Claima cache-augmented prefix — not corpus-in-context
§ 03  Context-window management

The window is a budget, and something has to be in charge of spending it.

Retrieval quality is only half the problem. A chat can walk itself out of a model's window no matter how tight recall is, and the naive fixes — truncate the oldest, truncate the biggest — are exactly the ones that produce confident nonsense. Three mechanisms handle it.

Mechanism 01

Tiered context

Background documents render as a summary plus the passages that matched. Four guards: always keep matched passages; a per-type exemption (a character sheet is mostly facts and survives summarizing — prose is made of its phrasing and doesn't); a compression-ratio guard that refuses when the summary would come out larger than the original; and never the pins or the strongest hits.

18,037 → 10,975 tokens (−39%) · recall unchanged

Mechanism 02

Context budget

Over budget, degrade in order of least damage: drop the weakest recalled documents down to a floor, then clip the largest down to a floor. Pins are never dropped outright, and every clip is marked in the text. Asking for an unreasonably small budget yields more than you asked for — that's the floors binding, which is the guard working rather than failing.

9,798 → 6,111 tokens · recall and MRR unchanged

Mechanism 03

Rolling memory

A per-chat context serialized to disk — document scores, entity scores, section provenance and a hierarchical summary. Recent turns stay verbatim as a real message array; older turns compress into a “story so far” block. A short pronoun-led follow-up can skip retrieval entirely and reuse what's already loaded.

The prompt is deliberately shaped for caching

Segments are ordered so that the stable half stays byte-identical across turns and the volatile half churns freely behind it. The stable half carries an explicit cache breakpoint.

Staticsystem prompt · canon precedence · lens persona
Stable pinsproject overview · structure · forced docs
Volatile recallthis turn's retrieved passages
Historyrecent verbatim · older rolled up
Querythe question
cached prefix rebuilt each turn

Verified live, not assumed

call 1:  input=12  cacheCreate=6384  cacheRead=0
call 2:  input=12  cacheCreate=0     cacheRead=6384   ✓

Cached input bills at roughly a tenth. A conversation whose pinned overview and spine don't change gets cheaper and faster from its second turn onward. Providers that cache long prefixes automatically get the same shape for free; a provider without a breakpoint mechanism still receives the identical content, so moving material into the cached slot can never silently drop it.

The discipline extends to the escape hatches: on-demand expansions land in the volatile half on purpose, because putting them in the cached half would invalidate the prefix for every later turn in the chat.

Is this CAG?

Partly, and the honest version is more interesting than the marketing one.

What is true: precomputed knowledge artifacts — a project overview and a project structure document, both generated in the background and never re-indexing themselves — are permanently resident in the window, inside an explicitly cached prompt segment, verified to bill at roughly a tenth from the second turn on. That is a real cache-augmented layer sitting in front of retrieval.

What is not true: Liminalis does not implement its own KV-cache reuse — it declares the breakpoint and the provider does the rest. And it emphatically does not load the corpus into context. The entire tiering-and-budget effort exists because a real corpus doesn't fit.

So: a cache-augmented prefix over a measured RAG stack. Anyone selling you corpus-in-context at ten thousand documents is selling you something else.

§ 04 measured
Bench44 hand-authored queries · a real 135-document project
Rulepinned scaffold documents excluded from rank positions
§ 04  The bench

The whole ladder, including the rungs that did nothing.

Forty-four hand-authored queries against an isolated copy of a real project. Documents that are pinned on every turn regardless of the question are excluded from rank positions — leaving them in would burn the top four slots and make recall@5 a measure of padding rather than of retrieval.

Configuration recall@5 recall@10 hit@10 MRR tokens
Baseline (pre-tuning) 0.269 0.273 0.318 0.283 19,351
+ vector tuning 0.439 0.478 0.440 25,490
+ BM25 hybrid 0.550 0.827 0.576 25,379
+ reranker 0.871 0.892 0.977 0.788 17,612
+ tiered context 0.871 0.892 0.977 0.788 9,379
+ contextual embeddings 0.876 0.903 1.000 0.865 9,798
  ⤷ with a 6k context budget 0.876 0.903 1.000 0.865 6,111

End to end: recall@5 3.3×, MRR 3.1×, hit@10 from 0.318 to 1.000 — every query in the bench now surfaces a correct document inside the top ten — at 49% fewer tokens than the original baseline.

The finding that reframed everything

The dominant bottleneck was a config default, not an algorithm

A vector-search result cap of five — tuned years earlier for short conversational recall — was quietly starving a document corpus. Widening it, and loosening the similarity floor, moved recall@5 from 0.269 to 0.439 on its own. The knee is around 30–60 results; a hundred adds nothing.

The same lesson appeared a second time, elsewhere in the system, in a completely different guise.

Components don't decompose

The reranker was worth +0.011 on its own

Added to the untuned pipeline, the cross-encoder moved recall@5 by about one percentage point — the kind of result that gets a component cut. Added to a widened candidate pool, the same component took the same metric from 0.269 to 0.871.

Neither number means anything alone. Reporting either one without the other would have been true and useless.

A second axis: detail recall

Rank metrics don't tell you whether the answer was right. A separate bench of 41 specific factual questions — every fact programmatically verified to appear in its stated source document before being accepted as ground truth — measures whether the fact survives into the answer. Run twice for 82 attempts.

Configuration detail recall tokens
Full fidelity 0.720  (59/82) 19,095
+ tiered context 0.707  (58/82) 11,074

Tiering costs one attempt in eighty-two — inside the noise — for 42% fewer tokens.

§ 04b what didn't work
Why publish thisa bench you only quote when it agrees with you isn't a bench
§ 04b  Negative results

Six things that were built, measured, and then not shipped.

Killed

On-demand expansion fired zero times

A mechanism letting the model request more of a document mid-answer. Across 41 detail-recall queries, under two different prompts — one hedged, one forceful — it fired zero times, because a model asked to answer will nearly always answer. It cost about 210 tokens per turn to sit there doing nothing. Removed.

Off by default

Diversity reranking was a measured no-op

Maximal-marginal-relevance plus adaptive result counts: identical recall, about 147 fewer tokens. And actively risky on narrative material — an arc of similar scenes is exactly the near-duplicate cluster the algorithm suppresses, and a reader asking about the arc wants all of it.

Retracted

Every per-category number was inside its own noise band

Run at higher repetition, the per-query-kind figures wouldn't hold still — including the one the entire initiative had been launched to fix, which turned out to be 0.58 plus or minus 0.10. Eight of forty-five queries flip between repeats within a single run. Those numbers were withdrawn rather than quoted.

The grader was wrong

The headline weakness was an artifact of scoring

“Multi-document synthesis is the weak spot” was at best half true. Those ground-truth items averaged twice as many checkable clauses as the others, the grader was binary, and the grader never saw the question. Re-scored with an adequacy judge, the supposedly weakest category came out strongest — provisionally, and labelled as such.

No winner

No synthesis artifact beat doing nothing

Three variants, 180 attempts each: 0.717 for the control, 0.733 and 0.711 for the two candidates. All three within four attempts of each other; a sign test over the discordant queries put it at chance. Also worth noting: the run using a stronger model came out worst — under a tight output cap, a better model just writes more concisely, and the cap was the binding constraint.

Ground truth

Two “failures” were the bench being wrong

One item asked what a character calls another; the expected answer was a third character's nickname for her. The system answered correctly and was marked failed in every run across three configurations. Separately, a suspected hallucination turned out to be a real detail — and a proposed “record negative facts” feature would have taught the system to deny a true fact.

And the limits of the bench itself

The rank bench is structurally blind to compression — tiered context and the context budget each roughly halved the tokens with zero movement in it, which means only the detail bench can price them at all. Two retrieval categories remain measurably softer than the rest. None of that is hidden, because a benchmark you only quote when it agrees with you is not a benchmark.

§ 05 pipeline graph
Shapehardcoded workers → typed nodes walked by one executor
§ 05  Knowledge processing as data

The analysis passes stopped being code and became a graph.

Three hardcoded background workers — entity extraction, summarization, motif tagging — became three seeded pipeline definitions executed by a single generic worker walking a graph of typed nodes and edges. The point isn't elegance for its own sake. It's that a user-defined pass gets the identical protocol with an arbitrary key, instead of needing three more hardcoded fields in the index.

01

A real executor

Topological walk, cycle rejection at save time, per-node execution tracing, and hard runaway caps on node executions, model calls, and total spend. Definitions are validated when saved, not when they explode.

02

Per-pass state, generically keyed

Processing state moved from three named fields to a dictionary keyed by pass id — so adding a pass is data, not a schema change.

03

One owner for the hard parts

The pending-document query, burst concurrency, the credit gate, batched index writes and completion marking are all now owned once, by the executor, rather than re-implemented in each poll loop.

How it was migrated — the part worth reading

Replacing the workers that build the knowledge layer is a change that could silently corrupt everything downstream. So before the switch, a shadow-run guard ran the new pipeline definitions against the three legacy workers on identical documents, with real model calls, and diffed the results.

It caught a genuine blocker: the new completion marker didn't write the legacy fields, so a pipeline-processed document would have vanished from the motif index and from retrieval context — silently, with no error.

The fix deliberately did not dual-write. Dual-writing would have re-coupled the generic path to the three legacy fields, which is precisely the wrong direction for a system whose entire purpose is arbitrary user-defined passes. The readers were migrated instead. And before the legacy fields were deleted, the real production index — 26MB of it — was audited entry by entry, the backfill persisted to disk, and only then were they removed.

Honest status The engine, executor, definitions API and run-as-job path are built and live. The visual node-graph builder is not started — it needs a decision on a graph library that hasn't been made. Trigger kinds beyond document-triggered aren't built either.

§ 06 cost & operations
Principlea soft stop, never a failure
§ 06  Running it without surprises

Cost is an architectural concern, not a billing page.

Model routing

Seven roles through one router

Reply, scaffolding, critique, summarize, extract, narration support, plus base chat and analysis — each independently assignable to a model, surfaced in the UI as a role-to-model matrix.

Two fixes that came out of building it, both of which were costing real money: retrieval scaffolding is mechanical JSON work and now falls back to the cheap analysis model rather than riding the expensive chat one; and a per-session model override now applies only to the reply, where before it silently dragged every background call onto the expensive model too.

Throttling

Bounded in one place

A budget gate inside the router throttles background roles so a fan-out can't starve the foreground reply. It is deliberately the only gate — adding a second one outside the router self-deadlocks, which is written down where someone would otherwise try it.

Running out

A soft stop that destroys nothing

Exhausting credits refuses new work only. In-flight work finishes; the document is picked up on the next tick after a top-up. Plus a project-level master pause for the autonomous workers that deliberately doesn't gate chat or explicitly submitted jobs — the line is autonomous versus requested — and is lossless by construction, because a paused document is simply never marked and stays pending.

Migrations

Blue/green whole-corpus re-index

Changing embedding models means re-embedding everything, all or nothing, because embeddings cannot be mixed inside one collection. So it publishes only on a clean finish — the first real run failed two documents and correctly refused to publish, leaving the project searchable on its old vectors. It checkpoints per document, so running out of credits resumes rather than restarts; it rewrites a status file continuously, so a stall is legible; and it is embeddings-only, so a re-index can't accidentally re-fire the expensive workers. Rollback was verified by turning the flag off and confirming the bench returned to its exact prior numbers.

Diagnostics built for the failure you can't see

Retrieval miss or synthesis miss?

An explain mode writes a file per failing query answering the one question that changes what you fix: did the expected source reach the context at all? It split fifteen failures into eleven where the source was present and the answer still failed, and four where it never arrived. Those are two entirely different bugs.

Guards that were proven to fail

Several offline and live guards were validated break-then-restore — the code was deliberately broken to confirm the test actually catches it. A green test nobody has ever seen go red is not evidence.

Attributed spend

Per-call usage logging with role attribution, so a feature's cost can be decomposed into which stage spent it — which is how you find out that three quarters of a feature's cost lives in one step.

Where this goes next.

The same engine, exposed to other software — a self-describing tool API and a clean-room MCP server, where the lens descriptor doubles as the tool catalogue.