Releases: itdtllc/fornixdb
Release list
v1.6.1 — documentation updates
Documentation updates. Wording and example text only — no functional change.
v1.6.0 — a topic has to name what a memory is about
Tagging could only ever add, so a bulk pass was a decision you could not take back — and a pass you cannot undo is a pass you make timidly. This release supplies the missing verb, and then, with undo in hand, makes the suggester worth trusting: it was proposing words like "now" and "users", which would connect every memory to every other one.
Added
untag <id> <topic>takes a topic off a memory — the counterparttagnever had. Removing a topic removes an edge, never a memory. When the last memory carrying a topic lets it go, the topic name goes with it, because a vocabulary word no memory uses connects nothing and would keep being offered as a suggestion.
Changed
- The dream's topic suggester now proposes subjects rather than words. It was offering "now", "never", "day", "users" and "app" — all of which pass the retrieval stopword filter, appear all over a project, and would connect every memory to every other. Three things changed: a topic-side stoplist (kept separate from the retrieval one, which protects ranking and must not widen), a floor of three memories before an existing topic is worth spreading, and a shape rule that rejects identifier fragments like
e01,v2and72b. - The untagged-memory worklist now says how many rows actually carry a suggestion, not just how many are untagged. The old count claimed more reviewable work than existed.
Fixed
- The reality check no longer reports a file that is sitting right there when its name holds more than one space. The candidate walk stopped after the first space, so
~/Desktop/Field Notes Draft.mdread as the missing directory~/Desktop/Field. It now walks up to three words, and still reports a spaced path that is genuinely gone.
Documentation
- The dream worklist's
reprojectguidance no longer reads as "apply what you just saw". The worklist prints the best fifteen;--applywrites every proposal above the margin, which on a lived-in store is an order of magnitude more and is not trustworthy at the low end.
Full changelog: https://github.com/itdtllc/fornixdb/blob/main/CHANGELOG.md
v1.5.0 — a store you forgot should stay forgotten
A number the disk-budget policy acts on was counting files nobody meant to keep, and there was no way to take one out — so the fix is a verb that removes a store from the count and, more to the point, remembers that you did.
Added
-
usage --forget <path>stops counting a store toward machine usage, and--unforgetputs it back. Any store opened with--dbjoins the machine registry permanently, so backups and throwaway stores registered themselves and inflated the total — on a real machine it read 63.8 MB against a true 16.8 MB.The choice is remembered rather than merely applied. Every connect re-registers, so removing the entry alone is undone by the next incidental open. A forgotten path is recorded and consulted at registration, so it takes
--unforgetto reverse. The store file is never touched, and a path can be forgotten before it exists, so one that reappears stays excluded.
Fixed
- A function-local
from pathlib import Pathin one branch of the CLI dispatcher madePathlocal to the whole function, so any use of it earlier in that function raisedUnboundLocalError.
pip install "git+https://github.com/itdtllc/fornixdb@v1.5.0"
1132 tests + 1 skip. No store-format change; v14 schema unchanged.
v1.4.1 — one injection, counted once
A measurement that decides what gets built has to be right before the thing gets built. This one was not, and it was pointing at a working channel.
The bug
The usefulness scan charged twice for every push delivered on the per-turn seam. On that seam the host echoes a hook's output back into the attachment's content field, so the transcript records the same injected block in two places — while the model is shown it once. The scan summed both.
The tool-call seams carry the block once and were never affected, so the entire error fell on a single channel.
Why it mattered
The error was invisible for the reason such errors usually are: it was consistent. The per-turn channel simply looked expensive — expensive enough that trimming it was the obvious next piece of work.
Corrected, it costs about half what it appeared to. That puts it mid-pack among the push channels rather than worst, and the channel it would have been trimmed in favour of is now the dearest of them.
What changes for you
- Measured push cost falls by roughly a seventh on a lived-in store, so
fornixdb valueis that much less pessimistic about what memory costs. - The per-channel cost-per-reference figures introduced in 1.3.0 are now correct for every channel.
- No behavior changed — only the accounting of what was already happening. Nothing pushes differently, and no stored data is affected.
If you acted on a per-channel cost figure from 1.3.0 or 1.4.0, re-run fornixdb value --transcripts <dir> before acting further.
1101 tests + 1 skip.
Install: pip install "git+https://github.com/itdtllc/fornixdb@v1.4.1"
v1.4.0 — memories that can find each other
A memory reaches a model one of two ways: by its words, or by association. Association needs edges — shared topics and links — and a store that stops making them keeps answering keyword questions while quietly losing the other kind. That failure does not look like a fault: recall still works, the field just stops settling.
Weaving can finish
dream --weave --passes Nkeeps weaving until nothing is proposed or N passes have run. A pass proposes a reviewable number of pairs, which is right for a move that needs judgement and wrong for the one move here that does not — arelatesedge only ever adds. Clearing a real backlog previously meant running the same command by hand two dozen times. One pass remains the default, and without--weaveit does nothing.- The pair scan now reports how many candidates it found alongside the capped number it shows. It had been finding hundreds and reporting fifteen, so a worklist reading "15 connections to weave" was describing a backlog twenty times that size.
Topics, asked for and proposed
- The MCP
rememberandremember_manytools accepttopics. They had no way to set one at all, so every memory stored through that path was unclusterable by construction. - Storing from the CLI without
--topicnow says so and names the repair, the same way the write path speaks up when it splits an oversized gist. - The consolidation worklist lists memories carrying no topic, with suggestions. Suggestions prefer topic names the store already uses — reusing a name creates an edge to the memories carrying it, while coining a fresh word connects the memory to nothing.
Measured
On a lived-in store, weaving the association backlog — 354 links across 752 memories, no memory rewritten or removed — took the field settle rate of a low-glue workload from 19.8% to 50.2% across four hundred real queries: 122 gained an answer, none lost one. That is a young project's memory reaching what a mature one already had.
Restoring topics to untagged memories was worth a further 2.7 points on the same corpus, and suggestions drawn from existing topic names beat suggestions picked for distinctiveness alone.
Recall ranking is unaffected by either change — verified rather than assumed, by running the ranking fence before and after.
If your store has been accumulating memories without weaving, this is the release to run dream --weave --passes 30 on.
1096 tests + 1 skip.
Install: pip install "git+https://github.com/itdtllc/fornixdb@v1.4.0"
v1.3.1 — agreement up close, and a redemption that sticks
Two places where the system drew a conclusion its evidence did not support.
Agreement has to be concentrated
The abstention gate — the check that decides whether the store actually holds an answer, rather than letting noise pose as one — ends on a count of how many content words a query and a recalled memory agree about. That count was documented as not scaling with document length. It did.
Counted across a whole memory, a long one about something else collects the query's words scattered over pages of unrelated prose, in senses that have nothing to do with the question. Two of them was the whole bar.
A memory's detail now contributes only through its single most agreeing window, so several of the query's words have to appear near one another rather than merely somewhere in the same record. The gist always counts wherever that window falls: it is the summary, and since 1.3.0 it is capped, so it cannot become a haystack of its own.
Both options were measured before choosing. Counting the gist alone would have been simpler and would have cost real answers whose agreement genuinely lives in their detail; the window keeps those and removes almost all of the accidental agreements. Ranking is unaffected — the count feeds only the gate, and that was verified rather than assumed.
A redemption that sticks
Suppression — muting a memory that is pushed constantly and never used — is decided by scanning the host's session transcripts. Those transcripts do not change. So a memory redeemed today still showed the same pushes and the same zero references tomorrow, re-qualified at once, and was suppressed again.
Every deliberate "this one matters" signal the system has — an explicit suppress --undo, a show, a mark_helpful, a rewrite — was losing to the evidence it had just overruled.
Suppression now has to be re-earned from pushes that happen after the redemption, and the scan reports which memories it spared and why.
Schema
Store schema v14 adds one nullable column recording the push count a redemption overruled. Existing stores migrate forward automatically on open; memories redeemed before it existed keep being judged on their whole history, which is the behavior they have today.
1074 tests + 1 skip.
Install: pip install "git+https://github.com/itdtllc/fornixdb@v1.3.1"
v1.3.0 — a gist stays a gist, and both channels count
A gist is what recall returns and what a proactive push delivers, but nothing on the write path kept one short. Gists grew into walls of text, and a push — which truncates — delivered them as a headline cut mid-sentence. This release fixes that where memories are written, and teaches the usefulness measurement to see the half of memory use it had been blind to.
Gists stay gists
- A gist over 400 characters is split at the last sentence or paragraph boundary at or before the limit, and the overflow moves into
detail. Nothing is lost. Enforced in the store itself, so every writer — CLI, MCP, hooks, importers — inherits it. - The limit is measured, not chosen: across a corpus of real sessions, the rate at which a memory is referenced downstream peaks in the 301–400 band and falls away above it.
gist-backfillbrings gists written before the ceiling into line with it. Dry-run unless--apply; superseded rows are never touched, so history is not rewritten. Re-embedding the changed rows is part of the operation and most of its value — a memory's first vector chunk is its name and gist together, so an oversized gist embedded as one blurred vector.
Both channels count
A memory reaches the model two ways: FornixDB pushes it, or the agent goes and gets it. Only the first was measured, so the honesty layer answered "did memory help?" from the more expensive and less-referenced of the two.
- The usefulness scan and
valuenow measure the pull channel beside the pushed ones, and report what a downstream reference cost on each channel — the like-for-like comparison between paying up front and paying on demand. - Pulls are detected by the shape of a result rather than by parsing the command that produced it, so a write is never counted as a read.
- Note when comparing with earlier figures: a citation is credited to whichever delivery preceded it, so a pull of an already-pushed memory takes the credit, and the referenced-push rate reads slightly lower than it did when pulls were invisible.
Also
meta-gccollects per-session bookkeeping keys from sessions that ended long ago, and runs when a dream pass opens. A key whose session has no recorded row belongs to a session still running, and is never collected.--since-daysonfield-statsandfloor-stats. Without a window, both average across every configuration a store has ever run under — which is how a closed trial's readings go on being counted after the dial that produced them is switched off.recallandtimelineon the CLI default to a 4,000-character output budget, matching the MCP tools.--max-chars 0restores unlimited output.- Fixed:
doctorno longer reports config keys that are read through a named constant or built per session from a prefix — every warning it raised on a lived-in store was one of these. - Fixed: use-credit and the floor outcome join are restricted to memories that were actually pushed, so a pulled-only memory cannot have credit an earlier push earned written back to zero.
1049 tests + 1 skip.
Install: pip install "git+https://github.com/itdtllc/fornixdb@v1.3.0"
v1.2.2 — documentation updates
Documentation updates. Wording only — no functional change to FornixDB.
The roadmap's reasoned-dreaming section and two comments in the reasoned-pairs probe now describe the models a negative result was measured against by what they are (three locally-hosted models in the 70B–120B range, two of them reasoning models) rather than by tag. The measurement and the verdict are unchanged.
987 tests.
v1.2.1 — documentation updates
Documentation and example updates.
- The reasoned-pairs probe under
examples/now takes its stores, its models and its endpoint from the environment —FORNIXDB_PROBE_STORES,FORNIXDB_PROBE_MODELS,FORNIXDB_PROBE_ENDPOINT— instead of values fixed in the file, so it runs anywhere without editing it. Unset, it probes this machine's default store with one small local model.
That probe asks whether a real, lived-in store holds enough reasoned structure to be worth mining, so being able to point it at your own stores is the whole point of running it.
No functional change to FornixDB itself. 987 tests.
v1.2.0 — fast when it's full, honest when it's empty
Two things a memory does badly as it fills up, fixed independently — plus one addition.
Speed at size
At 12,500 memories a recall cost 380ms and opening a session cost 606ms. Neither was doing too much work; both were doing the same work the slow way. Similarity search compared the query against every stored vector one at a time in Python, and the supersede chain that brief walks fifteen times had no index on the column it walks.
| before | after | |
|---|---|---|
recall |
380 ms | 27 ms |
brief (session open) |
606 ms | 18 ms |
The search is still exact — every vector scored, nothing approximated, no ANN index introduced — and what comes back is unchanged, verified rank-for-rank against the full golden set rather than assumed. Nothing is cached between calls, so a memory written a moment ago is still visible to the very next recall.
Existing stores need no migration step and nothing to run: the new index appears on the next open.
An honest "nothing relevant"
Asked how tall a mountain is or how long to boil an egg, recall could still claim it had an answer — because a note in the store happened to reuse one common word from the question.
Every signal the abstention gate had was a magnitude, and a magnitude cannot separate "this answers the question" from "this contains that word": one accidental word in a short note outweighs several real ones in a long one. The gate now also counts how many distinct meaningful words the question and the candidate answer actually agree on, because a real answer agrees about several things and a coincidence agrees about one.
Twenty ordinary out-of-store questions leaked six before this and none after, on two stores of different shape and size, with no question the store can answer falling silent.
Added
projects— the project labels in a store, their row counts, and which of them are spellings of one project.projects --normalizeproposes the fold and changes nothing;--applywrites it. Propose-not-dispose: deciding two labels mean one project is the owner's call.
Changed
- Declaring a project in a prompt now reports its canonical name rather than the words typed, so a project has one name however you reach for it. This is what lets an alias carry a spelling a directory name cannot hold.
Known limit
The abstention gate's stopword list is English-only, and the default embedder is English-derived already. On a non-English store the gate becomes more permissive, not more silent.
987 tests. Full detail in CHANGELOG.md.