Skip to content

Releases: MatthewSherlin/mushroomdb

v0.9.2

Choose a tag to compare

@github-actions github-actions released this 20 Oct 06:24
d8637eb

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_rule no longer drops an edge another rule still desires. When a rule was explicitly rebuilt (or rebuilt after index drift), apply_streaming_rebuild and apply_via_rebuild retracted 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_at handle 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 (via ensure_provenance_loaded_mut) but not when the provenance was decoded for an as-of handle via from_persist. The fix calls dedup_provenance inside from_persist (row 112).
  • forget of 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_node counts the correct number of derived edges when the deleted node is only a via-hop. provenance_touching found 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

Choose a tag to compare

@github-actions github-actions released this 17 Oct 01:25
9ed3859

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 with rebuild_rule (row 113).
  • stats().edges no 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_edge of an edge a rule derived answered Ok(false) instead of refusing it as rule-owned, in a batch as well, and delete_node reported the deleted node's rule-derived edges as manual (rows 108, 110). On a store where 0.8.0 lost edges, delete_node no longer counts the lost ones (row 111).

v0.9.0 — the Rust library is mushroomdb

Choose a tag to compare

@github-actions github-actions released this 16 Oct 17:33
1db04c9

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, not core_api. The crate on crates.io has always been mushroomdb, but its library was named core_api, so a project that ran cargo add mushroomdb had to write use core_api::…. It is use mushroomdb::… now. What to change depends on how your Cargo.toml names the dependency:

    • mushroomdb = "0.8" (what cargo add mushroomdb writes): bump the version to 0.9, and change core_api:: to mushroomdb:: 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, so use core_api::… keeps working. Rename the key to mushroomdb and your imports to mushroomdb:: 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

Choose a tag to compare

@github-actions github-actions released this 16 Oct 03:17
24189e8

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, and open_at still show both owners.
    The deduped edge keeps the dropped rule's weight property until the edge is next re-derived.
    The snapshot encoder recounts edge_count from the merged CSR rows instead of carrying the header
    value forward arithmetically, so every new snapshot's header is correct. stats().edges and
    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

Choose a tag to compare

@github-actions github-actions released this 15 Oct 15:33
d7bd196

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 — stats reported edges neighbors did 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 store main snapshotted while two rules owned an edge is not repaired by this release, and its stats edge 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

Choose a tag to compare

@github-actions github-actions released this 14 Oct 16:20
c5cc99d

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 mcp snapshots 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 mushroomdb and install.sh pick 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 install has 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, and watch. query gains as_of, mask and namespace options for time-travel, key-masking and namespace-scoped reads. watch streams the raw MutationEvent log over GET /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 stats on 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 mcp snapshots 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: serve has 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, so node_history, was_linked and reads by date reach as far back as before; the store directory gains one wal.<N>.archive per snapshot. Skipped, silently, when another process holds the write lock, and by the server mushroomdb doctor starts to check that it answers; MUSHROOMDB_NO_AUTO_SNAPSHOT=1 in the server's environment turns it off.

Fixed

  • An insert that names a property twice indexes the value that is stored. insert_node with the same field twice, or a Cypher CREATE with 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 snapshot twice, a serve --snapshot-every tick on an idle store, a graceful shutdown just after one — wrote an empty file over the previous WAL archive. node_history, was_linked and reads by date lost everything that archive held, and because the commit numbers were then handed out again, asof and query … as_of could answer an old commit with a later state while stats reported complete history. It affected a store with no full-text pair, property index or multiplicity declared, so not a store created by mushroomdb mcp or serve since 0.7, which declare the memory schema; a store from demo, 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_at and query … as_of read commits out of the archives was written last, and a snapshot killed before it — mushroomdb snapshot, a serve --snapshot-every tick or a shutdown — left a store that never got one: every commit in an archive was out of range from then on, with stats reporting 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).
  • /watch answers a client that closes. The server only wrote to a /watch socket 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. /subscribe was 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.bin and then replaces wal.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 with corrupt 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

Choose a tag to compare

@github-actions github-actions released this 13 Oct 17:22
545fcea

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

  • true and false are 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, and CREATE and SET with a boolean value. They used to be read as variable names, so a comparison was an unbound variable error 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 <> …. true and false are 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 /algo request with a field the config does not have is 400. POST /algo/pagerank, /algo/wcc and /algo/degree dropped such a field without a word: {"weight": "score"} answered 200 with unweighted scores, and max_iter for max_iters ran 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 ValueError and HTTP answers 400. A NaN used to come back as a NaN score for every node (row 75).
  • Over MCP, find_similar refuses a min, k or limit that 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).
  • remember refuses 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 snapshot and Python's snapshot() all do — delete_node removed the node and left every edge the snapshot held for it. Reading the neighbours of a node that had been linked to it failed with corrupt data: topology id N has no key; a pattern match returned a row with one end missing; stats and 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 verify checks 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 snapshot enable_multiplicity takes and an auto-migration all leave the WAL whole, and the next open replayed all of it over the snapshot. After a rename_node that open failed with corrupt data: wal replay RenameNode n→m: duplicate node key: m and 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 — move snapshot.bin aside 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 snapshot and serve --snapshot-every archive 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.bin is 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 with corrupt 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) and rename_node(b, a), was_linked, node_history, edge_history and edges_at on that node never came back, on every surface, the MCP was_linked tool included (row 93). And was_linked now 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 /backup and GraphDb::backup_to left commit_times.bin behind, and restore did 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 a BatchBuilder or a submit_batch, the batch's own checks read b as a node with no stored properties, label or edges. So [rename a→b, insert_edge(b, d)] passed the cross-namespace guard that insert_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 stored id where 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 on b. Also only from a batch built in Rust (row 89).

  • remember's matched counts a fact endpoint that already exists, each key once (row 48).

  • **`remem...

Read more

v0.7.1

Choose a tag to compare

@github-actions github-actions released this 12 Oct 15:19
31da934

No new feature and nothing removed. A 0.7.0 store opens unchanged.

Fixed

  • rename_node moves a stored id with the key. upsert_entity's create and a Cypher CREATE {id: …} store the key as an id property, and a rename left it at the old key: WHERE n.id = '<new key>' found nothing and RETURN n.id printed the old key, with no error. The rename now rewrites an id that equals the old key, in the same commit, on every surface — the MCP tool, Python's rename_node, HTTP POST /nodes/{key}/rename and a batch. An id holding 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 old id. node_history shows the rewrite as a property change at the rename's commit. On /watch, a rename that moves an id emits prop_set and batch_applied with ops: 2, where a rename with no such id emits nothing, as before; and under the Batched fsync 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 id is not its key: mushroomdb query <db> "MATCH (n) WHERE n.id <> key(n) RETURN key(n), n.id". That also lists a node whose id was 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 for at_commit since edges_at took one, and the skill teaches that form, but the tool's inputSchema said integer: a client that checks arguments against the schema refused the taught call, and one that does not accepted it. at_commit is now typed as edges_at's at is — 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. remember declares Note.text as 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 a query created, recall then returned the note and not the person it was about. The recall tool's reply now ends with text 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 the mushroomdb schema apply <db> --memory-defaults command 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's recall rows 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 are was_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's docker job needed no other job, so a v* tag pushed the moving latest image while the binaries were still building, and pushed it even when they failed. It now needs github-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 — BatchBuilder or SharedDb::submit_batch; MCP, HTTP, Python and the CLI each send a rename alone. In such a batch, an insert_edge from the renamed node is checked as if the node were in the default namespace, so an edge across namespaces that a plain insert_edge refuses is committed; a remove_prop on the renamed node returns Ok and removes nothing; and a second rename of the same node does not move an id equal 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

Choose a tag to compare

@github-actions github-actions released this 02 Oct 04:09
4251e8c

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_similar without min, your results change, and nothing tells you.

min now 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 HTTP POST /find_similar it was 0.0: a call that never named min returned every hit scoring 0.0 or above, up to k, 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.0 in Python, "min": 0.0 in an HTTP body. Over MCP the default did not move; an explicit min is honoured there as before.

pairwise_similar is unchanged: its min still defaults to 0.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 pass min explicitly; 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.

  • recall answers ordinary language. "who is Matthew Sherlin?" finds the entity and the notes that name it. The gate that required a path, a mod::name, a snake_case word or backticks is deleted; an identifier still works.
  • recall tells "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.
  • remember takes what a sentence names. Beside text and about it accepts entities — each a key, a label, properties and aliases, created or described in the same commit — and facts, 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 of matthew used to fail. It now creates a provisional Entity, marked as one, that the schema tool counts and lists the first ten of. A provisional node never expires; describing it — upsert_entity, or remember's entities — clears the mark, and forget removes 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 in about, an entities key or label, and a fact's subject, predicate or object. In upsert_entity: the key, and the label of 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 by mushroomdb query <db> "MATCH (n) WHERE key(n) = '' DETACH DELETE n" or by forget(key="") in Python (the MCP forget refuses an empty key); a node under an empty label by forget with its key. An edge whose type is the empty string is retracted by forget(fact=…) in Python, or over HTTP by DELETE /edges//<src>/<dst> on a served store; over MCP forget refuses an empty predicate, so there it goes only with one of its endpoint nodes.
  • A node's label is fixed. upsert_entity with a label that differs from the stored one is refused, and writes nothing, where it used to be silently ignored. Name the label in remember's entities at first mention. remember itself does not refuse a disagreeing label yet (see the known limits).
  • A store that mcp, serve or demo creates can be recalled from. Each creates it with the memory schema: full-text on Note.text, Concept.summary and the name of Person, Org, Project, Concept, Event and the provisional Entity. remember declares the name of a label it has not seen when entities names 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. remember still declares what it writes, on any store: Note.text, and the name of a label entities names. So recall's "no text index" answer and the session brief's line appear only while the store has no text field at all. After the first remember — and on a 0.6 store that has already remembered a note — recall answers 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.rs drives 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_rule with the proposal's own arguments, then explain_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 by ingest-git included. --all-tools lists all twenty-five.
  • Four install hooks: the three opt-in ones (--intercept-grep, --impact-before-edit, --enrich-grep, now rejected with a message) and the PostToolUse touch hook, 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 own PostToolUse touch hook is gone too.
  • The sync git hooks. 0.7 writes no git hooks. install, uninstall and enable remove the sync block a 0.6 install put in post-commit, post-checkout and post-merge for this store, and doctor warns about one left behind. install --no-git-hooks is 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 UserPromptSubmit hook's dirty-tree nudge — what the changed files reach, who owns them, which learned concept just went stale. It read CO_CHANGED, IMPORTS and DESCRIBED_IN edges only ingest-git writes.
  • 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.py now defaults to --suite association and 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-git stays, 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 through apply_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-git no longer declares the about_* rules. remember writes a note's ABOUT edges 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, and remember works 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 of explain_association, which had no CLI door before. The code-graph why answered from CALLS, IMPORTS and 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 MCP recall tool's fix is the first section above; this is the automatic path. It prepends the untrusted-content frami...
Read more

v0.6.12

Choose a tag to compare

@github-actions github-actions released this 29 Sep 20:06
fab9bdc

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