Semantic search over a Markdown knowledge base, served over MCP.
Until this record, one constant, MAX_RAW_BINARY_BYTES (50 MiB,
grooveseek/src/parser/mod.rs), decided six things at once: the file size at
which the indexer skips a binary file before reading it, the same check on the
handle it reads from, the decompression budget of the .xlsx preflight, the
cumulative budget of .docx / .pptx part reads, the text budget of one PDF,
and the binary cap of get_document / resources/read. MAX_RAW_TEXT_BYTES
did the first two for Markdown and plain text.
Workbooks of 300 MB exist in ordinary office use, and they were skipped with a
warning. The cap cannot simply be raised for everyone: rebuild_index is an
MCP tool a client can call at any time, the file is held in memory whole while
it is parsed, calamine also loads the whole shared-strings table of an
.xlsx into memory, and an allocation failure aborts the process. Neither
catch_unwind nor running the parse on another thread stops an abort.
The question is how to let an operator admit such a file knowingly, without changing anything for a configuration that does not ask for it.
0. Rejected: products disagree on what it means. Recoll reads 0 as no
limit for filtermaxseconds and as “reject everything” for
compressedfilemaxkbs; Zoekt reads it as “use the default”; Open WebUI
treats an unset or 0 RAG_FILE_MAX_SIZE as no limit for uploads and as
100 MB for archive extraction, within one product. This repository
already refuses [embedding].max_input_chars = 0.-1. The most common choice (Elasticsearch indexed_chars, Apache Tika,
Solr). Rejected: a negative byte count in TOML does not read as an intent,
and it invites “and -2?”."unlimited". Taken. 0 and negative values are errors
whose message points at it.max_binary_file_size = "300 MiB" would
still skip a 300 MB workbook at decompression, and the only way through
would be "unlimited", which removes the zip-bomb check from every file."unlimited". Rejected: “no limit” would
silently remove a second defence.max_decompressed_size, 50 MiB by default and
independent of the raw cap. Taken.get_document / resources/read follow the index caps. Rejected:
one MCP request would then hold a 300 MB file in memory.uri, through the size recorded in the index
(ADR-0005).Where the keys live. A new [limits] section was rejected; [index]
already holds the settings of groove index and rebuild_index, and the
caps are a property of indexing.
.txt become one enormous chunk.[index] takes max_binary_file_size, max_text_file_size and
max_decompressed_size. Each is a byte count of at least 1, a size with a
unit (B, KB, MB, GB, TB in 1000s; KiB, MiB, GiB, TiB in
1024s; case-insensitive; no fractions), or "unlimited"
(case-insensitive). Each defaults to 50 MiB. 0, negative values,
fractions, unknown units and other words are errors that name the key.Registry::limits). The indexer
reads the raw caps from there on every read path (the full run, the watcher’s
create / modify and its rename), and the binary parsers are built with the
decompression budget (with_budget) and pass it to their deepest read. The
fixed-cap helpers remain for tests only, so no production path checks a
second time against 50 MiB.max_decompressed_size, and
so does the warning for a single .docx / .pptx part skipped for being
over it, which used to be dropped without a word; a raw-cap skip still says
file too large. The two keys are told apart on stderr.GET_DOCUMENT_BINARY_MAX_BYTES, so the same number answering a different
question is not mistaken for the index default. Reads also keep the built-in
decompression budget whatever max_decompressed_size says: get_document
and resources/read parse through parse_bytes_for_read, which the four
binary parsers answer with the default budget rather than the one they were
built with, so one MCP request stays as bounded as it was."unlimited" memory use is
bounded only by the files in the knowledge base.--config accepts them.max_binary_file_size admits the
file and max_decompressed_size admits what it inflates to. The value to
write for the second is measured on the operator’s workbook, not known here.groove doctor stays yellow for a knowledge base that raised the caps on
purpose. A document indexed past the read cap is reported as
larger-than-a-read-returns (Warning, exit 1), the same treatment text over
1 MiB already gets.max_decompressed_size takes every binary document off offer.
A read parses with the built-in budget, and the index records only raw size,
which cannot say which binary documents inflate past it. So while the key is
above the default, no binary hit carries a uri and resources/list offers
none; they stay searchable, and get_document still refuses one that
inflates past the budget with the decompression message, or returns it
without the parts that are over the budget on their own. Text is unaffected.
groove doctor names the withheld binary documents as binary-uris-withheld
(Warning, exit 1), so this too keeps a raised configuration yellow.
Recording the inflated size at index time would let the server offer the
binary documents that fit; that is not done yet.groove index --force runs or the file shrinks under the cap, because the
scan’s size skip protects existing rows rather than deleting them. An edit
that leaves the file over the cap keeps the old text searchable: the watcher
records only the new size, and the next scan skips the file again.[index]), when files are parsed from a handle instead of a
buffer (the warning’s “held in memory whole” stops being true), or when the
decompression budget moves to a ratio.grooveseek/src/parser/mod.rs (the value grammar and the
warning), the four parser modules and registry.rs (the budget reaches each
read), grooveseek/src/config.rs (the keys, both registry arms, the
untrusted rule), grooveseek/src/indexer.rs and
grooveseek/tests/index_size_caps.rs (the three read paths), and
grooveseek/src/server.rs (the read side does not move, its raw cap and its
decompression budget alike).