← Back to work
↗ Code Email
Active · 2026 Case study

AgentMemorySync

A shared memory layer for Claude Code and Codex CLI. Both agents work the same repository from one searchable history. Neither agent starts from zero.

2
agents, one timeline
0.60
paraphrase Recall@5, up from 0.00 lexical-only
128
passing tests
SHA-256
hash-chained audit log
Role · Sole engineer Stack · Python · FastAPI · SQLite FTS5 · fastembed (ONNX) · MCP
01 · The problem

Two agents on one repo, each with amnesia.

Run Claude Code and Codex CLI against the same codebase and they act like strangers. Every session re-reads the same files. Every session re-derives the same architecture. Every session re-explains the same decisions. Worse: the two agents can edit the same file minutes apart with no awareness of each other. Best case, duplicated work. Worst case, silent conflicts.

The bottleneck is not intelligence. The bottleneck is continuity. No shared, queryable record existed for what either agent had already done.

Before

Each new session re-reads context cold and rebuilds a mental model from scratch.

Symptom

Duplicated edits, contradictory changes, and repeated explanations across sessions.

Goal

One pooled history each agent retrieves from at the start of a session.

02 · Architecture

One memory store, reached two ways.

Claude Code integrates through session hooks; Codex through a FastAPI-backed MCP server. Both paths call the same write and recall logic, so everything either agent records lands in one SQLite database and one shared timeline, keyed by git repository root.

Claude Code
session hooks
Codex CLI
MCP client
hooks
· MCP
Sync service
FastAPI + MCP
write · recall · graph
Retrieval engine
FTS5/BM25 + embeddings · RRF
Symbol graph
AST-derived
SQLite store
timeline · RBAC · audit

Both clients write to and recall from a single store. No per-agent silos.

Ingest

The service writes sessions, decisions, and file touches as timeline events, and redacts secrets on the way in.

Index

Each event is indexed for full-text search (BM25) and embedded by a local model for dense retrieval. Vectors are cached in SQLite, keyed by content hash and model.

Recall

On session start, an agent asks for what matters. The service fuses the two rankings and returns a compact briefing.

03 · Retrieval design

Hybrid retrieval, fused with reciprocal rank fusion.

Lexical search nails exact identifiers: a function name, an error string, a file path. It is helpless when the query and the answer share no words. Dense embeddings catch that paraphrase and intent, but blur exact-token lookups. Neither method wins alone. Both run, and the system merges their rankings.

A
Lexical · SQLite FTS5 / BM25

FTS5's built-in BM25 ranking scores the event text directly. Fast, exact, and free of any model dependency. The store is the index. On the benchmark it recalls exact-identifier lookups at 1.00.

B
Dense · local sentence embeddings

bge-small-en-v1.5 runs on CPU through ONNX (via fastembed): a real 384-dimension embedding, no API key and no network after a one-time model download. Similarity is cosine against a measured 0.60 floor; if the model can't load, retrieval degrades to a lexical-hashing fallback and the dashboard reports it as degraded rather than pretending.

C
Fusion · reciprocal rank fusion

RRF combines the two ranked lists by rank position, not raw score, so the retrievers' incompatible scales never need calibrating. The constant k was swept on the benchmark and set to 5, not the textbook 60, which flattens a two-leg fusion. A result ranked highly by either method rises to the top.

Why this changed

An earlier build used hashed character n-grams as the "semantic" leg. That is a lexical signal in a vector's clothing: two paraphrases with no shared words hash into different buckets, so it added almost nothing over BM25. The rewrite swapped it for a trained embedding model, and rebuilt the evaluation to a split where queries are guaranteed to share zero content words with their answers, plus one keyword-sharing hard negative per case, so the number could not be gamed. The improvement below is against that harder test.

A parallel AST-derived symbol graph tracks calls, references, inheritance, and test links across the Python codebase. Recall expands from a single hit to every structurally connected symbol.

04 · Core algorithm

Reciprocal rank fusion, in the hot path.

The fusion step is small, and ranking quality leans on it. Each retriever returns an ordered list of event ids. RRF scores every id by 1 / (k + rank) and sums the scores across lists.

retrieval/fusion.py
# Reciprocal Rank Fusion: merge lexical + dense results
def fuse(ranked_lists: list[list[str]], k: int = 5) -> list[tuple[str, float]]:
    # k=5, not the textbook 60: 60 flattens a two-leg fusion
    scores: dict[str, float] = defaultdict(float)
    for results in ranked_lists:
        for rank, doc_id in enumerate(results):
            scores[doc_id] += 1.0 / (k + rank)      # rank is 0-based
    return sorted(scores.items(), key=lambda kv: kv[1], reverse=True)
 
# Hybrid recall: run both retrievers, fuse, hydrate
async def recall(self, query: str, repo: str, limit: int = 8) -> list[Event]:
    lexical = self.fts.search(query, repo, limit=50)   # BM25
    dense   = self.vectors.search(query, repo, limit=50) # bge-small, cosine ≥ 0.60
    fused   = fuse([lexical, dense])
    ids     = [doc_id for doc_id, _ in fused[:limit]]
    return self.store.hydrate(ids, repo=repo)         # RBAC-scoped

Illustrative excerpt, reconstructed from the public repo's design. See the source for the real implementation.

05 · Outcome

Measured, not asserted.

Retrieval quality answers to a labeled benchmark, not vibes. A 40-query, 60-document set with an enforced zero-lexical-overlap paraphrase split and hard negatives scores every change. The table below compares the naive scan, BM25 alone, and the production hybrid path.

System R@5 nDCG@10 exact paraphrase p50
like · naive scan 0.525 0.524 1.00 0.05 2.4 ms
lexical · BM25 only 0.500 0.500 1.00 0.00 8.5 ms
hybrid · production 0.800 0.670 1.00 0.60 21.4 ms

Adding the embedding leg lifts zero-overlap paraphrase recall from 0.00 to 0.60 while exact-identifier lookup stays pinned at 1.00 — the gain costs no regression.

0.00 → 0.60
Paraphrase Recall@5, lexical vs hybrid
nDCG@10 · MRR
Ranking quality, not just presence
~70×
Latency cut from caching vectors (1568 → 21 ms)
128
Passing tests in the suite
  • Both agents start each session with the other's relevant history instead of a cold read.
  • Security is first-class: per-repo RBAC, secret redaction on ingest, and SHA-256 hash-chained audit logs make the history tamper-evident.
  • No API keys, no per-token cost, no rate limits. The 64 MB embedding model runs locally on CPU; vectors cache in SQLite, and retrieval degrades to lexical if the model is ever unavailable.
Read the code, or see the rest of my work.

AgentMemorySync is open source.

↗ View on GitHub ← All work