Stash is a single Go binary that gives AI agents long-term memory backed by Postgres. Here's how the code is organized and why it works.

Stash gives AI agents persistent memory with Go and Postgres


Stash ships as a single Go binary that gives AI agents persistent memory. Episodes, facts, goals, hypotheses, causal links, working context — it all lives in Postgres. It includes an MCP server so any MCP-compatible AI agent can read and write memory without custom integration code. No cloud service. You self-host everything.

What pulled me in is how the project organizes its Go code. The internal/brain package owns all the memory logic, and the CLI layer in cmd/cli wires everything together through a clean command structure. Let me walk through it.

The brain package: where memory lives

The core of Stash is internal/brain/brain.go. This file defines the Brain type, which coordinates all memory operations. Each memory concept gets its own file in the same package:

  • internal/brain/episode.go
  • internal/brain/fact.go
  • internal/brain/goal.go
  • internal/brain/hypothesis.go
  • internal/brain/contradiction.go
  • internal/brain/causal.go
  • internal/brain/failure.go

This is a familiar Go pattern: define one type in a central file, spread its methods across multiple files by domain. Keeps each file focused. If you’ve worked on larger Go projects, you’ve probably seen this in HTTP handler packages or service layers.

Brain talks to Postgres. Each method handles a specific memory operation — storing a fact, recording an episode, consolidating knowledge. Data stays in Postgres, so you get ACID transactions, familiar tooling, and easy backups. No reinventing storage.

Namespaces and context isolation

Stash supports namespaces (see internal/brain/namespace.go), letting you isolate memory between different agents or sessions. The CLI handles this through cmd/cli/namespace.go.

Context management lives in internal/brain/context.go and cmd/cli/context.go. This is “working context” — the set of relevant memories an agent should consider for its current task. If you’ve worked with Go’s context package, note that the naming here is domain-specific. It’s not referring to context.Context, though the project does use Go’s standard context for request lifecycle management throughout.

Consolidation: turning raw memory into something useful

This is one of the more interesting pieces. internal/brain/consolidate.go contains the main consolidation logic, with specialized variants:

  • internal/brain/consolidate_failure.go
  • internal/brain/consolidate_goal.go
  • internal/brain/consolidate_hypothesis.go

Consolidation takes raw episodes and facts and distills them into higher-level knowledge. This matters because raw memory grows fast. Without consolidation, retrieval gets noisy and slow. An agent drowning in its own recall isn’t useful to anyone.

The CLI exposes this through cmd/cli/consolidate.go, wiring the consolidation commands into the CLI tool.

The CLI architecture

The entry point is cmd/cli/main.go, and cmd/cli/commands.go registers all the subcommands. Each command gets its own file:

cmd/cli/causal.go
cmd/cli/consolidate.go
cmd/cli/context.go
cmd/cli/contradiction.go
cmd/cli/failure.go
cmd/cli/goal.go
cmd/cli/hypothesis.go
cmd/cli/mcp.go
cmd/cli/namespace.go
cmd/cli/server.go
cmd/cli/serve_all.go

I like this one-file-per-command pattern. It scales. When you need a new memory type, you add a file in internal/brain/ for the logic and a file in cmd/cli/ for the command. No massive switch statements, no tangled registration code.

Environment configuration lives in cmd/cli/cli_env.go — Postgres connection string, server port, that sort of thing. Go projects that ship as a single binary tend to pull config from environment variables, and Stash follows that convention.

The MCP server

cmd/cli/mcp.go sets up the Model Context Protocol server. MCP is a standard for AI agents to interact with external tools. By implementing an MCP server, Stash lets any MCP-compatible agent (Claude, for example) store and retrieve memories without writing custom API code.

cmd/cli/server.go handles HTTP server setup, and cmd/cli/serve_all.go starts both the HTTP API and MCP server together. You run one binary and get everything. I appreciate that.

For Go developers, this is a nice example of a single binary serving multiple protocols. Go’s standard net/http package makes it straightforward to run an HTTP server alongside other listeners in separate goroutines. If you’ve dealt with goroutine lifecycle management before, the pattern will look familiar.

Memory decay

internal/brain/decay.go handles memory decay — reducing the relevance of old memories over time. Not all memories are equally useful. A fact learned yesterday usually matters more than one from six months ago. This feels right; human memory works similarly, though we don’t get to pick the decay function.

Decay runs as a background process. In Go, this typically means a goroutine with a ticker:

// Simplified pseudocode — not the project's actual implementation
func (b *Brain) startDecay(ctx context.Context, interval time.Duration) {
    ticker := time.NewTicker(interval)
    defer ticker.Stop()

    for {
        select {
        case <-ctx.Done():
            return
        case <-ticker.C:
            if err := b.applyDecay(ctx); err != nil {
                log.Printf("decay error: %v", err)
            }
        }
    }
}

A goroutine with a select on ctx.Done() and a ticker channel — bread and butter for Go background workers. The context cancellation gives you clean shutdown.

Bootstrap and wiring

internal/bootstrap/bootstrap.go handles initialization. Postgres connection gets established, Brain instance gets created. Keeping bootstrap logic separate from business logic is a pattern worth stealing. It makes testing easier — you can construct a Brain with a test database without going through the full bootstrap path.

Why this architecture works for AI memory

A few design choices worth calling out:

Postgres as the storage layer. No custom storage engine. Postgres handles indexing, full-text search, transactions. You get decades of battle-tested reliability without building anything. Boring technology, in the best sense.

Single binary deployment. Go’s static compilation gives you one binary with no runtime dependencies beyond Postgres. For self-hosted deployments, this simplicity is real. Fewer moving parts means fewer 3am pages.

Separation of brain logic and CLI. The internal/brain package knows nothing about HTTP or CLI concerns. You could embed it in a different Go application and use it as a library. The internal/ directory prevents external imports, but forking would give you direct access to the brain package.

One file per concept. Both in internal/brain/ and cmd/cli/, each memory type gets its own file. The codebase stays navigable even as memory types multiply.

If you’re building Go services that need clean separation between business logic and transport layers, this is a solid reference. The pattern of keeping domain logic in internal/ and wiring it up through cmd/ is one of the most common Go project layouts, and Stash follows it well. For more on structuring Go projects, check out how other Go projects organize their code.

What I’d want to see next

Stash is a focused project. Persistent, structured memory for AI agents, backed by Postgres, wrapped in a single Go binary. The codebase follows standard Go layout conventions and shows practical patterns for CLI tools with background workers.

The thing I keep thinking about is what happens when agents start sharing memory across namespaces — whether consolidation can work across boundaries, or whether that breaks the isolation model entirely. If you’re building AI agent infrastructure in Go, or you just want to see how a single binary can combine HTTP serving, background processing, and Postgres without becoming a mess, dig into the source. The internal/brain package alone is worth reading.