A sovereign, single-binary knowledge base whose primary reader is a coding agent.
You edit plain markdown in your IDE. Your agent (Claude Code, Codex) searches it over MCP, and when it finishes a piece of work it writes back what it learned, a decision, a gotcha, a post-mortem, so the next agent inherits it. That write-back loop is the point: the knowledge base documents itself and gets smarter every run. Mesh has no reasoning AI inside it; it is the fast engine (parse, index, graph, retrieve), and the agent is the librarian.
It is one Go binary, no cgo, no external services. Retrieving from Mesh is cheaper than having the agent read whole files: it returns ranked cards (title + the matched snippet + why it surfaced) and packs the best bundle that fits a token budget, so the agent reads one note instead of three.
Source-code indexing also supports mesh code reindex <vault> --through-owner:
a deterministic, acknowledged refresh without stopping the live index owner.
See source freshness for the owner upgrade requirement
and the opt-in workspace catch-up timer for server-side merges.
- The core, zero models: cheap card-based retrieval (FTS + graph-BM25 + tier-0, pure Go, no inference, near-zero CPU) plus the agent write-back flywheel, in a single no-glue binary.
mesh_searchhands the agent ranked cards (title + snippet + why); the agent reads the cards and picks the 1-2 notes worth fetching. A capable coding agent is already a stronger relevance judge than any bolt-on reranker, so for the agent consumer the agent is the reranker, free. This is the whole product for an agent. - Optional BYOAI add-ons (off by default; Ollama is never required):
- Vectors (
mesh embed) lift recall on paraphrase queries where keyword search breaks (13/20 -> 19/20 on the private corpus behinddocs/BENCHMARK.md; read that file's caveats before quoting the number). Worth turning on when queries paraphrase the notes; can point at a cloud endpoint for zero local CPU, or be skipped (FTS keyword recall is already 23/25). - Rerank lifts top-1 precision for a consumer that trusts the top result without reading the cards. Use either a local/cloud cross-encoder, or let an already-authenticated Codex/Claude subscription rank 12 compact cards with its cheapest suitable model and return only the best 5. The subscription path needs no API key and no Ollama; it never scans the vault or receives full note bodies. See
docs/BENCHMARK.mdfor the measured cross-encoder arm.
- Vectors (
- Also shipped: a keyboard TUI (
mesh tui) and a browser app (mesh ui) over the same index, plus the client side of sovereign team sync (mesh join/mesh sync/mesh conflicts). The team-sync server those talk to is the commercial product and is not in this repository, see LICENSING.md. All optional; the solo, local core stands alone.
Mesh is a self-contained Go module (github.com/bright-interaction/mesh, no cgo,
no external services). One command:
go install github.com/bright-interaction/mesh/cmd/mesh@latest
That puts the binary in $(go env GOPATH)/bin, so make sure that directory is on
your PATH.
From source instead (Go 1.26 or newer):
git clone https://github.com/bright-interaction/mesh
cd mesh
make install # builds a static binary to ~/.local/bin/mesh
make install writes to ~/.local/bin/mesh; override with make install BIN=/usr/local/bin/mesh. make build drops it in ./bin/mesh instead if you
would rather not install anything.
Released builds check the public Go module tag at most once per 24 hours. When a
newer version exists, mesh tui and mesh ui show a one-line upgrade banner.
Prebuilt users can run mesh upgrade [vault]; Mesh downloads from that vault's
joined hub, verifies the published SHA-256 and embedded release identity, then
atomically replaces the client. Go users also get the exact pinned
go install ...@vX.Y.Z command. The check is silent on network
failure, never blocks the TUI from opening, and can be disabled with
MESH_NO_UPDATE_CHECK=1. Developer builds and commit-SHA builds without a stamped
release identity do not make the request.
Use mesh upgrade --check [vault] for a read-only check. Automatic replacement
currently supports macOS and Linux; Windows users receive the exact download URL.
This repository ships a small sample vault in vault/: the real decisions and
gotchas written while building Mesh. It is the fastest way to see what the tool
returns before you have written any notes of your own. From a clone:
mesh index ./vault # parse + build the index
mesh search "rerank" --vault ./vault --budget 4000 # ranked cards, not whole files
Six ranked cards come back (abridged here, paths shortened):
1. Blending fused score into rerank does not beat pure rerank [tier-0] (decisions/blending-fused-score-...md)
# Blending fused score into [rerank] does not beat pure [rerank] ## Context ## Decision ...
~ fts
2. Cross-encoder rerank is the answer@1 lever that chunking was not [tier-0] (decisions/cross-encoder-rerank-...md)
# Cross-encoder [rerank] is the answer@1 lever that chunking was not ...
~ fts
3. Learned fusion weights help the no-reranker path but wash out under rerank [tier-0] (decisions/learned-fusion-weights-...md)
... It matters most for a vectors-on, [rerank]-off deployment, where vector ...
~ fts
4. Per-section embeddings do not beat whole-note ... [tier-0] (decisions/per-section-embeddings-...md)
~ linked from Blending fused score into rerank does not beat pure rerank
5. ...
6. ...
packed 6 cards, ~854 tokens (budget 4000)
Cards 1 to 3 matched the text. Card 4 did not: it surfaced because the graph
links it to card 1. That one-hop expansion is the part plain full-text search
cannot do. The trailing ~ fts / ~ linked from line is the "why it surfaced"
reason, which is what an agent reads before deciding which single note to open.
Six cards for ~854 tokens, instead of six note bodies.
Now your own vault:
mesh init my-vault # bootstrap a vault (starter index + first build)
mesh new decision "Use Postgres over Mongo" \
--do "..." --dont "..." --why "..." --vault my-vault # capture judgment; Mesh fills id/date/placement
mesh index my-vault # rebuild the index after edits
mesh search "Postgres" --vault my-vault --budget 4000
mesh watch my-vault # live-reindex as you edit (no manual index; Ctrl-C to stop)
mesh doctor my-vault # is the index fresh? any drift or lint problems?
Search matches the words in your notes, so query with terms the note actually uses. Semantic (paraphrase) matching is the optional BYOAI vector stage below.
Connect the vault to your agent with one command:
mesh install my-vault --client codex # or: claude-code, claude-desktop, cursor, vscode, windsurf
Restart or reconnect that client. Mesh will introduce itself once, offer a 60-second tour, and then stay out of the way. Its permanent MCP instructions remain small, and retrieval stays zero-model unless you explicitly enable the optional subscription reranker described below. Claude Code also receives SessionStart hooks; the other clients need no project prompt-file edits.
mesh watch is the local-first, Obsidian-like immediacy: edit a note in your
editor and it is searchable at once, no commit, no manual mesh index. It
reconciles at startup, on every change (debounced), and on a periodic safety
tick that always converges, so a missed file event never leaves the index stale.
mesh doctor is the one to put in CI, so treat its exit code as the contract: 0 only
when the index is fresh and every note is actually in it. It exits non-zero when the
index is stale, when there is no index yet, when any note fails to parse, and when two
notes claim the same id. The last two matter most: such a note is invisible to search and
to the graph, so doctor names the offending files and reports status: BROKEN instead of
a healthy looking summary.
A missing owning writer is the one thing doctor reports without failing on. It always
prints the owner line (owner: NONE plus how to start one), but on a fresh, in-sync vault
that is a NOTICE and the exit code stays 0, because mesh init leaves exactly that state
and CI checkouts legitimately have nothing running. It is only a failure in combination:
an index that has already drifted with nothing running to catch it up exits non-zero as
status: STALE. mesh status never fails on a missing owner at all; it reports counts.
A note id is vault-wide, not per folder. Two files that resolve to the same id (two
README.md with no frontmatter id:, or a note copied as a template that kept its
id: line) cannot both be indexed, so Mesh keeps one, quarantines the other, and says
which is which: mesh index, mesh init, mesh doctor and mesh health all name the
file and exit non-zero. The fix is to give one of the two a different id, in its
frontmatter or by renaming the file, then reindex.
Already have a Foam / Obsidian-style vault? Bring it up to the Mesh schema in one idempotent pass:
mesh migrate my-vault # dry run: shows what it would change, writes nothing
mesh migrate my-vault --apply # synthesize ids, updated->when, lift ## Related into related:
mesh index my-vault
mesh migrate and mesh scope backfill rewrite every note in the vault in place,
so both are a dry run unless you pass --apply. They also exit non-zero if any
file failed, so a partial rewrite never reads as success in a script.
.mesh/mesh.db is built from your markdown. The markdown is the source of truth and
nothing lives only in the index, so deleting it never loses a note.
If a command reports file is not a database (26) or database disk image is malformed (11), the index file is corrupt. Those are SQLite's two ways of saying the same thing:
26 means the file is not SQLite at all (something wrote over it), 11 means the header is
fine and a page inside it is not (a crash mid-write, a bad sector, a copy taken while a
write was in flight). Both are handled the same way. Rebuild it:
mesh index my-vault # detects the corrupt file, discards it, rebuilds
Or do it by hand, which is the same thing:
rm -f my-vault/.mesh/mesh.db my-vault/.mesh/mesh.db-wal my-vault/.mesh/mesh.db-shm
mesh index my-vault
Two related failures that are not corruption and must not be fixed by deleting anything:
database is locked (SQLITE_BUSY)means another mesh process (usually amesh sync --watchormesh mcp --watchdaemon) holds the write lock. Wait and retry, or stop the daemon. A full reindex only holds it for a few seconds.no index at <path>means there is no database yet. Runmesh index <vault>.index schema mismatchmeans the database was written by a different version of Mesh. The index is derived, so Mesh rebuilds rather than migrating, and only a WRITABLE open can do that.mesh doctor,mesh ui(without--own-index) and the TUI open the index read-only, so they report the mismatch and stop rather than answering from a schema they do not match.mesh mcpis not in that list: it elects itself the vault's owning writer when nothing else holds the lock and opens the index writable, so starting it against an index stamped with an older schema version rebuilds that index in place. Nothing is lost (the index is derived from your notes), but do not expectmesh mcpto leave an out-of-date index alone. Runmesh index <vault>once after upgrading.
Embeddings are the one thing a rebuild costs you: they are kept across schema upgrades
precisely because re-creating them is a paid API call, but they cannot survive a file
that SQLite cannot read. After recovering a corrupt index, re-run mesh embed if you
use semantic search.
The core above needs no models. These stages are optional and off by default. Vectors improve paraphrase recall and require an embedding endpoint; rerank improves the order after local FTS + graph candidate generation and can use either an HTTP endpoint or a developer's existing Codex/Claude subscription. Ollama is one optional local endpoint, not a Mesh dependency.
For an existing MCP install, enable the small subscription model without editing shell startup files or adding an API key:
mesh rerank setup my-vault --client codex # Codex: Luna, low effort
mesh rerank setup my-vault --client claude-code # Claude: Haiku 4.5, low effort
mesh rerank status my-vault # executable check; no quota
mesh rerank disable my-vault # remove this vault's opt-in
Or opt in while installing Mesh:
mesh install my-vault --client codex --rerank-agent codex
mesh install my-vault --client claude-code --rerank-agent claude
Setup writes the provider, pinned small model, and auto policy into a 0600
user-private config, keyed by the vault's canonical path and outside both the vault
and project. It does not run the provider CLI, test authentication, or make a model call. Restart the client;
the first ambiguous search checks the existing subscription login. Before changing
the setting, the command says exactly what routed searches send to the provider.
Environment variables remain available for one-shot mesh search / mesh eval
use or custom deployments:
# 1. Vectors: embed notes via any OpenAI-compatible /embeddings endpoint (Ollama, etc.)
export MESH_EMBED_ENDPOINT=http://localhost:11434/v1
export MESH_EMBED_MODEL=nomic-embed-text
export MESH_EMBED_DOC_PREFIX="search_document: " # nomic-style asymmetric models
export MESH_EMBED_QUERY_PREFIX="search_query: "
mesh embed my-vault # one vector per note
# 2a. No-Ollama subscription rerank: no API key; uses the CLI's existing login.
# Codex defaults to Luna/low. Claude defaults to Haiku 4.5/low.
export MESH_RERANK_AGENT=codex # or: claude
# Optional token/latency caps (these defaults are already applied):
export MESH_RERANK_CANDIDATES=12 # local FTS + graph cards sent to the small model
export MESH_RERANK_RESULTS=5 # cards returned to the calling agent
export MESH_RERANK_CARD_CHARS=500 # maximum matched-snippet bytes per card
export MESH_RERANK_POLICY=auto # default: exact/strong FTS stays local; ambiguous calls the model
# 2b. Or use a cross-encoder endpoint (see tools/rerank-server).
unset MESH_RERANK_AGENT
export MESH_RERANK_ENDPOINT=http://127.0.0.1:8787/rerank
export MESH_RERANK_MODEL=Xenova/ms-marco-MiniLM-L-6-v2
mesh status my-vault # checks configured signals (subscription checks spend no quota)
mesh economics my-vault # local, content-free call/token/cache/fallback counters
Vectors are optional at query time. If the configured embedding provider is down, blocked by the endpoint security policy, or returns an incompatible vector, an ordinary search continues with FTS + graph and labels that fallback in CLI/API/MCP output. A cancelled request and a search with an explicit per-call vector weight still fail loudly, so benchmarks can never claim semantic retrieval when it did not run.
The subscription presets run non-interactively in an empty temporary directory,
disable tools, MCP servers, hooks, settings discovery, and session persistence,
and mark the process as a Mesh LLM child. This prevents the strict-JSON child from
loading the workspace's own Stop hook. Exact repeat rankings are cached and calls
are serialized within each Mesh process, preventing its concurrent searches from
becoming a quota burst. This path sends the query and compact card fields to the
selected provider; it is therefore opt-in and configured only by the user-private
per-vault file or local process environment, never by project or vault configuration.
Override the model with MESH_RERANK_MODEL and the timeout
with MESH_RERANK_CMD_TIMEOUT (default 90 seconds). auto is the subscription
default: an exact note/title lookup or a clearly separated full-text winner stays
zero-model, while an ambiguous slate is reranked. Set MESH_RERANK_POLICY=always
only for an evaluation or when reranking is a hard operator requirement. The strong
match threshold is tunable with MESH_RERANK_CONFIDENCE_MARGIN (default 0.45).
Both HTTP endpoints above are local, and that is the supported endpoint default:
an endpoint you pass on the command line or set in the environment is operator input, so Mesh dials
it as given, localhost and 127.0.0.1 included. The SSRF guard applies to the
endpoint someone else could have written for you: the [embedding] / [rerank]
fields in .mesh/config.toml, which the web UI's settings page rewrites over
PUT /api/config. If your endpoint lives there and is private, opt in explicitly:
export MESH_ALLOW_PRIVATE_LLM_ENDPOINT=1 # allow a private/loopback endpoint that
# came from config.toml or the web UI
Mesh names that variable in the refusal itself, so you never have to find this paragraph to get unstuck.
Vector search in this repository is a brute-force cosine scan, which stays under 5 ms well past a few thousand notes. The commercial build adds an approximate (HNSW) index for vaults large enough to need one, see LICENSING.md.
Once set, mesh search / eval / mcp fuse the semantic signal and route the
bounded rerank according to its policy. Turning a stage OFF is safe (no embedder
means lexical-only). HTTP rerank and subscription policy always fail loudly when
unavailable. Subscription policy auto returns the local ranking with an explicit
fallback receipt, opens a five-minute per-process circuit, and records the failure;
it does not repeatedly spend quota or pretend the model answered. Tune the circuit
with MESH_RERANK_FAILURE_COOLDOWN in seconds. Unset MESH_RERANK_AGENT and
MESH_RERANK_ENDPOINT to turn rerank off for real. Pointing an endpoint at a cloud
provider sends bounded note text off-box; enabling subscription rerank sends only
the query plus compact cards.
A ready-to-run local cross-encoder server lives in tools/rerank-server/.
Got a set of labelled queries for your corpus? mesh tune cases.json --test held-out.json grid-searches the fusion weights to maximize answer@1 and prints
the held-out result plus the MESH_WEIGHT_FTS/GRAPH/VEC line to apply the
winner. It tunes the fused ranking, so it helps most when you run vectors
without a reranker (with a reranker on, the cross-encoder owns the top result and
fusion weights wash out). Always pass a held-out --test set; tuning to the
queries you report on is how you fool yourself.
mesh economics also attributes the first mesh_fetch after a search to its
content-free rank and route (within that MCP session and a ten-minute window), so
real use reveals whether agents choose rank 1 or keep digging without retaining
queries or note content.
mesh eval cases.json --require-rerank-win adds the economics gate when a
reranker is configured. It replays every labelled query through both the normal
route and the identical local Mesh route, then requires equal-or-better recall@5
and answer@1, a lower combined median after adding the second call's provider-reported
tokens when available (otherwise the same bundled tokenizer estimate), and zero
fallbacks. A failing gate is a reason to keep that provider/model
opt-in, not to tune until the sample agrees.
Mesh speaks MCP (JSON-RPC) over stdio. Point your agent at:
{ "command": "mesh", "args": ["mcp", "--vault", "/abs/path/to/my-vault", "--watch"] }The agent then gets: mesh_search (fused, budget-aware), mesh_fetch (a note or one heading by anchor), mesh_god_nodes (the hub map to orient), mesh_changed_since (deltas on resume), and the write-back tools mesh_append_note / mesh_write_entity. mesh_append_note also accepts type: map; an overview in why plus related entry points renders as a readable front-door page rather than an empty scaffold. The retrieval contract (how to query cheaply, and to write back when done) is served as the MCP initialize instructions and the mesh://contract resource, so any agent uses it well without extra prompting.
That is the whole setup. The server elects itself the vault's owning writer
when nothing else holds the vault (a claim in <vault>/.mesh/owner.lock), so a
note it writes back is queryable immediately and no separate daemon is needed.
Start a mesh watch or mesh sync --watch beside it and that one takes
ownership instead; the MCP server notices, reads the index rather than writing
it, and routes its write-backs through the owner. Exactly one process indexes
either way, which is the point.
Local file events carry their exact changed paths to that owner, and
mesh sync --watch publishes them to the local index before requesting a hub
round. From v0.19 onward, a separate serial sync worker keeps indexing responsive
even when a previous hub call is slow. One queued follow-up coalesces edits made
during sync; completion triggers local discovery without requesting another sync.
Shutdown drains the active durable sync round before releasing the index owner.
Startup still performs an authoritative full rebuild to catch offline edits and seed the incremental cache. Its log separates indexing from lifecycle-health work. From v0.20, contradiction checks tokenize guidance once per pass and restrict comparisons to shared tags, preserving the existing findings and 0.6 similarity threshold. This reduces work at startup and on the five-minute health cadence without skipping checks, adding model calls, or weakening index validation.
Retriever setup and graph refreshes do not call embedding models. Actual semantic
queries validate vector width, and explicit health checks still probe reachability
and dimensions. Local write acknowledgements do not need an embedding endpoint.
From v0.21, the read-only MCP acknowledgement wait applies its existing
deadline to refresh-lock acquisition, note reads, SQLite snapshots, and retriever
construction too. A timeout preserves the saved note and returns an index_stale
receipt; it does not prove the owner is down. Canceled construction leaves the
previous graph/retriever pair intact. This bounds cooperative readback work, not
the file's durable write, owner recovery, or the owning writer's indexing pass.
Targeted indexing avoids rediscovering a known changed path; note creation still
scans vault-wide ID claims, and periodic/remote-triggered passes still scan the
vault as the convergence safety net. This is not an end-to-end write latency SLO.
From v0.22, slow note creation and owner indexing operations emit structured
phase timings through the logger (stderr by default, never the JSON-RPC reply).
Operations under one second remain silent. Longer operations log a summary when
they return; work still in flight logs its active phase every ten seconds.
operation, process pid, and process-local trace_id correlate progress with
the summary. Labels are static: these records contain no note content, titles,
paths, credentials, or raw errors. Nested operation times overlap; do not sum
them. returned means the function returned, not that the write succeeded.
Look for note_plan.id_scan before publication; note_create separates file
claim/write/fsync and collision checks. mcp_write separates related-note lookup,
publication, acknowledgement and telemetry. owner_reconcile/owner_targeted
include mutex waits, queued operations, indexing and health. index_full,
index_incremental and index_targeted break indexing into discovery/parsing,
graph construction, communities, persistence and code linking. These diagnostics
add no model calls and do not skip durability, collision or index-validation
checks. They locate stalls; they do not fix them or impose new deadlines.
From v0.30, ordinary reader refreshes reuse the installed graph/retriever when
both the database and the consumed retrieval configuration are unchanged.
A dedicated read-only SQLite connection samples
data_version before and
after construction; values are never compared across connections. In v0.30–v0.31,
every database commit invalidates reuse, including graph/code-link, vector and
telemetry writes.
This is deliberately conservative, not a note-hashes-only cache. The monitor
holds no transaction between samples and does not occupy the normal read pool.
Queued callers recheck after acquiring the reload mutex, coalescing duplicate
startup/watch refreshes. Exact-version acknowledgement graph loads are unchanged;
a successful stable acknowledgement can prime the next ordinary refresh.
Construction consumes an immutable snapshot of parsed configuration, environment
and user-local subscription preferences; a fresh input comparison detects
same-size/mtime edits without mislabelling a racing configuration read. No model
is called. Config/monitor/optional-vector-read failures disable reuse, and a
relevant commit during construction forces another refresh on the next pass. Inputs and
digests are not logged. mcp_refresh.freshness_check and retriever_config time
the lightweight local checks. Hot replacement of an open database file is not a
supported rebuild and requires a reader restart. Those v0.30 changes require no
schema migration, new writer, longer deadline or weaker publication check.
From v0.31, full reader graph loads preallocate their maps from node/edge counts
read in the same SQLite snapshot, with hints capped at 65,536 entries per map.
Larger graphs still grow normally. Row-scan scratch is reused without sharing
mutable node attributes or edges. A synthetic 24,000-node / 48,000-edge benchmark
allocated 58.5 MB/load instead of 69.3 MB/load (15.5% less); measured runtime was
roughly 109–112 ms in both cases, so this is an allocation reduction, not a
demonstrated latency improvement or end-to-end acknowledgement SLO. The capacity
query is timed as load_graph.capacity.
From v0.32, an optional persisted retrieval revision lets ordinary reader refreshes reuse their graph/retriever after bookkeeping-only commits. The owning writable opener installs one derived singleton table and 30 SQLite AFTER triggers, atomically. The base schema version stays unchanged: existing notes, vectors, telemetry and pending candidates are not rewritten. Old readers still work; new readers with an old owner/database fall back to whole-database invalidation until an upgraded writer installs the tracker. Activation therefore requires upgrading/restarting the owner, not just the MCP reader. Never start a second writer to install it.
Triggers cover notes, nodes, edges, vectors, corpus stats, code tables, note/code
links and metadata (except health_completed_at). They also run for writes from
older processes that know nothing about this protocol. Usage/reuse counters,
health findings, dropped/pending notes and FTS tables are read live and do not
invalidate cached graph/retriever state. FTS is not an in-memory retrieval cache.
Adding cached database inputs requires reviewing the tracking protocol.
Readers validate the exact tracker definitions when SQLite's schema version
changes, then sample the epoch/revision on that same short read snapshot. Missing,
altered or additional persistent triggers, invalid/missing revision rows and read errors prevent
targeted reuse; schema/epoch changes invalidate it. Canonical partial installs
can be repaired by the owning opener with a new epoch; unrecognized definitions
are left untouched. Tracker rows and SQLite's schema counter are reserved, not
operator-editable metadata. No snapshot is held between samples. Model/config handling,
exact-version acknowledgement loads and final publication checks stay unchanged.
Content-free logs report reader freshness tracking mode retrieval_revision_v1
or the conservative data_version fallback.
On a synthetic 3,000-node graph, a real metric commit plus refresh took 0.37–0.41 ms with tracking versus 7.64–8.44 ms without, allocating about 29 KB versus 5.39 MB. This is not a live acknowledgement SLO. Triggers have write cost: an isolated 1,000-row UPDATE took 0.95–1.08 ms versus 0.36–0.49 ms without tracking; a one-row UPDATE took 79–151 µs versus 59–84 µs (three runs of 50 iterations). These are Apple M3 microbenchmarks, not full indexing timings. Avoiding repeated reader rebuilds is the intended tradeoff.
From v0.33, full vector loads look up each note through its existing primary-key
index instead of building a temporary retrieval-hash index. The original exact
node-ID, current retrieval-hash and canonical-model filters remain; metadata and
rows still share one cancellable read snapshot, with chunks ordered by index.
The lookup slices the five-byte note: prefix as bytes to preserve unusual IDs,
including embedded NULs, and still rejects other namespaces. No schema migration,
owner restart, embedding call or change to acknowledgement checks is required.
At v0.33, on an Apple M3 synthetic fixture with 3,000 notes and 2,350 768-dimensional
vectors, full loads took 10–11 ms versus 12–13 ms with distinct hashes. With all
notes sharing one hash, loads took 9.5–11.2 ms versus 875–942 ms: primary-key lookup
avoids walking all same-hash candidates for each vector. Allocation was unchanged
at about 22.3 MB/load. These microbenchmarks do not establish a live startup or
acknowledgement SLO. Run go test ./internal/index -run '^$' -bench '^BenchmarkVectorLookup$' -benchtime=3x -count=2 to compare both query strategies.
From v0.34, vector row scans borrow the driver's byte buffer until decoding is finished, avoiding an intermediate copy. Only independently allocated float slices enter the retriever; borrowed bytes never survive the next row or close. Cancellation checks and deferred row closure release the borrowed-buffer read hold and discard canceled partial results. Metadata/row snapshot consistency, chunk order, exact vector bits and stale/orphan/model filtering are unchanged. No schema migration, owner restart or model call is required.
The same 2,350-vector fixture now allocates about 15.0 MB/load instead of 22.3 MB
(33% less), with about 14,205 allocations instead of 21,253. Allocation profiles
confirm the scan's intermediate bytes.Clone is gone; SQLite's BLOB allocation
and the final float storage remain. Alternating 50-iteration runs measured
9.3–16.4 ms versus 11.2–11.7 ms for the prior loader, so this is not a demonstrated
latency improvement or an end-to-end acknowledgement SLO. Reproduce the current
loader with go test ./internal/index -run '^$' -bench '^BenchmarkVectorLookup$/^shared_hash=false$/^primary=true$' -benchtime=50x;
use a v0.33 checkout for the old allocation baseline.
From v0.29, reader-side traces distinguish acknowledgement polling from snapshot
installation. mcp_acknowledge separates target parsing/hashing, version refresh,
final database/file verification and poll waits. mcp_refresh and
mcp_version_refresh separate the reload mutex, graph loading and installation;
mcp_install_graph separates note fingerprints, retriever construction,
reconciliation counts and publication lock/swap. load_graph_snapshot and
load_versioned_graph show read-transaction acquisition, version checking where
applicable, graph loading and transaction completion. load_graph splits node,
edge and degree work; retriever_build splits ranker construction, configuration,
stored vectors (including optional ANN construction), and reranker/weight setup.
Construction does not call a model. The same silent-under-one-second and static,
content-free logging rules apply; nested times overlap and cannot be summed.
This is diagnostic coverage, not a latency fix: the acknowledgement deadline,
single-snapshot version gate and final current-file checks are unchanged.
From v0.28, adding a new note checks the notes primary key inside the writer
transaction and skips deleting a nonexistent FTS row. FTS5's node_id is
UNINDEXED, so the former delete scanned the search table even for a new ID.
Existing IDs still replace their search rows; removals and full rebuilds are
unchanged. This relies on Mesh's atomic note/search persistence invariant, not a
cached ID set. A full rebuild repairs independently damaged/orphaned search rows.
No schema migration, search-ranking, model or durability changes are required.
persist_note_upserts now separates derivation, existing-note lookup, search-row
deletion, note writes and search writes. In BenchmarkNewNoteUpsert (3,000 notes
with ~9 KB bodies, Apple M3, two runs of five iterations), retaining the old
missing-key scan cost 6.8-9.3 ms versus 0.09-0.11 ms without it. Both arms run the
same upsert and roll back each iteration; this isolates the avoided scan, not
end-to-end writeback or the remaining cost of editing an existing note.
From v0.27, a note edit refreshes only the added/changed/removed note IDs in the
note-to-code bridge. It no longer rereads every note or replaces unrelated links.
Full startup and code-index refreshes still rebuild all links, since symbol changes
can affect any note. Both paths share the same title/raw-file token matching,
ambiguity rules and writer-transaction replacement; no model calls or schema changes.
The symbol resolver is rebuilt from current indexed symbols, not cached. Slow
note_code_links traces separate metadata, resolution, file reads and persistence.
On a synthetic 3,000-note / 12,000-symbol vault, single-note bridge refresh took
6.7-7.0 ms versus 97.8-97.9 ms for full refresh, allocating 3.6 MB versus 109.4 MB
(BenchmarkCodeLinkRefresh, Apple M3, two runs of three iterations). These are
bridge-only measurements, not end-to-end writeback latency or a fix for the
previous intermittent persistence stall. Linking remains a best-effort step after
the note/FTS/graph commit; a full refresh repairs a previously failed bridge pass.
From v0.26, slow persistence logs distinguish index_write queue wait from
index_transaction authorization, SQLite begin, callback, commit/rollback and
lease release. persist_full and persist_incremental separate notes/search,
graph and vector pruning; persist_graph_delta separates encoding from node and
edge work, with persist_graph_nodes / persist_graph_edges splitting scans,
deletes and upserts. index_checkpoint shows the existing periodic checkpoint.
PASSIVE avoids waiting for readers, but still performs page copies and possibly
file sync (SQLite contract).
Automatic checkpoints inside commit remain part of the commit phase.
These are diagnostics, not a concurrency or durability change: no new writers, queue priority, timeout extension or weaker fsync settings. They remain silent below one second, use static labels without content/paths/SQL/error text, and report progress every ten seconds. Nested timings overlap, and a returned trace is not proof of success. Correlate process IDs and time windows; transaction and caller traces have separate IDs. They distinguish where time was spent, not whether the kernel delay came from storage, memory pressure or CPU scheduling.
From v0.25, note planning reads identity headers with at most four concurrent readers per scan and merges results in traversal order. Short notes no longer reserve the full 64 KiB head-read ceiling. The scan still checks fresh disk contents, including unindexed notes and metadata-preserving edits; it does not trust a stale index or metadata cache to declare an ID free. O_EXCL publication, fsync, cross-type collision checks and cancellation behavior are retained. This reduces planning work; it does not bound filesystem latency or prioritize writebacks over a running owner sweep.
From v0.24, incremental and targeted indexing compare the whole rebuilt graph with the committed database and write only changed nodes and edges. This keeps global community and supersession changes correct while avoiding a full graph table rewrite for a one-note edit. Notes, search rows, and graph deltas still commit atomically. The comparison remains linear in graph size; full/startup indexing still uses the authoritative full rewrite. No model calls or schema migration are added. See the persistence benchmark for the measured scope and reproduction command.
From v0.36, graph comparison reuses SQL scan records and stores each edge key only once. In the 24,000-node/48,000-edge single-label-edit benchmark, this reduced allocated bytes from 41.9 MB to 28.7 MB per operation (about 31%) and removed about 72,000 allocations. Three alternating 30-iteration runs on Apple M3 measured 70.2-84.3 ms before and 69.3-71.1 ms after; these are isolated persistence measurements, not an end-to-end writeback latency guarantee. Whole-graph comparison, nullable-row repair, changed-row-only writes, and atomic notes/search/graph publication are unchanged.
From v0.23, the owning watcher's lifecycle-health analysis runs in one background pass per Store, outside the reconciliation mutex. Slow note reads, source-tree walks and Git checks no longer hold up the next indexing callback. The normal single database writer still publishes the complete dead-reference, overdue and contradiction report atomically; there is no second writer. A newly created index can have no health report until the first background pass succeeds.
Health attempts retain the five-minute cadence, coalesce while running, and retry
failed/changed-input passes no sooner than thirty seconds later. Analysis has a
two-minute cooperative deadline, including Git and cancellable reads; publication
has a separate two-second budget within that deadline. Indexed note/code identity
is checked again inside the publication transaction. Cancellation, changed inputs,
ownership loss or failed publication preserves the previous report rather than
replacing it with partial findings. meta.health_completed_at records successful
background publication; background_health logs separate snapshot, analysis,
contradiction and publication costs. Sustained edits or repeatedly slow analysis
can keep that report stale; explicit health commands remain available.
Store shutdown cancels and joins its health pass before closing database pools. A kernel filesystem read cannot be forcibly canceled: any late read-only result is isolated and discarded, never published. This change removes health analysis from the serial indexing path, not all CPU/I/O contention or the brief health publication transaction. Index persistence and code linking remain separate costs. These are cooperative budgets, not hard wall-clock guarantees: existing ownership metadata locks and kernel commit/fsync calls cannot be forcibly interrupted. Shutdown joins any admitted publication before closing its database, even if that takes longer.
From v0.35, mesh mcp --http handles SIGINT/SIGTERM by stopping new request admission and
giving admitted requests 20 seconds to finish, including write-back receipts.
After that grace period it cancels remaining requests and closes their connections,
then still joins their handlers before stopping/joining the watcher and closing
the store. Expiry is reported as a shutdown error. The 20 seconds bounds graceful
HTTP draining, not total process exit: uninterruptible I/O and cleanup can take
longer. Allow additional supervisor stop time; a forced kill can still interrupt
a write or lose its response. A missing response does not prove a write failed:
check the saved note before retrying. Stdio and the separate sync owner are unchanged.
The --watch flag runs the live reindexer inside the server, so notes you (or a
teammate) edit in your editor become searchable in the same session without a
restart. Watch progress goes to stderr; stdout stays the pure JSON-RPC stream.
Omit it and the index only refreshes on the agent's own write-backs. On a vault
somebody else owns, --watch re-reads what that owner indexed rather than
indexing itself.
Not sure anything is indexing? mesh doctor <vault> names the owner, or prints
owner: NONE with the fix when there is none. That on its own is a notice, not a
failing exit; doctor fails when the index has drifted with no owner to catch it up.
Share a vault across a team with no git on any client. The sync client is part of the open core; the team-sync hub is the commercial / pro product (hosted at mesh.brightinteraction.com, or self-host under a commercial license, see LICENSING.md). Clients pull-reconcile against it:
# On each teammate's laptop (against a hosted or licensed hub):
mesh join https://mesh.example.com <invite-token> my-vault # clone, no git needed
# ... edit notes in your editor ...
mesh sync my-vault # push yours, pull theirs
Reconcile-first: mesh sync is a three-way merge. Two people adding blocks to the
same page auto-merge; a true overwrite of the same lines keeps the hub version and
saves yours to a *.sync-conflict-*.md sibling to resolve by hand. Long note names
use a reserved .sync-conflict-base-v1 directory to keep every component within
the filesystem limit while preserving the exact original name. Use v0.17 or newer
to resolve these overflow conflicts; older clients retain the bytes but cannot
reconstruct the base path. Deletes and
renames propagate; the hub authors git history attributed to each user. Add
mesh sync --watch for real-time SSE push (the hub's changes pull in as they
land). A standalone mesh-curator worker can reconcile non-trivial conflicts with
the team's own BYOAI model, committing the merged note back through the normal sync
path (the hub itself stays AI-free).
Before uploading, v0.18+ checks notes against the hub's content limits (1 MiB,
no NUL bytes). Invalid notes stay untouched locally and are not marked
synced; other notes and incoming changes continue syncing. mesh sync names
each blocked path and its exact content problem. Correct the file to resume
uploads automatically. The watcher reports unchanged content problems once per
process, reports edited/reappearing problems again, and retains the outstanding
count on subsequent sync receipts. This does not suppress unknown hub-side
permission/scope rejections or change their retry behavior.
The team dashboard separates actual cross-user reuse from the older
same-user/later-session proxy. Agent-authored notes are attributed at their first
hub publication; successful hosted MCP fetches count immediately, while local
stdio MCP fetches upload a content-free, retry-safe event on the next mesh sync.
The hub retains only aggregate same/cross event relations, never the reader,
query, snippet, or note body. Hosted mesh_append_note writes are committed to the
team Git history before success is returned, so they reach every sync client rather
than remaining as untracked files in the hub worktree.
An audit pass changed how several commands behave. Each of these is deliberate, and each one is the kind of change that is confusing if you meet it without warning.
mesh migrate and mesh scope backfill are dry runs by default. They rewrite every
note in the vault in place with no backup, so writing is now opt-in via --apply. This is
the one most likely to catch you: a script that calls either of them bare no longer
rewrites anything, and it exits 0, so it looks like it worked. Add --apply.
Read-only surfaces refuse an index written by an older Mesh. mesh doctor, mesh ui
(without --own-index) and the TUI open the index read-only, and only a writable open can
rebuild a changed schema, so nothing ever migrated an upgraded index. They now say so and
name the fix (mesh index <vault>) instead of answering from a schema they do not match.
That matters most for mesh_health and mesh doctor, which reported a CLEAN vault over an
index whose quarantine table their binary expected and the file did not have. Run
mesh index <vault> once after upgrading; your notes are untouched. mesh mcp is the
exception: it now elects itself the owning writer, opens the index writable, and therefore
rebuilds a schema-mismatched index rather than refusing it.
mesh doctor exits 1 when a note does not parse. It used to report status: healthy
while holding zero indexed notes, because it counted only what had made it into the index
and an unparseable note never gets there. It now reports status: BROKEN and names how
many notes are invisible to search. If you gate CI on mesh doctor, it can now fail.
mesh index and mesh init exit 1 when two notes claim one id. Only one of the two
can be indexed. Both commands used to pick a winner silently, so mesh init printed
"1 notes" for two files and exited 0, and the loser was missing from search with no
signal at all. They now name the file they left out and fail, and the winner is stable:
whichever file already holds the id in the index keeps it, so a rebuild and a live
mesh watch never disagree about which note the id means.
mesh index can delete a corrupt index. A .mesh/mesh.db that SQLite refuses to open
used to dead-end every command including the one that rebuilds it. mesh index now
removes and rebuilds it, strictly when the failure is a corrupt database and never for any
other open error, such as a busy lock. Your notes are the source of truth; the index is
derived (see Recovery above).
mesh conflicts resolve --take-mine exits non-zero when the hub refuses your push. It
used to delete your parked conflict sibling and print that it had pushed, even when the
hub had rejected the path for a role, ACL, scope or size reason. It now keeps the sibling,
names the refusal, and fails. Wrappers treating exit 0 as "resolved" should be rechecked.
mesh curator log --status rejects unknown values rather than silently matching
nothing. Valid values are failed and resolved.
GET /api/search defaults changed to limit=20 and budget=8000, matching the MCP
tool. Results are token-packed now; previously budget=0 skipped packing entirely and
limit had no ceiling. limit is capped at 100.
The hub's curation activity endpoint takes ?cursor, so failed jobs older than the
newest page are reachable. A malformed cursor returns 400 rather than being ignored.
Search no longer hangs on a very large repetitive note. One multi-megabyte note of repeated text, a pasted deploy log or a concatenated transcript, could pin a core at 100% for minutes with nothing able to cancel it, because SQLite was being asked to pick the result excerpt and that costs roughly the square of the number of matches inside a single document. Mesh builds the excerpt itself now. Matching and ranking still see the whole note, so nothing becomes less findable, and the excerpt looks the same. A search that somehow still runs long fails after 10 seconds with a message rather than hanging.
The search query itself is now capped, on mesh_search, mesh_code_search,
mesh_code_context, GET /api/search and POST /api/ask. A query over 4096 bytes is
refused with a message naming the limit, and a query is read as at most 64 distinct terms
on every surface, the CLI included. Repeating a word never changed which notes matched,
only how long the search took.
Set up and capture:
| Command | Purpose |
|---|---|
mesh install |
One-shot setup: register Mesh and deliver a one-time in-agent welcome; Claude Code also gets its SessionStart hook |
mesh install --rerank-agent codex|claude |
Install and explicitly opt the local MCP server into the pinned small subscription model; no API key or setup inference call |
mesh install --remove |
The inverse: drop the mesh entry from that client's config (and the session hooks on Claude Code). Run it before deleting the binary |
mesh init [path] |
Bootstrap a new vault |
mesh new <type> "<title>" |
Scaffold a note (id, date, placement, skeleton auto-filled) |
mesh migrate [vault] |
Bring a Foam / Obsidian-style vault up to the Mesh schema (dry run unless --apply) |
mesh ingest <source> |
Pull external knowledge (GitHub, Slack, Linear, Jira, Notion) into the vault, incrementally |
mesh extract <transcript> |
Turn an agent session transcript into candidate write-back notes to keep or discard |
mesh hooks install |
Wire Claude Code session hooks: read Mesh at session start, nudge write-back at the end |
Index and retrieve:
| Command | Purpose |
|---|---|
mesh index [vault] |
Parse + persist the index (.mesh/mesh.db). Non-zero if a note was left out because another note claims its id |
mesh watch [vault] |
Live-reindex on every change (debounced + periodic reconcile) |
mesh embed [vault] |
Embed notes via a BYOAI endpoint (turns on semantic search) |
mesh search "<query>" |
Fused, budget-packed retrieval (semantic + rerank when configured) |
mesh ask "<question>" |
Answer a question from your notes + code with citations (needs a BYOAI model) |
mesh code <search|context|reindex> |
Source-code index: find a symbol by name (file:line), or pair it with the notes about it |
mesh orient [vault] |
Print a session orientation: entry points, recent changes, how to retrieve |
mesh mcp [--vault] [--watch] |
Serve the agent retrieval + write-back surface (live-reindex with --watch) |
Inspect and maintain:
| Command | Purpose |
|---|---|
mesh status [vault] |
Index row counts + which retrieval signals are active |
mesh version |
The commit this binary was built from, plus the Go version. Include it in a security report (see SECURITY.md) |
mesh lint [vault] |
Frontmatter / links / filenames (non-zero exit for CI) |
mesh doctor [vault] |
Index freshness (drift), counts, health. Non-zero if the index is stale or any note is invisible to search |
mesh health [vault] |
Knowledge lifecycle: dead source refs, overdue reviews, contradictions, plus notes missing from the index |
mesh structure [vault] |
Grade the vault's organization; --wire-orphans proposes only shared-tag-corroborated links by default |
mesh flywheel [vault] |
Write-back reuse metrics: does written-back knowledge get used again? |
mesh economics [vault] |
Content-free retrieval economics: call rate, accounted tokens, cache/fallbacks and search-to-fetch choices |
mesh rerank <setup|status|disable> |
Reversible local-only subscription rerank setup for an existing MCP registration |
mesh guards <list|suggest> |
Turn gotchas into candidate pre-commit guards (knowledge to enforcement) |
mesh scope backfill |
Stamp an explicit access scope on notes that have none (which notes a given member may see; dry run unless --apply) |
mesh eval <cases.json> |
Gate-1 retrieval measurement vs FTS baselines; --require-rerank-win also gates rerank economics |
mesh tune <cases.json> |
Learn fusion weights from labelled queries (validated on held-out) |
View:
| Command | Purpose |
|---|---|
mesh tui [vault] |
Keyboard three-pane terminal view (notes, ranked search, preview + neighbors) |
mesh ui [vault] |
Browser app (graph, search, docs, API reference) over the same index, localhost |
mesh serve-ssh [vault] |
Serve the TUI over SSH so a teammate browses the graph with ssh, no install (key-auth, fail-closed: binds 127.0.0.1:2222 by default, and --allow-anonymous is refused off loopback) |
Team sync. These are the client side and ship here, but they all talk to a team-sync hub, which is the commercial product and is not in this repository (see LICENSING.md):
| Command | Purpose |
|---|---|
mesh join <hub> <invite> [vault] |
Join a team vault and clone it (no git). Needs a hub. |
mesh sync [vault] |
Reconcile with the hub (push local edits, pull teammates'). Needs a hub. |
mesh conflicts <list|diff|resolve> |
Review and resolve local sync-conflict siblings. Needs a hub. |
mesh curator <log|show|accept> |
Review what the BYOAI sync-curator merged, and failed on, across the team. Needs a hub plus the commercial curator. |
go build ./...
go test ./...
No cgo. Storage is pure-Go modernc.org/sqlite in WAL mode; the .mesh/ index is a derived, deletable artifact, the markdown is the source of truth.
Open core, dual-licensed. This repository (the single-user vault, graph, retrieval,
viewers, CLI, MCP surface, and the sync client) is the Mesh Sustainable Use
License (fair-code, see LICENSE): free to self-host, use internally or
commercially, and run for your own clients; you just cannot resell it as a hosted
service.
The team-sync hub and BYOAI sync-curator are a commercial product:
- Hosted at mesh.brightinteraction.com (managed team sync).
- Sovereign self-host under a commercial license + support, for EU / regulated orgs running the hub on their own infrastructure.
A commercial license to the core is available for uses the Mesh Sustainable Use License does not fit. See LICENSING.md and docs/OPEN-CORE.md.