Applications built on laconicd repeatedly pick the Record + attribute-
scan pattern for data that has mutable logical identity (game state,
user profiles, inventories). The query cost then grows linearly with
per-entity version count, and the fix reached for (cache, pagination,
compaction) hides the design mistake rather than correcting it. AI
agents hit this failure mode especially reliably: they pattern-match
the existing client's queryRecords usage as canon and never discover
the Naming API.
This commit adds four pieces of documentation that surface the right
primitive *before* a wrong one is committed to:
* docs/PATTERNS.md — primitive decision tree with concrete
anti-patterns (queryRecords-as-KV, compaction-for-latency,
cache-the-slow-query) and a worked example (mutable game state).
* AGENTS.md — explicit for AI coding agents; names the failure mode,
lists six rules, and tells agents to read PATTERNS.md before
writing their first queryRecords call.
* gql/cerc-io/laconicd/schema.graphql — prescriptive comments on
queryRecords (warning against mutable-identity usage),
getRecordsByIds (point-lookup clarification), and the Naming
API section (USE THIS for current-state lookups).
* README.md — new 'Designing state that lives on laconicd' section
between Usage and Tests, linking both new docs.
5.4 KiB
Agents: read this before touching laconicd-backed state
Written for AI coding agents (Claude, Codex, Copilot, etc.) working on laconicd itself or on applications that persist data through it. Humans can read it too — the content is the same — but the tone addresses a specific failure mode that agents exhibit more consistently than humans.
The failure mode this document exists to prevent
Agents extending an existing laconicd-backed codebase pattern-match
the visible API calls and treat them as canonical. If a codebase
uses queryRecords with attribute filtering for everything —
including where it's wrong — an agent will extend that usage
rather than discover the Naming API.
When the wrong pattern eventually scales badly (latency grows with accumulated record versions), agents reach for workarounds within the chosen pattern — pagination, caches, compaction — rather than re-evaluating the primitive choice. That drift compounds: each workaround entrenches the wrong design.
This has happened repeatedly on real projects. The architectural cost is high, because laconicd is append-only: wrong versions persist on chain as permanent garbage that you can't silently delete.
Concrete rules for agents
1. Before designing any laconicd-backed state, read docs/PATTERNS.md
Not after a first implementation. Not when you hit a scaling wall. Before. The decision tree there is the only reliable way to pick the right primitive.
2. Do not pattern-match from existing client code as if it were canon
A gateway, SDK, or service that already uses laconicd is an existing code sample. It is not evidence that the patterns it uses are correct. Check:
- Does the client use
setName/lookupNames/resolveNamesat all? If not, the chances it's modelling mutable identity withqueryRecords+attribute scan are high — that's almost always a latent latency bug. - Does
save_*in the client write only a Record, never a name? If your new feature has the same shape, you're about to extend the bug. - Does the client have "pick the latest" / "sort desc and take
first" logic after a
queryRecordscall? That is exactly whatresolveNamedoes at the chain level — the client is re-implementing it badly.
3. Questions an agent should ask itself before writing queryRecords
- "Am I looking up the current state of a logical entity?" If yes,
you want the Naming API, not
queryRecords. - "Will this query return more records as the same entity gets modified over time?" If yes, you want the Naming API.
- "Am I about to write client-side code that picks the latest of the returned list?" If yes, you want the Naming API.
If any answer is yes and you still want queryRecords, stop and
write a comment explaining why the Naming API doesn't fit. If you
can't, the Naming API does fit.
4. Do not propose caches, pagination, or compaction as the first fix for slow reads
Slow queryRecords on a mutable-identity use case is a design
smell, not a performance problem. The fixes agents reach for
first are all wrong:
- TTL caches hide the latency rather than fix it. First read per window still pays the full cost.
limit/offsetpagination caps per-request fetch cost but doesn't reduce the version-per-entity multiplier.delete_recordscompaction destroys the audit trail that the immutable-Record design was supposed to provide.
If you're reaching for these, you are almost certainly on the
wrong side of the decision tree in docs/PATTERNS.md. Go back
and re-pick the primitive.
5. If you find a codebase already on the wrong side
Do not compound the mistake. Options in order of preference:
- Migrate the specific feature you're touching to the Naming API. You can usually do this incrementally — start writing names on new writes, keep reading via attribute-scan until the name index is populated, then flip the read path.
- Write an ADR documenting the debt and which class of regression will appear (latency grows with history; first timeout hit around ~N records).
- Ship the feature on the Naming API even if the rest of the codebase doesn't — a single correct example in the codebase is worth more than consistency with a wrong pattern.
6. Read laconicd's schema and keeper source, not just the client
Before proposing a fix that involves "laconicd doesn't support X":
gql/cerc-io/laconicd/schema.graphql— the actual read API.x/registry/keeper/— the write and index path.proto/cerc/registry/v1/tx.proto— the mutation messages.
Many "laconicd limitations" agents assume are actually client
limitations. queryRecords supports limit and offset;
setName and lookupNames exist and work; NameRecord.history
returns block-height-tagged prior bindings. Verify capabilities
against source before recommending workarounds or upstream
changes.
Why this is in its own file
A PATTERNS.md on its own is a reference document humans browse.
An AGENTS.md is loaded into context and read top-to-bottom at
the start of agent sessions. Calling out the failure mode
explicitly — "agents tend to overlook the Naming API" — is load-
bearing because the people most likely to hit the failure are
the ones who wouldn't naturally pattern-match to a document
titled "Patterns."
If you are reading this and you are about to write your first
queryRecords call: stop. Read docs/PATTERNS.md. Then come
back.