Releases: alphabet-h/grooveseek
Release list
1.3.0 - 2026-09-03
Release Notes
Added
- groove can load a grammar it was not built with. A language that is not
compiled in — everything except Rust — arrives as a small library you
download and put in a directory, named by the newgrammar_dirkey or by
GROOVE_GRAMMAR_DIR. Nothing is downloaded automatically, and no library is
opened unless[parsers].enablednames the language it belongs to: opening
one runs its initialisers before a single symbol can be inspected, so groove
looks up the file by name from a fixed table rather than reading whatever is
in the directory. A plugin is checked for the ABI version it declares —
first, and on its own, since every other export is read through the signature
that version defines — then for the exports it must have, a tree-sitter
version this build speaks, a tags query that compiles against its own
grammar, a language name alang:filter can be written against, and exactly
one valid file extension, which must be the one the enabled id stands for. A
library found under one language's name that declares another is refused
rather than registered, so a mispackaged download cannot quietly take a file
type out of the index and put a different one in. See
"Placing a grammar plugin" in
docs/clients.md. - Python is the first grammar published this way.
groove-grammar-python
is a new release asset — one archive per platform, each with its.sha256,
built from the same four targets as groove itself. Download it, check the
hash, unpack the library into the grammar directory, and add"py"to
[parsers].enabled; a Python file is then indexed one definition at a time,
the way a Rust file already was, withclass/function/constantin
symbol_kindandlang:pythonamong the tags. It is a separate download
rather than a second compiled-in grammar because every language that is
compiled in is paid for by everyone, whether or not they index it — see
ADR-0013. - An id that needs a plugin says so, instead of reading as a typo. Writing
"py"in[parsers].enabledused to be answered with the list of supported
ids, as if it were misspelled. It now names the file to place and the
directory to place it in — or, on a machine where no such directory can be
determined, namesGROOVE_GRAMMAR_DIRinstead of a path that does not exist.
A plugin that is present but unusable is refused with its path and the reason.
Changed
- A run that cannot succeed still stops before it creates anything. Every
one of the failures above is decided while the parser registry is built,
which happens before the database is opened and before any model is
downloaded — so a missing or broken plugin costs you a message, not a
half-built index. - A
groove.tomlthat groove merely found cannot choose the grammar
directory.grammar_dirjoinsfastembed_cache_dir,
[transport.http].bindandkb_pathas a key that an untrusted config does
not get to set, because a grammar plugin is native code loaded into the
process. As with the cache directory, the safe value is applied whether or
not the key is present: omitting it would otherwise be a way to influence the
choice by saying nothing. Naming the config with--configaccepts it as
written, as before. Refusing a missing grammar is not affected by trust —
the same failure happens either way. - A
groove.tomlthat groove merely found cannot choose which parsers run
either. Guarding onlygrammar_dirguarded the wrong half: naming a
language in[parsers].enabledis what causes a plugin to be looked for at
all, and the same key can switch on the formats with the widest input surface
—pdf,xlsx,pptx,docx— that an operator had deliberately left off.
A discovered config now has[parsers]ignored with a warning naming what it
asked for, and the default set, Markdown alone, is used. Unlike the cache and
grammar directories, an absent key needs no substitute: omitting[parsers]
already means Markdown alone, so there is nothing quieter to fall back to.
[parsers.code]goes with it, having no parser left to configure.
If you keep agroove.tomlbeside a project and rely on it to index
anything but Markdown, name it — everywheregrooveruns, not only when
serving:groove --config ./groove.toml index --kb-path <kb>. This matters
most onindex, and not because the new index would merely be incomplete:
groove indexdeletes the documents it did not visit, so a rebuild that
collects only.mdremoves every.txt, PDF, Office document and source
file already indexed. APostToolUsehook fires on the next edit, so for
anyone using one that is the first thing that happens after upgrading. The
rebuild-on-edit.shrecipe now takesGROOVE_CONFIGfor this, and the
personaldeployment recipe names the config on bothindexandserve;
intranet-httpalready did. A config next to the binary, or one
groove service installplaced, is trusted as before and needs no change —
and--confignaming a file that is not there is an error rather than a
fallback to discovery, so do not add it to a setup that relies on the
binary-side location.
Fixed
- One deeply nested source file no longer stalls indexing. Working out the
scope a definition sits in means walking to the root of the syntax tree, and
that walk costs more the deeper the definition is, so the total grows with
the cube of the nesting: a single 10 KB file ofmod a{repeated a thousand
times took 64 seconds to index, and the byte ceiling that was supposed to
bound this never fired because the file was nowhere near 1 MiB. Since
rebuild_indexholds the embedder and the database for its whole run, one
such file in a knowledge base stopped every request the server had. A file
holding a definition nested under more than 64 syntax-tree ancestors is now
chunked by lines rather than by definition, and taggedparse:too-deepso
the choice is visible to a search. The file still contributes every byte it
has, as ADR-0012
promises; what it loses is the definition metadata. The bound counts
ancestors rather than seconds on purpose — a wall-clock budget would let the
same file produce different chunks on different machines, and those chunks
are the index. Definitions in groove's own sources sit under at most 8
ancestors, so real code has eight times the room it uses. An index built
before this release keeps its old chunks for files whose content has not
changed;groove index --forcerebuilds them. See
ADR-0014.
grooveseek 1.3.0
Download grooveseek 1.3.0
| File | Platform | Checksum |
|---|---|---|
| grooveseek-aarch64-apple-darwin.tar.xz | Apple Silicon macOS | checksum |
| grooveseek-x86_64-pc-windows-msvc.zip | x64 Windows | checksum |
| grooveseek-aarch64-unknown-linux-gnu.tar.xz | ARM64 Linux | checksum |
| grooveseek-x86_64-unknown-linux-gnu.tar.xz | x64 Linux | checksum |
groove-tray 1.3.0
Download groove-tray 1.3.0
| File | Platform | Checksum |
|---|---|---|
| groove-tray-x86_64-pc-windows-msvc.zip | x64 Windows | checksum |
groove-grammar-python 1.3.0
Download groove-grammar-python 1.3.0
| File | Platform | Checksum |
|---|---|---|
| groove-grammar-python-aarch64-apple-darwin.tar.xz | Apple Silicon macOS | checksum |
| groove-grammar-python-x86_64-pc-windows-msvc.zip | x64 Windows | checksum |
| groove-grammar-python-aarch64-unknown-linux-gnu.tar.xz | ARM64 Linux | checksum |
| groove-grammar-python-x86_64-unknown-linux-gnu.tar.xz | x64 Linux | [checksum](https://github.com/alphabet-h/grooves... |
1.2.0 - 2026-08-27
Release Notes
Added
- Source code is indexed one definition at a time. Enable it with
[parsers].enabled = ["md", "rs"]. A function, a struct, a method each
become their own chunk, carrying the doc comment written above them and the
scope they sit in, so a hit is something you can act on rather than a window
that starts mid-body. Everything no definition covers — imports, top-level
statements, the frame of animplblock, and any region the parser could not
understand — is filled in by line, so a file with a syntax error still
contributes the definitions around the break instead of collapsing into one
chunk. Rust is compiled in behind the default-ongrammar-rustfeature,
measured at just over a megabyte of binary; other languages arrive as
separate libraries you place, in a later release. See
ADR-0012
and ADR-0013. - Search results from source files carry
start_line,end_lineand
symbol_kind. The line range describes the chunk rather than the
definition it came from — a doc comment pulled in above a function is inside
it, and a long function split across chunks gives each piece its own — so
opening the file at that line always shows what was returned.symbol_kind
is the grammar's own word (function,class,method,constant, …), not
the language's keyword, and the set grows as languages are added. All three
keys are absent rather thannullon anything that did not come from a
source file, so no prose response changes shape. [parsers.code].max_chunk_chars(default 3500, counted in
non-whitespace characters) sets the budget for one chunk. A definition that
fits stays whole; one that does not is split into its nested definitions, or
by lines when it has none — the usual case for a long function. Changing it
does not re-chunk files whose content has not changed, since those never
reach the parser again;groove indexsays so and names--force, and keeps
saying so until the index actually matches the setting.
grooveseek 1.2.0
Download grooveseek 1.2.0
| File | Platform | Checksum |
|---|---|---|
| grooveseek-aarch64-apple-darwin.tar.xz | Apple Silicon macOS | checksum |
| grooveseek-x86_64-pc-windows-msvc.zip | x64 Windows | checksum |
| grooveseek-aarch64-unknown-linux-gnu.tar.xz | ARM64 Linux | checksum |
| grooveseek-x86_64-unknown-linux-gnu.tar.xz | x64 Linux | checksum |
groove-tray 1.2.0
Download groove-tray 1.2.0
| File | Platform | Checksum |
|---|---|---|
| groove-tray-x86_64-pc-windows-msvc.zip | x64 Windows | checksum |
groove-svc 1.2.0
Download groove-svc 1.2.0
| File | Platform | Checksum |
|---|---|---|
| groove-svc-x86_64-pc-windows-msvc.zip | x64 Windows | checksum |
1.1.0 - 2026-08-26
Release Notes
Added
-
list_topicsnow returns the directory tree beneath each topic. Every
entry carries achildrenarray: one node per path segment below the
category and topic, each withsegment,file_count(the documents under
that prefix, so a parent counts everything beneath it) and its own
children. Root and category-only entries, and topics whose documents sit
directly in the topic directory, carry[]. The tree is built from
documents.path, so a document whose frontmattertopic:overrides the
path-derived topic still contributes the directories after its second path
segment to the group it was filed under, and siblings are sorted by name so
the output does not depend on the order SQLite returns rows. This is a field
addition under the 1.0 freeze (docs/stability.md):
nothing existing changes shape, and the tool still takes no parameters.list_topics_returns_the_directory_tree_beneath_each_group_over_httpin
grooveseek/tests/mcp_protocol_surface.rsreads it back through the
Streamable HTTP transport from a seeded index, andsegment_tree's unit
tests ingrooveseek/src/db/meta.rspin the rules one at a time — the file
name is not a node, the first two segments are not repeated, a parent counts
every document beneath it, siblings are sorted.docs/filters.mdalso stops
saying thatcategorycan come from acategory:frontmatter field; there
is no such field, onlytopic:. -
A group that starts with
-is excluded from the search.rust -async
drops every chunk containingasyncfrom both halves of the hybrid: the
full-text half compiles to("rust") NOT ("async"), and the vector half
drops the candidates whose chunk matches the same negative expression, so
one FTS5 judgement (trigram, case-insensitive, diacritics removed) decides
the question for both legs. The judgment is made against the same FTS row a
positive match sees —heading, the contextual prefix, andcontent
together — not the body alone, so an excluded term in a heading also drops
the chunk.-"exact phrase"excludes a verbatim phrase; an unquoted
-wordis tokenized with the same rules as the rest of the query, so
-再ランキングalso excludesランキング— quote it to exclude only the
compound. The embedder, the reranker andmatch_spanssee the query with
its exclusions cut out, so a query without one is embedded exactly as
before. The response echoes what was excluded in
filter_applied.excluded_terms, and a query made only of exclusions is
refused ({"error": …}over MCP, stderr and a non-zero exit on the command
line, a load error for a golden file). An excluded phrase under the
three-character trigram floor excludes nothing; the parent retriever may
expand a hit into text that contains the excluded term, since exclusion is
judged on the hit chunk, not on content a later expansion adds.
a_chunk_holding_an_excluded_term_never_reaches_the_fts_legand
an_excluded_term_drops_the_vector_nearest_chunk_toopin the two halves
against a real FTS5 table. Rationale in
ADR-0011.
Changed
-
What concurrent HTTP clients pay for the search locks is now measured, and
the docs say so. docs/clients.md claimed "~10 qps
expected forsearch" with nothing behind it. Eight clients at once now have
a table in docs/deployment-topologies.md,
taken withcargo test -p grooveseek --release --test http_lock_contention -- --ignored --nocapture:
searchthroughput moves from ~7 to ~9 qps on a 9,813-chunk corpus and from
~12–16 to ~13–20 qps on a 794-chunk one, latency grows about 4.5× at eight
clients, and a second daemon on a copy of the same corpus adds only 12–32% —
one query embedding already runs across every core, so the lock is not
holding idle hardware back. The database side is where cores wait (the graph
tool keeps one busy), and its share overtakes the embedding at roughly five
thousand chunks; below that no lock refactor can raisesearchthroughput.grooveseek/tests/http_lock_contention.rsis the instrument: an ignored
integration test that starts a realgroove serve --transport http, releases
N threads from one barrier against/mcp, and prints the table with the
three discriminators the decision needed (one embedding versus one hybrid
fetch timed in-process, two daemons versus one, CPU per request). It asserts
only that it measured something — non-empty hits, no failed requests, the
latencies of a round summing to more than the round took — and runs on the
three-file fixture in the nightly--include-ignoredjob. -
A
-that begins a whitespace-delimited group changed meaning. Until
now-foowas searched for as the literal token-foo(a hyphen is a word
character, sosqlite-vecstays one token — that is unchanged). It is now
an exclusion. To search for a leading hyphen literally, quote it:
"-foo".---, a lone-, and- fooare not exclusions. Evaluation
history is fingerprinted withfts_query_version3 for this release, so
groove eval --fail-on-regressionwill not compare across the change.
Fixed
-
The
groove evaltranscript in the quick start now shows what the binary
prints, on both pages. docs/eval.ja.md had translated the
example without itsPer-querysection, so a Japanese reader was never shown
the one line that names a query that missed. The English example was not what
the binary prints either: it lacked thecorpus:line every run has carried
since 0.15.0, ended its per-query row with anexpected ... missingphrase
the formatter has no code for, gave the row a placeholder id where the real
one is the first 32 characters of the query, wrote the timestamp with a
+09:00offset where the binary writes UTC, and its aggregate could not have
come from its own row — one of two queries atrecall@10: 0.00does not
average torecall@10 1.000. Both examples are now derived from the golden
file above them: two hits at ranks 1 and 3 for the first query, a miss for the
second,recall@10 0.500,MRR 0.500,nDCG@10 0.460. The Japanese page
also gains the sentence on heading-less expected hits and the expansion of
nDCGthat its English twin already had.grooveseek/tests/docs_eval_transcript.rskeeps it that way. It parses the
golden fence on each page, runs the metric and formatting code the binary
runs over the hits the example assumes, and requires the transcript fence to
equal the output character for character. The ranks are the example's
premise, not a measurement — what the test pins is that the numbers, the
layout and the row id follow from them.eval::query_idis public for it,
so the row id rule lives in one place. -
The MCP tool descriptions, and twenty-three sentences that still credited
rmcp with checks it had stopped performing. ADR-0009
moved Host and Origin validation intogroove, which hands rmcp empty
allow-lists so it matches nothing; the doc comments around that code, the
anyhow::bail!an operator sees for anallowed_originsentry that will not
parse, andCONTRIBUTING.md's clippy step had not all been told. The two
that reach a caller are thedescription=strings, which are all an LLM
client is given:rebuild_indexrefuses a call that arrives during a rebuild
and did not say so, andget_connection_graphreturnssnippeton every
node plustruncatedandtruncation[]on the envelope and named neither.
Both facts were already on docs/mcp-tools.md and missing
from the only string a client reads.tools/listnow carries an assertion about description content in
grooveseek/tests/mcp_protocol_surface.rs, honest about being a substring
check: it catches a fact being deleted, not a description drifting from the
page.
Removed
-
The
.kb-mcpignoremigration check, and the module behind it. v1.0.0
added twogroove doctorfindings about an ignore file left under the name
the project used before
ADR-0007 — and
said in the same breath that they were for the migration and would go in
1.1.0. The module doc, thedocs/ARCHITECTURE.mdrow and
docs/usage.md all carried that date. This is that removal:
grooveseek/src/legacy.rs, the findings it fed
(indexed-despite-legacy-ignoreandlegacy-ignore-not-examined), and
ExclusionRules::ignore_only_from_bytes, which had no other caller, are all
gone. A.kb-mcpignorestill keeps nothing out — that is ADR-0007's
decision and has not changed — butdoctorno longer opens one to say so.doctortherefore asks two groups of question rather than three, and
doctor::runtakes(db, registry)rather than also akb_pathand an
exclude_dirs. That signature is internal:groove doctorstill requires
--kb-pathto find the index, and every remaining check, exit code and JSON
field keeps its name, type and meaning. The tests that covered the removed
findings went with them — the unit tests insrc/doctor.rs, two in
tests/doctor_cli.rs(one of them the#[ignore]end-to-end run that
proved the remedy) and the four helpers only those two called.
Internal
- Four comments named something other than what the code does. feature-55
demotedbuild_fts_queryto a#[cfg(test)]helper, and two comments
outside its file still described it as the entry point production calls: the
whole-query fallback is assembled byparse_queryinto a field
query_phrasesdoes not return, and the round-trip counter adb.rstest
pins sits above the early r...
1.0.1 - 2026-08-24
Release Notes
Added
-
A page about which shape to deploy in. docs/deployment-topologies.md
answers three questions the reference pages each answer a piece of: whether to
let a client spawngrooveor to leave one running, what residency actually
buys, and where the same-host boundary comes from.It began as an internal note measured against v0.26.0 and was re-measured
against v1.0.0 before publishing, because the two releases in between changed
exactly what it was about./api/search— which its benchmark named and its
fourth open question argued about — no longer exists. Origin validation, which
it listed as unresolved, ships on by default. All four of its open questions
were settled before 1.0.0, so they appear as outcomes with the ADR that
records each one, rather than as questions.The measurements are new: a resident daemon answers a search in about 200 ms
where the CLI takes about 3.1 seconds. The page splits that three seconds into
three terms and says which two were measured — process and database setup at
about 35 ms, the search itself at about 200 ms — leaving the model load, about
2.9 seconds, as the one derived by subtraction. It also says the thing the
internal note did not: residency is not the same as speed. A first query after hours
idle took 4.6 seconds, and a CLI search running alongside the daemon dragged it
from 200 ms to 2 seconds, which is the page's own "one process, one model"
warning arriving as latency.Nothing in it cites a line number. The version it replaced cited eleven, and
all eleven were wrong within five days.
Fixed
-
The reranker's documented latency was wrong by two orders of magnitude.
--helpand docs/usage.md said rerank adds "300–700 ms per
query on CPU withbge-v2-m3over 50 candidates". Measured against 1.0.0 on
one Windows machine, the same query takes 3.1–3.6 s without it and 74–87 s
with it throughgroove search, and 0.1 s against 74–79 s through a resident
daemon. Residency does not help: the daemon builds the reranker at startup, so
its second and later reranked queries have no model left to load and still take
74–79 s. The cost is the cross-encoder pass over the candidate pool.The number, the
--helpline, and the recommendation table built on the
number are replaced by the measurement and the conditions it was taken under.
Nothing about reranking changed — only what the tool says it costs, and
therefore the advice about when to switch it on. -
docs/mcp-tools.mddated therebuild_indexbound to a version that never
existed. It said the one-at-a-time refusal arrived inv0.28.0; 0.27.0 was
followed by 1.0.0, so there is no such release. The bound shipped in 1.0.0 and
the page now says so.
Internal
-
A Markdown link that no longer resolves now fails the test suite.
grooveseek/tests/docs_links_resolve.rswalks every.mdfile in the
repository and checks two things about each relative destination: that the file
is there, and that an#anchormatches a heading GitHub would generate in it.
It found one, in docs/stability.ja.md, which had
carried the English page's#stablesince it was translated — a link that
opened the right page at the wrong place for anyone who followed it. The
Japanese anchor is#コマンドライン.A throwaway script checked this once by hand during the README split and then
lived in a scratch directory. This is that check in the tree: 70 pages, 489
relative destinations, 35 of them anchored, 17 of those with Japanese
fragments.The anchor half follows
github-slugger, the implementation the remark and
MDX toolchains use to reproduce GitHub's anchors: downcase, delete everything
outside[\p{Word}\- ], then turn spaces into hyphens. The order is what
implementations get wrong —信頼する置き場所 / しない置き場所loses the slash
first and keeps two spaces, so its anchor carries two hyphens, and a slugger
that mapped spaces before stripping punctuation would reject a link this
repository contains. Counted twice, with a second implementation built on a
different principle (Unicode general categories and a line matcher, against
this one'schar::is_alphanumericand a parser): identical on all 792 anchors.Repeats retry their suffix until it is free, so
# Foo,# Foo,# Foo-1
endsfoo,foo-1,foo-1-1rather than handingfoo-1out twice. Which of
the two GitHub itself does is not documented and cannot be measured (POST /markdownrenders headings without ids); the retry is the side that can only
ever accept a missing anchor, where the counter can reject a working link and
stop CI.Pages are parsed rather than matched line by line, which is what makes a
#
inside a fenced TOML block not a heading and a reference-style link still a
link, and destinations are read as the URLs they are before they are read as
paths: the query and the fragment are cut off first (docs/usage.md?plain=1
is a link GitHub's own interface hands out), percent escapes are decoded the
way GitHub decodes them, and a rooted path —/x,\x, or the%2Fthat
decodes into one — is answered as the site-root path it is rather than by
asking a filesystem that differs between CI and a laptop. A fragment is read
in the language of the file it lands on: on a page a heading slug or an anchor
the page names outright with<a name="…">, on anything GitHub renders as
source a line range (#L10is checked against the file's length), and on a
directory the README GitHub shows underneath its file list. Destinations are
resolved lexically, counting depth from the
repository root, so a..that would climb above it is out of bounds wherever
the checkout happens to sit —exists()cannot ask that, and neither can
folding the path absolutely, which merely walks up one directory and back down
into whatever sits beside the checkout. On Actions that is the checkout
itself, since the path iswork/<repo>/<repo>. External
URLs are skipped entirely, on the URI grammar rather than a list of the
schemes seen so far, sotel:andMAILTO:are not looked for on disk — a
guard that can fail because
someone else's server is down stops being read — and what it cannot catch is
written down in the test: a link that resolves while the sentence around it
lies, which is the failure the same README split shipped seven of. -
A doc comment that names something this tree no longer has now fails CI.
cargo doc --no-deps --workspace --all-features --document-private-itemsruns
as the last step of thetestjob, and[workspace.lints.rustdoc]in the root
Cargo.tomldenies every warn-by-default rustdoc lint except one. Until now CI
ran fmt, clippy, check and test, none of which read a doc comment:
transport/http.rsnamedadmin_host_checkfor two days after
ADR-0009 deleted it, and
PR #219 converted references like it into links that nothing was yet checking.Twenty-four references were already broken when the check was switched on, and
the interesting ones were not typos.binary_size_exceededwas the old name of
size_cap_exceeded, cited twice.Refusal::messagehad become
Refusal::response. Six more —GraphNode,inspect,is_multiply_linked
twice,read_checkedtwice,recover_db— sat in module//!headers, where
a bare name does not resolve even to an item defined in the same file; they
are absolute paths now. The rest were prose that rustdoc was reading as a link:
a TOML section[eval], an interval[30,100], anarray<string>that parsed
as an unclosed HTML tag.private_intra_doc_linksis the one lint left atallow. It fires when a
public item's documentation links to a private one and that link resolves —
the item is there, it is simply not in the published set. This crate's Rust API
is Unstable by docs/stability.md and its rustdoc is
published nowhere, so pointing at the private helper that answers the question
is the useful thing to write. A name that no longer exists is a different lint,
and that one is denied: renamingsize_cap_exceededwhile leaving its four
doc references alone fails the build with four errors.--all-featuresis there for the reason clippy runs twice:test-helpersis
default-off and gates documented items, and rustdoc removes a gated item's doc
comment along with the item. Unlike clippy this needs only one run, because the
workspace's only#[cfg(not(feature = ...))]is inbenches/, whichcargo docdoes not document either way.It is a step in an existing job rather than a fourth job.
cargo doc --no-deps
wants exactly the dependency metadatacargo check --all-targetsalready
produced, so it costs one rustdoc pass — 15.8 s over the whole workspace,
measured — instead of a second dependency build, and it adds no cache entry to
a repository whose Actions caches already total 13.6 GB against a 10 GB limit.
Being in that job is also what puts it on all three operating systems, which is
the point:service/{linux,macos,windows}.rsare whole modules behind
#[cfg(target_os = ...)], so a single-OS doc check would never read two of
them.
grooveseek 1.0.1
Download grooveseek 1.0.1
| File | Platform | Checksum |
|---|---|---|
| grooveseek-aarch64-apple-darwin.tar.xz | Apple Silicon macOS | checksum |
| [grooveseek-x86_64-pc-windows-msvc.zip](https://github.com/alphabet-h/grooveseek/releases/download/v1.0.1/grooveseek-x86_64-pc-windows-msvc.zip... |
1.0.0 - 2026-08-22
Release Notes
Added
-
groove doctorsays what an old.kb-mcpignoreleft in the index.
ADR-0007 renamed the
project with no aliases and no automatic migration, so an ignore file under the
old name is not read and stops excluding anything at all. What comes back into
the index is whatever the two gates that still apply admit — the current
exclusion rules, and whether[parsers].enabledopens that extension. Nothing
said so.Two findings, both warnings.
indexed-despite-legacy-ignorenames the indexed
documents the old file matches that the current rules do not exclude — real
paths, up to five of them, not a count.legacy-ignore-not-examinedis what
comes out when the check could not be completed — the file is there and cannot
be read, or the filesystem will not say whether it is there at all: a check
that could not run is not a check that found nothing, and reporting the two
the same way is how a clean bill of health stops meaning anything. Its wording
claims only that, with what was actually observed carried alongside, because
one of those two cases never established that the file exists.The remedy has three branches, because the destination has three states. With
the name.grooveignorefree, the fix is a rename. With a working ignore
file there, it is copying over the lines you still want — never an overwrite.
And when the name is not free but nothing is being applied from it — a
directory, a refused link, a file over the cap, or a name the filesystem would
not answer for — the remedy says that and sends you to the destination first,
because neither renaming nor copying into it produces an ignore file and the
documents just reported would stay indexed either way. No branch asks for a
deletion: a knowledge base whose new file is broken looks identical from here,
and the old file may be the only copy of the patterns."Free" throughout means the filesystem said so, not that it failed to say
otherwise. Whether a name is free, taken, or unanswerable is one three-valued
question with one implementation, asked by both the check and the remedy.The old file goes through the same
ExclusionRulesthe index walk asks, so
this is not a second implementation of the exclusion rule; and it is asked
about documents that are in the database, which is what lets the finding
carry paths instead of the observation that a filename exists. The check is
for the migration and is due to be removed in 1.1.0, in one file. -
All four front pages open with a banner —
README.md,README.ja.md,
and the twodocs/indexpages. It draws what separates this from a plain
vector store: a semantic path and a lexical path converging on one node, and
ranked results leaving it for an MCP client — the RRF fusion of the
sqlite-vec and FTS5 legs.It carries no words. A wordmark would repeat the
# GrooveSeekheading
directly beneath it, a caption strip at this width is illegible on a phone,
and text drawn into an image is text no screen reader and no translation can
reach. Thealtattribute carries the meaning instead, and it is written per
language.Light and dark, chosen by
prefers-color-scheme, like the logo and the
screenshot before it. WebP rather than PNG — 33 KB against roughly 1 MB, for
an image every visitor loads. The PNGs are committed as a fallback, and
assets/README.mdsays what to swap.That file also stops repeating a claim it could never check. It recorded
a report thatraw.githubusercontent.comserves.svgastext/plain,
which is why nothing here references an SVG; the report could not be measured
when it was written because the host answered 429 all session. Measured now
against this repository's own files, the host answersimage/svg+xml— and
image/webpfor the banner. Whether GitHub's Markdown renderer would then
display an SVG is a second question, about itscamoproxy, and is still
untested; the PNGs stay for that reason rather than the old one.The 56-pixel logo those four pages used to open with is gone, since the
banner carries the mark at its centre. No page embedsassets/logo-*any
more —/uinever did, drawing the diamond as a character and its favicon
as an inlinedata:URI. The files stay: the SVGs are where the mark is
defined, which is what the banner's colours were measured against.
Changed
-
groove status,groove service statusandgroove service listprint
their results on stdout. All three used to write everything to stderr, so
groove status | grep Documentsreceived nothing — the pipe looked like it
would work and silently did not.
ADR-0008 declared the
stdout/stderr split frozen but left these three explicitly unsettled;
ADR-0010
settles them, because after 1.0.0 moving them would be a major release.A caller redirecting with
2>&1is unaffected. A caller capturing stderr
alone now reads nothing where it used to read the counts.What stays on stderr is everything that is not an answer:
index's progress,
the confirmations fromservice install/uninstall/tray-install/
tray-uninstall, andstatus's "No index found" — which reports an
inability to answer and leaves stdout empty. The wording of these lines is
still not stable; only the channel is.groove doctor --format jsonremains
the machine-readable route todocumentsandchunks. -
One implementation now answers
HostandOriginwherever they are
asked,/mcpincluded. The two questions had four implementations —
rmcp's for/mcp, and GrooveSeek's own for/healthzand for the admin
routes — fed one list and expected to agree. Measured, they did not: with
the identical allow-list,
Host: user:pw@127.0.0.1:PORT,@127.0.0.1:PORT,127.0.0.1@localhost,
127.0.0.1:65536andlocalhost:abcwere accepted on/mcpand refused
next door, and the admin refusal bodies were missing theForbidden:
prefix the others carried./mcptherefore refuses those five spellings now, where it used to
answer 200. All are malformed Hosts that no browser or MCP client
constructs, and the change can only refuse more, never less: no spelling was
found that rmcp refused and GrooveSeek accepted. Its refusal wording is
unchanged. See ADR-0009.What each route is compared against has not changed:
allowed_origins
still reaches/mcpand the admin routes only, the admin routes still match
Hostagainst a loopback-only list of their own, and/healthzstill
validatesHostalone and only whenhealthz_public = false.Two consequences worth having on their own: a refused request no longer
reserves a session seat before being turned away (measured with
max_sessions = 1, a foreignHostused to get429and now gets403),
and refusal logging on/mcpis bounded — rmcp wrote one line per refusal
with no limit, and the gate carries the same one-line-a-minute budget the
session limit has used since v0.27.0. -
An admin refusal no longer repeats the header it refused. The 403 body
readHost 'kb.example.lan' not in admin allow-list, echoing caller-supplied
bytes back;/healthznext door and rmcp on/mcpboth say only that the
header was not allowed. This surface now says the same, and the rejected
value goes to the log instead, where the operator who can act on it will see
it. -
rebuild_indexrefuses a second call while one is running. A rebuild
re-embeds the whole corpus while holding the embedder and the database, so a
second call never ran beside the first — it queued behind it, withsearch,
get_documentand/uiunavailable for the sum of the two. Nothing bounded
how many could queue: the session gate lets every non-initializerequest
past without taking a seat, somax_sessionsdid not apply, and
spawn_blockingcannot be aborted, so closing the connection did not stop one
either. A few dozen bytes of request bought a full re-vectorisation, as many
times over as the caller liked.The second caller now gets an error naming how long the running rebuild has
been going, instead of a wait with no upper bound. The bound is on the MCP
tool:groove indexruns in its own process and still overlaps a served
rebuild.
Security
-
/uiand/api/admin/statusvalidateOrigin. The admin routes are
served by GrooveSeek rather than by rmcp, so the check that guards/mcp
never reached them: any page open in the operator's browser could call them
cross-origin. Nothing leaked — they areGETs, and a foreign page cannot read
a response that carries no CORS headers — but nothing kept that true either,
and the first admin route with a side effect would have inherited the gap.
They now compare against the same effective[transport.http].allowed_origins
rmcp gets, with a test that asks both surfaces about one origin and requires
the same answer. As on/mcp, a request carrying noOriginstill passes,
so the tray,curland the page's own status poll are unaffected. -
Admin refusals no longer write a log line each. Bound to a non-loopback
address, the peer check refuses before anything else looks at the request, so
a stream of cheap requests wrote an unbounded stream of lines to the daemon's
log file. The session gate on/mcpalready thinned its refusals to one line
a minute, carrying the count of what it stood for; the admin gates now share
one such budget between them. -
/uiis served with a Content Security Policy andnosniff. The policy
isdefault-src 'none'plus exactly what the page uses — ...
0.27.0 - 2026-08-18
Release Notes
Added
-
The documentation is published as a site.
docs/is the GitHub Pages
publishing source, so the twenty-two reference pages and nine ADRs are
readable at https://alphabet-h.github.io/grooveseek/ without cloning
anything. Both languages are published; every page already linked to its
counterpart, andjekyll-relative-links— on by default — resolves those
links, so the language switch is the one that was already in the text.The repository root was the other possible source and was not chosen: it
would have published ninety-four Markdown files, thirty-two of them synthetic
test fixtures, plus the source tree as static files, and would have needed an
exclusion list maintained against a repository that is mostly not
documentation.Six links inside
docs/pointed outside it — at
grooveseek/examples/andgroove.toml.example— and would have resolved to
nothing on a site whose root isdocs/. They are absolute now, for the same
reason the README's images are. -
The README has a face: a mark, a screenshot of
/ui, and three badges.
ADR-0007 accepted,
knowingly, that "GrooveSeek" says nothing about what the product does and that
searching for "groove" lands in music software — and concluded that this "makes
the first line of the README load-bearing". The mark is lines of a document
with the shipped◆marking the passage a search found, in the same accent
the web interface uses; light and dark variants are selected with<picture>.The badges are CI, latest release, and the licence. There is deliberately no
crates.io or downloads badge: every crate here ispublish = false, so both
would be false.Images are referenced by absolute URL rather than repository-relative path,
because a release archive ships this README withoutassets/— the same
reason the documentation links were made absolute in the previous change.
They point at PNG renders rather than the SVG sources: an absolute URL
resolves toraw.githubusercontent.com, which is reported to serve.svgas
text/plainso an<img>will not render it, and the screenshots are PNG in
any case.assets/README.mdrecords the reasoning and how to regenerate. -
[transport.http].allowed_origins. Names the browser origins the server
accepts. Needed when a browser reaches groove through a reverse proxy, because
the browser then sends the public origin and the loopback default will not
match it. Entries carry a scheme and bracket IPv6, since they are compared as
RFC 6454(scheme, host, port)triples.Setting it replaces the default list rather than extending it, matching
allowed_hosts. Keep the loopback entries alongside your public origin if
browser-based clients also reach you over loopback.An empty list disables validation entirely and now warns at startup. Like
allowed_hosts,healthz_publicandmax_sessions, the key is ignored when
it comes from a config file groove discovered rather than one you passed with
--config— otherwise whoever can write agroove.tomlbeside the binary
could name their own origin, or blank the list, and turn the check off. -
A stability policy: docs/stability.md. It states what
1.0.0 will freeze and — more usefully — what it deliberately will not. Without
it, tagging 1.0.0 would promise that everything observable stays fixed until
2.0.0: 408 public Rust items across 24 modules, 138 command-line flags, 6 MCP
tools, 11 configuration sections, and a SQLite schema.Stable from 1.0.0: subcommand names and documented flags, exit codes, the
stdout/stderr split, the JSON fromsearchandgraph(fields may be added, so
ignore ones you do not recognise), MCP tool and prompt names with their schemas,
thekb://resource scheme,/mcpand/healthz, configuration keys and
defaults, the default embedding model, and the names written into your
filesystem.Explicitly not stable:
/uiand/api/*(loopback-only admin surface, due
to be rebuilt), all human-readable text output, the internal database schema, log
wording, and the Rust API. Reasoning: ADR-0008.
Changed
-
groove service uninstallandservice statustake--service-name
instead of a positional.install,tray-installandtray-uninstall
already named the instance with a flag, so the same thing had two spellings —
install --service-name workagainstuninstall work.
docs/stability.md freezes subcommand positionals as well
as long flags, which would have kept both forever, and a positional cannot be
taken away afterwards at all.The two also gained the name validation the other three already had. A name
installrefuses can never have been installed, so nothing that used to work
stops working. -
docs/stability.mdnow says what it freezes, rather than leaving it to be
inferred.Which flags. The promise is scoped to the
groovebinary and to flags this
documentation describes — and "documented" is now checked by a test rather
than assumed. Two flags were undocumented and would have been left unfrozen by
accident:groove validate --schema, the only way to point validation at a
schema that does not sit beside the knowledge base, and--fail-fast. Both
are written up in docs/usage.md now.Which output. Every subcommand that takes
--formatis listed in one of two
groups, because nine of them were in neither and silence reads as a promise.
The JSON ofsearch,graph,doctorandvalidateis stable, as is
validate --format github. Text output is not, from any subcommand; neither
aregraph --format dotand--format svg, which are drawings; neither is
the JSON ofevalandtune, whose numbers are expected to improve —eval
already stamps its history with ametric_versionfor that reason.Which channel. The stdout/stderr split is stated as it actually is. Six
subcommands produce a result on stdout;index,statusandservicewrite
everything to stderr, sogroove status | …receives nothing. That was true
before and the document said otherwise. -
The command line and the MCP tools now use the same noun for the same
thing, and docs/stability.md says which parts of the two
surfaces correspond. Both are frozen at 1.0.0, so this is the last release
that can move either one.Two names were one concept called two things.
groove graph --excludeis now
--exclude-paths, matching the tool'sexclude_paths; and the tool'spath
is nowstart, matchinggroove graph --start. The tool took the flag's word
rather than the other way round, because--pathbeside--kb-pathreads as
the corpus, andget_documentkeepspathfor the document it fetches.What remains different is deliberate, and is now written down instead of being
inferred: a repeatable flag is singular where the array it fills is plural
(--path-glob/path_globs,--tag-any/tags_any), tool names and
subcommand names do not correspond at all (get_connection_graphis
groove graph), andrerankis a per-call boolean while--rerankerpicks a
model. Neither shape is unusual —gh --labelfills the REST API'slabels,
anddocker --publishfills Compose'sports— so the rule is that the
mapping is predictable, not that the strings are equal.Values are held to a stricter rule, because a name that differs costs a
lookup while a value that differs fails the call outright:seed_strategynow
takesall_chunksandall-chunkson both sides. Copying either spelling
from one surface to the other used to be rejected — by clap on one side and by
unknown seed_strategyon the other. There is one table of accepted
spellings and both parsers read it, so a strategy cannot become reachable on
one surface only;--helpstill advertises the one spelling the command
line's own conventions produce.A test pins the pairing itself. Adding a parameter to either surface fails
until the table names its counterpart or records why it has none, which puts
the question in front of whoever adds it while the answer is still free. -
docs/ARCHITECTURE.mdstopped calling/uia disposable placeholder. It
still described the file as "a disposable placeholder — a proper redesign is
expected in Phase 3+" after that redesign had shipped. -
/uishows the knowledge-base path the way it was typed. Windows
canonicalisation returns an extended-length path, so the status band read
\\?\C:\noteswhere the operator had passedC:\notes. The prefix is now
stripped for display only;/api/admin/statusstill returns what it returned,
because the tray reads that field too. -
The README is an entry point again, and the reference it used to carry now
lives underdocs/. It had grown to 1,057 lines, of which 1,004 — 95% —
were configuration, CLI and client reference that a first-time reader has to
scroll past to reach "what is this and how do I install it". Those five
sections moved verbatim intodocs/configuration.md,docs/usage.md,
docs/clients.md,docs/mcp-tools.mdanddocs/behavior.md(each with its
.ja.mdpair), and the README is now 112 lines: what it is, how to install
it, a quick start, and an index of the rest.Links into the old sections change. Anchors that pointed at, say,
README.md#config-file-discoverynow live at
docs/configuration.md#config-file-discovery; the section names and their
anchors are unchanged, only the file is. Everything inside the repository
that referenced them was updated in the same commit.Two things are read outside the repository and were handled separately: a
re...
0.26.0 - 2026-08-17
Release Notes
Changed
-
BREAKING — the project is now GrooveSeek, and the command is
groove.
The old name collided inside its own category (github.com/moikas-code/kb-mcp
is also a knowledge-base MCP server) and bound the product to one of the two
ways it is read — a browser opening/uiis the other. The rename lands now
because the name is written into your filesystem, and after 1.0.0 changing it
would mean carrying a "look for the old name too" layer for all of 1.x.
Reasoning and the candidates that were measured and rejected:
ADR-0007.There is no automatic migration, and no aliases. A 0.26.0 binary does not
see anything left by 0.25.0. To carry an install over:Old New kb-mcp(command)groovekb-mcp.tomlgroove.toml.kb-mcp.db.groove.db.kb-mcpignore.grooveignore.kb-mcp-eval-history.json.groove-eval-history.json.kb-mcp-eval.yml.groove-eval.ymlKB_MCP_CONFIG_HOMEGROOVE_CONFIG_HOMEKB_MCP_TRAY_LOGGROOVE_TRAY_LOGKB_MCP_BINGROOVE_BINKBMCP_BENCH_KBGROOVE_BENCH_KBkb-mcp-svc/kb-mcp-traygroove-svc/groove-tray<config_dir>/kb-mcp/<service>/<config_dir>/groove/<service>/Renaming the files is enough — the formats did not change, so the index does
not need rebuilding. A service registered bykb-mcp service installmust be
uninstalled with the old binary beforegroove service installis run;
the new binary does not know the old registration exists..mcp.jsonentries need their"command"updated togroove. The MCP server
now identifies itself asgrooveseekinserverInfo.name.
Fixed
-
The watcher missed every file inside a directory that was newly created
under the knowledge base — on Linux. Copy a folder of notes into a watched
KB and its contents stayed unindexed until the next fullgroove index; the
directory event arrived, the files' did not.This is not a debounce or a deadline: the events are unobservable. inotify
watches are per-directory, so a file written into a directory that was created
microseconds earlier is reported by no watch at all — not the parent's, which
only names the directory, and not the new directory's, which is registered too
late. Measured on Ubuntu 22.04 with raw inotify: the file was on disk 0.79 ms
aftermkdir, and the earliest a watcher could register the new watch was
2.41 ms. Nothing insidenotifyrecovers it, so the watcher now looks inside
a directory once when it appears.Windows was never affected —
ReadDirectoryChangesWwatches the subtree from
a single handle — which is why this survived unnoticed until a Linux-only CI
failure.What gets indexed is decided by the full index walk's filter, now reachable
for a subtree, so a directory drop and a latergroove indexagree. The count
is logged: a directory drop is never a silent bulk index.
grooveseek 0.26.0
Download grooveseek 0.26.0
| File | Platform | Checksum |
|---|---|---|
| grooveseek-aarch64-apple-darwin.tar.xz | Apple Silicon macOS | checksum |
| grooveseek-x86_64-pc-windows-msvc.zip | x64 Windows | checksum |
| grooveseek-aarch64-unknown-linux-gnu.tar.xz | ARM64 Linux | checksum |
| grooveseek-x86_64-unknown-linux-gnu.tar.xz | x64 Linux | checksum |
groove-tray 0.26.0
Download groove-tray 0.26.0
| File | Platform | Checksum |
|---|---|---|
| groove-tray-x86_64-pc-windows-msvc.zip | x64 Windows | checksum |
groove-svc 0.26.0
Download groove-svc 0.26.0
| File | Platform | Checksum |
|---|---|---|
| groove-svc-x86_64-pc-windows-msvc.zip | x64 Windows | checksum |
0.25.0 - 2026-08-16
Release Notes
Added
-
kb-mcp graph --format dotand--format svg(E-3). The walk's value is
its shape, and neither existing format showed it:jsonandtextlist the
same nodes without saying where the search branched.dotemits a Graphviz program to pipe atdot -Tsvg, open in a DOT viewer,
or paste into a web one.svgis a finished drawing that needs nothing
installed — which is the point of having it. Nodes are coloured by BFS
depth, edges carry the similarity score, and both formats state when a limit
cut the walk short, so a picture is never read as the whole neighbourhood.No drawing dependency was added. A general graph would need a layout
engine, but this one is a tree — each node carries a singleparent_idand
the walk never reaches a node twice — so depth becomes the column and sibling
order the row, in one pass. The candidate crate was also last published 16
months ago; not needing it at all is the better answer.The formats live on a
graph-only enum. Sharingsearch's would have grown a
search --format dotwith no graph to draw.Escaping is worth a note for anyone extending this: the DOT grammar says the
only escape inside a quoted string is\", so a backslash is not an escape
character to the lexer — while the label renderer does read\nand\las
directives. On the reference corpus 74 of 8773 headings contain a double quote
and 4 contain a backslash, so both paths run on the first real graph rather
than in theory.
kb-mcp 0.25.0
Download kb-mcp 0.25.0
| File | Platform | Checksum |
|---|---|---|
| kb-mcp-aarch64-apple-darwin.tar.xz | Apple Silicon macOS | checksum |
| kb-mcp-x86_64-pc-windows-msvc.zip | x64 Windows | checksum |
| kb-mcp-aarch64-unknown-linux-gnu.tar.xz | ARM64 Linux | checksum |
| kb-mcp-x86_64-unknown-linux-gnu.tar.xz | x64 Linux | checksum |
kb-mcp-tray 0.25.0
Download kb-mcp-tray 0.25.0
| File | Platform | Checksum |
|---|---|---|
| kb-mcp-tray-x86_64-pc-windows-msvc.zip | x64 Windows | checksum |
kb-mcp-svc 0.25.0
Download kb-mcp-svc 0.25.0
| File | Platform | Checksum |
|---|---|---|
| kb-mcp-svc-x86_64-pc-windows-msvc.zip | x64 Windows | checksum |
0.24.0 - 2026-08-15
Release Notes
Added
-
kb-mcp evalreports a corpus that quotes its own golden set (D-12).If you keep notes about the evaluation inside the knowledge base being
evaluated, a note that quotes a golden query verbatim becomes the strongest
match for that query — it takes the top slot and pushes the labelled answer
down. The more you write about the evaluation, the harder it is to pass,
and until now nothing said so; the one case found on the reference corpus was
noticed only because someone happened to read the per-query rows.Each run now scans the indexed corpus once and reports documents that quote
two or more distinct golden queries verbatim, on stderr and in
--format jsonunderfindings. The exit code is unchanged — a quote is
either a note that leaked in or the source the query was written from, in
which case the document belongs in that query'sexpected, and only the
author of the golden set can tell which.Requiring two quotes rather than one is the whole design, and it is measured:
golden queries are often topic names (cross-encoder,torch.compile), which
appear verbatim in the documents explaining them, so reporting single matches
produced 8 findings, all false positives, on a healthy 662-document
corpus — where the rule as shipped produced exactly one, and it was the note
that was in fact documenting the golden set. Reasoning:
ADR-0006.
kb-mcp 0.24.0
Download kb-mcp 0.24.0
| File | Platform | Checksum |
|---|---|---|
| kb-mcp-aarch64-apple-darwin.tar.xz | Apple Silicon macOS | checksum |
| kb-mcp-x86_64-pc-windows-msvc.zip | x64 Windows | checksum |
| kb-mcp-aarch64-unknown-linux-gnu.tar.xz | ARM64 Linux | checksum |
| kb-mcp-x86_64-unknown-linux-gnu.tar.xz | x64 Linux | checksum |
kb-mcp-tray 0.24.0
Download kb-mcp-tray 0.24.0
| File | Platform | Checksum |
|---|---|---|
| kb-mcp-tray-x86_64-pc-windows-msvc.zip | x64 Windows | checksum |
kb-mcp-svc 0.24.0
Download kb-mcp-svc 0.24.0
| File | Platform | Checksum |
|---|---|---|
| kb-mcp-svc-x86_64-pc-windows-msvc.zip | x64 Windows | checksum |
0.23.0 - 2026-08-15
Release Notes
Added
-
kb-mcp doctor(D-8). Asks the index whether it is in the state it should
be, and reports; it never repairs.Search reads three tables that have to agree about a chunk — its text, its
embedding, its full-text row. When they stop agreeing nothing errors. A
chunk with no embedding is simply never a vector hit; one with no full-text
row is never a keyword hit. That this happens is not hypothetical —
backfill_ftsexists precisely to repair it — but until now the only way to
discover it was to run a full index and watch the repair go by.It also explains what the MCP resource surface is holding back: an extension
no longer in[parsers].enabled, a document larger than a resource read
returns, or a size not recorded yet because the document was indexed by an
earlier version. Those answers come from calling the server's own
paths_with_unregistered_extensionandServableRulesrather than
recomputing something equivalent — a doctor that answers a slightly different
question than the server is worse than no doctor.Report on stdout,
--format text|json, exit0(nothing to report),1
(findings),2(could not run — usually no index). Each finding carries the
command that fixes it. Not implemented on purpose: a--fixflag. The
narrower version of this contract already exists and already says report,
suggestkb-mcp index, never delete.Like
searchandeval, it opens the database, and opening one applies any
pending schema migration — read-only about its findings, not about the file.
Fixed
-
A document the resource surface offered could be one a read refuses.
Indexing accepts 50 MiB of text;resources/readreturns at most 1 MiB. A
Markdown or plain-text document in between was indexed, listed under its topic
group, given aurion itssearchhits — and refused when a client followed
that link. v0.22.0 recorded this as a known limitation because the only way to
know a file's size was to stat every indexed file on every listing, which
would have made an offer a live filesystem probe rather than a property of the
index.The index now knows the size.
documentsgains a nullablesize_bytes,
written wherever a document row is written, and the predicate that decides
what is offered applies the same per-extension cap a read applies —
max_bytes_for, the chooserload_document_blockingalready passes to
read_checked— so the listing and the read cannot enforce different limits.
A binary document over the text cap is still offered, because a read truncates
its extracted text rather than refusing it. Reasoning:
ADR-0005; the
principle it preserves is
ADR-0004's.resources/listand theurion asearchhit are now one predicate rather
than two calls that happened to agree. They each tested the parser registry
separately, which was harmless only while that was the whole rule; adding the
size condition to one of them is exactly what would have produced the defect
being fixed. The hit itself is unaffected either way — an unservable document
stays findable and simply carries no link.A file that grows past the index cap after being indexed is refused and
its new size recorded, by the full run and the watcher alike. A refusal
preserves the row, so otherwise the recorded size would stay the last one
small enough to index while the file became one no read can return. This is
knowable — kb-mcp stat'd the file in order to refuse it — unlike a file
deleted or replaced after indexing, which a listing still cannot answer for.Existing indexes: the column is added on open with every row NULL, which
means "not recorded" and is treated as servable, so nothing disappears from a
listing after an upgrade. Onekb-mcp indexfills it in without
re-embedding: the sizes come from the disk scan, so documents whose content
hash is unchanged — which is all of them, on a knowledge base that was just
upgraded — are backfilled even though that path writes no document row.
kb-mcp 0.23.0
Download kb-mcp 0.23.0
| File | Platform | Checksum |
|---|---|---|
| kb-mcp-aarch64-apple-darwin.tar.xz | Apple Silicon macOS | checksum |
| kb-mcp-x86_64-pc-windows-msvc.zip | x64 Windows | checksum |
| kb-mcp-aarch64-unknown-linux-gnu.tar.xz | ARM64 Linux | checksum |
| kb-mcp-x86_64-unknown-linux-gnu.tar.xz | x64 Linux | checksum |
kb-mcp-tray 0.23.0
Download kb-mcp-tray 0.23.0
| File | Platform | Checksum |
|---|---|---|
| kb-mcp-tray-x86_64-pc-windows-msvc.zip | x64 Windows | checksum |
kb-mcp-svc 0.23.0
Download kb-mcp-svc 0.23.0
| File | Platform | Checksum |
|---|---|---|
| kb-mcp-svc-x86_64-pc-windows-msvc.zip | x64 Windows | checksum |