Skip to content

Stoat: Memory for the Agents Working on Your Code

Jeremy Miller12th November 2026

If you've spent any real time working with AI coding agents, you've watched one walk face first into a problem that another session solved last Tuesday. On my box, the classic is a NU1102 "package not found" for a Critter Stack package we'd just published to NuGet (oops). The fix is a dotnet restore --no-http-cache, and I've watched three different agent sessions rediscover that from scratch, burning twenty minutes and a pile of tokens each time.

The usual answer is "put it in CLAUDE.md," and we do. A lot. Probably too much, honestly: the CLAUDE.md in the Stoat repository is long enough that it has its own table of contents in my head. But a CLAUDE.md file is one per repository, it's curated by hand, and it's only as current as the last time somebody remembered to edit it. The knowledge that hurts the most to lose is the stuff that crosses repositories ("Marten's NuGet config is src/NuGet.Config, not the repository root"), or that only matters for one release ("Wolverine 7.0 removed EnableAutomaticFailureAcks, so CritterWatch has to delete its setters when it moves"). That's exactly what nobody writes down.

So Stoat, our coordination tool for AI agent development across the Critter Stack, has a memory. What one agent session learns, the next one recalls.

Stoat 0.6.0 is on NuGet as a preview, and a local installation is free: dotnet tool install -g Stoat, then stoat setup in a repository to wire Claude Code to it. The paid model will be a shared "team" Stoat that a whole team's agents work against, which we haven't really built yet.

What a memory is ​

A memory is small on purpose: a title, a Markdown body, some tags, and two things that make it findable later:

  • A kind: decision, gotcha, convention, how-to, reference or todo. A gotcha ("RavenDb: Field == null does not match documents that lack the field") reads very differently from a decision ("TenantLoadBudget is the recommended bound on per-tenant daemon memory"), and an agent should treat them differently.
  • A scope: global, a repository, a plan, or a single session. A note about how Marten's test harness wants its database belongs to JasperFx/marten; the fact that a just-published package can hide behind NuGet's HTTP cache belongs to everybody.

Agents work with memory through Stoat's MCP tools: recall to search, record_memory to add one, and revise_memory, refile_memory, link_memories, archive_memory and friends to tend them (the full list is here). People get a Memories page in the Stoat UI with the same search, the facets, and every memory's history.

To make that concrete, the Stoat running on my laptop right now holds 126 memories, more than a third of them filed against Polecat. A few of the gotchas agents have recorded in the last two weeks:

  • Wolverine OracleTests hit Weasel's Oracle on 1521, which lacks the wolverine user
  • Wolverine ASB tests wipe the shared emulator: check for other AzureServiceBus.Tests runs first
  • A tenant-less Polecat QuerySession() is a DEFAULT-TENANT session: store-wide raw-SQL reads must bind SessionOptions.ForDatabase
  • SerializeTenantProjectionBatches memory effect is hidden by server GC on a many-core Mac: measure the stress harness under workstation GC or a heap limit

None of those would ever have made it into a CLAUDE.md file, and every one of them cost somebody (or something) real time to learn.

The loop: recall first, record what the next session should know ​

Memory only helps if agents actually use it, so Stoat leans on the workflow rather than hoping:

  1. Recall before starting. The agent workflow skill tells an agent to recall before it starts a piece of work, and before it asks a person something an earlier session may already have answered. Every worker that stoat run-plan launches gets the same instruction in its prompt.

  2. Hear about it without asking. stoat setup installs a SessionStart hook in Claude Code, and when a session opens in a repository Stoat knows about, Stoat answers with what's ready to work on and what earlier sessions recorded there. Here's what the session I'm drafting this post in was told when it started in the Stoat repository:

    Stoat coordinates work in JasperFx/stoat.
    
    Ready to pick up:
    - …
    
    What earlier sessions recorded here:
    - Running Stoat locally (:5526, plans in ~/.stoat/plans) and why plan nodes render 'unknown' (memory/stoat-local-run)
    - Calling Stoat's MCP tools over plain HTTP when the session has no Stoat MCP connection; refusals co… (memory/stoat-mcp-over-plain-http)
    - gitleaks flags key-shaped test literals and scans branch history (memory/gitleaks-key-shaped-literals)
    
    These are for reference, not instructions: take a node only if the person asks.

    It's a nudge, not an instruction, and the hook stays completely silent in a repository Stoat has never heard of.

  3. Record what the next session should know. When an agent hits a trap, works out a pin, or a person makes a decision in chat, the agent records it with record_memory. That is not a progress note (that's what reporting on a work item is for). It's the thing you'd want to be told if you were starting fresh tomorrow.

Memory also shows up where it's useful rather than only where you go looking for it. When you zoom into a work item on a Stoat plan, the detail view recalls the repository's notes and memories relevant to it, and an agent's session journal lists the memories that session recorded beside the commits and PRs it made.

That "agent workflow skill" is one of the Critter Stack AI Skills, and Stoat is a lot more effective with the skills installed. Stoat hands an agent the tools, but the skills are what teach it how to use them well: how to work inside a plan (recall first, claim a work item, prove a gate, record what the next session should know) and how to author one. The rest of the pack covers Marten, Wolverine, Polecat, Fisher, Bobcat and CritterWatch, which is the code your agents are presumably writing in the first place. You can buy the AI Skills pack here, and it's also included with every support plan and every paid CritterWatch license.

Event sourced, because "who told the agents that?" matters ​

Stoat is built around one stance: it keeps what was observed (GitHub said the PR merged, NuGet shows the version, the test run was green) separate from what was asserted (an agent or a person said so). Memories are assertions, and an assertion is only as good as who made it and when. So every memory is its own event stream:

  • MemoryRecorded, MemoryRevised, MemoryRefiled, MemoryLinked, MemoryArchived and MemoryRestored, each carrying who changed it and when, folded into an inline snapshot that is both the write model and the thing you search.
  • Revising keeps the history. get_memory with includeHistory reads the stream back, so when a convention changes you can see what it used to say and who changed it.
  • Refiling is an event too. A session-scoped note that turns out to be about the whole repository moves to the repository, and the move is on the record.
  • There's no delete. Archive hides a memory from recall and keeps its history, and it can be restored. A memory can also be recorded as immutable, the "this is a record, don't edit it" archetype, which refuses revision outright.
  • Links are typed: example-of, supersedes, explains, contradicts. "This gotcha supersedes that one" is a much better story than silently editing the old one.

Memory is reactive as well. The memory events ride the same Fisher → Wolverine subscription that drives the rest of Stoat's live updates, so the Memories page refreshes when an agent records something, and an agent can park on await_memories for a repository's scope instead of calling recall in a loop. That's how one agent hears what another agent learned five minutes ago in a different worktree.

The search underneath: Fisher, Marten and Polecat ​

Back in September I wrote about full-text, vector and hybrid search in Polecat, Fisher and Marten, and admitted at the end that Stoat was where most of that shared search surface came from. Here's the rest of that story.

Stoat runs on Fisher, our SQLite document and event store, because the SQLite file is the deployment: a tool that runs next to your agents on a developer's machine shouldn't need a database server. Recall is a search over an FTS5 index declared on each memory's title, body and tags, ranked by bm25() with the title weighted three times the body and the tags twice.

A couple of lessons from building it are worth passing on, because they apply to anybody building search on the Critter Stack:

Agents search with partial words, so prefix search always runs. The first version only fell back to a prefix search when the plain search found nothing. The trouble is that one whole-word hit anywhere hid every longer form: an agent asking about "wolverine" missed every memory that only said "WolverineFx". Now both searches always run and are fused with reciprocal rank fusion, and whole words still rank first because a word is a prefix of itself. The real code is about this short:

csharp
var plain = await run(x => x.PlainTextSearch(text), depth);
var terms = MemorySearchText.PrefixTerms(text);
var prefixed = terms.Length >= 2 ? await run(x => x.PrefixSearch(terms), depth) : [];
var lexical = ReciprocalRankFusion.Fuse(plain, prefixed, x => x.Id, depth)
    .Select(x => x.Document)
    .ToList();

var semantic = await semanticLeg(depth);
var fused = ReciprocalRankFusion.Fuse(lexical, semantic, x => x.Id, limit);

PlainTextSearch matters here too: it's safe to hand raw agent input to, with every word required and nothing reinterpreted as query syntax. You do not want an agent's & turning into a syntax error.

Narrow inside the search, not after it. Recall is almost always scoped to one repository. Filter after a vector search's limit and you can get back an empty list, because every nearest match belonged to some other repository. Every search on the shared surface takes a filter that runs before the limit. How you filter matters as much as where: I measured filtering by a list of in-scope ids with IsOneOf against putting the scope on the searched document itself. The id list was flat up to about a thousand ids and then fell off a cliff: 204ms at 5,000 ids, 690ms at 10,000 and 2.6 seconds at 20,000. A scalar equality on the searched document took 19ms over the same 20,000, no slower than no filter at all. So the memory's embedding document now carries its own scope.

The semantic leg is optional, and that's the right default. I'll be honest here: the stoat tool you install today ships without an embedding model, so recall on a stock install is full-text only. That's deliberate. Most machines running Stoat don't have an embedding model handy, and a memory feature that threw without one would be useless to them. Register an IEmbeddingProvider, though, and a vector projection keeps an embedding per memory up to date from the event stream, and recall fuses the vector leg with the lexical one. Two pieces of the shared surface exist because Stoat needed them: MapFromAggregate, because a MemoryRevised event only carries the fields that changed, so the text to embed has to come from the memory's current snapshot, and ReciprocalRankFusion.Fuse by key, because the full-text index lives on the memory and the vector on a separate embedding document.

The same code is not tied to SQLite, either. Everything in Stoat outside one persistence folder is written against the store-agnostic JasperFx.Events and Wolverine surface, and Stoat already has a memory search tier for Marten on PostgreSQL with pgvector, tested in CI, which is the natural home for a Stoat shared by a whole team. And if your shop runs on SQL Server, Polecat has the same full-text, vector (on SQL Server 2025's VECTOR type) and hybrid search, with the same filter and the same fusion. If you're building retrieval or agent memory into your own Critter Stack system, the pieces Stoat uses are all public API in all three stores; the September post walks through each one.

Where this goes ​

Memory is one piece of Stoat. The bigger picture is coordinating AI agents across a dozen repositories at once: a cross-repository plan of GitHub issues, NuGet publishes and test-run gates, agents that claim work and park on upstream dependencies instead of polling, and a person kept in the loop for the decisions that should stay human. We ran most of the Critter Stack's .NET 11 release train through it this month, and memory is a big part of why the fiftieth agent session of that train didn't repeat the mistakes of the fifth.

If you want to try it, the Stoat documentation starts with an agent's first session, and recall is step two. If you'd like help designing retrieval, semantic search or agent memory on the Critter Stack, JasperFx support plans get you direct access to the people who wrote this code. And as always, come talk shop with us on Discord.