Inspiration

I kept having the same conversation with my AI assistant.

I'd feed it a paper, work through the argument, close the tab. Next session: blank slate. Feed it a second paper that directly contradicted the first, and it would cheerfully agree with both. The "memory" features I tried were a vector store bolted onto a chat loop — retrieval, not memory. They could find things. They couldn't change their mind.

That gap is the whole project. Human memory isn't an append-only log. It consolidates, it supersedes, it decays when neglected, and — critically — it can hold two conflicting beliefs in tension instead of letting the most recent one silently win.

So I built PaperPlanes around one constraint: memory is the product, not a feature. Feed it papers, and it remembers across sessions, links ideas that connect, flags claims that conflict, and lets you rewind to ask what did you believe on Tuesday?

What it does

Upload an arXiv paper. PaperPlanes parses it, extracts subject-predicate-object claims, embeds them, and checks each one against everything it already knows. When a new claim conflicts with a stored one, both are marked disputed and retained — neither wins by default. When a fact is genuinely superseded, the old version is invalidated, not deleted, so the history stays queryable.

Four surfaces on top of that: Chat (memory-grounded conversation), Library (ingested papers + extracted claims), Memory Inspector (timeline, diff, and graph views of what the agent knows), and Contradictions (standing tensions awaiting human resolution).

The one to actually watch is the time-travel slider in the Memory Inspector. Drag it backward and the agent's knowledge reverts to its earlier state — because every fact carries both event time (valid_at / invalid_at) and transaction time (created_at / expired_at).

How I built it

I synthesized six memory papers into a single architecture rather than picking one:

Mechanism Source What it does here
Bi-temporal facts Zep Contradicted knowledge is invalidated, never deleted — point-in-time (as_of) queries
ADD / UPDATE / INVALIDATE / NOOP Mem0 New facts are consolidated against existing memory, not blindly appended
Linked memory notes A-MEM Notes carry keywords/tags and LLM-refined same_topic / contradicts links
Recency × importance × relevance, Ebbinghaus decay Generative Agents / MemoryBank Memories strengthen on access, fade when neglected
Async reflection ("sleep-time compute") Letta Background worker distills episodic memory into cited insights
Working / episodic / semantic / procedural tiers CoALA All four tiers in one CockroachDB cluster

The stack. Two compiled LangGraph pipelines (chat and ingestion) on FastAPI / Python 3.12, a React 19 + Vite frontend, and CockroachDB as the memory substrate. Every LLM call goes through Amazon Bedrock — Nova Pro for chat and claim extraction, Nova Lite for fast judge decisions, Titan Text Embeddings V2 for 1024-dim vectors. S3 holds PDFs, EC2 runs the box.

Why CockroachDB specifically. Not as a vector store with SQL attached — as the thing that makes the memory model correct:

  • VECTOR(1024) columns with C-SPANN indexes for ANN retrieval
  • SERIALIZABLE isolation, so concurrent consolidation can't silently lose writes
  • Bi-temporal schema + recursive-CTE traversal for the memory graph
  • LangGraph checkpoints via langchain-cockroachdb, so a crash mid-conversation doesn't lose the thread
  • The Managed MCP Server, which gives the agent a memory_introspect tool: it composes read-only SQL against its own memory to answer meta-questions like "how many papers did I read this month?" The service-account key is scoped to mcp:read, the server rejects anything that isn't a SELECT, and every introspection is written to an audit log.

Retention follows Ebbinghaus, \( R = e^{-\Delta t / S} \), and each access reinforces strength as \( S \leftarrow S \cdot (1 + \frac{1}{n+1}) \) — so a note read twice climbs 1.0 → 2.0 → 3.0 and one left alone eventually falls below the archive floor.

Challenges I ran into

"Could you just use a flat file?" This is the sharpest question you can ask an agentic-memory project, and I decided to answer it with a benchmark instead of a paragraph. 25 concurrent writers incrementing one counter: the in-memory-dict analog kept 1 of 25 — 24 updates silently lost. CockroachDB kept 25 of 25, detecting and auto-retrying 58 serialization conflicts. That number reframed the whole project for me. Memory consolidation is a concurrent read-modify-write, so it needs real transactions.

Never hold a transaction open across an LLM call. My first consolidation path did exactly that — read similar notes, ask Nova what to do, write the result, all inside one transaction. Under SERIALIZABLE, with a multi-second model call in the middle, contention went through the roof. The fix was a house rule: decide outside the transaction, write inside it, and wrap every write in a retry loop for SQLSTATE 40001.

The contradiction detector has an honest envelope, and I'm shipping it that way. It reliably fires on near-paraphrase conflicts about the same subject. But when I ran two independently-written papers that disagree thematically — RHN (1607.03474) vs Melis (1707.05589), both about RHN perplexity on Penn Treebank — their extracted phrasings landed at cosine 0.49–0.52, below the 0.60 candidate floor I'd calibrated for paraphrase detection. The judge never got invoked. I could have hidden this. Instead it's documented in the README and pinned by the test itself, with the fix scoped: entity-based candidate matching on canonical subjects/objects rather than raw statement embeddings. A measured limitation beats an unmeasured claim.

Getting it actually deployed. CockroachDB Cloud signs with its own CA, so sslmode=verify-full fails against system roots — and it fails identically whether your cert path is wrong or your sed never applied. I spent a genuinely humbling amount of time debugging a TLS error that was really a string-substitution that had silently no-opped.

What I learned

  • Isolation level is a product decision. SERIALIZABLE isn't database pedantry when your write path is read-modify-write under concurrency; it's the difference between memory that's correct and memory that's approximately correct.
  • "Invalidate, don't delete" costs almost nothing and buys everything. Every interesting question — what changed, when, why, what did you believe before — becomes answerable for the price of two extra timestamp columns.
  • Testing against mocks would have taught me nothing. The tests that found real bugs are the 10 live integration tests that assert on the actual rows CockroachDB holds after real Bedrock calls — that strength really went 1.0 → 2.0 → 3.0 in the database, that the superseded row really carries derived_from, that both claims really came back disputed.
  • Graceful degradation beats branching. Both graphs self-degrade node-by-node — no AWS credentials, no retrievable context, an unreachable DB — instead of switching between a "real" and a "demo" pipeline. One compiled graph runs everywhere from CI to production, which means CI actually tests the thing that ships.

Proof, not adjectives

Everything above is reproduced by a script or test in the repo:

  • 0 writes lost under contention — 25/25 vs 1/25, 58 conflicts retried (app.scripts.demo_concurrency)
  • ANN stays index-served at scale — over ~10,000 notes, EXPLAIN ANALYZE shows a C-SPANN vector search on notes_embedding_idx (~15 ms), never a full scan (app.scripts.seed_scale --explain)
  • Survives a crash mid-conversation — after a live DB restart, the next chat turn returns 200 (was 500 pre-fix) (scripts/demo_restart_durability.sh)
  • Real papers, real pipeline — 4 arXiv papers → 424 extracted claims (NCF 96 · Dacrema 178 · RHN 89 · Melis 61)
  • 171 tests, of which 10/10 live integration tests inspect real rows with real Bedrock and no mocks, plus 4/4 memory-eval probes

What's next

Entity-based contradiction candidate matching (the scoped fix above), multi-user collaborative memory where two researchers' libraries can disagree, and pushing more of the reflection pass into CockroachDB changefeeds so consolidation is event-driven rather than polled.

Why it's a product, not a demo

Every data route is token-authenticated, the whole stack is one docker compose up, it self-degrades rather than crashing when a dependency is missing, and it's running in production right now on HTTPS with a real database behind it. The memory engine is the moat: any team shipping a research or knowledge assistant needs exactly this layer and almost nobody builds it properly.

Built With

Share this project:

Updates