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_introspecttool: 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 tomcp:read, the server rejects anything that isn't aSELECT, 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
strengthreally went 1.0 → 2.0 → 3.0 in the database, that the superseded row really carriesderived_from, that both claims really came backdisputed. - 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 ANALYZEshows a C-SPANNvector searchonnotes_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(was500pre-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
- amazon-bedrock
- amazon-ec2
- amazon-nova
- amazon-web-services
- caddy
- cockroachdb
- docker
- fastapi
- langchain
- langgraph
- mcp
- model-context-protocol
- nginx
- postgresql
- pytest
- python
- react
- sql
- tailwindcss
- titan-embeddings
- vector-database
- vector-search
- vite

Log in or sign up for Devpost to join the conversation.