Skip to main content
Glama

firm-memory

A memory layer that lets our AI coding agents remember how this firm builds software, so they stop relearning the same things on every call.

CodeGraph answers "what is the code doing?" Firm Memory answers "why do we build it this way?"

CodeGraph stays authoritative for current code behaviour. Memory holds contextual engineering knowledge — and may go stale, which is why when memory and the current code disagree, the code wins.


The shape of it

  OpenCode        On-call        Future agent                 An engineer
      └───────────────┼───────────────┘                            │
                      │  MCP                                       │  browser
             ┌────────▼─────────┐                        ┌─────────▼────────┐
             │  Firm Memory MCP │  transport adapter     │   Review UI      │
             └────────┬─────────┘                        └─────────┬────────┘
                      │                                            │
                      │   search · get                             │
                      │   ingest · propose ──► candidate queue ──►  │  approve
                      │   correct                (shared)          │  amend
                      │                                            │  reject
             ┌────────▼────────────────────────────────────────────▼────────┐
             │   Firm Memory      taxonomy · scope · provenance · lifecycle  │
             └────────────────────────────┬─────────────────────────────────┘
                                          │  MemoryProvider
                              ┌───────────▼────────────┐
                              │         mem0           │  embeddings · search
                              └────────────────────────┘

Agents propose; they never write. Everything they put forward waits in the candidate queue until a person opens the review UI and endorses it — that gate is the only path into the retrievable pool.

The platform owns what a firm memory means. The provider owns how it is stored and retrieved. MCP owns how agents access it, and the review UI owns how a human decides. That separation is the whole point: a second provider can be introduced without changing OpenCode or the MCP contract.


Related MCP server: Agent Memory Control Plane

Quick start

uv sync                       # installs everything, including the dev group
cp .env.example .env          # then fill in the two credentials
# .env  (gitignored)
OPENROUTER_API_KEY=sk-or-...
FIRM_MEM0_PG_DSN=postgresql://mem0:pw@localhost:5432/mem0

A database with pgvector, if you do not already have one:

docker compose -f examples/docker-compose.yml up -d

mem0 creates the vector extension and its table on first use, so there is no schema step.

from firm_memory import FirmMemory, MemoryScope, MemoryType

memory = FirmMemory.from_env()          # scoped to this checkout + its domains + the firm

for hit in memory.search("why does OMS reject orders after 15:20"):
    print(hit.id, hit.content, hit.provenance.reference)

proposal = memory.propose(
    "Cash strategies stop sending at 15:20 because the exchange rejects after that.",
    type=MemoryType.BUSINESS_RULE,
    scope=MemoryScope(domains=("execution",), repos=("oms", "gateway")),
    reference="mr-4821",
)
# Not stored as knowledge yet — it is queued for a human:
print(proposal.accepted, proposal.candidate_id, proposal.decision.reason)

memory.approvals.approve(proposal.candidate_id, approver="ashish")

Run the MCP server for agents:

firm-memory-mcp        # stdio; five tools, listed under "The agent surface" below

Run the review UI for the humans who approve what those agents propose:

export FIRM_MEMORY_REVIEW_TOKEN=$(openssl rand -hex 32)
firm-memory-review     # http://127.0.0.1:8765

Ingesting raw material

Nothing is stored directly. Raw material is distilled into candidates, and a candidate becomes firm knowledge only when a person approves it:

from firm_memory import SourceDocument, MemoryScope, Provenance

candidates = memory.ingest(SourceDocument(
    content=mr_discussion_text,
    scope=MemoryScope(domains=("execution",), repos=("oms",)),
    provenance=Provenance(source="merge-request", reference="mr-4821"),
    kind="merge request discussion",
))

for candidate in memory.approvals.pending():
    print(candidate.id, candidate.memory.type, candidate.memory.content)

memory.approvals.approve(candidates[0].id, approver="ashish")   # only now is it retrievable

ingest() never writes to the provider. Extracting nothing is a normal and frequent outcome — most discussions contain no durable knowledge.

Two extractors, chosen with FIRM_MEMORY_EXTRACTOR:

Value

What runs

provider (default)

mem0's own extractor, including its deduplication against memories already in the pool — but only its read-only phases, so it never writes. A second batched call types each fact against the firm taxonomy.

llm

The platform's own prompt, written for engineering memory from the start and returning type and confidence directly. One call.

none

Ingestion disabled. Search and hand-curation still work.

mem0's extraction prompt is written for a consumer assistant — its examples are "User has a dog named Max". The firm's instructions steer it, but anything that still comes back in that shape is caught by the taxonomy and dropped. Run both against real MRs before committing to either.

Try it now

Point it at one PR review comment and see what it proposes:

uv sync
cp .env.example .env          # then fill in the two credentials

python examples/review_comment.py

See examples/ for details.


The five things this package owns

1. Taxonomy

A provider's stock extraction is tuned for consumer assistants (food, hobbies, music). Ours is tuned for trading systems. Thirteen types, each with the description that drives extraction:

ARCHITECTURE_DECISION · REJECTED_APPROACH · CONVENTION · REVIEW_PATTERN · BUG_FIX · TASK_LEARNING · TOOLING_SETUP · DEPENDENCY_DECISION · PERFORMANCE_FINDING · BUSINESS_RULE · PRODUCTION_ISSUE · OWNERSHIP · TERMINOLOGY

Enforced before anything reaches a provider. Both spellings resolve — the member name (BUSINESS_RULE) and the stable wire slug (business_rules).

Just as important are the exclusions: no source code, diffs or stack traces; no secrets; no facts about individual engineers; no transient state.

2. Scope

Independent attributes, not a hierarchy — because firm knowledge does not respect a tree:

{"firm": true, "domains": ["execution"], "repos": ["oms", "gateway"]}

A memory spanning three repos is stored once and reachable from each of them. There is deliberately no engineer-level and no team-level scope: the same question must return the same firm knowledge whoever asks, and an identity axis would split one fact into copies that drift apart.

3. Tiers

The lifetime axis, orthogonal to approval status:

Tier

What it holds

Task-scoped?

EPISODIC

Per-MR working memory — findings and their dispositions

Yes, required

DURABLE

Distilled knowledge, written through the approval gate

Never

INDEX

One verbatim card per closed issue/MR, kept document-shaped

Never

Search excludes EPISODIC by default. That default is load bearing: to a vector store an absent task filter means "don't care", not "unset", so without it every MR's scratch state joins ordinary recall. It has a contract test.

4. Provenance and lifecycle

Every memory carries where it came from, so an engineer can trace a citation back to the MR, issue or interview behind it — and correct it.

Candidate ─► taxonomy / scope / provenance checks ─► human approval ─► provider.insert()

V1 is fully human approved; confidence is recorded from the start so automation can be switched on later without a migration. Business rules, architecture decisions, firm conventions and production-critical knowledge always need a person, whatever the confidence.

Nothing deletes. A correction demotes and flags; supersession names the replacement. The record that a decision was made — and unmade — survives.

5. Reliability

Memory is best effort. Reads never raise: a provider outage or a breach of the bounded timeout yields an empty result and a recorded metric, so a failed recall cannot fail a code review. Writes do raise — silently dropping a memory an engineer just approved would be worse than an error.


The agent surface

Five MCP tools. None of them writes to the pool.

Tool

What it does

memory_search

Find knowledge relevant to the task in hand.

memory_get

Fetch one memory by id, to cite it or verify a claim.

memory_ingest

Hand over raw material — an MR discussion, an incident write-up, an interview — to be distilled into candidates.

memory_propose

Put forward one already-distilled fact.

memory_correct

Report a memory as stale or wrong. It is demoted and flagged, never deleted.

memory_ingest and memory_propose are the same gate reached from two distances. An agent that has read a discussion should hand over the discussion and let the platform's extractor and taxonomy do the judging; an agent that has already concluded something specific proposes that. Neither stores anything: the raw text is never kept, only the facts extracted from it, and each of those waits for a person.

Extracting nothing is a normal outcome, and the tool description says so — an agent that reads count: 0 as a failure will retry and just burn tokens.


Reviewing what agents propose

The gate is only real if someone can see the queue. firm-memory-review serves one page that lists every candidate with its type, scope, confidence and the provenance needed to check it, and lets a reviewer approve, reject, or correct it first.

export FIRM_MEMORY_REVIEW_TOKEN=$(openssl rand -hex 32)
export FIRM_MEMORY_REVIEW_APPROVER=ashish
export FIRM_MEMORY_CANDIDATES_URL=postgresql://mem0:pw@localhost:5432/mem0
firm-memory-review

Keep the token constant — in .env rather than generated per run — so it is pasted once. For a team, issue one each and skip the two variables above:

export FIRM_MEMORY_REVIEW_TOKENS="ashish:$(openssl rand -hex 32),priya:$(openssl rand -hex 32)"

For the whole loop on a fresh machine — agent ingests over MCP, human approves here — follow examples/MCP_ROUND_TRIP.md.

An extracted fact is frequently 90% right — the rule is real but the wording is the model's, the type is one category off, or the scope is narrower than the fact actually is. So a reviewer may amend content, type, scope and confidence on the way through, and the amended memory records who changed it and which fields moved. tier and task are deliberately not amendable: they describe how a memory was written, not what a reviewer thinks of it.

Four behaviours are worth knowing about, because each one is a place where a comfortable lie would leave firm knowledge in a state nobody intended:

  • An unreachable queue is not an empty queue. It returns 503 and says so. Showing "nothing to review" for a queue that is actually full is this tool's worst possible failure.

  • Approval claims the candidate atomically (DELETE ... RETURNING). Two reviewers on one candidate is a race exactly one wins; the other gets a 409 telling them to reload.

  • Any failure after that claim puts the candidate back — a provider outage and a mistyped amendment alike. A reviewer's edit must not be able to destroy the candidate it was meant to improve.

  • Every decision is attributed, by the token. There is no name field. A token is issued to a person, so presenting it both admits you and says who is approving — a name in the request body is refused outright. That is stronger than a typed name, which is a claim rather than a fact, and it is attribution rather than authorisation: every reviewer can do the same things, and a memory is owned by the repo and the firm, never by a person.

The page loads nothing from the internet — no CDN, no web font, no analytics. Candidates are unreviewed statements about the firm's trading systems, and a review tool that reached out to third parties while displaying them would undo the self-hosted, no-egress deployment. There is a test on it.

The token is held in the tab's sessionStorage and never in a URL. The service refuses to start without one, refuses to start a token with no reviewer name attached, refuses two reviewers sharing a token — that would make an approval untraceable to either — binds to 127.0.0.1 unless told otherwise, and warns when it is bound anywhere wider.

Where the queue lives

A candidate is proposed by a bot in a CI job that ends minutes later, and approved by an engineer somewhere else hours after that. A queue that cannot span those two processes is a queue nothing ever gets reviewed from — so the location is configured as a URL whose scheme picks the store:

FIRM_MEMORY_CANDIDATES_URL

Store

Reaches

postgresql://…

Postgres table, separate from the memory pool

Any machine

file:///srv/queue.json

One JSON file, written atomically

One host

memory:// (default)

A dict

One process

Unset with no FIRM_MEMORY_CANDIDATES_PATH means in-process, which is right for tests and wrong for a deployment. An unrecognised scheme is refused rather than quietly downgraded: a misconfigured queue that silently became a dict would accept everything the bot proposed and lose all of it, with no error anywhere.

The table is created on first use, so there is no migration step — and it is deliberately not part of the memory pool. An unreviewed candidate is not firm knowledge, and putting it in the retrievable pool would leave every recall one status-filter bug away from returning things nobody approved.


Layout

src/firm_memory/
├── models.py          canonical Memory · status · tier
├── taxonomy.py        the firm's vocabulary and its exclusions
├── scope.py           firm / domains / repos
├── provenance.py      where a memory came from
├── lifecycle.py       approval policy and status transitions
├── memory.py          the API agents and applications import
├── config.py          platform settings
├── metrics.py         failure and latency counters
├── repo.py            deterministic repo identity
├── providers/
│   ├── base.py        the interface: insert · search · get · update
│   ├── registry.py    configuration-driven selection
│   ├── inmemory.py    dependency-free provider for tests and local use
│   └── mem0/          namespace · filters · mapping · settings · provider
│                      · extraction (mem0's extractor, read-only)
│                      · embedders (fastembed correction)
├── ingestion/
│   ├── extraction.py  raw material -> candidates (the only way in)
│   ├── llm.py         the completion client (LiteLLM by default)
│   ├── approval.py    the human gate
│   ├── amendment.py   what a reviewer may correct on the way through
│   ├── store.py       where candidates wait (dict, file)
│   ├── postgres_store.py  ...and across machines
│   └── selection.py   which of those a queue URL names
├── mcp/
│   ├── tools.py       the five tools (no SDK dependency)
│   └── server.py      thin transport adapter
└── review/
    ├── service.py     the review operations, framework free
    ├── app.py         the HTTP surface and its status codes
    ├── settings.py    token, bind address, rate limit
    ├── auth.py        access (a token) vs attribution (a typed name)
    ├── throttle.py    a per-client bound
    └── static/        one self-contained page; no CDN, no web font

examples/
├── review_comment.py   one review comment, end to end
├── PIPELINE.md         how it works, with diagrams
└── docker-compose.yml  Postgres with pgvector

tests/
├── unit/          modules in isolation
├── integration/   the API across layers, incl. provider swap
├── contract/      against the real mem0 filter pipeline
└── mcp/           the agent-facing surface

Development

uv run pytest -q                          # 438 tests (1 skipped without the mcp extra)
uv run pytest --cov --cov-report=term     # 94% coverage
uv run ruff check src tests examples

CI runs the same three on Python 3.11 and 3.12.

The Postgres candidate queue is held to the same suite as the other two stores, but only when a database is reachable:

docker compose -f examples/docker-compose.yml up -d
FIRM_MEMORY_TEST_PG_DSN=postgresql://mem0:pw@localhost:5432/mem0 uv run pytest -q

Without it those cases skip rather than fail, so a clone still runs green.

The contract tests are the ones to watch. They run our filters through mem0's real preprocessing and pgvector's SQL builder, pinning constraints found by reading its source — flat OR branches, flat metadata keys, list values meaning one of, and the top-level entity key Memory.search requires. If a mem0 upgrade breaks one, they fail loudly instead of the pool quietly going empty.

Related MCP Connectors

Related MCP Servers

  • A
    license
    Not graded
    quality
    B
    maintenance
    Enables AI agents to capture, store, and retrieve durable learnings from projects via MCP tools, providing a queryable memory of product and technical lessons across repos.
    MIT
  • A
    license
    Not graded
    quality
    B
    maintenance
    Provides coding agents with governed semantic memory and code-graph context via MCP, enabling code-linked recall, blast-radius impact analysis, and lifecycle-aware memory management.
    3
    Apache 2.0