Repository navigation
Releases: MatthewSherlin/mushroomdb
Release list
v0.9.2
No format version change. A 0.9.1 store opens under 0.9.2, and a 0.9.2 snapshot opens under 0.9.1: the one addition, an optional snapshot section (16), is ignored by 0.9.1.
Upgrade if a store was written by 0.8.0 with a top-k rule beside another rule over the same edge type. 0.9.1 left a top-k edge that 0.8.0 removed while no rule counted it missing until its source changed; it now comes back at the first write after open (row 113). A snapshot also records that the repair has run, so a store with top-k rules — every agent-memory store — does not pay for it at every open (row 116). Four smaller corrections follow (rows 50, 84, 112, 115).
Fixed
- A top-k edge that 0.8.0 removed while no rule counted it comes back at the first write after open. 0.9.1's repair found only edges a rule still counted, so this case needed a change to the source or
rebuild_rule. The first write after open now re-evaluates every top-k source holding fewer than its cap (row 113). rebuild_ruleno longer drops an edge another rule still desires. When a rule was explicitly rebuilt (or rebuilt after index drift),apply_streaming_rebuildandapply_via_rebuildretracted stale edges but did not push them to the reclaim queue, so a competing rule never got the chance to take them. The fix adds the push; the second-pass reclaim now fires for rebuild the same way it already did for incremental writes (row 84).- An
open_athandle on a store snapshotted while two rules both owned the same edge now shows one owner, not two. The row 107 dedup ran on the first write (viaensure_provenance_loaded_mut) but not when the provenance was decoded for an as-of handle viafrom_persist. The fix callsdedup_provenanceinsidefrom_persist(row 112). forgetof a property no longer misreports the derived-edge count on a store row 111 repaired. Before the fix, the snapshot's provenance was decoded into the as-of handle but not reconciled, so the derived-edge count it saw could differ from the live count. The fix runs row 111's repair before computing the report (row 115).- A snapshot records that the repair of 0.8.0's lost edges has run, so an open does not repeat it. The repair re-evaluates every top-k source short of its cap, which on a store with such rules — every agent-memory store — cost each open about 40 %. A snapshot whose state has had the repair now carries an optional section 16, and an open of it skips the repair; a snapshot written before this release is repaired at each open until the next snapshot. 0.9.1 ignores the section (row 116).
delete_nodecounts the correct number of derived edges when the deleted node is only a via-hop.provenance_touchingfound edges where the node is src or dst, but missed edges where the node is the via hop. The fix counts the edges the apply's own deltas retracted instead (row 50).
v0.9.1 — stores 0.8.0 had damaged get their edges back
No format version change. A 0.9.0 store opens under 0.9.1, and a 0.9.1 snapshot opens under 0.9.0.
Upgrade if a store was written by 0.8.0 with two rules over one edge type and a snapshot. 0.8.2 repaired such stores where both rules still held the edge; this release repairs the ones where 0.8.0 had already lost it, at the first write after open (row 111). It also fixes three smaller things a first write after open got wrong on a snapshotted store (rows 108–110).
Fixed
- A store in which 0.8.0 had already lost a derived edge gets it back at the first write after open. Before 0.8.1, a second rule over the same edge type could take an edge from the first and then let it go, removing the edge while the first rule still counted it (row 103). 0.8.2 repaired stores snapshotted while both rules held the edge, but not ones where the edge was already gone: the rule counted an edge the graph did not hold, and no later write brought it back. The first write after open now drops what a rule counts and the graph does not hold, and re-derives those sources, so every edge a rule still counted is back, live, after a reopen and through
open_at. History shows the edges returning at that write. Until it, reads show the store as it was opened. Deleting and recreating both rules is no longer needed (row 111). A top-k edge that 0.8.0 removed while no rule counted it comes back when its source changes, or withrebuild_rule(row 113). stats().edgesno longer reads low after a rule lets go of an edge the graph did not hold. Each such retraction counted as a removed edge until the next snapshot (row 109).- The first write after opening a snapshotted store sees which rule owns an edge. On that write,
insert_edgeof an edge a rule derived answeredOk(false)instead of refusing it as rule-owned, in a batch as well, anddelete_nodereported the deleted node's rule-derived edges as manual (rows 108, 110). On a store where 0.8.0 lost edges,delete_nodeno longer counts the lost ones (row 111).
v0.9.0 — the Rust library is mushroomdb
No format version change, and nothing changes for the CLI, the MCP server, HTTP, Python or TypeScript: a 0.8.2 store opens under 0.9.0 and the other way round. The minor version moves because one thing breaks for an embedded Rust user, below.
Changed
-
The Rust library is imported as
mushroomdb, notcore_api. The crate on crates.io has always beenmushroomdb, but its library was namedcore_api, so a project that rancargo add mushroomdbhad to writeuse core_api::…. It isuse mushroomdb::…now. What to change depends on how yourCargo.tomlnames the dependency:mushroomdb = "0.8"(whatcargo add mushroomdbwrites): bump the version to0.9, and changecore_api::tomushroomdb::in your code. This is the change that breaks.core-api = { package = "mushroomdb", … }: bump the version; nothing else. The dependency key sets the name you import it under, souse core_api::…keeps working. Rename the key tomushroomdband your imports tomushroomdb::when it suits you.
Nothing changes for the CLI, the MCP server, HTTP, Python or TypeScript, and the on-disk format is identical: no store, WAL, snapshot or response carries the library's name.
v0.8.2
No format version change. A 0.8.1 store opens under 0.8.2, and a 0.8.2 snapshot opens under 0.8.1 and 0.8.0.
Upgrade if two of your rules derive the same edge type and you snapshot. 0.8.1 stopped a second rule from taking an edge another rule had derived before a snapshot; this release repairs a store 0.8.0 had already snapshotted in that state, at the first write after it opens, and counts every snapshot's edges from its rows so a wrong count corrects itself at the next snapshot (row 107). A store in which 0.8.0 had already lost such an edge is not repaired: delete and recreate both rules to restore it (row 111). It also closes the last case of a top-k rule losing its next-best edge beside another rule (row 83).
Fixed
- A store snapshotted while two rules owned one edge is repaired at the first write after open.
On stores written before 0.8.1 (row 103) where a snapshot was taken while two rules both owned
the same edge: the double ownership survived in the snapshot's provenance section, the edge count
in the snapshot header was one too high per affected edge, and a write that caused the second rule
to retract silently removed the edge even though the first rule still wanted it. The first write
after opening such a store now deduplicates provenance, keeping the first owner in byte order on
rule name; until that write,stats().rules,explain, andopen_atstill show both owners.
The deduped edge keeps the dropped rule's weight property until the edge is next re-derived.
The snapshot encoder recountsedge_countfrom the merged CSR rows instead of carrying the header
value forward arithmetically, so every new snapshot's header is correct.stats().edgesand
edge_count()use the row count rather than the header on stores with a V8 base. Stores where
the edge was already lost before this release (the losing write ran on 0.8.0 or 0.8.1) are not
repaired; delete and recreate both rules to restore those edges (row 111 will reconcile at the
first write automatically) (row 107). - A via-hop top-k rule keeps its next-best edge when its best match was another rule's. The fix 0.8.1 made for plain top-k rules, now for a via-hop one: when its best match was an edge another rule owned and that match went away — a changed property or the destination deleted — the next-best edge stayed missing until
rebuild_rule(row 83).
v0.8.1
No format version change. A 0.8.0 store opens under 0.8.1, and a 0.8.1 store opens under 0.8.0.
Upgrade if two of your rules derive the same edge type. Every entry under Fixed is a rule-engine defect in 0.8.0 and earlier, found by running the same writes against a fresh build of the same nodes and comparing. Two of them left stats counting derived edges that neighbors and queries did not return (rows 103 and 106), one deleted an edge you had written yourself (row 103), and the others left an edge missing or one that should have gone (rows 83, 84, 104, 105). A store that 0.8.0 snapshotted while two rules owned one edge is not repaired by this release: its edge count can stay too high, and the repair is owed to 0.8.2 (row 107). A store whose double ownership is still only in its WAL is repaired when 0.8.1 opens it.
Also here: the skill an assistant reads names the store's path once and fits its budget on a long path, and a memory-shaped run to 1,000,000 nodes is published in dogfood/results/scale-memory-1m.md.
Changed
- The skill names the store's path once. The skill an assistant re-reads on every turn spelled the store's absolute path out six times, so on a typical project path it ran past its 6,000-byte budget; the commands it shows now write the store as
'<db>'and the path is given once. The test that holds the skill to its budget renders a 102-byte path where it rendered a 17-byte one (row 68).
Fixed
- A top-k rule keeps its next-best edge when its best match was another rule's. Two rules over the same labels and edge type, one of them top-k: when the top-k rule's best match for a source was an edge the other rule already owned, it recorded nothing and dropped its own next-best edge, and when that match went away — a changed property or the destination deleted — the next-best edge stayed missing until
rebuild_rule. Not yet for a via-hop top-k rule (row 83, in part). - Derived edges are where the rule counts say after a snapshot. On a store opened from a snapshot, a rule that desired an edge another rule over the same edge type had derived before the snapshot recorded it as its own too; the edge was counted twice, and when that second rule let go of it the edge went from the graph while the first rule went on counting it —
statsreported edgesneighborsdid not return. The same happened to an edge you had written yourself before the snapshot: a rule that desired it took it as its own, and deleted it when it stopped desiring it. A storemainsnapshotted while two rules owned an edge is not repaired by this release, and itsstatsedge count can stay too high (rows 103 and 107). - Deleting a top-k rule's best match promotes the next one. A KeyMatch top-k rule whose source named the deleted node chose it again: the rule's edge count included an edge the graph did not have, and the next-best match was not taken (row 106).
- Deleting a via node removes the edges derived through it. A via-hop rule's edge stayed after the node it hopped through was deleted (row 104).
- A via-hop rule that links a label to itself derives the edge into a new node. Inserting nodes in one order derived the edge and in the other did not (row 105).
- A via-hop rule takes back an edge another rule gave up. When a plain rule stopped deriving an edge that a via-hop rule over the same edge type also derives, the edge was removed and not added back; the via-hop rule is now re-evaluated for that source (row 84, in part).
v0.8.0
No format version change. A 0.7.2 store opens under 0.8.0, and a 0.8.0 snapshot opens under 0.7.2, which ignores the full-text index the snapshot now carries and builds its own.
Upgrade if you snapshot a store with no full-text pair, property index or multiplicity declared. In 0.7.2 and earlier, an archiving snapshot taken with nothing written since the last one — mushroomdb snapshot twice, a serve --snapshot-every tick on an idle store, a graceful shutdown just after a snapshot — wrote an empty file over the previous WAL archive: its history was lost, and reads by date could answer an old commit with a later state. A store created by mushroomdb mcp or serve since 0.7 declares the memory schema and was not exposed. What was lost is not recoverable (row 100).
The rest of what is here:
- a store with text opens about nine times faster once it has a snapshot;
mushroomdb mcpsnapshots a store whose WAL has grown;- binaries for Intel macOS, musl Linux and Windows;
- a TypeScript client that wraps every route;
- five more corrections, three of them to snapshots.
Each engine change was given to an independent reviewer that ran the same writes two ways and compared the answers; what each review found, and what was done about it, is in the defect ledger row named beside the entry.
Added
- Three more platforms. A release now carries binaries for Intel macOS, for x86_64 Linux with musl (Alpine and images with no glibc) and for x86_64 Windows, beside Apple Silicon and the two glibc Linux targets;
npx mushroomdbandinstall.shpick the right one. Windows had never been built: the storage layer synced a directory by opening it as a file and flushed the WAL through a read-only handle, both of which Windows refuses. Each new target is built on every pull request and run through a demo store, a query, three snapshots and a backup. The test suite itself still runs on Linux and macOS only, and there are no wheels for the new targets yet. On Windows that is the binary and the launcher:mushroomdb installhas not been run there and the hooks it writes are shell scripts, and a store belongs on a local NTFS disk. - The TypeScript client now wraps every HTTP and WebSocket route the server exposes. Fifteen methods are new:
health,metrics,findSimilar,createNode,deleteNode,nodeEdges,nodeHistory,edgeHistory,wasLinked,createEdge,deleteEdge,setNodeProp,removeNodeProp,backup, andwatch.querygainsas_of,maskandnamespaceoptions for time-travel, key-masking and namespace-scoped reads.watchstreams the rawMutationEventlog overGET /watch; close resolves within 3 s even when the server-side loop does not echo the close frame.
Performance
- Opening a snapshotted store no longer rebuilds its full-text index. A snapshot of a store with a declared pair carries the index, and the open reads it and applies what was written since. Measured on release builds: a 30,000-entity store with three pairs opens in 21 ms where it took 183 ms, and
mushroomdb statson a code graph of this repository (11,197 nodes, 13 pairs) takes 30 ms where it took 252 ms. Since 0.7 every new store declares the memory schema's pairs, so every store with text paid the rebuild on every open. The snapshot grows by the index, about a third on the first of those stores, and takes longer to write by the encode (65 ms to 82 ms there). No container version change: 0.7.1 opens such a snapshot, ignores the index and builds its own. A store that has never been snapshotted, or whose snapshot predates this, builds the index at open as before (row 36). - Replaying a snapshot's declarations no longer builds the rule indexes. Every truncating snapshot re-emits one WAL record for each declared pair, and replaying those counted as a write: a store with any declaration scanned every node at open to build the indexes rules match through, where a store with none defers that to its first write. It was 46 of the remaining 52 ms on the code-graph store above (row 36).
mushroomdb mcpsnapshots a store whose WAL has grown. Nothing snapshotted a store that was only ever used through the MCP server, which is how an assistant's memory is used:servehas a timer and its shutdown, and the MCP server had neither, so each session opened the store by replaying everything written to it since it was created. It now takes the automatic snapshot when it opens a store whose WAL is past 4 MiB, and again when its session ends. The WAL is archived, as for every snapshot mushroomdb takes on its own, sonode_history,was_linkedand reads by date reach as far back as before; the store directory gains onewal.<N>.archiveper snapshot. Skipped, silently, when another process holds the write lock, and by the servermushroomdb doctorstarts to check that it answers;MUSHROOMDB_NO_AUTO_SNAPSHOT=1in the server's environment turns it off.
Fixed
- An insert that names a property twice indexes the value that is stored.
insert_nodewith the same field twice, or a CypherCREATEwith a repeated key, stores the last value and used to index both: the first value's words stayed searchable until the store was next opened. - A read sees a view's value as of the commit it reads at. The lock-free reader —
reader()in the Rust API, and what the HTTP server answers a request with a role token from (/query, a node, its edges, its neighbourhood) — returned a view's property as it stood at the reader's last fold, which happens every 64 commits. Until then a node read back with no value for a view created since, an old count or sum after an edge or a neighbour's property changed, and a deleted view's property still on it. A read through the handle itself, which is every other read, was always current. The writer now hands each commit's view values to the reader with the commit (row 97). - A snapshot of a store with nothing new no longer destroys its history. Two archiving snapshots with no write between them —
mushroomdb snapshottwice, aserve --snapshot-everytick on an idle store, a graceful shutdown just after one — wrote an empty file over the previous WAL archive.node_history,was_linkedand reads by date lost everything that archive held, and because the commit numbers were then handed out again,asofandquery … as_ofcould answer an old commit with a later state whilestatsreported complete history. It affected a store with no full-text pair, property index or multiplicity declared, so not a store created bymushroomdb mcporservesince 0.7, which declare the memory schema; a store fromdemo, the Rust API or Python with no declaration was exposed. Such a snapshot now leaves the WAL and the archives as they are. What was overwritten is not recoverable (row 100). - A store's first archiving snapshot can be interrupted without costing its reads by date. The marker that lets
asof,open_atandquery … as_ofread commits out of the archives was written last, and a snapshot killed before it —mushroomdb snapshot, aserve --snapshot-everytick or a shutdown — left a store that never got one: every commit in an archive wasout of rangefrom then on, withstatsreporting complete history and nothing else wrong. It is written first now. A store this has already happened to is not repaired: nothing in it distinguishes it from one whose history was cut by a truncating snapshot, and reading such a store's archives as if they were whole would answer wrongly. Its history reads (node_history,was_linked) are unaffected (row 98). /watchanswers a client that closes. The server only wrote to a/watchsocket and never read it, so a client's close frame went unanswered: the client waited out its own timeout, and the server kept the connection's task and its place in the event channel until a later commit gave it something to write and the write failed — on a store nobody was writing to, until the server stopped. The socket is read now, which also answers a ping./subscribewas not affected (row 99).- A store's first snapshot failing no longer loses edge properties. When the first snapshot of a store failed at its write — a full disk — the handle went on with no edge properties: a rule's weights read as absent, and the next snapshot that worked made that permanent (row 96).
- A snapshot interrupted before its WAL is replaced leaves a store that opens. A truncating or archiving snapshot writes
snapshot.binand then replaceswal.bin. A crash between the two left the new snapshot beside the WAL it was made from, and the next open replayed that WAL over it: after a rename the open failed withcorrupt data: wal replay RenameNode, and the store did not open again. Such a snapshot now writes a small file,snapshot.intent, before it starts and empties it when it has finished, and an open that finds it does not apply the old WAL a second time. A backup carries the file. This was 0.7.2's known limit (row 92).
v0.7.2
No format version change. A 0.7.1 store opens under 0.7.2, and a 0.7.2 snapshot opens under 0.7.1, which ignores the two optional sections this release adds.
Most of what is here was found by running the same writes two ways and comparing: one batch against separate commits, a reopened store against the handle that never closed, a handle that snapshots against one that never does. Five entries under Fixed are in 0.7.1 and earlier and are the reason to upgrade:
- deleting a node after a snapshot left its edges behind, and reading a neighbour of it failed (row 95);
- a rename followed by a snapshot that kept its WAL left a store that did not open again (row 91);
- a rename onto a key a deleted node once held, followed by any snapshot, did the same (row 94);
- a history read on a node renamed away and back never returned (row 93);
- a backup did not carry the map a read by date needs (row 86).
Before upgrading a store that already will not open, read the entries for rows 91 and 94: one kind opens again under this release with nothing lost, and one has to be recovered by hand first.
Known and not fixed: a crash between a truncating snapshot and the replacement of its WAL, on a store where a node was renamed before the snapshot, leaves a store that does not open. The window is two filesystem calls wide; the recovery is under row 91's entry (row 92).
Several inputs that 0.7.1 accepted are refused; each is under Changed.
Added
trueandfalseare literals in the Cypher subset. In any case, wherever a literal is read:WHERE n.active = true,WHERE n.active <> false,MATCH (n {active: true}),RETURN true AS t, andCREATEandSETwith a boolean value. They used to be read as variable names, so a comparison was anunbound variableerror and a write was refused;WHERE n.active, the bare form, was the only way to ask and still works. A node with no such property matches neither= …nor<> ….trueandfalseare reserved: naming a variable or an alias either one is now a parse error (row 63).PageRankConfig::validate, for an embedded caller that takes a config from somewhere it does not control.- The MCP registry card offers the npm launcher, so a client that installs from the registry needs no Rust toolchain.
Changed
- An
/algorequest with a field the config does not have is 400.POST /algo/pagerank,/algo/wccand/algo/degreedropped such a field without a word:{"weight": "score"}answered 200 with unweighted scores, andmax_iterformax_itersran with the default. The message names the field and the ones the route accepts.{}is still every default. A caller that sends a stray field gets 400 where 0.7.1 answered (row 87). - PageRank refuses a damping that is not a number from 0 to 1. Python raises
ValueErrorand HTTP answers 400. A NaN used to come back as a NaN score for every node (row 75). - Over MCP,
find_similarrefuses amin,korlimitthat is not a number. It used to take the default and echo it, so a caller who sent"0.5"got results filtered at 0.8. Absent or null still takes the default (row 74). rememberrefuses a key, predicate or label made only of invisible characters — a zero-width space, an ASCII control character, a byte-order mark — as it already refused an empty one, before it writes anything (row 78).- A provisional stub with a long key is linked by its whole key alone. A stub's aliases are its key and each word of it. A key that implies more than 32 — a sentence, a URL with a slug and a query string — used to get them all, past the cap an entity is held to; it now carries the one (row 53).
Fixed
-
Deleting a node after a snapshot removes its edges. On a store that had taken a snapshot — which
serve --snapshot-every,mushroomdb snapshotand Python'ssnapshot()all do —delete_noderemoved the node and left every edge the snapshot held for it. Reading the neighbours of a node that had been linked to it failed withcorrupt data: topology id N has no key; a pattern match returned a row with one end missing;statsand degree views went on counting the edges; and the next snapshot kept them. It is in 0.7.1 and earlier. A store already left that way is repaired when 0.7.2 opens it: the leftover edges are dropped, and the next snapshot no longer carries them.mushroomdb verifychecks checksums and cannot see such edges; it reports a store in that state as intact (row 95). -
Three more writes after a snapshot now act on what the snapshot holds. Deleting a view removes its property from every node, where the snapshot's values stayed readable. A rule deleted, snapshotted and created again derives its edges after a reopen, where the old snapshot's ownership records came back in their place. And an aggregate view created after a snapshot counts the neighbours the snapshot holds, where it started from zero. An average, minimum or maximum with nothing left to aggregate is absent, where it kept the snapshot's number; and a view over an edge type a rule derives is right after a reopen that replays writes, where it kept the snapshot's counts (row 95).
-
A view cannot take the name of a property nodes already carry, and a refused view leaves nothing behind. On a store with a snapshot the name check missed properties the snapshot held. And the refusal came after the record was written to the WAL, so the store that had just refused the view failed its next open (row 95).
-
A store that renamed a node and then took a snapshot keeping its WAL opens again.
mushroomdb snapshot --keep-wal,snapshot_with(keep_wal: true), the snapshotenable_multiplicitytakes and an auto-migration all leave the WAL whole, and the next open replayed all of it over the snapshot. After arename_nodethat open failed withcorrupt data: wal replay RenameNode n→m: duplicate node key: mand the store did not open again; after a key was deleted and inserted again the store opened holding an older value, with no error. Such a snapshot now records how much of the WAL it holds, and the open does not apply those writes a second time. It is in 0.7.1 and earlier (row 91).A store already in that state is not repaired by upgrading, because its snapshot carries no such record. If the store has never taken a truncating or an archiving snapshot — its directory holds no
wal.<N>.archive, and it was never snapshotted with--truncate— movesnapshot.binaside and keep it: the store opens from the WAL alone, and the next snapshot is a good one. Do not do this on a store that has archives:mushroomdb snapshotandserve --snapshot-everyarchive by default, and without its snapshot such a store opens holding only what was written since the last archive, with no error.The same state can still follow a crash between a truncating snapshot and the replacement of its WAL, a window two filesystem calls wide. That is not fixed here (row 92). A store left that way opens once
wal.binis moved aside: the snapshot holds every node, edge and rule, and the full-text pairs and equality indexes have to be declared again, as may the multiplicity opt-in. -
A node renamed onto a key a deleted node once held no longer makes the store unopenable.
delete_node(b),rename_node(a, b), then any snapshot, and every later open failed withcorrupt data: v8: labels section has 2 entries but id table has 1 total slots. The snapshot was right and its reader was not, so a store left that way by a truncating or an archiving snapshot opens again under this release with nothing lost. One left that way by a snapshot that kept its WAL does not come back right: it opens, and reads of the renamed node's neighbours fail. Recover it as the entry above says before opening it with this release. It is in 0.7.1 and earlier (row 94). -
A history read on a node renamed away and back again returns. After
rename_node(a, b)andrename_node(b, a),was_linked,node_history,edge_historyandedges_aton that node never came back, on every surface, the MCPwas_linkedtool included (row 93). Andwas_linkednow follows the node, not the name: an edge written while a node was called one thing and deleted while it was called another is gone, where it went on answering "linked" after two renames; and an earlier node that held the same key and was deleted is another node, whose edges and whose deletion no longer count. -
A backup carries the commit-time map.
mushroomdb backup,POST /backupandGraphDb::backup_toleftcommit_times.binbehind, andrestoredid not name it, so a restored store had its history and not the map a read by date resolves through. A backup taken by 0.7.1 or earlier does not hold the map and restores without it (row 86). -
A batch sees a node it renamed. After
rename_node(a, b)in aBatchBuilderor asubmit_batch, the batch's own checks readbas a node with no stored properties, label or edges. So[rename a→b, insert_edge(b, d)]passed the cross-namespace guard thatinsert_edge(a, d)is refused by;[rename a→b, delete_edge(b, d)]deleted an edge a rule owns;[rename a→b, remove_prop(b, "name")]returned Ok and removed nothing; and a chain[a→b, b→c]left a storedidwhere two separate renames move it. The checks now read through to the stored node. Reachable only from a batch built in Rust: MCP, HTTP, the Python binding and the CLI each send a rename alone (row 79). -
A key reused inside a batch names the new node. A batch that renamed a node away from a key, or deleted it, and then used the key again wrote against the old node:
[delete_node a, insert_node a, set_prop(a, …)]lost the property, and[rename a→b, insert_node a, insert_edge(a, c)]put the edge onb. Also only from a batch built in Rust (row 89). -
remember'smatchedcounts a fact endpoint that already exists, each key once (row 48). -
**`remem...
v0.7.1
No new feature and nothing removed. A 0.7.0 store opens unchanged.
Fixed
-
rename_nodemoves a storedidwith the key.upsert_entity's create and a CypherCREATE {id: …}store the key as anidproperty, and a rename left it at the old key:WHERE n.id = '<new key>'found nothing andRETURN n.idprinted the old key, with no error. The rename now rewrites anidthat equals the old key, in the same commit, on every surface — the MCP tool, Python'srename_node, HTTPPOST /nodes/{key}/renameand a batch. Anidholding anything else is the caller's and is left alone; a node that stores none gains none. The rename is still one WAL frame, and a read as of a commit before it still answers with the old key and the oldid.node_historyshows the rewrite as a property change at the rename's commit. On/watch, a rename that moves anidemitsprop_setandbatch_appliedwithops: 2, where a rename with no suchidemits nothing, as before; and under theBatchedfsync policy its frame is fsynced, where a bare rename's is not. It predates 0.7 (row 72).A node renamed by 0.7.0 or earlier is not repaired on open. To list every node whose stored
idis not its key:mushroomdb query <db> "MATCH (n) WHERE n.id <> key(n) RETURN key(n), n.id". That also lists a node whoseidwas written as something else on purpose, so repair by key, one node at a time:mushroomdb query <db> "MATCH (n) WHERE key(n) = 'q2' SET n.id = 'q2'". -
was_linked's schema declares the date it accepts. The handler has taken an RFC 3339 date forat_commitsinceedges_attook one, and the skill teaches that form, but the tool'sinputSchemasaidinteger: a client that checks arguments against the schema refused the taught call, and one that does not accepted it.at_commitis now typed asedges_at'satis — a string or a non-negative integer — and the description leads with the date. Nothing about the handler changed (row 71). -
A store without the memory defaults keeps saying so after its first
remember.rememberdeclaresNote.textas it writes, so "this store has no text index" stopped at the first note, and nothing replaced it: on a 0.6 store, or one aquerycreated,recallthen returned the note and not the person it was about. Therecalltool's reply now ends withtext index incomplete: recall does not search Person.name, and the session brief carries the same line just above its reach line, naming each memory-default field the store has not declared and holds a value for, with themushroomdb schema apply <db> --memory-defaultscommand that indexes them. The check is bounded: it looks at the first 1,000 nodes of each memory label, so a field only a later node carries is not named. Nothing is declared for you: an existing store is still upgraded only by that command. A store with no node under a memory label — one with its own schema — is told nothing, and so is the prompt hook's digest, which fires every turn. Python'srecallrows are unchanged;fulltext_pairs()is how a Python caller sees what is declared. The tool's older "no text index" answer now replaces each control character in the store path it prints with a space, as the new line does (row 73).
Changed
- The default tool listing is 27,371 bytes, from 27,067; all 25 tools are 28,172, from 27,868 (
scripts/measure-tool-listing.py). The 304 bytes arewas_linked's schema and description. Still 23 tools by default; the script puts the default listing at about 6,842 tokens a session, at its estimate of 4 bytes per token. - The container image waits for the release.
release.yml'sdockerjob needed no other job, so av*tag pushed the movinglatestimage while the binaries were still building, and pushed it even when they failed. It now needsgithub-release, which needs all three binaries: the image is pushed only after the release is published. A tag publishes exactly what it did. The workflow still runs no test, so a tag still goes only on a commit whose CI is green (row 61).
Known limits
- A batch that renames a node cannot see that node's stored properties in the ops after the rename. Only a batch built in Rust can put a rename beside another op —
BatchBuilderorSharedDb::submit_batch; MCP, HTTP, Python and the CLI each send a rename alone. In such a batch, aninsert_edgefrom the renamed node is checked as if the node were in the default namespace, so an edge across namespaces that a plaininsert_edgerefuses is committed; aremove_propon the renamed node returns Ok and removes nothing; and a second rename of the same node does not move anidequal to the key in between. It predates this release (row 79, deferred to 0.7.2).
Not in this release
At 0.7.0 the defect ledger deferred fifteen rows to 0.7.1. This release closes three of them — 71, 72 and 73 — and row 61, which was deferred to 0.8. The other twelve move to 0.7.2 unchanged: rows 47, 48, 50, 51, 52, 53, 57, 64, 65, 74, 75 and 78. The known limits under 0.7.0 below say so where they name one. Row 79 is new, and is the known limit above.
v0.7.0
0.7 is the release in which remember followed by recall works on a new store. The memory write path is what it is for; the code-graph door is what it removes.
This is the first breaking release. Under Cargo semver the minor slot is the breaking position for 0.x, so a Cargo dependency on ^0.6.x does not match 0.7.0 and stays where it is. Nothing else unpinned is held back. An unpinned pip install mushroomdb, npx mushroomdb, cargo install mushroomdb-cli and the container image pulled without a tag each take 0.7 the next time they resolve, with no deliberate upgrade. Two things name their version and stay on it: an MCP entry that install already wrote, until install is run again, and the Claude Code plugin, whose .mcp.json and hook script pin the launcher and move only when the plugin or its marketplace is updated.
If you call
find_similarwithoutmin, your results change, and nothing tells you.
minnow defaults to 0.8 on every surface that has a default, through one constant,core_api::FIND_SIMILAR_DEFAULT_MIN. The MCP tool's vector mode already used 0.8. In the Python binding and on HTTPPOST /find_similarit was0.0: a call that never namedminreturned every hit scoring 0.0 or above, up tok, and now returns only those scoring 0.8 or above — which can be none. No error, no warning, fewer results.To keep the old behaviour, name it:
min=0.0in Python,"min": 0.0in an HTTP body. Over MCP the default did not move; an explicitminis honoured there as before.
pairwise_similaris unchanged: itsminstill defaults to0.0, over MCP and in Python. The 0.6.10 and 0.6.11 notes both proposed this change for 0.7 and told callers to passminexplicitly; this is that release.
The memory write path
On 0.6.12, remember followed by recall returned nothing. The store held the note and the index held its words; recall discarded every topic that did not look like a code identifier.
recallanswers ordinary language. "who is Matthew Sherlin?" finds the entity and the notes that name it. The gate that required a path, amod::name, a snake_case word or backticks is deleted; an identifier still works.recalltells "no text index" from "no match". A store that cannot search says so, with the command that fixes it, instead of answering as if the topic were unknown.remembertakes what a sentence names. Besidetextandaboutit acceptsentities— each a key, a label, properties and aliases, created or described in the same commit — andfacts, relationships among them. The reply says what it created, what it matched, and what it could only stub.- An unknown subject is stubbed, not refused.
remember {about: ["matthew"]}on a store that has never heard ofmatthewused to fail. It now creates a provisionalEntity, marked as one, that theschematool counts and lists the first ten of. A provisional node never expires; describing it —upsert_entity, orremember'sentities— clears the mark, andforgetremoves it. A fact's subject and object get the same treatment. At most 20 are stubbed per call; any past that are named in the reply as not created, and the rest of the call still commits. - A key, a label or a predicate that is empty or only whitespace is refused. In
remember: a key inabout, anentitieskey or label, and a fact'ssubject,predicateorobject. Inupsert_entity: thekey, and thelabelof a create. Over MCP and in Python alike, the error names the argument and its position, and the call writes nothing. A raw write can still store any of them. A node under the empty key is removed bymushroomdb query <db> "MATCH (n) WHERE key(n) = '' DETACH DELETE n"or byforget(key="")in Python (the MCPforgetrefuses an empty key); a node under an empty label byforgetwith its key. An edge whose type is the empty string is retracted byforget(fact=…)in Python, or over HTTP byDELETE /edges//<src>/<dst>on a served store; over MCPforgetrefuses an empty predicate, so there it goes only with one of its endpoint nodes. - A node's label is fixed.
upsert_entitywith alabelthat differs from the stored one is refused, and writes nothing, where it used to be silently ignored. Name the label inremember'sentitiesat first mention.rememberitself does not refuse a disagreeing label yet (see the known limits). - A store that
mcp,serveordemocreates can be recalled from. Each creates it with the memory schema: full-text onNote.text,Concept.summaryand thenameofPerson,Org,Project,Concept,Eventand the provisionalEntity.rememberdeclares thenameof a label it has not seen whenentitiesnames one, up to 32 text fields in all; past that the entity is still written and is not searchable. - The memory schema is never applied to an existing store unasked. A full-text index is rebuilt every time the store is opened, so the defaults go on when you ask:
mushroomdb schema apply <db> --memory-defaults.rememberstill declares what it writes, on any store:Note.text, and thenameof a labelentitiesnames. Sorecall's "no text index" answer and the session brief's line appear only while the store has no text field at all. After the firstremember— and on a 0.6 store that has already remembered a note —recallanswers from the fields the store has and says nothing about the rest: an entity written before, or by another path, is not found by its name until the defaults are applied (see the known limits). - A topic is at most 24 search terms. A pasted paragraph is cut to its first 24 content words: glue words and repeats are dropped first, and the rest are not searched.
- The first minute is a test.
crates/cli/tests/first_run.rsdrives the real binary over stdio through the flows that failed on 0.6.12, and through the loop the README's quick start prints:remember,recall,suggest_rules,create_rulewith the proposal's own arguments, thenexplain_association.
Removed — the code-graph door
- Seven MCP tools:
explore,map,context,impact,owners,why,sync. There is now one tool surface: every store advertises the same twenty-three, a store built byingest-gitincluded.--all-toolslists all twenty-five. - Four install hooks: the three opt-in ones (
--intercept-grep,--impact-before-edit,--enrich-grep, now rejected with a message) and thePostToolUsetouchhook, which was written on every install and re-extracted edited files into the code graph. An install, uninstall or enable on 0.7 removes all four from a settings file that still has them, so an upgraded machine is not left invoking a subcommand that no longer exists. The Claude Code plugin's ownPostToolUsetouchhook is gone too. - The
syncgit hooks. 0.7 writes no git hooks.install,uninstallandenableremove thesyncblock a 0.6 install put inpost-commit,post-checkoutandpost-mergefor this store, anddoctorwarns about one left behind.install --no-git-hooksis still accepted, as a no-op. - Ten CLI subcommands:
sync,touch,intercept,impact-hook,enrich,map,explore,context,impact,owners. The first five were hook bodies, and a hook a 0.6 install wrote keeps calling them until it is removed, so they are still accepted, unlisted: each does nothing, exits 0 and prints one line on stderr saying how to remove the hook. - The
UserPromptSubmithook's dirty-tree nudge — what the changed files reach, who owns them, which learned concept just went stale. It readCO_CHANGED,IMPORTSandDESCRIBED_INedges onlyingest-gitwrites. - The code-graph benchmark suite. It measured the door this release removes. Its committed results stay as the record of the measurement that deprecated the door. The harness files stay too, because the association suite runs through them:
benchmarks/agent-tasks/run.pynow defaults to--suite associationand refuses--suite code.
After upgrading, run mushroomdb install once to remove the retired hooks; mushroomdb doctor reports any that remain.
To keep any of it: pin mushroomdb@0.6.x.
Every retired hook, in a settings file or a git hook, is recognised as ours only when its program is mushroomdb — npx … mushroomdb@…, a path ending /mushroomdb, or node …/mushroomdb.js. A hook of your own is never touched.
Kept, and changed
ingest-gitstays, positioned as a data source. Its store is now an ordinary memory store — the same twenty-three tools, the same brief — and it declares its schema throughapply_schema, so re-ingesting is idempotent. Into an existing store it declares its own missing rules and full-text pairs, as 0.6 did, and never the memory defaults.ingest-gitno longer declares theabout_*rules.rememberwrites a note'sABOUTedges itself, and those rules took ownership of the edges, which made re-remembering a note fail. A store that already carries them from 0.6 keeps them, andrememberworks alongside them.mushroomdb why <db> <a> <b>stays and answers a different question: every rule edge between two keys with the evidence that derived it, from the association engine. It is the shell form ofexplain_association, which had no CLI door before. The code-graphwhyanswered fromCALLS,IMPORTSand co-change. It prints each edge once per direction, so a symmetric rule shows two lines for one pair: that is the engine's existing behaviour, now visible in the CLI.- The prompt hook answers ordinary language. It used to say nothing unless the prompt named a path, a
mod::name, a snake_case word or something in backticks — so on a memory store it was silent on every prompt. The MCPrecalltool's fix is the first section above; this is the automatic path. It prepends the untrusted-content frami...
v0.6.12
What's Changed
- docs: the tool counts match the code, and 0.6.10 reaches the front page by @MatthewSherlin in #26
- v0.6.11 — time in the graph by @MatthewSherlin in #27
- fix: a date resolves to the frame its commit actually wrote by @MatthewSherlin in #28
- bench: the association gate passes, on the run that carries the date fix by @MatthewSherlin in #29
- chore(release): one version in one place, and CI for what ships untested by @MatthewSherlin in #30
- fix(server): health discloses liveness, and a request has a deadline by @MatthewSherlin in #31
- feat(server): shutdown drains, and the log is quiet when nothing is wrong by @MatthewSherlin in #32
- chore: v0.6.12 — a date that lands where it says, and a server you can run by @MatthewSherlin in #33
Full Changelog: v0.6.10...v0.6.12