← Blog Architecture 24 September 2026 10 min read

Memory without a vector database.

Luminair remembers with a folder of plain Markdown notes. A small digest decides which of them ride on each prompt. The day one 73 KB note slipped into the wrong folder, every prompt grew to about 100 KB, and we learned what memory really costs.

Fig 01  From a folder of notes to one promptThe Journal digest
THE VAULT · A FOLDER ON DISK Luminair Journal/ Global Rules/ Projects/ Research/ Notes/ (silent) Harness/ --- id: 20260914T0930-… kind: rule scope: folder --- # then Markdown DIGEST · REBUILT FOR EVERY MESSAGE Rules in full, never clipped Dead ends always ride Asks and notes ranked, top 14 Long notes one line + a pointer Docs never ride; searchable instead RANKING: WORD MATCH + OPTIONAL VECTORS + STANDING HARNESS FILTER · EVERY TURN, EVERY MODEL kind: doc → skip over 24 KB → skip WHAT THE MODEL READS Session contract Journal digest Connected tools Core rules (Harness) AGENTS.md, goal, handoffs Your message HARNESS skip ….md (doc, …B): docs and oversized notes never ride on the prompt LOGGED ONCE PER FILE
Two roads into the prompt. The digest picks from the whole vault, message by message. The Harness folder holds the app's standing rules and rides on every turn, so it has a gate: reference docs and anything over 24 KB are left out, and the skip is written to the debug log.

The short version

  1. An AI session forgets everything when it ends. Luminair gives it a memory made of ordinary Markdown files in a folder you can open in Finder or Obsidian.
  2. Each note says what it is (kind) and who it is for (scope: one session, one project folder, or everywhere).
  3. Before every message, a digest picks what rides along: rules in full, dead ends always, and a ranked, capped handful of everything else.
  4. One 73 KB reference doc in the always-on Harness folder pushed prompts from about 30 KB to about 100 KB. Docs and notes over 24 KB are now skipped there.
02Built with Luminair

The memory we use every day.

The Journal is not a feature we designed on a whiteboard. It is the memory the Luminair team has leaned on while building Luminair: sessions on different models writing down what they decided, what failed, and what the owner asked for, so the next session does not start from zero.

That is why the source reads like a logbook. The digest module opens by saying its selection rules are "a port, not a redesign", and that "every one of them was paid for in a real failure". This post walks through those failures, because they are the most useful thing we know about agent memory.

Luminair · sessions side by side · Journal button pinned
Luminair desktop app with three chat sessions side by side, a folder sidebar on the left and a row of icons in the top bar 1
Pin 1 marks the book icon in the top bar: that is the Journal. Every session in these panes reads from the same vault, whichever model it runs on. (Demo workspace.)
03Context is a budget

Every token has to earn its place.

Think of a model's context window as a desk. Anything you put on the desk, the model has to look at before it answers. A bigger desk helps, but a desk buried in paper is worse than a small tidy one.

In September 2025 Anthropic put a name to managing that desk. Its engineering post on effective context engineering describes it as "the set of strategies for curating and maintaining the optimal set of tokens (information) during LLM inference", and warns that models have an "attention budget" that long contexts draw down. The goal it sets is to find "the smallest possible set of high-signal tokens that maximize the likelihood of some desired outcome."

The same post describes structured note-taking: "the agent regularly writes notes persisted to memory outside of the context window", and those notes "get pulled back into the context window at later times". It also mentions Anthropic's file-based memory tool. Files, not a database. That is the design the Journal landed on too, for three plain reasons:

You can read itEvery memory is a .md file. Open it, fix it, delete it. Nothing hides in an index you cannot inspect.
Anything can edit itThe vault is a real Obsidian vault. Git, Finder and iCloud can touch it; the app watches for changes.
It degrades, never failsNo server has to be up for memory to work. Vectors are an optional upgrade, not a dependency.
04A note is a file

Three fields do most of the work.

By default the vault lives at ~/Documents/Luminair Journal. A note is Markdown with a small block of YAML frontmatter on top. The parser writes Luminair's own keys in a fixed order (id, kind, scope, then optional ones such as cwd and session) and keeps any keys it does not know, so nothing a person or Obsidian adds is silently dropped.

Global Rules/App Rules.mdshape of a real vault note
---
id: 20260825T1309-a1c7
kind: rule
scope: global
title: "App Rules (all users, all models)"
created: "2026-08-25T20:09:00.000Z"
updated: "2026-09-03T10:31:04.121Z"
summary: "These rules are baked into the Luminair app itself …"
---

## 1. App Rules (all users, all models)
…
The frontmatter of an actual note in the team's vault, body shortened. The id is a timestamp plus four random hex characters, the format lib/vault/paths.js generates.

kind says what the note is. A rule is standing law. A note is knowledge. A doc is reference material, a notebook page. The app also writes a few kinds of its own: an ask captured from a prompt, and a dead end (stored as ruledout) for an approach that has been proven not to work.

scope says who should see it: session (the default when a model files a note), folder (one project, matched by its path), or global (every session in every project). When two layers say the same thing, global wins over folder, and folder wins over session. The loser is hidden, never deleted.

A folder that stays quietThe top-level Notes/ folder is a private scratchpad. Nothing under it is ever shown to a session, not as a rule, not as a search result. Any folder can be marked silent the same way with silent: true on its folder note.
05How notes reach a prompt

A digest, rebuilt for every message.

A library does not hand you every book when you ask a question. It hands you the house rules, a warning about the shelves that are closed, and a few books that match what you asked. The digest works the same way. It is built by lib/vault/digest.js, and the prompt hook calls it through scripts/journal.js digest with the session id, the project folder and the prompt.

01

Rules ride in full

Anything of kind rule, at any scope that applies, is printed completely and is never cut by the cap. Rules tagged with a trigger group (deploy, git, design and so on) print in full when the prompt touches that area and as a short index line otherwise.

02

Dead ends always ride

Re-proposing something already proven dead is the failure the Journal exists to prevent, so every open dead end is included, every turn.

03

Everything else is ranked, then capped

Open asks and notes compete for a default of 14 places. Docs, pasted text and generated index notes never compete: they stay searchable instead.

word match (BM25)meaning match (optional)recency · 21-day half-lifekind weight
04

Long notes become a pointer

A note over 260 characters contributes its first sentence plus the command that fetches the rest, journal.js read <id>. The source puts the trade plainly: the pointer costs about 20 characters and loses nothing.

So where are the vectors?

There is no vector database. Word match (BM25) is the workhorse, because it is exact, explainable and the only signal that reliably finds file names and error strings. If Ollama is running on the Mac, notes also get an embedding from nomic-embed-text, computed in the background when a note is written and cached in a single vectors.json file in the vault's hidden .luminair folder.

On the read path the only live call is embedding the prompt itself, with a 400 ms timeout. Past that, or with Ollama off, the digest falls back to word match and says nothing. The two rankings are combined with Reciprocal Rank Fusion, which reads only the order each one produced, because their raw scores are on incomparable scales.

Meaning-match is an upgrade to ranking, never a dependency of it.

From the header of lib/vault/embed.js.

Two lessons already in the code

On 16 August 2026, one 8,000-character research note filed as global rode in full on every single message, because the rule at the time treated every global item as standing law. Now only an actual rule gets uncapped treatment; a global note is ranked like anything else.

Later, the cap was shared between rules and everything else. Once the team had 49 rules against a cap of 14, the arithmetic went negative and every open ask and dead end vanished from the digest, "hidden behind a block that still looked full", in the words of the comment. The cap now belongs to asks and notes alone.

06The 73 KB note

One file, every prompt, three times the size.

Separate from the digest there is a second road into the prompt. Since 30 August 2026 a Journal folder called Harness/ holds the app's standing brain: rules that ride on every session, every model, every lane. The owner edits it like any other folder; shipped builds carry a bundled copy.

The function that injects it, harnessBlock() in desktop/main.js, joined every .md file in that folder. That was fine while the folder held rules. On 10 September 2026 a 73 KB reference document, the knowledge base for Solace (Luminair's support assistant), landed there with kind: doc. From then on, about 70 KB of every prompt in every session was that document.

Fig 02  Size of each prompt, per turnKilobytes · approximate
Beforerules only
~30
~30 KB
Afterdoc in Harness
~30
~70 KB: one doc
~100 KB
050100 KB
Figures are the approximate sizes recorded in the incident note at the top of desktop/lib/vault/harness-filter.js, not a benchmark. The hatched segment is the single 73 KB document.

The note records the knock-on effects too. Session transcripts grew about three times faster, and the larger resumed transcripts that followed are what pushed some Claude CLI turns past the app's start watchdog. It was fixed on 14 September and shipped in the build committed on 16 September (91ebf349). The fix is small enough to quote whole:

desktop/lib/vault/harness-filter.jslines 26–35
// Returns { ok, reason, body } for one Harness note's raw file text.
function harnessEligible(raw, opts = {}) {
  const max = opts.maxBytes || MAX_NOTE_BYTES;
  const fm = frontmatter(raw);
  const body = String(raw || '').replace(/^---\n[\s\S]*?\n---\n/, '').trim();
  if (!body) return { ok: false, reason: 'empty', body: '' };
  if (String(fm.kind || '').toLowerCase() === 'doc') return { ok: false, reason: 'doc', body };
  if (Buffer.byteLength(body, 'utf8') > max) return { ok: false, reason: 'oversized', body };
  return { ok: true, reason: '', body };
}

MAX_NOTE_BYTES is 24 * 1024. There is a second guard at write time as well: the store refuses to create a kind: doc inside Harness/ and files it under Notes/ instead.

The general lessonAnything that rides on every turn is multiplied by every turn. A rule is small and always relevant. A document is large and usually not. Keep them on different roads: rules in the always-on block, documents one search away.
07Find it in the app

See what your model sees.

  1. 1Click the book icon in the top bar, or press ⌘⇧B, to open the Journal on this session's note. From there you can browse folders and open the graph.
  2. 2In any session, type //context in the composer and send. The Context budget sheet lists every block Luminair injects and what it costs.
  3. 3Find the row Journal digest: rules and open items the hook injects. It is rebuilt per message.
  4. 4Untick Include optional Journal notes and old conversation summaries to send only standing rules and pins on the next turn.
//context · Context budget
Context budget

Tokens are estimated at four characters each.

…estimated dispatch context
…last turn processed
…this session · … turns
… / …files read / changed
Session contract: base rules, origin, recap fences, Luminair toolson every turn, every lane
3Journal digest: rules and open items the hook injectsrebuilt per message
Turn contract: working state that survives compactionappears from the 2nd turn
AGENTS.md from the projectnone in this folder
Connector awareness…
Tools attached (MCP)…
Drawn with the exact labels from build 1.0.236 (a subset of the rows). Values are left as "…" on purpose: they depend on your session, and we will not print made-up numbers.
Journal · graph view
The Journal graph view: a folder tree on the left and a network of dots on the right, marked by kind, with Global, Local, Folder and Session layer buttons
The graph view, captured from an earlier build when the panel still carried its old name, Scrapbook, in a demo vault, shown here inverted to greyscale. Each dot is a note; the legend marks rules, notes, asks, index notes and dead ends. The layer buttons switch between global, local, folder and session views.
08Memory that tidies itself

Capture is easy. Keeping is hard.

In May 2026 Anthropic added memory to Claude Managed Agents. In its words, memory "lets each agent capture what it learns as it works", and a process called dreaming "refines that memory between sessions". Dreaming can update memory automatically or leave changes for a human to review.

In August, The New Stack reported on a talk by Lamis Mukta of Anthropic. The article notes that a single instructions file gets hard to manage as it grows, and that memories go stale. Mukta's picture of good retrieval is a bookshelf you scan by title before pulling one book down. The same article carries a warning from engineer Jayakumar Ramalingam:

“A bad answer normally dies with the session; a bad memory can influence thousands of future sessions.”Jayakumar Ramalingam, quoted in The New Stack, 12 August 2026

The Journal met both problems on its own scale, and its answers are deliberately conservative:

  • Distill. journal.js distill folds a quiet session's raw asks into what was decided, what is still open and what was a dead end. The raw items are archived, not deleted: they stop riding in the prompt and stay searchable.
  • Promote, but only with a yes. journal.js promote finds a request that came back in three or more separate sessions and proposes it as a one-line rule. It never writes the rule itself. As the module says, an unapproved standing rule "would be injected into every prompt of every session forever".
  • Keep the history. A merged note keeps earlier phrasings under "Said again" for people to read, but the digest cuts them out. Overridden notes are hidden, not erased.

Not measured yet

  • How much the digest improves answer quality, on any model
  • How often meaning-match changes which notes are picked versus word match alone
  • Typical digest size across different users' vaults

Sources

Give your sessions a memory

Open the Journal with ⌘⇧B and watch what rides on your next prompt.

Download Luminair →