← Blog

Your agent's memory queue shouldn't be a folder of files

· Michael Hermus
  • engineering
  • dogfooding
  • agent memory

The recall post landed on the most important decision in the whole design: a small cheap model proposes memories, and the capable agent commits them. The proposer cannot write. That came from a test run where a proposer with write access recorded a confident lesson that was simply wrong, and a wrong memory is worse than none, because recall keeps handing it back as fact.

That split is still right. But it has a consequence we didn't design for, and didn't notice for a while.

A proposal that isn't a memory yet, and isn't thrown away either, has to live somewhere. The split creates a second store: a queue of claims proposed and not yet judged. We built the memory tier carefully and wrote about curating it, then let that queue happen by accident, the way these things always do. A folder of files.

What the queue actually has to do

Written out plainly, this queue stops looking like a scratch file.

It holds a claim: who proposed it, when, and what became of it. Beyond that, it has to:

  • Survive the session that created it. Sessions often end before anyone judges what's in them, and the next session should inherit those proposals, not lose them.
  • Answer "what's still waiting in this conversation?" cheaply, on every turn. That's the moment a nudge either fires or doesn't.
  • Let a verdict be undone. Verdicts are made by fallible readers.
  • Keep what it rejected, not just what it kept.

That last one is easy to miss. Storing a memory that turns out to be junk is cheap and reversible: delete the row. Discarding a claim that was actually true isn't: it vanishes silently, and the lesson gets re-learned the expensive way months later. So a dismissal has to be cited and reversible. The queue isn't a to-do list you clear. It's a durable log of judgments, including what it said no to.

Where folders went wrong

One file per session, appended as proposals arrived, read back on review. It worked on day one, then degraded in the generic ways file queues do, nothing exotic about ours.

It read everything to answer anything. The only way to find what was unsettled was to list the directory and fold every file. By the time we measured: 576 proposals across 62 session files, to surface the 34 actually pending, on every turn. The cost tracked the whole history instead of the small part anyone cared about, and it stayed tolerable only because we were the only ones using it.

Positions drifted, and got used as addresses. The review surface numbered pending items; the agent acted on them a turn later, by number. Those numbers were assigned over a filtered set, sorted by a day-granular timestamp, so a claim's position could shift between the read and the write. An agent reading a list in one turn and writing a verdict in the next could commit that verdict against the wrong claim. Nothing errors. You just get a confident, wrong judgment.

Sessions ended and left their queues behind. A file whose owning session is gone belongs to nobody, and the only evidence for whether it's abandoned or just quiet is a modification time.

And the durability story was one machine's disk. Every ordinary file-queue failure lives here: an interrupted append leaves a partial record that parses as garbage, or worse, as something valid; two writers racing interleave their lines; none of it is visible until a reader trips over it.

You can fix each of these in a file, but fixing them means writing an index, a lock protocol, a compaction pass, and a recovery path: a database, badly, inside your agent's tooling. We made this argument about the memory tier itself in the first post. We just hadn't noticed it applied a second time, to the thing standing in front of it.

The one requirement that rules out the obvious stores

So put the queue rows in a real store. Which one?

The constraint that makes this interesting is the opposite of what memory wants:

An unverified claim must never be retrievable by meaning.

The memory tier's whole point is that a relevant note surfaces when the agent is about to act. The queue's whole point is that its contents haven't earned that yet. Put a proposal and a verified memory in the same searchable pool, and the first meaning-based recall returns an unreviewed guess next to reviewed knowledge, ranked by similarity, indistinguishable from it. That's the exact failure the propose/commit split exists to prevent, reintroduced one layer down.

The two obvious options both fall over here.

A vector store can't express it. Its whole contract is that what goes in comes back by similarity; there's no "hold this but never surface it" mode. So you'd keep the queue in the same index and filter it at read time, which puts the guarantee in every caller that queries, forever, holding only as long as nobody forgets. This series keeps landing on the same point from different angles: a property maintained by discipline at every call site isn't a property. Our recall path issues queries from several places. It only takes one miss.

A dedicated memory API can't express it either, for a different reason. Those products model one thing well: a memory, with recall over it. A queue isn't a memory. It's a small workflow table: a typed row moving through proposed, then kept or dismissed, with a citation, a correction pointer, and a record of reversals. That needs exact enumerable queries, not similarity. A memory-shaped API forces you to serialize the workflow into a text blob and run the state machine yourself, which is the folder again, wearing an API.

The queue and the memory it feeds have genuinely different requirements. Nothing narrow enough to serve one well fits the other.

What a general-purpose backend gives you that neither does

What we needed was mundane, and that's the point: a typed record with exact indexed lookups, declared store-only, so it can never be indexed for search. Rows persist, are readable by id, and are fully queryable by their structured fields, but no search and no grounded answer can ever return them. Not deprioritized. Absent from that path entirely, by declaration, in the schema.

That's the difference between an air gap and a convention. The queue lives in the same backend as the memory it feeds, under the same ownership rules and the same tamper-evident audit trail, and still can't compete with it for the agent's attention.

The structured half does what the file couldn't. Each proposal is a row with typed fields, so a verdict is a field update, reversible, with the reason attached, instead of a rewrite of an append-only file. Sorting runs on the store's own millisecond-precision timestamp, so positions stop drifting and an address still means the same thing a turn later. And the query that runs most often gets an index built for it: "what's unsettled in this conversation" is a conjunction of two fields, declared as one combined lookup, so the hot path is a single exact query instead of a scan of the whole history.

What we would tell you

  • Separating proposal from commitment creates a second store. Adopt that split, and you have a queue whether you designed one or not. Design it.
  • A staging area's requirement inverts memory's. Memory should surface; unverified claims must not. Put both in one searchable pool, and the filter separating them lives in every caller, and fails the first time one forgets.
  • Make the exclusion structural, not disciplined. A store-only type can't be searched by accident. A convention can, and eventually will.
  • A dismissal is a decision, so keep it. Storing a wrong memory is reversible; discarding a true claim isn't. Cite dismissals, make them undoable, and keep the rejected rows.
  • Positions aren't identifiers. An agent reading a numbered list and acting a turn later is treating those numbers as addresses. Sort on something that doesn't move.
  • File queues converge on a bad database. Once you're writing an index, locking, compaction, and recovery, you've built a database inside your tooling. Notice earlier than we did.

The tier that holds our agents' memory is the one we sell, and the queue in front of it runs on the same backend: same ownership model, same tamper-evident audit trail, a type that's structurally unsearchable. Stand up the knowledge base and enroll an agent with its own private memory:

npm i -g @vectros-ai/cli
vectros login
vectros bootstrap --blueprint agentic-sdlc --no-seed
vectros join agentic-sdlc --role member

The capture-and-review loop that fills that queue is still what we run on our own work rather than something you install, same caveat as the recall post. That's about to change: we're close to publishing the source. What you can use today is the substrate underneath it, already running on our own production queue: typed records, ownership-fenced, searchable by meaning when you want it, and structurally unsearchable when you must not have it.