Skip to content

Design decisions

The main choices behind Jezero, in a short decision-record form: the context, what was decided, what follows from it, and the alternatives. The reasons come from the README, the changelog and comments in the code. Where the project does not record a reason, the entry says so and describes the trade-off as it shows up in use.

Decision In one line
One SQLite file with FTS5 no database server, backups are a file copy
Keyword search over summaries, not vectors literal, fast, explainable; synonyms are the cost
Summaries and atoms, written once by a cheap model pay once per session, never per read
Recency as a bounded boost newer wins ties, old knowledge stays findable
later instead of superseding the library never rewrites an item; agents check for newer ones
A read-only MCP server agents cannot change memory
A separate inbox for uploads one write path, outside MCP, reusing the connectors
Device keys created on the device the server only ever stores a digest
One standard-library client core, copied into each plugin runs on any computer's python3, installs per agent
Hooks, so agents use Jezero by habit context arrives without being asked for
The source's project beats the model's folders say where work happened
Summarise finished sessions, in stable segments no repeated calls on a growing session
Secrets only by environment variable name the config can be shared
The overview endpoint outside MCP one less tool for agents to choose from
A terminal app built with Textual people look through the same door as agents
Connectors register themselves a new source is one file
Unattended runs that fail loudly one run at a time, silence is an alert

One SQLite file with FTS5

Context. The library is written by a few connectors on one server and read by many agents at once. It holds thousands to tens of thousands of short records.

Decision. Store everything in one SQLite file: the items, an FTS5 full-text index kept in sync in the same transaction, and the connectors' state. Use WAL mode so readers work while a connector writes.

Consequences. There is no database server to run, secure or upgrade, and a backup is a file copy. Writers are serialised by SQLite, which suits a pipeline that runs one connector run at a time. The index needs SQLite 3.45 or newer for accent-insensitive trigrams. Schema migrations are not implemented yet: init refuses an unknown schema version and a tokenizer different from the one the file was built with. Following the project's rule "keep it small", the store adds no dependency.

Alternatives. A database server (PostgreSQL with full-text search) would add a service to operate for a load SQLite handles. A dedicated vector database would add one too, for a kind of search Jezero does not use (next entry).

Keyword search over summaries, not vectors

Context. Agents look for exact things: an error, a command, a service name, a past decision about a named component. The text available for each session is a summary and atoms written for retrieval.

Decision. Rank with BM25 over the summary fields, using a trigram tokenizer that ignores case and accents and matches substrings. Quote every query term, so queries are literal and cannot inject operators. The full mechanics are in How search ranks results.

Consequences. Reads take milliseconds with no GPU and no model, so ten or more agents can query at once, and a hit can be explained by the words it contains. Plurals and word fragments match through substrings. A query in different words from the item misses it; that cost is reduced by the summariser's "questions that would find this", by summarizer.output_language, and by telling agents to retry with synonyms or the singular. Terms under three characters cannot be searched on their own.

Alternatives. Embeddings over raw transcripts were rejected in the README's terms: transcripts are mostly tool output, and summaries plus questions make BM25 work well while keeping the index small. Vector search is deferred, not ruled out: it can be added later in the same file with sqlite-vec if keyword search falls short, and a retrieval benchmark is on the roadmap to tell.

Summaries and atoms, written once by a cheap model

Context. Sessions are long, noisy and numerous. An agent that finds one needs the outcome, not the transcript.

Decision. One LLM call per session (or segment) writes a title, a summary, questions, up to twelve typed atoms and a project slug, through a fixed JSON schema. Use a cheap, fast model, on any Anthropic or OpenAI-compatible endpoint. Keep the redacted original next to the item. See Summaries and atoms.

Consequences. The expensive step runs once per item and never on reads ("write once, read many"). Unchanged text is never summarised twice, thanks to a content hash. The library's quality depends on the summariser: a missed decision is not in the atoms, though it may still be in the original. Changing the model or the language means re-summarising on purpose (jezero resummarize). The provider sees the redacted text of every session.

Alternatives. Indexing raw transcripts (see above). A stronger, more expensive model would write better summaries at a price paid on every session of every agent, including backfills of thousands; the config steers towards a cheap one. Summarising on each device would put a model key on every computer and duplicate the pipeline; the inbox keeps it in one place.

Recency as a bounded boost

Context. An old price or an abandoned decision should not outrank the current one. But old knowledge, such as how an obscure error was fixed, must stay findable.

Decision. Re-rank the best BM25 candidates by a factor between 1 - recency_weight and 1 that halves its distance from the floor every recency_half_life_days (0.3 and 90 days by default).

Consequences. Recency reorders relevant items and never makes an irrelevant one relevant. An old item still wins when it is clearly more relevant (about 1.43 times, with the defaults). Only the candidate pool is re-ranked, so very recent but weak matches can stay out. Recency cannot tell whether a newer item changed an older one, which is why later exists.

Alternatives. Sorting by date, or filtering to recent items, would hide old fixes. Pure BM25 would let stale decisions win on wording alone.

later instead of superseding

Context. Decisions change. Each item summarises one session, so nothing in the library marks a decision as superseded: the session that reversed it is a separate item.

Decision. Add the MCP tool later(item_id, query?): newer items of the same project on the same subject, newest first, with a flag saying whether they matched the subject or are the project's timeline as a fallback. The server instructions and the plugin protocol tell agents to call it before answering with a decision, plan, status or number. The matching rules are in How search ranks results.

Consequences. Items are immutable history; the answer about the current state is assembled at read time, and the agent can say when and where something changed. It costs one more call, and it depends on agents following the instruction. The project is the boundary: a change recorded under another project is not found.

Alternatives. Rewriting or merging old items when a newer session contradicts them would need a write path from reading agents (or a model judging contradictions at ingest time), and would lose the history of what was believed when.

A read-only MCP server

Context. Agents read untrusted content and can be manipulated by it. A shared memory that agents can write is a shared memory that one fooled agent can poison for all the others.

Decision. Expose no write tool. Annotate every tool read-only, open the database read-only for every request, and let only connectors, run by the admin, write items. The plugin protocol tells agents there is nothing to save.

Consequences. The worst a manipulated agent can do through Jezero is read what its key allows. Memory grows only from real sessions, and agents cannot record a note on purpose: what they did is captured when the session is ingested. See Security model.

Alternatives. A save or remember tool, common in agent memory servers, was left out for the reason above.

A separate inbox for uploads

Context. Sessions happen on several computers, and the connectors run on the server. The read-only MCP channel cannot carry them.

Decision. Add PUT /inbox/v1/<agent>/<path>, a plain HTTP route for keys with upload = true. Store each file as sent, in <inbox>/<device>/<agent>/..., mirroring the agent's own store, and let the usual connectors read those folders. Set each file's modification time to the upload time.

Consequences. Summarising, redaction and deduplication stay in one place: a device's folder is read by the same connector code as a local store. No SSH access between computers is needed. A key can write only into its own device folder, and only paths that match its agents' layouts. Using the upload time keeps a device that was offline for a week from uploading "into the past" behind a connector's cursor. Raw transcripts are kept on the server, unredacted. An earlier receiver connector type was removed when the inbox replaced it.

Alternatives. Syncing agent folders over SSH, which the README explicitly avoids. A write tool in MCP, which would break the previous decision.

Device keys created on the device

Context. Each computer needs its own key, and the admin of the server needs to approve it. Sending a secret through chat or email to whoever installs a plugin is the usual weak point.

Decision. jezero_client.py init creates the key on the device and prints only its SHA-256 digest, as a ready-made jezero device approve line. The server stores the digest; the approval also creates the device's inbox connectors, disabled.

Consequences. Nothing secret passes through the approval, and a copy of the server's config holds no usable key. Revoking (jezero device revoke) and rotating (jezero device rotate) are edits to that config, which the server reads again within a second, so neither needs a restart. On the device, agents reach the server through a local bridge that reads the key file, so no agent's configuration holds the key either. Sandboxed agents can hold their own key in their config folder. A lost device key cannot be recovered; the device makes a new one.

Alternatives. Admin-generated tokens remain available for other clients (jezero new-token prints the token once and its digest for the config), but for devices they would mean handing a secret over.

One standard-library client core, copied into each plugin

Context. Plugins run on computers Jezero does not control, inside five different agents, each with its own plugin system and install path.

Decision. Put all client logic in one file, clients/core/jezero_client.py: Python 3.9 or newer, standard library only, so it runs with the python3 that ships with macOS. Plugins are thin adapters that map their agent's events to commands of that file. scripts/build_clients.py copies the core, the project rules and the shared skill into every plugin, and CI checks that plugins/ matches clients/.

Consequences. Nothing to install on a device besides the plugin; each plugin folder is complete and installs straight from GitHub through its agent's own mechanism. There is a single source for the logic, but several copies are installed, so a computer can run different core versions in different agents (the app's Agents screen shows them). The standard-library constraint rules out conveniences such as an HTTP client library. The server package ships the same core as jezero/client_core.py for the app.

Alternatives. A pip-installed client package would need a Python environment on every device. Plugin-specific implementations would multiply the logic by five.

Hooks, so agents use Jezero by habit

Context. An MCP tool that an agent is never prompted to use is rarely used. People do not mention Jezero in their prompts, and should not have to.

Decision. The plugins hook into agent events. At session start they add a short protocol (search before non-trivial work, check later, cite ids, never guess, nothing to save) and the project's five most recent items. On the first prompt they add up to three related items, chosen by a two-word rule rather than a score (why). Hooks never block or break the agent: 2.5-second timeouts, a catch-all, uploads in a detached process.

Consequences. Agents use the library without being asked, as in the README's example. The cost is a little context in every session and two calls to the server at its start. The rule for related items is strict on purpose, so the block stays rare enough to be read. When the server is down, sessions lose the context and nothing else.

Alternatives. Relying only on the MCP server's instructions and tool descriptions. Those exist too, but they wait for the agent to decide to use a tool; the hooks put the protocol and the context into the conversation itself.

The source's project beats the model's

Context. timeline, later and the session-start context all work per project. The model can propose a project, but its guess follows whatever the text mentions.

Decision. A hint from the source wins: the session's working folder (configured paths, then the git repository, the main one for worktrees, then common root folders) or the project a device found and sent with the upload. The same project rules run on the server and on devices. jezero retag re-applies hints to stored items without a model.

Consequences. Items land in the project where the work happened, even when a session talks about another one. A session run outside any recognisable folder falls back to the model's guess, or the connector's default project.

Alternatives. Trusting the model's tag, which was the first behaviour and the reason for the change.

Summarise finished sessions, in stable segments

Context. Sessions grow for hours or days, and a long one does not fit one cheap call.

Decision. Defer a session until it has been quiet for settle_minutes (30). Cut long sessions into segments of at most max_input_chars (60,000) whose boundaries depend only on the blocks themselves, with ids built from the session and the segment's first block.

Consequences. An ongoing conversation is not summarised every few minutes, and as a session grows only its last segment changes and is summarised again. A finished session reaches the library within about an hour. A long session becomes several items. See segmenting.

Alternatives. Summarising a whole session at once, which is impossible above the input limit and would be repeated in full on every change.

Secrets only by environment variable name

Context. The config describes machines, sources and clients. It should be possible to share it, review it and keep it in version control.

Decision. Fields ending in _env hold the name of an environment variable; connector options that look like secrets are rejected; client keys appear only as digests. A .env file next to the config is loaded, and the environment wins over it.

Consequences. A config file leaks nothing. check-config can tell which variables are missing without printing values. Setting up a service means providing the variables, for example with a systemd EnvironmentFile.

Alternatives. Inline secrets with a "do not commit" warning, which is how leaks usually start.

The overview endpoint outside MCP

Context. The app's home screen needs counts per source, per project and per day.

Decision. Serve them at GET /api/v1/overview, for any client key and only its sources, instead of adding an MCP tool. The code states the reason: agents do not get one more tool to choose from.

Consequences. The MCP surface stays at five tools, all useful to an agent. The app uses two interfaces, MCP and one HTTP route, with the same key.

Alternatives. An overview MCP tool, rejected for the reason above.

A terminal app built with Textual

Context. People also want to look: what Jezero holds, whether each source is alive, what an agent receives, and whether the plugins on this computer are up to date.

Decision. jezero with no command opens a terminal app built with Textual, with Home, Search, Timeline, Agents and Agent view screens. It reads through the same MCP tools and key as an agent, plus the overview endpoint; jezero ui --demo runs it on made-up data.

Consequences. The project does not record why a terminal app was chosen. As it shows up in use: the app runs wherever the jezero command is installed, over SSH included, and needs no web server, browser session or extra port to secure. Because it reads like an agent, what people see is what agents can see, limited by the same source patterns. The cost is two dependencies (Textual and Rich) in a project that keeps dependencies few, and a terminal is a narrower canvas than a web page.

Alternatives. A web dashboard served by jezero serve is the obvious one; it would widen the HTTP surface that the security model keeps small.

Connectors register themselves

Context. Every new source used to mean editing a central list.

Decision. Each connector module declares a CONNECTOR_TYPE with its build function and is discovered automatically; other packages can add connectors through the jezero.connectors entry point group. Planned types are listed without a build, so check-config can tell a typo from a type that is not implemented yet.

Consequences. Adding a connector is one file. A connector only knows its source's format; redaction, hashing, summarising and cursors stay in the pipeline. See Add a source.

Alternatives. A hard-coded registry, which was the earlier state.

Unattended runs that fail loudly

Context. Jezero runs from a timer, and a connector that quietly stops bringing anything in looks exactly like a quiet week.

Decision. jezero run --due runs only connectors whose interval has elapsed, one run at a time (a lock file), and a failing connector no longer stops the others. Connectors can report warnings, for example when a source format changes; the pipeline records them as the connector's error and holds the cursor. jezero health flags stale and failing connectors, can post a webhook alert with de-duplication, and can ping a push monitor on every check, so that silence itself becomes an alert.

Consequences. A problem stays visible until it is fixed, instead of a cursor moving past data that was not read. A dead timer is caught by the monitor on the other end, not by Jezero. See Monitor Jezero.

Alternatives. Logging errors and moving on, which hides format changes until someone notices missing memories.

See also