ADR 0012: Body match is an optional mode of LOOKUP
Status: proposed (2026-09-07). Workstream 2 of the token-efficient retrieval plan.
Context
LOOKUP matches tags and titles only. The retrieval baseline of 2026-09-07 hit 27 of 27 vocabulary and decision questions and 0 of 23 body questions: the term was in a section nobody tagged, so the agent fetched decoys, and the decoy fetches are where the tokens went. The answer must name the section so the next call is one section fetch.
Two homes were considered. A sidecar per client process would crawl every world it serves, so read load scales with clients rather than content, every answer lags the crawl, and two crawl loops need maintaining. The store already holds an in-memory catalog rebuilt at load and updated on each write, and the write path already has the full body in hand. A section index has exactly that lifecycle.
The lookup plan kept full text out of core because ranking cannot be specified across implementations. That constraint still holds and shapes the contract.
Decision
LOOKUP gains an optional request key match with values catalog (default)
and body. No new verb. Catalog mode is byte-identical to today: a request
without match gets the response it always got. When the request carries
match, the response echoes the mode it answered in; a server without body
match ignores the key, and a client treats any response lacking match: body
as a catalog answer.
The spec fixes recall and leaves order to the implementation:
- The unit is the section, split and slugged by the one rule FETCH uses for
#anchor. The splitter moves fromclient/mdoutlinetoprotocol/mdoutlineso server and client cannot disagree; the client package re-exports it. - Section text is a heading's own content, not its subtree, so a hit names the innermost section.
- Terms are whitespace-split, lowercased, at least two characters, matched on alphanumeric token boundaries and as whole words.
- A section matches when every term is in its text, its heading trail, or
the document's tags or title. Every matching, readable section under the
scope and filter is a candidate;
limittruncates after ordering. - Importance is a prior among matches and never admits a non-match. Ties
break by path then anchor. Nothing else about order is specified. The
reference server ranks by BM25 with heading and tag terms weighted higher,
scaled by
0.7 + 0.3 * importance. - Body rows carry
#anchorin the path,title › headingin the title, and a fifthSnippetcolumn of one line at most 240 bytes.
The index lives in server/internal/catalog beside the catalog, in memory,
rebuilt with the catalog at load and updated per write. No persistence and no
library. Memory stays under three times body bytes. Servers that implement it
list lookup-match: catalog, body under ## Capabilities in their agent
manifest, which is advisory: the manifest is author-published, so the
response echo is the authoritative signal.
Consequences
- The
protocolmodule gains a markdown parser (goldmark, already used by the client). Stripped server binaries grow by about 1 MB. The alternative, a second hand-rolled splitter, risks anchor drift against links already published, which is the failure the shared rule exists to prevent. - The bucket store snapshot holds no bodies, so its section index costs one blob read per current document at open and per changed document at refresh; unchanged documents carry their sections forward by body hash. An on-disk index waits until a world passes about 100 MB of bodies.
mark_lookupon demarkus-mcp and the broker gainsmatch;mark_lookup_allmerges body rows;demarkus lookupgains-match body. No new MCP tool, so the schema budget is unchanged.- Any pressure to specify ranking or snippet content in the spec is refused; the retrieval benchmark judges the reference implementation, not the spec.