Download Latest Version v2.8.3 source code.zip (1.3 MB) Google Add to Preferred Sources
Home / v2.8.3
Name Modified Size InfoDownloads / Week
Parent folder
README.md 2026-08-16 23.8 kB
v2.8.3 source code.tar.gz 2026-08-16 1.3 MB
v2.8.3 source code.zip 2026-08-16 1.3 MB
Totals: 3 Items   2.6 MB 5

[2.8.3] - 2026-08-16

Security

  • qmd update no longer runs a project-local .qmd/index.yml's update: commands without approval (#886). That file arrives with a git clone and is adopted automatically for any command run inside the tree, so cloning a repository and running qmd update executed shell commands chosen by whoever wrote it. On a terminal QMD now lists the commands and asks; with nobody to ask it skips them and keeps indexing. Approvals are recorded per config file and per command set in <config dir>/trusted.json, so editing a command — or a git pull that rewrites one — asks again. New qmd trust, qmd trust list and qmd trust revoke manage approvals, and QMD_TRUST_UPDATE_HOOKS=1 opts unattended runs back in. Commands in your own ~/.config/qmd/*.yml, including anything qmd collection update-cmd writes, are unaffected.

  • The same project-local trust gate now covers collection path values that resolve outside the project and non-default models.embed / models.rerank / models.generate URIs (#889). In-project paths still index unattended; out-of-project directories are skipped until qmd trust, and custom model URIs are not loaded or downloaded. QMD_TRUST_LOCAL_CONFIG=1 opts unattended runs back in (and QMD_TRUST_UPDATE_HOOKS=1 still does). qmd collection add records trust as it writes, the same way update-cmd does.

  • Indexing no longer follows file symlinks or glob ../ / absolute patterns out of the collection directory. fast-glob already skipped symlinked directories, but a file symlink (or a mask like ../**/*.md) still resolved via realpath and ingested the target. qmd:// filesystem resolution uses the same containment check, so qmd://collection/../../../etc/passwd no longer produces a path outside the collection.

  • qmd mcp --http now validates the Origin and Host headers on every request and answers 403 when they name anything but a loopback address (#881). Binding to localhost is no defence against the user's own browser: a page can re-point its hostname at 127.0.0.1 (DNS rebinding) and read the indexed corpus through POST /query or POST /mcp. Requests with no Origin (curl, MCP clients, editors) are unaffected. Extend the allowlists with QMD_ALLOWED_ORIGINS / QMD_ALLOWED_HOSTS, or set QMD_ALLOWED_ORIGINS=* behind your own authenticating proxy. A wildcard bind (--host 0.0.0.0) skips the host check and warns at startup.

Changed

  • Dependencies: node-llama-cpp 3.18.1 → 3.20.0 (llama.cpp b8390 → b10361, 2026-08-11). Also safe patch/minors: picomatch 4.0.4 → 4.0.5, web-tree-sitter 0.26.8 → 0.26.12, tsx 4.21.0 → 4.23.12, vitest 3.2.4 → 3.2.7. No zod/vitest major; @modelcontextprotocol/server stays 2.0.0 (no 2.x patch). flake.nix FOD hashes are not updated here.
  • generate and query expansion now await LlamaContextSequence.dispose() before disposing the parent context. node-llama-cpp 3.20 made sequence dispose async; the library's context-onDispose path does not wait.

  • MCP server now speaks protocol revision 2026-07-28 via the official TypeScript SDK 2.x (@modelcontextprotocol/server). HTTP is sessionless (no Mcp-Session-Id, no initialize handshake, no idle-session TTL / [#816] reaper). Clients send version and capabilities in _meta; server/discover is implemented; Streamable HTTP POST requires Mcp-Method / Mcp-Name (mismatch → -32020); tools/list is deterministic and carries ttlMs / cacheScope. 2025-era stdio clients still work (serveStdio dual-speak); 2025-era HTTP initialize is answered per-request without minting a session. Existing tools (query / get / multi_get / status), stdio EOF shutdown, and named-index daemon PIDs are unchanged. No release.

  • qmd pull (and implicit model downloads in embed/query) no longer print node-llama-cpp's download progress bar. The bar redraws every few kilobytes and flooded agent transcripts with thousands of tokens (#776). Pass qmd pull --progress to show it on an interactive terminal.

  • --full-path no longer degrades silently when a result cannot be resolved on disk (#785). A fallback there means the file moved or was deleted since the last index, so search, query, get and multi-get now print a notice to stderr naming how many results fell back and suggesting qmd update; stdout stays machine-readable.
  • search/query now decide per result whether to show the docid under --full-path, matching multi-get and get: a result that resolved shows its on-disk path and no docid, one that did not keeps its qmd:// URI and its docid, so it is still addressable. Previously the docid was dropped for every row whenever the flag was set, leaving unresolved rows with neither a usable path nor an identifier.
  • search --format csv always emits the docid column, empty for rows that resolved to an on-disk path. Under --full-path the header previously dropped the column entirely — which also disagreed with the empty-result header, always printed with docid. Column positions are now stable across runs and formats.

Fixed

  • Concurrent first-open of a cold index no longer fails with table documents_fts already exists on Bun/macOS. FTS5 CREATE VIRTUAL TABLE IF NOT EXISTS is not atomic across WAL connections: two processes can both see a missing table on their schema snapshot and the loser throws. Table create and legacy-schema repair now use the same BEGIN IMMEDIATE + double-check as the FTS sync triggers, and treat a concurrent "already exists" as success when the table is present.

  • Nix flake qmd-node-modules FOD hashes updated for x86_64-linux and aarch64-darwin after the MCP SDK 2.0 bump. nix build / Nix GHA was failing with a fixed-output hash mismatch.

  • CJK FTS rebuild no longer skips leftover fts5(name, body, content='documents') tables when fts_cjk_normalized_version is already stamped, and schema repair now checks live FTS columns (PRAGMA table_info) as well as sqlite_master.sql. The MCP HTTP test helper still seeds that legacy table; startMcpHttpServer / createStore on it must not throw no such column: T.name (#792 regression).

  • qmd collection add --glob is no longer silently ignored. parseArgs ran with strict: false, so OpenClaw's --glob memory.md (and any other --glob) fell through, the default **/*.md was used, and a second collection on the same path collided as a duplicate instead of indexing the requested mask (#536). --glob is now an alias for --mask.

  • The bin/qmd trampoline now execs process.execPath instead of re-resolving node from PATH. Native addons (better-sqlite3) are compiled for the Node that installed qmd; a version manager (nvm, fnm, mise) selecting a different major in the working directory used to spawn that other binary and fail with NODE_MODULE_VERSION / ERR_DLOPEN_FAILED (#577 leftover; [#319]). bun bin/qmd still resolves node from PATH so Node-ABI addons are not loaded into bun.

  • qmd update / qmd collection add no longer swallow unreadable files silently (#460). readFileSync failures (ETIMEDOUT on APFS compressed files, EAGAIN, EACCES, …) still skip the file so the rest of the collection indexes, but the CLI now warns with the path and error code and reports the skip count. The SDK update() result includes skipped.

  • The architecture diagram no longer draws Vec expansions into BM25 search (#680). lex expansions are FTS-only; vec and hyde expansions are vector-only. The original query still goes to both backends.

  • qmd bench no longer runs to a wall of 0.00 when the fixture collection is missing or empty (#716). It errors up front with the same "Collection not found" / index hint as qmd search -c, and if every backend still scores zero it warns on stderr to check qmd ls.

  • qmd cleanup now reclaims the content and FTS space left behind after a wrong-directory qmd update. Deactivating files (the next update in the right directory) only tombstoned the documents rows; cleanup deleted those rows and vacuumed, but never dropped the unreferenced content hashes and never ran FTS5 optimize, so documents_fts_data kept the old bodies (#550). Cleanup now deletes inactive docs, then orphaned content, then compact FTS, then vacuum. --dry-run reports the content hashes too.

  • Concurrent query calls with rerank: true on a cold MCP server no longer race ensureRerankContexts(). Embed already serialized context creation; rerank did not, so two overlapping first queries both saw an empty pool, both created ranking contexts, and the inactivity timer disposed the loser (Object is disposed, [#682]). Callers now await the in-flight create.

  • Embedding-context pool size no longer assumes every GGUF costs 150 MB of VRAM (the nomic-embed figure). Larger models such as Qwen3-Embedding-0.6B are ~1190 MB per 2048-token context; opening 8 of those exhausted an 8 GB card so qmd query failed with Failed to create any rerank context even though the reranker itself was fine. The pool is now sized from the weight file, and 1 GB is reserved for the reranker (#799). Default embeddinggemma/nomic throughput is unchanged. QMD_EMBED_PARALLELISM still overrides.

  • Multi-collection -c A -c B (and SDK/MCP collections: [A, B]) no longer searches globally then post-filters. A large unrelated collection could fill the FTS/ANN top-k so the requested collections vanished, yielding false-empty results even though each collection matched on its own. searchFTS / searchVec now search each requested collection, then merge by score (#775). Single-collection exact-scan (#791, [#803]) is unchanged.

  • Query expansion no longer consumes caller intent, and a cached expansion whose sub-queries all miss is dropped instead of replaying forever (#818). Intent still steers reranking and snippet/chunk selection; it just no longer enters the expansion prompt or cache key, where the model copied meta-language ("so I can compare spend settings") into lex/vec terms that matched nothing.

  • Files whose names differ only in the characters the legacy slug collapsed to - (spaces, underscores) no longer evict each other from the index (#717). The handalized-path migration now skips any row whose path is still owned by a file in the current scan, so it only adopts genuinely stale pre-2.6 rows. qmd get and qmd ls <prefix> also match _ and % in paths literally instead of as SQL LIKE wildcards, so qmd get 2026_06_16.md no longer returns a sibling 2026-06-16.md.

  • CLI multi-get and SDK/MCP multi_get now share one comma-list resolver. Collection-prefixed paths (qmd/docs/SYNTAX.md) work in both transports, unanchored LIKE '%name' no longer silently fetches a different document for a filename fragment (NTAX.md ≠ SYNTAX.md), and ambiguous names across collections error with the candidate list instead of LIMIT 1 (#759).

  • The Nix flake wrapper now seeds the same pre-import env as bin/qmd. Nix installs exec bun src/cli/qmd.ts directly, so they previously skipped the launcher: qmd mcp could leak llama/ggml native logs onto JSON-RPC stdio, and Darwin CLI exits dumped a ggml Metal residency-set stack trace after an otherwise successful query. The wrapper now quiets those logs for mcp and sets GGML_METAL_NO_RESIDENCY=1 on Darwin unless QMD_METAL_KEEP_RESIDENCY=1 (#723).

  • Rerank context creation no longer swallows the real failure. A VRAM OOM or corrupt model previously produced only Reranker unavailable — skipping reranking (and a dead identical retry whose comment claimed it disabled flash attention, which ranking contexts never supported). The warning now includes the underlying message so the two cases are distinguishable (#782).

  • The Nix flake package now ships skills/ next to src/ in $out/lib/qmd/. findPackageRoot() walks up from the wrapped src/cli/qmd.ts looking for a sibling skills/ directory; without it, qmd skill show and qmd skills list always failed with "QMD skill not found" on Nix-installed binaries (#722).

  • /release step 1 no longer points at a missing script. skills/release/scripts/release-context.sh now exists: it silently installs git hooks and prints version info, working-tree status, commits and files since the last tag, [Unreleased], and the previous changelog entry. The skill's process list also drops the duplicate step 7 and checks dependency updates before cutting the release (#796).

  • store.searchVec() (and SDK searchVector()) now embed the query with the store's pinned embed model instead of the global QMD_EMBED_MODEL. A store created with a non-default models.embed previously failed with Dimension mismatch ... Expected N ... received M and loaded the wrong (often much larger) model at query time. Hybrid/precomputed/session search paths were unaffected and stay unchanged (#690).

  • qmd collection add --mask "a.md,*.txt" now indexes the union of each pattern. The comma-separated form was documented and commonly guessed, but the joined string was passed to fast-glob as one literal glob, so it matched zero files with no error. Brace form {a.md,*.txt} is unchanged. The same split applies on qmd update for stored comma-list masks (#557).

  • NixOS / immutable-root installs no longer crash qmd embed with EACCES when node-llama-cpp tries to compile llama.cpp into a read-only node_modules. The flake wrapper puts Nix's glibc and libstdc++ on LD_LIBRARY_PATH so prebuilt binaries can dlopen them, and getLlama() uses build: "never" when the llama directory is not writable (#574).

  • CJK FTS rebuild no longer raises "database is busy" when flushing insert batches. The streaming .iterate() cursor stayed open across BEGIN on the same connection; the scan now uses keyset-paginated LIMIT batches so each SELECT finalizes before the insert transaction starts (#797).

  • Quoted FTS phrases containing dotted tokens (e.g. "1.0.21") now match the indexed document. The porter unicode61 tokenizer stores dotted strings as adjacent parts, but phrase sanitization stripped the dots into a single token (1021) that could never hit. Dotted tokens inside quotes are split into adjacent phrase terms, matching the bare-term rewrite from [#563] (#757).

  • scripts/build.mjs no longer passes shell: true to spawnSync on Windows. With the default Node install path (C:\Program Files\nodejs\node.exe), cmd.exe split the unquoted process.execPath at the space, the tsc spawn failed, and prepare could still report success with no dist/ — leaving bin/qmd at "not built". The helper always receives a real binary path plus an args array, so no shell is needed. A spawn error now prints the missing binary path instead of failing silently. (#681)

  • Case-sensitive collections no longer collapse distinct document identities that differ only by path casing. The implicit COLLATE NOCASE legacy migration was unsafe for filesystems that contain both README.md and readme.md; case-only legacy migrations must now be explicit and operator-reviewed (#801).

  • cleanupOrphanedVectors now runs its orphan count and both DELETEs in a single immediate transaction. An interruption between the two DELETEs (crash, SQLITE_BUSY) could desync vectors_vec from content_vectors, leaving stale metadata rows that make a later reactivation of the same content hash look already-embedded — so qmd embed skips it and the document becomes silently unsearchable by vector, with no orphan left to clean up (#766).

  • qmd embed no longer splits a UTF-16 surrogate pair (emoji, etc.) across a chunk boundary. A chunk ending or starting mid-pair produced an unpaired surrogate in the chunk text, which some remote embedding APIs reject as invalid JSON — permanently failing that chunk on every retry, since the boundary calculation is deterministic. This covers both the character-based chunker and chunkDocumentByTokens's recursive re-splitting for astral-plane-dense content, which could previously drive the char budget low enough to reproduce the same split (#777).

  • insertContext looks up store_collections by name. [#754] retargeted the query from the dropped collections table but left WHERE id = ?, and store_collections has name TEXT PRIMARY KEY with no id column — the call threw no such column: id. Matches deleteContext / updateStoreContext (#853).

  • qmd collection add with no path argument now errors with usage instead of silently indexing the current working directory (#684). Pass . to index CWD, matching the documented examples.

  • qmd status reports orphaned embedding chunks, qmd update hints when they exceed 10% of vectors, and qmd cleanup --dry-run previews what would be removed. Incremental update still does not auto-prune vectors (a transient empty mount would otherwise force a full re-embed) (#768).

  • qmd --index <name> mcp --http --daemon now scopes PID/log files per index (mcp-<name>.pid) and passes the resolved database path to the child, so a named-index daemon no longer collides with the default mcp.pid or opens the default store (#772).

  • Opening a store no longer throws SQLiteError: no such column: T.name when documents_fts is still the legacy fts5(name, body, content='documents') schema. CREATE VIRTUAL TABLE IF NOT EXISTS left that table in place, and the CJK FTS rebuild's DELETE FROM documents_fts compiled against documents.name, which does not exist (#792).

  • Rerank cache keys now include the resolved models.rerank URI, so swapping the configured reranker no longer serves the previous model's cached scores (#764).

  • vsearch -c <collection> no longer returns empty results for small collections crowded out of the global ANN candidate pool. searchVec now exact-scans the collection's vectors with vec_distance_cosine when the set is within 20k rows (ANN + post-filter cannot see collections that never enter global top-k, and sqlite-vec caps k at 4096 so a larger multiplier alone is not enough). Larger collections still use capped ANN over-fetch (#791, [#803]).

  • multi-get --format files now emits the docid as its own CSV field (#docid,path,...) instead of prepending it into the path field with a space (#docid path,...), matching search --format files and keeping naive comma-splitting usable (#760).

  • qmd embed now takes an exclusive process lock (.qmd-embed.lock next to the index DB) so concurrent invocations no longer race on vectors_vec and fail with UNIQUE constraint failed: vectors_vec.hash_seq. A second embed exits early with Another embed process is already running. Skipping. Stale locks from crashed processes are recovered via PID identity checks (#825).

  • windows prepare fix [#778] keeps dist build [#824]

  • qmd mcp (stdio) now shuts down gracefully when stdin reaches EOF instead of orphaning to PID 1 when the parent MCP client dies (#751): the server closes its transport, gives in-flight request handlers a bounded window to settle, closes the store (which disposes its llama.cpp instance), and lets the process drain via process.exitCode (no forced process.exit(), which has caused exit-time native crashes before).

  • qmd --version no longer reports an unrelated repository's commit (#787). The commit was discovered at runtime with git -C <installDir> rev-parse, and git -C walks up, so any install nested inside another checkout reported that checkout's HEAD — a global npm install under a git-managed prefix such as Homebrew's /opt/homebrew claimed Homebrew's commit as qmd's. Identical tarballs reported different "commits" depending only on where they were installed, which is why one issue can collect three distinct hashes for the same published build. scripts/build.mjs now stamps the commit it built from into dist/cli/build-info.json (suffixed -dirty when built from a modified tree), and the runtime prefers that. Source checkouts still resolve their own HEAD, but only after confirming the enclosing repository is qmd's; anything else reports no commit rather than a misleading one. The lookup also no longer interpolates the install path into a shell string, so a path containing a space stops silently dropping the commit, and --version from a build runs no subprocess at all.

  • qmd doctor no longer false-positives .etag HTTP sidecars (written by qmd pull next to each download) as invalid GGUF models. The model-cache check now only inspects real .gguf files, so a sidecar that happens to sort before the blob no longer poisons the report. [#812]

  • qmd mcp stop and qmd mcp --http --daemon now verify that a pidfile PID still belongs to a qmd process before signalling it or refusing to start. Recycled PIDs (common after reboot) are treated as stale: the pidfile is unlinked instead of SIGTERM'ing an unrelated process or blocking daemon start with a false "Already running" error (#806).

  • qmd collection add now rejects missing paths and regular files before creating collection configuration or index state. The error reports both the received and resolved path so malformed shell arguments can be corrected.

  • Claude Code plugin: scope the plugin source to ./skills so installs copy just the skills (~50 KB) instead of the entire repository. Previously a canonical install materialized ~230 MB / 9,000+ items into ~/.claude/plugins/cache/ — including a full npm dependency install triggered by the repo-root package.json. Both skills (qmd and release) still ship, unchanged. (#790)

  • Claude Code plugin: releases now bump the plugin version in .claude-plugin/marketplace.json in lockstep with package.json. The plugin cache is keyed on this version, and it had been stuck at 0.1.0 since February — so installed plugins never received skill updates (users who installed in February are still being served that snapshot today, despite the qmd skill nearly tripling in size since). Also bumps the plugin to 2.6.3 as a one-time catch-up so existing installs pick up the current skill on their next claude plugin update. (#789)

Changed

  • --full-path no longer degrades silently when a result cannot be resolved on disk (#785). A fallback there means the file moved or was deleted since the last index, so search, query, get and multi-get now print a notice to stderr naming how many results fell back and suggesting qmd update; stdout stays machine-readable.
  • search/query now decide per result whether to show the docid under --full-path, matching multi-get and get: a result that resolved shows its on-disk path and no docid, one that did not keeps its qmd:// URI and its docid, so it is still addressable. Previously the docid was dropped for every row whenever the flag was set, leaving unresolved rows with neither a usable path nor an identifier.
  • search --format csv always emits the docid column, empty for rows that resolved to an on-disk path. Under --full-path the header previously dropped the column entirely — which also disagreed with the empty-result header, always printed with docid. Column positions are now stable across runs and formats.
Source: README.md, updated 2026-08-16