{
  "version": "https://jsonfeed.org/version/1.1",
  "title": "Avinash Gosavi",
  "home_page_url": "https://www.avinashgosavi.com/",
  "feed_url": "https://www.avinashgosavi.com/feed.json",
  "description": "Portfolio and blog of Avinash Gosavi — Staff Software Engineer on distributed systems, API design and reliability, with privacy-first AI tools on the side.",
  "icon": "https://www.avinashgosavi.com/assets/icons/icon-512.png",
  "favicon": "https://www.avinashgosavi.com/favicon.svg",
  "language": "en",
  "authors": [{ "name": "Avinash Gosavi", "url": "https://www.avinashgosavi.com/" }],
  "items": [
    {
      "id": "https://www.avinashgosavi.com/post/paper-tldr-hnsw/",
      "url": "https://www.avinashgosavi.com/post/paper-tldr-hnsw/",
      "title": "Paper TL;DR: HNSW — How Vector Search Skips Most of the Haystack",
      "summary": "A TL;DR of HNSW (Malkov & Yashunin): a layered graph that finds approximate nearest vectors in roughly logarithmic time, behind much of today's vector search.",
      "image": "https://www.avinashgosavi.com/assets/og/paper-tldr-hnsw.png",
      "date_published": "2026-10-20T20:45:00+00:00",
      "date_modified": "2026-10-23T01:35:48+00:00",
      "tags": ["Research","Agent memory"],
      "content_html": "<p><strong>Paper:</strong> Yu. A. Malkov &amp; D. A. Yashunin, <a href=\"https://arxiv.org/abs/1603.09320\"><em>Efficient and robust approximate nearest neighbor search using Hierarchical Navigable Small World graphs</em></a> (arXiv, first posted March 2016; later published in IEEE Transactions on Pattern Analysis and Machine Intelligence, <a href=\"https://doi.org/10.1109/TPAMI.2018.2889473\">DOI 10.1109/TPAMI.2018.2889473</a>)</p>\n\n<p><strong>One-line TL;DR:</strong> Stack a few neighbor graphs on top of each other — sparse ones with long jumps at the top, a dense one with every item at the bottom — and you can find the closest vectors to a query by hopping down the stack instead of checking everything.</p>\n\n<h2 id=\"the-question\">The question</h2>\n\n<p>Every memory or <a href=\"https://www.avinashgosavi.com/post/agent-memory-glossary/#rag\">RAG</a> system eventually asks the same thing: <em>which stored <a href=\"https://www.avinashgosavi.com/post/agent-memory-glossary/#embedding\">embeddings</a> are closest to this query vector?</em> Checking every vector works, but the cost grows linearly with the collection. Exact shortcuts like k-d trees stop helping once vectors have hundreds of dimensions. So practical systems use <em>approximate</em> nearest neighbor (ANN) search: accept that you’ll occasionally miss a true neighbor, and measure how often you don’t with <a href=\"https://www.avinashgosavi.com/post/agent-memory-glossary/#recall-at-k\">recall</a>.</p>\n\n<p>One family of ANN methods builds a <em>proximity graph</em> — each item linked to its near neighbors — and searches by greedily walking toward the query. The authors’ own earlier method, Navigable Small World (NSW), did this but had two problems: search cost grew faster than logarithmically as the data grew, and on low-dimensional or tightly clustered data the greedy walk could get stuck in a “false” local minimum. HNSW is their fix.</p>\n\n<h2 id=\"how-it-works\">How it works</h2>\n\n<p>The paper describes the idea as a <a href=\"https://en.wikipedia.org/wiki/Skip_list\">skip list</a> where the linked lists are replaced by proximity graphs.</p>\n\n<ul>\n  <li><strong>Layers.</strong> Every item lives in the bottom layer. When it’s inserted, it also draws a random top layer from an exponentially decaying distribution, so only a few items reach the upper layers. Upper layers are sparse and their links span long distances; the bottom layer is dense with short links.</li>\n  <li><strong>Search.</strong> Start at a fixed entry point on the top layer. On each layer, greedily move to whichever neighbor is closer to the query until none is, then drop down a layer and continue from there. On the bottom layer, keep a wider list of candidates (size <code class=\"language-plaintext highlighter-rouge\">ef</code>) and return the best k. A bigger <code class=\"language-plaintext highlighter-rouge\">ef</code> means higher recall and more work.</li>\n  <li><strong>Insertion.</strong> A new item is placed by running the same descent, then — on each layer from its own top layer down — linking to up to <code class=\"language-plaintext highlighter-rouge\">M</code> neighbors found by a wider search (<code class=\"language-plaintext highlighter-rouge\">efConstruction</code>). If a neighbor ends up with too many links, its list is pruned back to a cap.</li>\n  <li><strong>A smarter way to pick neighbors.</strong> Rather than always linking to the closest candidates, the heuristic keeps a candidate only if it’s closer to the new item than to any neighbor already chosen. That spreads links out in different directions, which keeps separate clusters connected — exactly where plain NSW got stuck.</li>\n</ul>\n\n<figure class=\"diagram\">\n<svg viewBox=\"0 0 360 418\" role=\"img\" aria-labelledby=\"hnsw-dg-title hnsw-dg-desc\" xmlns=\"http://www.w3.org/2000/svg\">\n  <title id=\"hnsw-dg-title\">How an HNSW search hops down the layers</title>\n  <desc id=\"hnsw-dg-desc\">A sketch of three stacked neighbor graphs. The top layer has only two items joined by one long link; the middle layer has four; the bottom layer has every item, each linked to close neighbors. A search starts at the entry point on the top layer, hops to the neighbor closer to the query, and when no neighbor is closer it drops to the same item one layer down and continues. On the bottom layer it keeps a list of ef candidates around the query and returns the best k.</desc>\n  <defs>\n    <marker id=\"hnsw-dg-arrow\" viewBox=\"0 0 10 10\" refX=\"9\" refY=\"5\" markerWidth=\"7\" markerHeight=\"7\" orient=\"auto-start-reverse\">\n      <path class=\"dg-head\" d=\"M0 0 L10 5 L0 10 z\" />\n    </marker>\n  </defs>\n  <rect class=\"dg-group\" x=\"10\" y=\"10\" width=\"340\" height=\"84\" rx=\"12\" />\n  <text class=\"dg-label\" x=\"22\" y=\"30\">Layer 2</text>\n  <text class=\"dg-note\" x=\"92\" y=\"30\">few items · long links</text>\n  <rect class=\"dg-group\" x=\"10\" y=\"108\" width=\"340\" height=\"84\" rx=\"12\" />\n  <text class=\"dg-label\" x=\"22\" y=\"128\">Layer 1</text>\n  <rect class=\"dg-group\" x=\"10\" y=\"206\" width=\"340\" height=\"124\" rx=\"12\" />\n  <text class=\"dg-label\" x=\"22\" y=\"226\">Layer 0</text>\n  <text class=\"dg-note\" x=\"92\" y=\"226\">every item · short links</text>\n  <line class=\"dg-edge\" x1=\"62\" y1=\"162\" x2=\"142\" y2=\"162\" />\n  <line class=\"dg-edge\" x1=\"142\" y1=\"162\" x2=\"224\" y2=\"162\" />\n  <line class=\"dg-edge\" x1=\"34\" y1=\"262\" x2=\"62\" y2=\"284\" />\n  <line class=\"dg-edge\" x1=\"62\" y1=\"284\" x2=\"88\" y2=\"258\" />\n  <line class=\"dg-edge\" x1=\"88\" y1=\"258\" x2=\"116\" y2=\"282\" />\n  <line class=\"dg-edge\" x1=\"116\" y1=\"282\" x2=\"142\" y2=\"266\" />\n  <line class=\"dg-edge\" x1=\"142\" y1=\"266\" x2=\"170\" y2=\"288\" />\n  <line class=\"dg-edge\" x1=\"170\" y1=\"288\" x2=\"196\" y2=\"258\" />\n  <line class=\"dg-edge\" x1=\"196\" y1=\"258\" x2=\"224\" y2=\"280\" />\n  <line class=\"dg-edge\" x1=\"224\" y1=\"280\" x2=\"250\" y2=\"264\" />\n  <line class=\"dg-edge\" x1=\"250\" y1=\"264\" x2=\"278\" y2=\"286\" />\n  <line class=\"dg-edge\" x1=\"278\" y1=\"286\" x2=\"304\" y2=\"262\" />\n  <line class=\"dg-edge\" x1=\"304\" y1=\"262\" x2=\"330\" y2=\"282\" />\n  <line class=\"dg-edge\" x1=\"34\" y1=\"262\" x2=\"88\" y2=\"258\" />\n  <line class=\"dg-edge\" x1=\"88\" y1=\"258\" x2=\"142\" y2=\"266\" />\n  <line class=\"dg-edge\" x1=\"170\" y1=\"288\" x2=\"224\" y2=\"280\" />\n  <line class=\"dg-edge\" x1=\"224\" y1=\"280\" x2=\"278\" y2=\"286\" />\n  <line class=\"dg-edge\" x1=\"278\" y1=\"286\" x2=\"330\" y2=\"282\" />\n  <line class=\"dg-edge\" x1=\"116\" y1=\"282\" x2=\"170\" y2=\"288\" />\n  <line class=\"dg-line\" x1=\"70.0\" y1=\"62.0\" x2=\"215.0\" y2=\"62.0\" marker-end=\"url(#hnsw-dg-arrow)\" />\n  <line class=\"dg-line dg-dash\" x1=\"224.0\" y1=\"70.0\" x2=\"224.0\" y2=\"153.0\" marker-end=\"url(#hnsw-dg-arrow)\" />\n  <line class=\"dg-line\" x1=\"232.0\" y1=\"162.0\" x2=\"295.0\" y2=\"162.0\" marker-end=\"url(#hnsw-dg-arrow)\" />\n  <line class=\"dg-line dg-dash\" x1=\"304.0\" y1=\"170.0\" x2=\"304.0\" y2=\"253.0\" marker-end=\"url(#hnsw-dg-arrow)\" />\n  <line class=\"dg-line\" x1=\"298.1\" y1=\"267.4\" x2=\"284.6\" y2=\"279.9\" marker-end=\"url(#hnsw-dg-arrow)\" />\n  <ellipse class=\"dg-line dg-dash\" cx=\"264\" cy=\"274\" rx=\"30\" ry=\"26\" />\n  <circle class=\"dg-dot\" cx=\"62\" cy=\"62\" r=\"6\" />\n  <circle class=\"dg-dot\" cx=\"224\" cy=\"62\" r=\"6\" />\n  <circle class=\"dg-node\" cx=\"62\" cy=\"162\" r=\"6\" />\n  <circle class=\"dg-node\" cx=\"142\" cy=\"162\" r=\"6\" />\n  <circle class=\"dg-dot\" cx=\"224\" cy=\"162\" r=\"6\" />\n  <circle class=\"dg-dot\" cx=\"304\" cy=\"162\" r=\"6\" />\n  <circle class=\"dg-node\" cx=\"34\" cy=\"262\" r=\"6\" />\n  <circle class=\"dg-node\" cx=\"62\" cy=\"284\" r=\"6\" />\n  <circle class=\"dg-node\" cx=\"88\" cy=\"258\" r=\"6\" />\n  <circle class=\"dg-node\" cx=\"116\" cy=\"282\" r=\"6\" />\n  <circle class=\"dg-node\" cx=\"142\" cy=\"266\" r=\"6\" />\n  <circle class=\"dg-node\" cx=\"170\" cy=\"288\" r=\"6\" />\n  <circle class=\"dg-node\" cx=\"196\" cy=\"258\" r=\"6\" />\n  <circle class=\"dg-node\" cx=\"224\" cy=\"280\" r=\"6\" />\n  <circle class=\"dg-node\" cx=\"250\" cy=\"264\" r=\"6\" />\n  <circle class=\"dg-dot\" cx=\"278\" cy=\"286\" r=\"6\" />\n  <circle class=\"dg-dot\" cx=\"304\" cy=\"262\" r=\"6\" />\n  <circle class=\"dg-node\" cx=\"330\" cy=\"282\" r=\"6\" />\n  <path class=\"dg-query\" d=\"M258 269 l8 8 m0 -8 l-8 8\" />\n  <text class=\"dg-body\" x=\"264\" y=\"243\" text-anchor=\"middle\">query</text>\n  <text class=\"dg-note\" x=\"62\" y=\"84\" text-anchor=\"middle\">entry point</text>\n  <text class=\"dg-note\" x=\"338\" y=\"318\" text-anchor=\"end\">ef candidates → best k</text>\n  <text class=\"dg-strong\" x=\"22\" y=\"356\">1</text>\n  <text class=\"dg-body\" x=\"42\" y=\"356\">Start at the entry point on the top layer</text>\n  <text class=\"dg-strong\" x=\"22\" y=\"376\">2</text>\n  <text class=\"dg-body\" x=\"42\" y=\"376\">Hop to the neighbor closest to the query</text>\n  <text class=\"dg-strong\" x=\"22\" y=\"396\">3</text>\n  <text class=\"dg-body\" x=\"42\" y=\"396\">No closer neighbor? Drop down a layer</text>\n  <text class=\"dg-strong\" x=\"22\" y=\"416\">4</text>\n  <text class=\"dg-body\" x=\"42\" y=\"416\">At the bottom, return the best k</text>\n</svg>\n<figcaption>A sketch, not real data: upper layers let the search take long jumps, the bottom layer lets it finish the job up close.</figcaption>\n</figure>\n\n<p>The knobs the paper discusses are few: <code class=\"language-plaintext highlighter-rouge\">M</code> (links per item; it suggests roughly 5–48, higher for high-recall or high-dimensional data), <code class=\"language-plaintext highlighter-rouge\">efConstruction</code> (build quality) and <code class=\"language-plaintext highlighter-rouge\">ef</code> (search effort). The graph links cost roughly 60–450 bytes per item on top of the vectors themselves.</p>\n\n<h2 id=\"what-they-found\">What they found</h2>\n\n<ul>\n  <li><strong>Roughly logarithmic search.</strong> The authors argue search cost scales as O(log N) under idealized assumptions, and see logarithmic scaling empirically on low-dimensional data. Construction is roughly O(N log N) there.</li>\n  <li><strong>Fastest in their vector benchmarks.</strong> Single-threaded on the ann-benchmarks suite, HNSW clearly beat NSW, FLANN, Annoy, VP-tree and FALCONN (an LSH library) on SIFT, GloVe, DEEP and CoPhIR, and stayed ahead on random 4-dimensional data.</li>\n  <li><strong>Not just for vectors.</strong> Because it only needs a distance function, it also led on non-vector data — sparse text, topic histograms, image signatures, even DNA strings compared by edit distance. The biggest gain over NSW was roughly three orders of magnitude on one low-dimensional, clustered dataset.</li>\n  <li><strong>Speed versus memory against compression.</strong> On 200 million SIFT vectors, HNSW used more RAM than Faiss’s product-quantization index (about 64 GB versus 23.5–30 GB) but was more accurate, faster to search and much faster to build.</li>\n</ul>\n\n<h2 id=\"why-it-matters-for-builders\">Why it matters for builders</h2>\n\n<ol>\n  <li><strong>It’s probably already in your stack.</strong> The paper notes that Faiss added its own HNSW implementation, and RAG’s Wikipedia index used a FAISS HNSW index (see the <a href=\"https://www.avinashgosavi.com/post/paper-tldr-retrieval-augmented-generation/\">RAG TL;DR</a>). <a href=\"https://www.avinashgosavi.com/post/vaultmem-part-2-multimodal-temporal-backends/\">VaultMem</a> has an optional <code class=\"language-plaintext highlighter-rouge\">HNSWVectorIndex</code> that wraps the <a href=\"https://github.com/nmslib/hnswlib\">hnswlib</a> library.</li>\n  <li><strong>Small collections don’t need it.</strong> The graph has real overhead, so brute force is often fine early on — VaultMem Part 2 falls back to exact cosine search below 1,000 memories and shows where the crossover happens.</li>\n  <li><strong>Recall is a dial, not a constant.</strong> <code class=\"language-plaintext highlighter-rouge\">ef</code> trades speed for recall at query time, without rebuilding. When a memory system “forgets” something it stored, the index settings are one more place to look — alongside the embeddings and the <a href=\"https://www.avinashgosavi.com/post/agent-memory-glossary/#re-ranking\">re-ranking</a> step.</li>\n  <li><strong>The index decides which candidates you even see.</strong> Better embeddings (<a href=\"https://www.avinashgosavi.com/post/paper-tldr-sentence-bert/\">Sentence-BERT</a>) and better prompt placement (<a href=\"https://www.avinashgosavi.com/post/paper-tldr-lost-in-the-middle/\">Lost in the Middle</a>) only help with the neighbors the index returns.</li>\n</ol>\n\n<h2 id=\"honest-caveats\">Honest caveats</h2>\n\n<ul>\n  <li><strong>The O(log N) story has gaps.</strong> The guarantee leans on idealized graphs the heuristic doesn’t actually build, and the authors say high-dimensional behavior isn’t proven. On the 200M-vector test, query time drifted somewhat from pure logarithmic growth.</li>\n  <li><strong>Memory hungry.</strong> All those links live alongside the vectors, so it needs more RAM than compressed indexes like product quantization.</li>\n  <li><strong>No deletes or updates — at the time.</strong> The paper lists element updates and removal as future work. That matters for memory systems that edit or <a href=\"https://www.avinashgosavi.com/post/agent-memory-glossary/#memory-unlearning\">unlearn</a> facts; check what your library supports today.</li>\n  <li><strong>Hard to distribute.</strong> Every search starts at the top layer, which can become a hotspot, and the authors found that simply splitting data across machines scales poorly in total throughput.</li>\n  <li><strong>Benchmarks are a snapshot.</strong> Results depend on the hardware and the library versions of the time, and some baselines were older builds.</li>\n</ul>\n\n<p>Earlier in the series: <a href=\"https://www.avinashgosavi.com/post/paper-tldr-retrieval-augmented-generation/\">RAG</a> (the retrieve-then-generate loop this index sits inside) and <a href=\"https://www.avinashgosavi.com/post/paper-tldr-sentence-bert/\">Sentence-BERT</a> (the embeddings it searches over).</p>\n\n<p>Terms like <a href=\"https://www.avinashgosavi.com/post/agent-memory-glossary/#hnsw\"><em>HNSW</em></a>, <a href=\"https://www.avinashgosavi.com/post/agent-memory-glossary/#embedding\"><em>embedding</em></a>, <a href=\"https://www.avinashgosavi.com/post/agent-memory-glossary/#semantic-search\"><em>semantic search</em></a>, <a href=\"https://www.avinashgosavi.com/post/agent-memory-glossary/#top-k-retrieval\"><em>top-k retrieval</em></a> and <a href=\"https://www.avinashgosavi.com/post/agent-memory-glossary/#recall-at-k\"><em>Recall@k</em></a> are defined in the <a href=\"https://www.avinashgosavi.com/post/agent-memory-glossary/\">agent memory glossary</a>.</p>\n"
    },
    {
      "id": "https://www.avinashgosavi.com/post/paper-tldr-retrieval-augmented-generation/",
      "url": "https://www.avinashgosavi.com/post/paper-tldr-retrieval-augmented-generation/",
      "title": "Paper TL;DR: RAG — The Paper That Named Retrieval-Augmented Generation",
      "summary": "A TL;DR of the RAG paper (Lewis et al., 2020): a language model paired with a searchable Wikipedia index, so swapping the index updates what it knows.",
      "image": "https://www.avinashgosavi.com/assets/og/paper-tldr-retrieval-augmented-generation.png",
      "date_published": "2026-10-20T15:45:00+00:00",
      "date_modified": "2026-10-23T01:35:48+00:00",
      "tags": ["Research","Agent memory"],
      "content_html": "<p><strong>Paper:</strong> Patrick Lewis, Ethan Perez, Aleksandra Piktus, Fabio Petroni, Vladimir Karpukhin, Naman Goyal, Heinrich Küttler, Mike Lewis, Wen-tau Yih, Tim Rocktäschel, Sebastian Riedel &amp; Douwe Kiela (Facebook AI Research, University College London, New York University), <a href=\"https://arxiv.org/abs/2005.11401v4\"><em>Retrieval-Augmented Generation for Knowledge-Intensive NLP Tasks</em></a> (arXiv, 2020; revised 2021)</p>\n\n<p><strong>One-line TL;DR:</strong> Instead of hoping a language model memorized the right fact, give it a search step: look up a few relevant Wikipedia passages, generate the answer from them, and train the search and the writer together.</p>\n\n<h2 id=\"the-question\">The question</h2>\n\n<p>By 2020, large language models clearly stored a lot of facts in their weights. But the paper points out the downsides: that knowledge is hard to expand or revise, the model can’t show where an answer came from, and it can hallucinate. Earlier hybrid models that paired a language model with a retriever (REALM and ORQA) had only been tried on <em>extractive</em> question answering — pulling answer spans out of passages rather than writing new text. This paper asks whether one general recipe can combine the two: a model that retrieves documents <em>and</em> generates free-form answers, fine-tuned end to end for many different tasks.</p>\n\n<h2 id=\"how-it-works\">How it works</h2>\n\n<p>The paper splits knowledge into two kinds of memory:</p>\n\n<ul>\n  <li><strong>Parametric memory</strong> — what’s baked into the model’s weights. Here that’s BART-large, a pre-trained sequence-to-sequence generator.</li>\n  <li><strong>Non-parametric memory</strong> — an external index you can search. Here that’s a December 2018 Wikipedia dump cut into 100-word chunks (about 21 million of them), each turned into an <a href=\"https://www.avinashgosavi.com/post/agent-memory-glossary/#embedding\">embedding</a> by a BERT document encoder from DPR (Dense Passage Retrieval). The vectors live in a FAISS index using an <a href=\"https://www.avinashgosavi.com/post/paper-tldr-hnsw/\">HNSW</a> approximation, so lookups stay fast.</li>\n</ul>\n\n<p>At question time, a second BERT encoder turns the input into a query vector, the index returns the <a href=\"https://www.avinashgosavi.com/post/agent-memory-glossary/#top-k-retrieval\">top-k</a> passages, and BART generates an answer conditioned on the input plus each passage. Nobody labels which passage is “right”: the retriever learns only from whether the final answer comes out well.</p>\n\n<p>Two variants decide how the passages get combined:</p>\n\n<ul>\n  <li><strong>RAG-Sequence</strong> uses the same passage for the whole answer, then weighs the answers it would give for each passage.</li>\n  <li><strong>RAG-Token</strong> can lean on a different passage for each word, so one answer can stitch together facts from several documents.</li>\n</ul>\n\n<p>One practical shortcut: only the query encoder and the generator are trained. The document encoder — and so the whole 21-million-vector index — stays frozen, because re-embedding Wikipedia during training is expensive. The authors report they didn’t need that step for strong results.</p>\n\n<figure class=\"diagram\">\n<svg viewBox=\"0 0 360 428\" role=\"img\" aria-labelledby=\"rag-dg-title rag-dg-desc\" xmlns=\"http://www.w3.org/2000/svg\">\n  <title id=\"rag-dg-title\">How RAG retrieves, then generates</title>\n  <desc id=\"rag-dg-desc\">The input goes to a BERT query encoder, which turns it into a query vector. The vector searches a FAISS index of about 21 million 100-word Wikipedia chunks from December 2018, whose document encoder stays frozen. The top-k passages go to a BART generator, the model's parametric memory, which writes the answer. The only training signal is whether the final answer is right, and it updates just the query encoder and the generator. The index can be swapped for one built from a 2016 Wikipedia dump, which changes what the model knows without retraining.</desc>\n  <defs>\n    <marker id=\"rag-dg-arrow\" viewBox=\"0 0 10 10\" refX=\"9\" refY=\"5\" markerWidth=\"7\" markerHeight=\"7\" orient=\"auto-start-reverse\">\n      <path class=\"dg-head\" d=\"M0 0 L10 5 L0 10 z\" />\n    </marker>\n  </defs>\n  <rect class=\"dg-pill\" x=\"22\" y=\"8\" width=\"268\" height=\"40\" rx=\"20\" />\n  <text class=\"dg-body\" x=\"156\" y=\"33\" text-anchor=\"middle\">Input: a question or claim</text>\n  <line class=\"dg-line\" x1=\"156\" y1=\"48\" x2=\"156\" y2=\"70\" marker-end=\"url(#rag-dg-arrow)\" />\n  <rect class=\"dg-box\" x=\"22\" y=\"72\" width=\"268\" height=\"48\" rx=\"8\" />\n  <text class=\"dg-strong\" x=\"156\" y=\"93\" text-anchor=\"middle\">Query encoder</text>\n  <text class=\"dg-note\" x=\"156\" y=\"111\" text-anchor=\"middle\">BERT · trained</text>\n  <line class=\"dg-line\" x1=\"156\" y1=\"120\" x2=\"156\" y2=\"152\" marker-end=\"url(#rag-dg-arrow)\" />\n  <text class=\"dg-note\" x=\"166\" y=\"141\">query vector</text>\n  <rect class=\"dg-group\" x=\"10\" y=\"154\" width=\"292\" height=\"124\" rx=\"12\" />\n  <text class=\"dg-label\" x=\"22\" y=\"174\">Non-parametric memory</text>\n  <rect class=\"dg-box\" x=\"22\" y=\"184\" width=\"112\" height=\"62\" rx=\"8\" />\n  <text class=\"dg-strong\" x=\"78\" y=\"206\" text-anchor=\"middle\">Wikipedia</text>\n  <text class=\"dg-note\" x=\"78\" y=\"224\" text-anchor=\"middle\">Dec 2018 dump</text>\n  <text class=\"dg-note\" x=\"78\" y=\"240\" text-anchor=\"middle\">~21M chunks</text>\n  <line class=\"dg-line\" x1=\"140\" y1=\"215\" x2=\"172\" y2=\"215\" marker-start=\"url(#rag-dg-arrow)\" marker-end=\"url(#rag-dg-arrow)\" />\n  <rect class=\"dg-box dg-muted\" x=\"178\" y=\"184\" width=\"112\" height=\"62\" rx=\"8\" />\n  <text class=\"dg-strong\" x=\"234\" y=\"206\" text-anchor=\"middle\">Wikipedia</text>\n  <text class=\"dg-note\" x=\"234\" y=\"224\" text-anchor=\"middle\">2016 dump</text>\n  <text class=\"dg-note\" x=\"234\" y=\"240\" text-anchor=\"middle\">no retraining</text>\n  <text class=\"dg-note\" x=\"156\" y=\"266\" text-anchor=\"middle\">FAISS + HNSW · document encoder frozen</text>\n  <line class=\"dg-line\" x1=\"156\" y1=\"278\" x2=\"156\" y2=\"310\" marker-end=\"url(#rag-dg-arrow)\" />\n  <text class=\"dg-note\" x=\"166\" y=\"299\">top-k passages</text>\n  <rect class=\"dg-box\" x=\"22\" y=\"312\" width=\"268\" height=\"48\" rx=\"8\" />\n  <text class=\"dg-strong\" x=\"156\" y=\"333\" text-anchor=\"middle\">Generator</text>\n  <text class=\"dg-note\" x=\"156\" y=\"351\" text-anchor=\"middle\">BART · trained · parametric memory</text>\n  <line class=\"dg-line\" x1=\"156\" y1=\"360\" x2=\"156\" y2=\"382\" marker-end=\"url(#rag-dg-arrow)\" />\n  <rect class=\"dg-pill\" x=\"22\" y=\"384\" width=\"268\" height=\"40\" rx=\"20\" />\n  <text class=\"dg-body\" x=\"156\" y=\"409\" text-anchor=\"middle\">Answer, written from the passages</text>\n  <path class=\"dg-line dg-dash\" d=\"M290 404 H326 V96 H294\" marker-end=\"url(#rag-dg-arrow)\" />\n  <path class=\"dg-line dg-dash\" d=\"M326 336 H294\" marker-end=\"url(#rag-dg-arrow)\" />\n  <text class=\"dg-note\" transform=\"translate(346 250) rotate(-90)\" text-anchor=\"middle\">learns only from the final answer</text>\n</svg>\n<figcaption>Only the query encoder and the generator are trained. The index stays frozen, so swapping it for an older Wikipedia dump changes what the model knows.</figcaption>\n</figure>\n\n<h2 id=\"what-they-found\">What they found</h2>\n\n<ul>\n  <li><strong>Strong open-domain question answering.</strong> On Natural Questions, WebQuestions and CuratedTrec, RAG beat the previous best systems in the paper’s comparison, while writing answers instead of copying spans. On TriviaQA the picture is mixed: it led on one test split but not the standard one.</li>\n  <li><strong>It can answer even when the passage doesn’t spell it out.</strong> On Natural Questions, RAG still got about 11.8% of answers right in cases where the exact answer wasn’t in any retrieved passage — something a pure extractor can’t do.</li>\n  <li><strong>More factual, more specific text.</strong> In a human evaluation of generated Jeopardy-style questions, raters judged RAG more factual than plain BART far more often than the other way round (42.7% vs. 7.1% of cases), and more specific too.</li>\n  <li><strong>Small next to giant models.</strong> RAG has 626M trainable parameters; the largest closed-book model it’s compared to, T5-11B, has 11 billion — yet RAG scored higher on open-domain QA.</li>\n  <li><strong>Swap the index, update the knowledge.</strong> The authors built a second index from a 2016 Wikipedia dump and asked “Who is {position}?” about 82 world leaders who changed between 2016 and 2018. With the matching index RAG got roughly 70% right; with the mismatched one, 4–12%. Changing what the model knows meant replacing the index, not retraining.</li>\n</ul>\n\n<h2 id=\"why-it-matters-for-builders\">Why it matters for builders</h2>\n\n<ol>\n  <li><strong>It’s the template most “chat with your docs” systems still follow.</strong> Embed chunks, search by meaning, paste the top results into the prompt, generate. Agent memory systems do the same thing with memories instead of Wikipedia pages — the <a href=\"https://www.avinashgosavi.com/post/agent-memory-glossary/#storing-and-finding-memories\">glossary’s “Storing and finding memories” section</a> walks through each piece.</li>\n  <li><strong>Updating knowledge is a data problem, not a training problem.</strong> The hot-swap experiment is the cleanest argument for keeping facts outside the weights: if something changes, you change the index. The same idea is why memory systems store facts with timestamps and <a href=\"https://www.avinashgosavi.com/post/agent-memory-glossary/#versioned-facts\">versions</a> rather than hoping the model “knows” the latest.</li>\n  <li><strong>Provenance comes for free.</strong> Because the answer is conditioned on specific passages, you can show where it came from — useful for debugging and for trust.</li>\n  <li><strong>Retrieval quality still caps everything.</strong> The retriever here is learned only through the final answer, and the paper reports it can fail badly (see caveats). Later work in this series picks up from there: <a href=\"https://www.avinashgosavi.com/post/paper-tldr-lost-in-the-middle/\">Lost in the Middle</a> shows that even good passages help less when they land mid-prompt, and <a href=\"https://www.avinashgosavi.com/post/paper-tldr-memgpt/\">MemGPT</a> treats the context window as something to manage rather than fill.</li>\n</ol>\n\n<h2 id=\"honest-caveats\">Honest caveats</h2>\n\n<ul>\n  <li><strong>This is 2020-era plumbing.</strong> “RAG” today usually means an instruction-tuned LLM plus an off-the-shelf retriever, with no joint training. The paper’s specific recipe — BART, DPR, a frozen Wikipedia index — is mostly of historical interest now; the <em>idea</em> is what stuck.</li>\n  <li><strong>Retrieval can collapse.</strong> In early story-generation experiments, the retriever learned to return the same documents no matter the input, after which the generator ignored them and RAG behaved just like plain BART. The authors suggest tasks with long outputs or less need for facts may give the retriever too weak a signal.</li>\n  <li><strong>Not state of the art everywhere.</strong> On FEVER fact verification, RAG came within about 4.3% of the best pipeline (3-way) without being given gold evidence — close, but not ahead.</li>\n  <li><strong>The index is only as good as its source.</strong> The authors note that Wikipedia will likely never be fully factual or free of bias, and that the same misuse concerns as other text generators apply.</li>\n  <li><strong>Big infrastructure for its time.</strong> The paper notes the Wikipedia index initially needed about 100 GB of CPU memory, cut to 36 GB with FAISS compression.</li>\n</ul>\n\n<p>Earlier in the series: <a href=\"https://www.avinashgosavi.com/post/paper-tldr-sentence-bert/\">Sentence-BERT</a> (the kind of embeddings that make the search step work), <a href=\"https://www.avinashgosavi.com/post/paper-tldr-lost-in-the-middle/\">Lost in the Middle</a> (where retrieved passages land in the prompt) and <a href=\"https://www.avinashgosavi.com/post/paper-tldr-memgpt/\">MemGPT</a> (an agent that pages its own memory in and out). Next in the series: <a href=\"https://www.avinashgosavi.com/post/paper-tldr-hnsw/\">HNSW</a>, the graph index behind fast nearest-neighbor lookups like this one.</p>\n\n<p>Terms like <a href=\"https://www.avinashgosavi.com/post/agent-memory-glossary/#rag\"><em>retrieval-augmented generation</em></a>, <a href=\"https://www.avinashgosavi.com/post/agent-memory-glossary/#embedding\"><em>embedding</em></a>, <a href=\"https://www.avinashgosavi.com/post/agent-memory-glossary/#semantic-search\"><em>semantic search</em></a>, <a href=\"https://www.avinashgosavi.com/post/agent-memory-glossary/#top-k-retrieval\"><em>top-k retrieval</em></a> and <a href=\"https://www.avinashgosavi.com/post/agent-memory-glossary/#hnsw\"><em>HNSW</em></a> are defined in the <a href=\"https://www.avinashgosavi.com/post/agent-memory-glossary/\">agent memory glossary</a>.</p>\n"
    },
    {
      "id": "https://www.avinashgosavi.com/post/paper-tldr-lost-in-the-middle/",
      "url": "https://www.avinashgosavi.com/post/paper-tldr-lost-in-the-middle/",
      "title": "Paper TL;DR: Lost in the Middle — Where You Put Context Matters",
      "summary": "A TL;DR of Lost in the Middle (Liu et al., 2023): models use facts at the start and end of a long prompt far better than the middle, and what it means for RAG.",
      "image": "https://www.avinashgosavi.com/assets/og/paper-tldr-lost-in-the-middle.png",
      "date_published": "2026-10-20T10:45:00+00:00",
      "date_modified": "2026-10-23T01:35:48+00:00",
      "tags": ["Research","Agent memory"],
      "content_html": "<p><strong>Paper:</strong> Nelson F. Liu, Kevin Lin, John Hewitt, Ashwin Paranjape, Michele Bevilacqua, Fabio Petroni &amp; Percy Liang (Stanford, UC Berkeley, Samaya AI), <a href=\"https://arxiv.org/abs/2307.03172v3\"><em>Lost in the Middle: How Language Models Use Long Contexts</em></a> (arXiv, 2023)</p>\n\n<p><strong>One-line TL;DR:</strong> Give a model a long prompt with one useful passage in it, and how well it answers depends on <em>where</em> that passage sits: best at the very start or very end, noticeably worse in the middle — even for models built for long contexts.</p>\n\n<h2 id=\"the-question\">The question</h2>\n\n<p>Retrieval-augmented generation and agent memory both work the same way at the last step: fetch some passages, paste them into the prompt, and hope the model reads them. Bigger context windows make it tempting to paste in more. This paper asks a simple question first: does the model actually use everything in a long prompt equally well?</p>\n\n<h2 id=\"how-they-tested-it\">How they tested it</h2>\n\n<p>Two tasks, each designed so the authors could move the one relevant piece of information around while keeping everything else the same.</p>\n\n<ul>\n  <li><strong>Multi-document question answering.</strong> Questions from NaturalQuestions-Open, each paired with one Wikipedia passage that contains the answer plus distractor passages that look relevant but don’t. Prompts held 10, 20 or 30 passages, and the answer passage was moved from first to last.</li>\n  <li><strong>Key-value retrieval.</strong> A synthetic stress test: a JSON object of random UUID keys and values (75, 140 or 300 pairs), and the model has to return the value for one key. No language cues, just “find this and copy it.”</li>\n</ul>\n\n<p>They ran open models (MPT-30B-Instruct, LongChat-13B) and API models (GPT-3.5-Turbo, Claude-1.3, and their longer-context versions, plus GPT-4 on a subset).</p>\n\n<h2 id=\"what-they-found\">What they found</h2>\n\n<ul>\n  <li><strong>A U-shaped curve.</strong> Accuracy was highest when the answer passage came at the very start or the very end, and dropped when it sat in the middle — by more than 20% in some settings. The authors describe this as a primacy bias plus a recency bias.</li>\n  <li><strong>The middle can be worse than nothing.</strong> In one 20-document setting, GPT-3.5-Turbo with the answer buried mid-prompt scored lower than the same model answering with no documents at all.</li>\n  <li><strong>A longer window didn’t fix it.</strong> When a prompt fit in both a model and its extended-context version, the two scored almost the same. A bigger window lets you paste more; it doesn’t make the model read it better.</li>\n  <li><strong>More retrieved documents stop helping early.</strong> In an open-domain QA case study, the retriever kept finding the answer more often as it returned more passages, but the model’s accuracy flattened out well before that. Going from 20 to 50 passages bought only a point or two, at much higher cost and latency.</li>\n  <li><strong>Asking the question twice helps for lookups.</strong> Putting the query both before and after the data made key-value retrieval near-perfect, but it barely changed the multi-document QA results.</li>\n</ul>\n\n<figure class=\"diagram\">\n<svg viewBox=\"0 0 360 300\" role=\"img\" aria-labelledby=\"litm-dg-title litm-dg-desc\" xmlns=\"http://www.w3.org/2000/svg\">\n  <title id=\"litm-dg-title\">The U-shaped curve from Lost in the Middle</title>\n  <desc id=\"litm-dg-desc\">A qualitative sketch, not the paper's data. The horizontal axis is where the one useful passage sits in a prompt of many passages, from first to last. The vertical axis is answer accuracy. The curve is high when the passage is first (primacy bias), dips lowest when it is in the middle, and rises again when it is last (recency bias).</desc>\n  <text class=\"dg-label\" x=\"50\" y=\"18\">Answer accuracy</text>\n  <line class=\"dg-axis\" x1=\"50\" y1=\"28\" x2=\"50\" y2=\"206\" />\n  <line class=\"dg-axis\" x1=\"50\" y1=\"206\" x2=\"340\" y2=\"206\" />\n  <path class=\"dg-line dg-curve\" d=\"M70 52 C120 175 269 175 319 70\" />\n  <circle class=\"dg-dot\" cx=\"70\" cy=\"52\" r=\"5\" />\n  <circle class=\"dg-dot\" cx=\"194.5\" cy=\"146.5\" r=\"5\" />\n  <circle class=\"dg-dot\" cx=\"319\" cy=\"70\" r=\"5\" />\n  <text class=\"dg-body\" x=\"82\" y=\"48\">primacy</text>\n  <text class=\"dg-body\" x=\"307\" y=\"56\" text-anchor=\"end\">recency</text>\n  <text class=\"dg-strong\" x=\"194.5\" y=\"180\" text-anchor=\"middle\">lost in the middle</text>\n  <rect class=\"dg-pill\" x=\"58\" y=\"218\" width=\"25\" height=\"18\" rx=\"4\" />\n  <rect class=\"dg-box dg-muted\" x=\"89\" y=\"218\" width=\"25\" height=\"18\" rx=\"4\" />\n  <rect class=\"dg-box dg-muted\" x=\"120\" y=\"218\" width=\"25\" height=\"18\" rx=\"4\" />\n  <rect class=\"dg-box dg-muted\" x=\"151\" y=\"218\" width=\"25\" height=\"18\" rx=\"4\" />\n  <rect class=\"dg-box dg-muted\" x=\"182\" y=\"218\" width=\"25\" height=\"18\" rx=\"4\" />\n  <rect class=\"dg-box dg-muted\" x=\"213\" y=\"218\" width=\"25\" height=\"18\" rx=\"4\" />\n  <rect class=\"dg-box dg-muted\" x=\"244\" y=\"218\" width=\"25\" height=\"18\" rx=\"4\" />\n  <rect class=\"dg-box dg-muted\" x=\"275\" y=\"218\" width=\"25\" height=\"18\" rx=\"4\" />\n  <rect class=\"dg-pill\" x=\"306\" y=\"218\" width=\"25\" height=\"18\" rx=\"4\" />\n  <text class=\"dg-note\" x=\"70.5\" y=\"256\" text-anchor=\"middle\">first</text>\n  <text class=\"dg-note\" x=\"194.5\" y=\"256\" text-anchor=\"middle\">middle</text>\n  <text class=\"dg-note\" x=\"318.5\" y=\"256\" text-anchor=\"middle\">last</text>\n  <text class=\"dg-label\" x=\"194.5\" y=\"284\" text-anchor=\"middle\">Where the answer passage sits</text>\n</svg>\n<figcaption>A sketch of the shape the paper reports, not its numbers: the same passage helps most at the edges of the prompt and least in the middle.</figcaption>\n</figure>\n\n<h2 id=\"why-it-matters-for-builders\">Why it matters for builders</h2>\n\n<ol>\n  <li><strong>Order is part of retrieval.</strong> Ranking isn’t just about <em>which</em> passages make the cut but <em>where</em> they land. Putting the best matches first (and not burying them under weaker ones) is cheap and directly addresses this. A <a href=\"https://www.avinashgosavi.com/post/agent-memory-glossary/#re-ranking\">re-ranking</a> pass earns its keep here.</li>\n  <li><strong>Fewer, better passages beat more passages.</strong> The case study suggests a tighter <a href=\"https://www.avinashgosavi.com/post/agent-memory-glossary/#top-k-retrieval\">top-k</a> can be nearly as accurate and a lot cheaper. It also connects to the idea of <a href=\"https://www.avinashgosavi.com/post/agent-memory-glossary/#sufficient-evidence\">sufficient evidence</a>: the goal is enough context, not all of it.</li>\n  <li><strong>Test position, not just recall.</strong> A memory system can retrieve the right fact and still fail if the fact lands mid-prompt. Benchmarks like <a href=\"https://www.avinashgosavi.com/post/paper-tldr-locomo-long-term-memory-benchmark/\">LoCoMo</a> measure the end answer; this paper’s trick of moving the gold passage around is an easy extra check to run on your own pipeline.</li>\n  <li><strong>Keep the important stuff at the edges.</strong> Designs like <a href=\"https://www.avinashgosavi.com/post/paper-tldr-memgpt/\">MemGPT</a> keep a small, always-visible working context near the top of the prompt. This paper is one reason that kind of layout makes sense.</li>\n</ol>\n\n<h2 id=\"honest-caveats\">Honest caveats</h2>\n\n<ul>\n  <li><strong>These are 2023 models.</strong> The results cover GPT-3.5, Claude-1.3, MPT and LongChat, with only partial GPT-4 runs because of cost. Newer models may show a weaker (or different) position effect, so it’s worth measuring on whatever you actually use.</li>\n  <li><strong>The “why” is still open.</strong> The authors test a few explanations — architecture, instruction tuning, model size — and call those analyses preliminary. Encoder-decoder models were fairly robust within their training length but showed the U-shape on longer inputs; base models without instruction tuning showed it too.</li>\n  <li><strong>One answer per prompt.</strong> Each test has a single relevant passage. Real questions often need several facts combined, which this setup doesn’t measure.</li>\n  <li><strong>Greedy decoding only,</strong> and the key-value task is deliberately artificial, so it shows a capability gap rather than typical real-world behavior.</li>\n</ul>\n\n<p>Earlier in the series: <a href=\"https://www.avinashgosavi.com/post/paper-tldr-memgpt/\">MemGPT</a> (paging what goes into the context window), <a href=\"https://www.avinashgosavi.com/post/paper-tldr-generative-agents/\">Generative Agents</a> (ranking memories by recency, importance and relevance) and <a href=\"https://www.avinashgosavi.com/post/paper-tldr-sentence-bert/\">Sentence-BERT</a> (the embeddings behind most of the retrieval step). Next in the series: <a href=\"https://www.avinashgosavi.com/post/paper-tldr-retrieval-augmented-generation/\">RAG</a>, the 2020 paper that named the retrieve-then-generate setup this paper stress-tests.</p>\n\n<p>Terms like <a href=\"https://www.avinashgosavi.com/post/agent-memory-glossary/#context-window\"><em>context window</em></a>, <a href=\"https://www.avinashgosavi.com/post/agent-memory-glossary/#position-bias\"><em>position bias</em></a>, <a href=\"https://www.avinashgosavi.com/post/agent-memory-glossary/#re-ranking\"><em>re-ranking</em></a> and <a href=\"https://www.avinashgosavi.com/post/agent-memory-glossary/#top-k-retrieval\"><em>top-k retrieval</em></a> are defined in the <a href=\"https://www.avinashgosavi.com/post/agent-memory-glossary/\">agent memory glossary</a>.</p>\n"
    },
    {
      "id": "https://www.avinashgosavi.com/post/paper-tldr-generative-agents/",
      "url": "https://www.avinashgosavi.com/post/paper-tldr-generative-agents/",
      "title": "Paper TL;DR: Generative Agents — Memory Streams, Reflection and Plans",
      "summary": "A TL;DR of Generative Agents (Park et al., 2023): how 25 LLM characters in a sandbox town remember, reflect and plan, and which parts builders still copy.",
      "image": "https://www.avinashgosavi.com/assets/og/paper-tldr-generative-agents.png",
      "date_published": "2026-10-19T21:40:00+00:00",
      "date_modified": "2026-10-23T01:35:48+00:00",
      "tags": ["Research","Agent memory"],
      "content_html": "<p><strong>Paper:</strong> Joon Sung Park, Joseph C. O’Brien, Carrie J. Cai, Meredith Ringel Morris, Percy Liang &amp; Michael S. Bernstein (Stanford, Google Research, Google DeepMind), <a href=\"https://arxiv.org/abs/2304.03442\"><em>Generative Agents: Interactive Simulacra of Human Behavior</em></a> (UIST 2023)</p>\n\n<p><strong>One-line TL;DR:</strong> Give an LLM a running log of everything it observes, a way to score which memories matter right now, and a habit of periodically writing down higher-level conclusions — and simulated characters start to behave believably over days, not just single replies.</p>\n\n<h2 id=\"the-setup\">The setup</h2>\n\n<p>The authors built <strong>Smallville</strong>, a small sandbox town populated by 25 characters. Each one is driven by an LLM (gpt-3.5-turbo in the paper) and starts from a single paragraph describing who they are: job, relationships, habits. From there the characters wake up, go to work, chat, form opinions of each other and make plans — all generated, step by step, by the model.</p>\n\n<p>The interesting part isn’t the town. It’s the memory design that keeps each character consistent over time, which the paper splits into three pieces: a <strong>memory stream</strong>, <strong>reflection</strong> and <strong>planning</strong>.</p>\n\n<h2 id=\"1-the-memory-stream\">1. The memory stream</h2>\n\n<p>Every character keeps a <strong>memory stream</strong>: a plain list of memories, each written in natural language with a creation time and a “last accessed” time. The basic memories are <strong>observations</strong> — things the character perceived, such as (to give an illustrative example) “a neighbor is watering the garden.”</p>\n\n<p>The stream grows far too large to fit into a prompt, so each time a character needs to act, it retrieves only the most useful memories. Each memory gets three scores:</p>\n\n<ul>\n  <li><strong>Recency:</strong> recently used memories score higher, fading exponentially with each in-game hour (the paper uses a decay factor of 0.995).</li>\n  <li><strong>Importance:</strong> when a memory is created, the model rates it from 1 (mundane, like cleaning a room) to 10 (poignant, like asking someone out).</li>\n  <li><strong>Relevance:</strong> cosine similarity between the memory’s embedding and the current situation’s embedding.</li>\n</ul>\n\n<p>Each score is scaled to 0–1 and the three are added up (the paper weights them equally). The top-scoring memories that fit into the context window go into the prompt.</p>\n\n<h2 id=\"2-reflection\">2. Reflection</h2>\n\n<p>Raw observations are too low-level to generalize from. Fifty separate observations of a neighbor reading in the library don’t, by themselves, tell a character “she cares a lot about her research.”</p>\n\n<p>So characters <strong>reflect</strong>. When the summed importance of recent memories passes a threshold (150 in the paper — roughly two or three times per simulated day), the model looks at the 100 most recent memories, asks itself three high-level questions about them, retrieves memories relevant to each question, and writes down insights that cite the memories they came from.</p>\n\n<p>Those insights go back into the same memory stream, where they can be retrieved — and reflected on again. The result is a <strong>reflection tree</strong>: raw observations at the leaves, more abstract conclusions higher up.</p>\n\n<figure class=\"diagram\">\n<svg viewBox=\"0 0 378 366\" role=\"img\" aria-labelledby=\"ga-dg-title ga-dg-desc\" xmlns=\"http://www.w3.org/2000/svg\">\n  <title id=\"ga-dg-title\">How a generative agent remembers, retrieves and reflects</title>\n  <desc id=\"ga-dg-desc\">Each character keeps a memory stream of natural-language entries with a creation time and a last-accessed time: observations, plans and reflections. When the character needs to act, every memory is scored on recency (decaying each game hour), importance (rated 1 to 10 by the model) and relevance (cosine similarity of embeddings); the scores are scaled to 0 to 1 and added. The top memories go into the prompt to choose the next action. When the importance of recent memories adds up past a threshold, the character reflects, and the insights it writes are stored back in the memory stream.</desc>\n  <defs>\n    <marker id=\"ga-dg-arrow\" viewBox=\"0 0 10 10\" refX=\"9\" refY=\"5\" markerWidth=\"7\" markerHeight=\"7\" orient=\"auto-start-reverse\">\n      <path class=\"dg-head\" d=\"M0 0 L10 5 L0 10 z\" />\n    </marker>\n  </defs>\n  <rect class=\"dg-group\" x=\"6\" y=\"4\" width=\"348\" height=\"118\" rx=\"12\" />\n  <text class=\"dg-label\" x=\"22\" y=\"26\">Memory stream</text>\n  <text class=\"dg-note\" x=\"338\" y=\"26\" text-anchor=\"end\">text + created + last used</text>\n  <rect class=\"dg-box\" x=\"22\" y=\"40\" width=\"96\" height=\"66\" rx=\"8\" />\n  <text class=\"dg-body\" x=\"70\" y=\"66\" text-anchor=\"middle\">Observations</text>\n  <text class=\"dg-note\" x=\"70\" y=\"86\" text-anchor=\"middle\">what it saw</text>\n  <rect class=\"dg-box\" x=\"132\" y=\"40\" width=\"96\" height=\"66\" rx=\"8\" />\n  <text class=\"dg-body\" x=\"180\" y=\"66\" text-anchor=\"middle\">Plans</text>\n  <text class=\"dg-note\" x=\"180\" y=\"86\" text-anchor=\"middle\">day → minutes</text>\n  <rect class=\"dg-box\" x=\"242\" y=\"40\" width=\"96\" height=\"66\" rx=\"8\" />\n  <text class=\"dg-body\" x=\"290\" y=\"66\" text-anchor=\"middle\">Reflections</text>\n  <text class=\"dg-note\" x=\"290\" y=\"86\" text-anchor=\"middle\">insights</text>\n  <line class=\"dg-line\" x1=\"180\" y1=\"122\" x2=\"180\" y2=\"150\" marker-end=\"url(#ga-dg-arrow)\" />\n  <rect class=\"dg-group\" x=\"6\" y=\"152\" width=\"348\" height=\"112\" rx=\"12\" />\n  <text class=\"dg-label\" x=\"22\" y=\"174\">Score each memory · sum of 0–1</text>\n  <rect class=\"dg-box\" x=\"22\" y=\"186\" width=\"92\" height=\"64\" rx=\"8\" />\n  <text class=\"dg-body\" x=\"68\" y=\"208\" text-anchor=\"middle\">Recency</text>\n  <text class=\"dg-note\" x=\"68\" y=\"226\" text-anchor=\"middle\">decays each</text>\n  <text class=\"dg-note\" x=\"68\" y=\"241\" text-anchor=\"middle\">game hour</text>\n  <text class=\"dg-strong\" x=\"124\" y=\"224\" text-anchor=\"middle\">+</text>\n  <rect class=\"dg-box\" x=\"134\" y=\"186\" width=\"92\" height=\"64\" rx=\"8\" />\n  <text class=\"dg-body\" x=\"180\" y=\"208\" text-anchor=\"middle\">Importance</text>\n  <text class=\"dg-note\" x=\"180\" y=\"226\" text-anchor=\"middle\">rated 1–10</text>\n  <text class=\"dg-note\" x=\"180\" y=\"241\" text-anchor=\"middle\">by the model</text>\n  <text class=\"dg-strong\" x=\"236\" y=\"224\" text-anchor=\"middle\">+</text>\n  <rect class=\"dg-box\" x=\"246\" y=\"186\" width=\"92\" height=\"64\" rx=\"8\" />\n  <text class=\"dg-body\" x=\"292\" y=\"208\" text-anchor=\"middle\">Relevance</text>\n  <text class=\"dg-note\" x=\"292\" y=\"226\" text-anchor=\"middle\">cosine</text>\n  <text class=\"dg-note\" x=\"292\" y=\"241\" text-anchor=\"middle\">similarity</text>\n  <line class=\"dg-line\" x1=\"98\" y1=\"264\" x2=\"98\" y2=\"292\" marker-end=\"url(#ga-dg-arrow)\" />\n  <line class=\"dg-line\" x1=\"262\" y1=\"264\" x2=\"262\" y2=\"292\" marker-end=\"url(#ga-dg-arrow)\" />\n  <rect class=\"dg-box\" x=\"22\" y=\"294\" width=\"152\" height=\"66\" rx=\"8\" />\n  <text class=\"dg-strong\" x=\"98\" y=\"318\" text-anchor=\"middle\">Top memories</text>\n  <text class=\"dg-note\" x=\"98\" y=\"338\" text-anchor=\"middle\">go into the prompt</text>\n  <text class=\"dg-note\" x=\"98\" y=\"353\" text-anchor=\"middle\">→ next action</text>\n  <rect class=\"dg-box\" x=\"186\" y=\"294\" width=\"152\" height=\"66\" rx=\"8\" />\n  <text class=\"dg-strong\" x=\"262\" y=\"318\" text-anchor=\"middle\">Reflect</text>\n  <text class=\"dg-note\" x=\"262\" y=\"338\" text-anchor=\"middle\">when importance</text>\n  <text class=\"dg-note\" x=\"262\" y=\"353\" text-anchor=\"middle\">adds up past 150</text>\n  <path class=\"dg-line dg-dash\" d=\"M338 327 H368 V73 H341\" marker-end=\"url(#ga-dg-arrow)\" />\n</svg>\n<figcaption>Everything lives in one stream. Retrieval ranks it three ways; reflection writes new, higher-level memories back into it (dashed loop).</figcaption>\n</figure>\n\n<h2 id=\"3-planning\">3. Planning</h2>\n\n<p>To keep behavior coherent across a day, each character drafts a rough plan (about five to eight chunks), then breaks each chunk into hour-long actions, then into 5–15 minute steps. Plans are stored in the memory stream too. At every step, the model decides whether something it just noticed is worth reacting to; if so, it rewrites the plan from that point.</p>\n\n<h2 id=\"what-they-tested\">What they tested</h2>\n\n<p><strong>Interviews.</strong> The authors “interviewed” characters with questions about themselves, their memories, plans, reactions and reflections. 100 human evaluators ranked answers from the full architecture, versions with pieces removed, and human crowdworkers role-playing the same characters. The full architecture ranked most believable; removing reflection, then planning, then memory each made answers noticeably worse. The crowdworker answers landed below every version that still had memory.</p>\n\n<p><strong>Emergent behavior over two game days.</strong> One character was seeded with the idea of throwing a Valentine’s Day party. Without further scripting, word spread from 1 character to 13 of 25, and 5 of the 12 who were invited showed up. A mayoral candidacy spread from 1 character to 8 the same way.</p>\n\n<h2 id=\"why-it-matters-for-builders\">Why it matters for builders</h2>\n\n<ol>\n  <li><strong>Rank memories on more than similarity.</strong> Recency + importance + relevance is a simple, readable scoring rule, and variations of it show up across agent memory systems. <a href=\"https://www.avinashgosavi.com/post/vaultmem-encrypted-memory-for-ai-agents/\">VaultMem</a>, for example, re-ranks on cosine similarity plus a significance score that decays over time.</li>\n  <li><strong>Store conclusions, not just events.</strong> Reflections are cheap summaries the agent can retrieve later, and keeping links back to the source memories makes them auditable.</li>\n  <li><strong>Memory and planning are the same store.</strong> Writing plans into the memory stream means “what am I doing next?” is answered by the same retrieval as “what happened?”</li>\n</ol>\n\n<h2 id=\"honest-caveats\">Honest caveats</h2>\n\n<ul>\n  <li><strong>Retrieval still misses.</strong> The authors show characters failing to recall something they had heard, or recalling a detail of an event but not the event itself.</li>\n  <li><strong>Embellishment.</strong> Characters sometimes added details nobody had said, or blended in the model’s world knowledge — one described a neighbor as the author of a famous 18th-century economics book.</li>\n  <li><strong>Too polite.</strong> The authors attribute overly formal, overly agreeable dialogue to instruction tuning; one character rarely said no to suggestions, and her own interests drifted as a result.</li>\n  <li><strong>Expensive.</strong> Running 25 characters for two game days took multiple days and, in the authors’ words, “thousands of dollars in token credits.”</li>\n  <li><strong>Believability isn’t accuracy.</strong> The evaluation measured whether behavior <em>seemed</em> human, not whether memories were retrieved correctly. Benchmarks like <a href=\"https://www.avinashgosavi.com/post/paper-tldr-locomo-long-term-memory-benchmark/\">LoCoMo</a> test that more directly.</li>\n</ul>\n\n<p>Earlier in the series: <a href=\"https://www.avinashgosavi.com/post/paper-tldr-memgpt/\">MemGPT</a> (an OS-style alternative where the model pages its own memory), <a href=\"https://www.avinashgosavi.com/post/paper-tldr-complementary-learning-systems/\">Complementary Learning Systems</a> (the brain’s version of turning episodes into general knowledge — close in spirit to reflection), and <a href=\"https://www.avinashgosavi.com/post/paper-tldr-sentence-bert/\">Sentence-BERT</a> (the kind of embeddings behind the relevance score). Next in the series: <a href=\"https://www.avinashgosavi.com/post/paper-tldr-lost-in-the-middle/\">Lost in the Middle</a>, on what happens to retrieved text once it lands in a long prompt.</p>\n\n<p>Terms like <a href=\"https://www.avinashgosavi.com/post/agent-memory-glossary/#reflection\"><em>reflection</em></a>, <a href=\"https://www.avinashgosavi.com/post/agent-memory-glossary/#consolidation\"><em>consolidation</em></a> and <a href=\"https://www.avinashgosavi.com/post/agent-memory-glossary/#decay\"><em>decay</em></a> are defined in the <a href=\"https://www.avinashgosavi.com/post/agent-memory-glossary/#how-memories-change-over-time\">agent memory glossary</a>.</p>\n"
    },
    {
      "id": "https://www.avinashgosavi.com/post/paper-tldr-memgpt/",
      "url": "https://www.avinashgosavi.com/post/paper-tldr-memgpt/",
      "title": "Paper TL;DR: MemGPT — Treating the Context Window Like RAM",
      "summary": "A TL;DR of MemGPT (Packer et al., 2023): how an LLM can page information in and out of a small context window with function calls, like an operating system.",
      "image": "https://www.avinashgosavi.com/assets/og/paper-tldr-memgpt.png",
      "date_published": "2026-10-19T10:40:00+00:00",
      "date_modified": "2026-10-23T01:35:48+00:00",
      "tags": ["Research","Agent memory"],
      "content_html": "<p><strong>Paper:</strong> Charles Packer, Sarah Wooders, Kevin Lin, Vivian Fang, Shishir G. Patil, Ion Stoica &amp; Joseph E. Gonzalez (UC Berkeley), <a href=\"https://arxiv.org/abs/2310.08560\"><em>MemGPT: Towards LLMs as Operating Systems</em></a> (arXiv, 2023)</p>\n\n<p><strong>One-line TL;DR:</strong> Instead of waiting for bigger context windows, MemGPT treats the prompt like a computer’s RAM and lets the model itself move information between that small fast space and larger external storage, using function calls.</p>\n\n<h2 id=\"the-problem\">The problem</h2>\n\n<p>An LLM can only “see” what fits in its context window. Long chats and big document collections don’t fit, so something has to decide what stays in view. Operating systems solved a version of this decades ago: programs act as if they have lots of memory, while the OS quietly pages data between fast RAM and slower disk. The paper calls its version <strong>virtual context management</strong>.</p>\n\n<h2 id=\"how-it-works\">How it works</h2>\n\n<p>MemGPT splits memory into two places.</p>\n\n<p><strong>Main context</strong> — what’s actually in the prompt — has three parts:</p>\n\n<ul>\n  <li><strong>System instructions:</strong> read-only rules, including how to use the memory functions.</li>\n  <li><strong>Working context:</strong> a small, fixed-size scratchpad of key facts (for example, things learned about the user). The model can only change it through function calls.</li>\n  <li><strong>A FIFO message queue:</strong> the recent conversation. Its first entry is a running summary of older messages that were pushed out.</li>\n</ul>\n\n<p><strong>External context</strong> lives outside the prompt:</p>\n\n<ul>\n  <li><strong>Recall storage:</strong> the full message history, searchable on demand.</li>\n  <li><strong>Archival storage:</strong> a store for arbitrary text the model chooses to save or look up later.</li>\n</ul>\n\n<figure class=\"diagram\">\n<svg viewBox=\"0 0 360 432\" role=\"img\" aria-labelledby=\"memgpt-dg-title memgpt-dg-desc\" xmlns=\"http://www.w3.org/2000/svg\">\n  <title id=\"memgpt-dg-title\">MemGPT's two tiers of memory</title>\n  <desc id=\"memgpt-dg-desc\">Main context is what is in the prompt: read-only system instructions, a small working context of key facts edited through function calls, and a FIFO message queue that starts with a summary of older messages. External context sits outside the prompt: recall storage holds the full message history and archival storage holds any text the model chooses to save. The oldest messages are evicted from the queue into recall storage, and the model searches and saves across both tiers with function calls.</desc>\n  <defs>\n    <marker id=\"memgpt-dg-arrow\" viewBox=\"0 0 10 10\" refX=\"9\" refY=\"5\" markerWidth=\"7\" markerHeight=\"7\" orient=\"auto-start-reverse\">\n      <path class=\"dg-head\" d=\"M0 0 L10 5 L0 10 z\" />\n    </marker>\n  </defs>\n  <rect class=\"dg-group\" x=\"6\" y=\"4\" width=\"348\" height=\"206\" rx=\"12\" />\n  <text class=\"dg-label\" x=\"22\" y=\"26\">Main context · in the prompt</text>\n  <rect class=\"dg-box\" x=\"22\" y=\"38\" width=\"316\" height=\"48\" rx=\"8\" />\n  <text class=\"dg-strong\" x=\"180\" y=\"59\" text-anchor=\"middle\">System instructions</text>\n  <text class=\"dg-note\" x=\"180\" y=\"77\" text-anchor=\"middle\">read-only rules, incl. the memory functions</text>\n  <rect class=\"dg-box\" x=\"22\" y=\"94\" width=\"316\" height=\"48\" rx=\"8\" />\n  <text class=\"dg-strong\" x=\"180\" y=\"115\" text-anchor=\"middle\">Working context</text>\n  <text class=\"dg-note\" x=\"180\" y=\"133\" text-anchor=\"middle\">small scratchpad of key facts</text>\n  <rect class=\"dg-box\" x=\"22\" y=\"150\" width=\"316\" height=\"48\" rx=\"8\" />\n  <text class=\"dg-strong\" x=\"180\" y=\"171\" text-anchor=\"middle\">FIFO message queue</text>\n  <text class=\"dg-note\" x=\"180\" y=\"189\" text-anchor=\"middle\">running summary + recent messages</text>\n  <line class=\"dg-line dg-dash\" x1=\"80\" y1=\"200\" x2=\"80\" y2=\"302\" marker-end=\"url(#memgpt-dg-arrow)\" />\n  <text class=\"dg-note\" x=\"90\" y=\"228\">oldest messages</text>\n  <text class=\"dg-note\" x=\"90\" y=\"244\">evicted, not deleted</text>\n  <line class=\"dg-line\" x1=\"280\" y1=\"202\" x2=\"280\" y2=\"302\" marker-start=\"url(#memgpt-dg-arrow)\" marker-end=\"url(#memgpt-dg-arrow)\" />\n  <text class=\"dg-note\" x=\"270\" y=\"266\" text-anchor=\"end\">function calls:</text>\n  <text class=\"dg-note\" x=\"270\" y=\"282\" text-anchor=\"end\">search, save, edit</text>\n  <rect class=\"dg-group\" x=\"6\" y=\"290\" width=\"348\" height=\"138\" rx=\"12\" />\n  <rect class=\"dg-box\" x=\"22\" y=\"306\" width=\"152\" height=\"86\" rx=\"8\" />\n  <text class=\"dg-strong\" x=\"98\" y=\"332\" text-anchor=\"middle\">Recall storage</text>\n  <text class=\"dg-note\" x=\"98\" y=\"354\" text-anchor=\"middle\">full message history,</text>\n  <text class=\"dg-note\" x=\"98\" y=\"372\" text-anchor=\"middle\">searchable</text>\n  <rect class=\"dg-box\" x=\"186\" y=\"306\" width=\"152\" height=\"86\" rx=\"8\" />\n  <text class=\"dg-strong\" x=\"262\" y=\"332\" text-anchor=\"middle\">Archival storage</text>\n  <text class=\"dg-note\" x=\"262\" y=\"354\" text-anchor=\"middle\">any text the model</text>\n  <text class=\"dg-note\" x=\"262\" y=\"372\" text-anchor=\"middle\">chooses to keep</text>\n  <text class=\"dg-label\" x=\"22\" y=\"416\">External context · outside the prompt</text>\n</svg>\n<figcaption>Main context is the prompt; external context is everything the model can page back in. The model itself moves information between them.</figcaption>\n</figure>\n\n<p>The clever part is who moves things around: <strong>the model does</strong>. Its output is parsed as function calls (search recall, write to archival, edit working context), and the results are fed back in. A few mechanics make this work:</p>\n\n<ul>\n  <li><strong>Memory pressure warnings.</strong> When the prompt gets close to full (the paper’s example uses 70%), the system adds a warning so the model can save what matters before it’s lost.</li>\n  <li><strong>Eviction with summaries.</strong> When the prompt is full, a chunk of the oldest messages is evicted and folded into the running summary. Evicted messages aren’t deleted — they stay in recall storage.</li>\n  <li><strong>Heartbeats.</strong> A function call can ask to keep going (<code class=\"language-plaintext highlighter-rouge\">request_heartbeat</code>), so the model can chain several lookups before replying.</li>\n  <li><strong>Events.</strong> User messages, system alerts and scheduled timers all wake the agent up, so it doesn’t only act when a person types.</li>\n</ul>\n\n<h2 id=\"what-they-tested\">What they tested</h2>\n\n<p>Two settings, each with a “can it really remember?” task:</p>\n\n<ul>\n  <li><strong>Multi-session chat.</strong> In a deep memory retrieval task, the agent is asked about something from an earlier session. MemGPT answered far more of these correctly than the same models with a fixed context. In a “conversation opener” task, its openers drew on what it knew about the person, and the authors found that saving facts to working context was key.</li>\n  <li><strong>Document analysis.</strong> For question answering over many documents, MemGPT could page through more search results than fit in one prompt. In a nested key-value lookup task, where one value points to another key, fixed-context models broke down as nesting got deeper, while MemGPT with GPT-4 kept answering by looking things up repeatedly.</li>\n</ul>\n\n<h2 id=\"why-it-matters-for-builders\">Why it matters for builders</h2>\n\n<ol>\n  <li><strong>Memory can be a set of tools, not just a bigger prompt.</strong> Giving the model explicit read/write/search functions makes memory something you can inspect and log.</li>\n  <li><strong>Separate “always in view” from “look it up.”</strong> A small working context of key facts plus searchable history is a pattern that shows up again and again in agent memory designs.</li>\n  <li><strong>Plan for overflow.</strong> Warning before the window fills, and summarizing instead of silently truncating, is a simple idea worth copying.</li>\n</ol>\n\n<h2 id=\"honest-caveats\">Honest caveats</h2>\n\n<ul>\n  <li><strong>It leans on the model’s function-calling skill.</strong> The authors report that results dropped noticeably with GPT-3.5, which was weaker at calling functions.</li>\n  <li><strong>Retrieval is still the bottleneck.</strong> With embedding search, the right document often wasn’t near the top, and the agent tended to stop paging before exhausting the results.</li>\n  <li><strong>More model isn’t always better.</strong> On the nested lookup task, the GPT-4 Turbo version did worse than GPT-4, and some versions gave up after too few lookups.</li>\n  <li><strong>Self-managed memory is a judgment call by the model.</strong> What gets saved, summarized or forgotten depends on the model’s choices, which is harder to predict than a fixed rule.</li>\n</ul>\n\n<p>Earlier in the series: <a href=\"https://www.avinashgosavi.com/post/paper-tldr-locomo-long-term-memory-benchmark/\">LoCoMo</a> (a benchmark for exactly this kind of long-term memory), <a href=\"https://www.avinashgosavi.com/post/paper-tldr-sentence-bert/\">Sentence-BERT</a> (the embeddings behind most semantic search), and <a href=\"https://www.avinashgosavi.com/post/paper-tldr-complementary-learning-systems/\">Complementary Learning Systems</a> (a brain-inspired split between fast and slow memory). Next in the series: <a href=\"https://www.avinashgosavi.com/post/paper-tldr-generative-agents/\">Generative Agents</a>, which ranks a long memory stream by recency, importance and relevance.</p>\n\n<p>Terms like <a href=\"https://www.avinashgosavi.com/post/agent-memory-glossary/#virtual-context\"><em>virtual context</em></a>, <a href=\"https://www.avinashgosavi.com/post/agent-memory-glossary/#context-window\"><em>context window</em></a>, <a href=\"https://www.avinashgosavi.com/post/agent-memory-glossary/#episodic-memory\"><em>episodic memory</em></a> and <a href=\"https://www.avinashgosavi.com/post/agent-memory-glossary/#top-k-retrieval\"><em>top-k retrieval</em></a> are defined in the <a href=\"https://www.avinashgosavi.com/post/agent-memory-glossary/\">agent memory glossary</a>.</p>\n"
    },
    {
      "id": "https://www.avinashgosavi.com/post/agent-memory-glossary/",
      "url": "https://www.avinashgosavi.com/post/agent-memory-glossary/",
      "title": "An Agent Memory Glossary: Terms in Plain Language",
      "summary": "Plain-language definitions of the terms that come up when building memory for AI agents — episodic vs. semantic memory, embeddings, HNSW, re-ranking and more.",
      "image": "https://www.avinashgosavi.com/assets/og/agent-memory-glossary.png",
      "date_published": "2026-10-19T02:45:00+00:00",
      "date_modified": "2026-10-23T01:35:48+00:00",
      "tags": ["Agent memory","Explainers"],
      "content_html": "<p>The vocabulary around AI agent memory borrows from neuroscience, databases, search and cryptography all at once, which makes it easy to talk past each other. This is a working glossary — short, plain definitions, with links to where each idea shows up in my own projects. I’ll keep adding to it.</p>\n\n<h2 id=\"kinds-of-memory\">Kinds of memory</h2>\n\n<p id=\"context-window\" class=\"glossary-term\"><strong>Context window (working memory).</strong> What the model can see right now: the prompt, the recent conversation and anything retrieved for this turn. Fast and precise, but limited in size and gone when the session ends. Everything else on this list exists because of that limit.</p>\n\n<p id=\"episodic-memory\" class=\"glossary-term\"><strong>Episodic memory.</strong> Specific events: <em>what happened, when, with whom.</em> “On Tuesday you said the demo slipped to Friday.” In <a href=\"https://www.avinashgosavi.com/post/vaultmem-encrypted-memory-for-ai-agents/\">VaultMem</a> these are one of the memory types the agent can store and retrieve.</p>\n\n<p id=\"semantic-memory\" class=\"glossary-term\"><strong>Semantic memory.</strong> General facts and knowledge, detached from when they were learned. “Flash Attention reduces memory traffic by tiling.” Often distilled from many episodes.</p>\n\n<p id=\"procedural-memory\" class=\"glossary-term\"><strong>Procedural memory.</strong> How to do things: workflows, preferences about process, steps that worked before. “When debugging, check the logs before the config.”</p>\n\n<p id=\"persona-memory\" class=\"glossary-term\"><strong>Persona (profile) memory.</strong> Stable facts about the person the agent works with — name, role, standing preferences. Small, high-value and read on almost every turn.</p>\n\n<p id=\"virtual-context\" class=\"glossary-term\"><strong>Virtual context (paging).</strong> Treating the context window like a computer’s RAM: the agent moves information in and out of the prompt — saving facts to external storage, searching old messages, summarizing what gets pushed out — usually through function calls. Popularized by <a href=\"https://www.avinashgosavi.com/post/paper-tldr-memgpt/\">MemGPT</a>.</p>\n\n<h2 id=\"storing-and-finding-memories\">Storing and finding memories</h2>\n\n<p id=\"embedding\" class=\"glossary-term\"><strong>Embedding.</strong> A list of numbers that represents the meaning of a piece of text, so that similar meanings land close together. Most memory systems store an embedding next to each memory. See the <a href=\"https://www.avinashgosavi.com/post/paper-tldr-sentence-bert/\">Sentence-BERT TL;DR</a> for where modern sentence embeddings come from.</p>\n\n<p id=\"semantic-search\" class=\"glossary-term\"><strong>Semantic search.</strong> Finding memories by meaning instead of exact words: embed the query, then return the stored memories whose embeddings are closest.</p>\n\n<p id=\"top-k-retrieval\" class=\"glossary-term\"><strong>Top-k retrieval.</strong> Returning the <em>k</em> closest matches (say, the top 10) and putting them into the context window. Simple and effective; also where most retrieval mistakes start.</p>\n\n<p id=\"rag\" class=\"glossary-term\"><strong>Retrieval-augmented generation (RAG).</strong> Search first, then write: fetch the passages most relevant to the input and have the model generate its answer from them, instead of relying only on what’s stored in its weights. The term comes from a 2020 paper that also showed you can update what the system knows by swapping the search index — see the <a href=\"https://www.avinashgosavi.com/post/paper-tldr-retrieval-augmented-generation/\">RAG TL;DR</a>.</p>\n\n<p id=\"hnsw\" class=\"glossary-term\"><strong>Vector index (HNSW).</strong> A data structure that makes “find the closest vectors” fast at scale. HNSW (Hierarchical Navigable Small World) builds layered graphs of neighbors so a search can hop quickly toward the right region instead of comparing against every vector. (<a href=\"https://www.avinashgosavi.com/post/paper-tldr-hnsw/\">Paper TL;DR</a>)</p>\n\n<p id=\"re-ranking\" class=\"glossary-term\"><strong>Re-ranking.</strong> A second, slower and more careful pass over the top results — often a cross-encoder that reads the query and each candidate together — to put the truly relevant ones first.</p>\n\n<p id=\"position-bias\" class=\"glossary-term\"><strong>Position bias (lost in the middle).</strong> Models tend to use information at the start and end of a long prompt better than information buried in the middle, so <em>where</em> retrieved memories land in the prompt affects whether they get used. Putting the strongest matches first, and keeping top-k small, helps. See the <a href=\"https://www.avinashgosavi.com/post/paper-tldr-lost-in-the-middle/\">Lost in the Middle TL;DR</a>.</p>\n\n<p id=\"hybrid-search\" class=\"glossary-term\"><strong>Hybrid search.</strong> Combining meaning-based search with keyword search (or filters like dates and tags), because embeddings are bad at exact names, numbers and IDs.</p>\n\n<p id=\"context-frame\" class=\"glossary-term\"><strong>Context frame.</strong> The situation a memory was formed in — topic, people involved, task, session — stored alongside it so retrieval can filter by context, not just content. This is the core idea in <a href=\"https://www.avinashgosavi.com/post/attractor-memory-engine/\">Attractor</a>.</p>\n\n<p id=\"sufficient-evidence\" class=\"glossary-term\"><strong>Sufficient evidence.</strong> The idea that a memory system’s job isn’t just to return <em>relevant</em> memories but a set that is <em>enough</em> to answer. Ranking memories one at a time can miss this: several records may say the same thing (so the set gets bloated), or two weak-looking records may only make sense together (so both get dropped). One recent paper defines a sufficient set as a small subset that leads the agent to the same decision it would make with its full memory (<a href=\"https://neurips.cc/virtual/2026/169578\">Beyond Marginal Influence</a>).</p>\n\n<h2 id=\"how-memories-change-over-time\">How memories change over time</h2>\n\n<p id=\"consolidation\" class=\"glossary-term\"><strong>Consolidation.</strong> Turning many specific episodes into durable, general knowledge — summarizing a month of conversations into “prefers short answers,” for example. The brain’s version is covered in the <a href=\"https://www.avinashgosavi.com/post/paper-tldr-complementary-learning-systems/\">Complementary Learning Systems TL;DR</a>.</p>\n\n<p id=\"reflection\" class=\"glossary-term\"><strong>Reflection.</strong> Having the agent periodically read its recent memories and write down higher-level conclusions (“she cares a lot about her research”), stored back as memories that point to the evidence they came from. Introduced for LLM agents in <a href=\"https://www.avinashgosavi.com/post/paper-tldr-generative-agents/\">Generative Agents</a>, which also ranks memories by recency, importance and relevance together.</p>\n\n<p id=\"decay\" class=\"glossary-term\"><strong>Decay (forgetting).</strong> Letting memories that are never used fade in importance, so old noise doesn’t crowd out what matters. Forgetting is a feature.</p>\n\n<p id=\"event-time\" class=\"glossary-term\"><strong>Event time vs. write time.</strong> Two different timestamps for one memory: when the thing happened, and when it was saved. A photo from 2019 imported today should come back for “what did I do in 2019?”, not for “what did I add today?”. <a href=\"https://www.avinashgosavi.com/post/vaultmem-part-2-multimodal-temporal-backends/#temporal-search\">VaultMem Part 2</a> keeps them as separate fields (<code class=\"language-plaintext highlighter-rouge\">captured_at</code> and <code class=\"language-plaintext highlighter-rouge\">created_at</code>) for exactly this reason.</p>\n\n<p id=\"versioned-facts\" class=\"glossary-term\"><strong>Versioned facts (temporal state).</strong> Keeping every value a fact has had — “lives in Boston”, then “lives in Seattle” — as a list of versions linked to where each came from, instead of overwriting the old one. The newest version answers “where does she live?”, while the older ones still answer “where did she live before?”. The <a href=\"https://www.emergentmind.com/papers/2610.21900\">TEM paper</a> adds one more step: older versions are marked as historical before the model sees them, and the authors report that this label alone gave large gains on questions about repeatedly updated facts.</p>\n\n<p id=\"associative-memory\" class=\"glossary-term\"><strong>Associative (content-addressable) memory.</strong> Retrieving a whole memory from a partial or noisy piece of it, rather than by key or address. Hopfield networks are the classic example — see <a href=\"https://www.avinashgosavi.com/post/paper-tldr-hopfield-1982/\">Hopfield (1982)</a> and <a href=\"https://www.avinashgosavi.com/post/paper-tldr-hopfield-networks-is-all-you-need/\">the modern version</a>.</p>\n\n<h2 id=\"privacy\">Privacy</h2>\n\n<p id=\"zero-knowledge-storage\" class=\"glossary-term\"><strong>Zero-knowledge storage.</strong> The service storing your data cannot read it, because it’s encrypted with a key only you hold. The operator sees ciphertext. This is VaultMem’s core design (<a href=\"https://www.avinashgosavi.com/post/vaultmem-your-ai-agent-shouldnt-read-your-diary/\">why it matters, in plain language</a>).</p>\n\n<p id=\"client-side-encryption\" class=\"glossary-term\"><strong>Client-side encryption.</strong> Encrypting data before it leaves your device (or your session), so the server only ever handles encrypted bytes. The usual way to get zero-knowledge storage.</p>\n\n<p id=\"pii-sanitization\" class=\"glossary-term\"><strong>PII sanitization.</strong> Detecting and masking personal details — names, emails, phone numbers — before text is sent to a model provider, then restoring them in the response. It closes a gap encryption at rest can’t: the model still has to read the text. See <a href=\"https://www.avinashgosavi.com/post/vaultmem-part-3-pii-sanitizer/\">VaultMem Part 3</a>.</p>\n\n<p id=\"memory-unlearning\" class=\"glossary-term\"><strong>Memory unlearning (forget requests).</strong> Deleting a specific fact when the user asks — and only that fact. Done well, the fact can no longer be recalled or pieced together from other memories, while everything unrelated survives. The <a href=\"https://www.emergentmind.com/papers/2610.18052\">Bemula benchmark</a> tests exactly this and reports two common failures: the fact stays extractable with simple prompts, or the system deletes far more than it was asked to.</p>\n\n<h2 id=\"measuring-it\">Measuring it</h2>\n\n<p id=\"recall-at-k\" class=\"glossary-term\"><strong>Recall@k.</strong> Of the questions asked, how often the right memory appears somewhere in the top <em>k</em> results. Measures “did we find it at all.”</p>\n\n<p id=\"mrr\" class=\"glossary-term\"><strong>MRR (mean reciprocal rank).</strong> Rewards putting the right memory near the top: 1 if it’s first, ½ if second, ⅓ if third, averaged over all questions.</p>\n\n<p id=\"locomo\" class=\"glossary-term\"><strong>LoCoMo.</strong> A benchmark of very long, multi-session conversations with questions across single-hop, multi-hop, temporal, open-domain and adversarial categories. See the <a href=\"https://www.avinashgosavi.com/post/paper-tldr-locomo-long-term-memory-benchmark/\">LoCoMo TL;DR</a>.</p>\n\n<hr />\n\n<p>Missing a term, or think one of these is off? I’d like to hear it — I’ll keep this page up to date.</p>\n"
    },
    {
      "id": "https://www.avinashgosavi.com/post/paper-tldr-hopfield-1982/",
      "url": "https://www.avinashgosavi.com/post/paper-tldr-hopfield-1982/",
      "title": "Paper TL;DR: Hopfield (1982) — Memory as a Valley You Roll Into",
      "summary": "A TL;DR of John Hopfield's 1982 paper on associative memory: energy landscapes, Hebbian storage, recall from partial cues and its limits — with a NumPy demo.",
      "image": "https://www.avinashgosavi.com/assets/og/paper-tldr-hopfield-1982.png",
      "date_published": "2026-10-18T22:00:00+00:00",
      "date_modified": "2026-10-23T01:35:48+00:00",
      "tags": ["Research","Agent memory"],
      "content_html": "<p><strong>Paper:</strong> J. J. Hopfield, <a href=\"https://www.pnas.org/doi/10.1073/pnas.79.8.2554\"><em>Neural networks and physical systems with emergent collective computational abilities</em></a>, PNAS 79(8), 1982</p>\n\n<p><strong>One-line TL;DR:</strong> connect lots of simple on/off neurons with symmetric weights and the network gets an energy that only goes down as it runs — so stored memories become stable valleys, and a partial or noisy cue rolls downhill into the full memory.</p>\n\n<p>Almost everything I’ve built around memory started here. It’s also one of the most readable classic papers I know: five pages, very little math, a physicist explaining computation.</p>\n\n<h2 id=\"the-setup\">The setup</h2>\n\n<ul>\n  <li><strong>Neurons</strong> are either on or off (+1 or −1).</li>\n  <li><strong>Every neuron connects to every other</strong>, and the connection strengths are <strong>symmetric</strong>: the weight from A to B equals the weight from B to A.</li>\n  <li><strong>Storing a memory</strong> uses a Hebbian rule — “neurons that fire together wire together.” For each pattern you want to remember, strengthen connections between neurons that agree and weaken those between neurons that disagree. Store several patterns by adding up their contributions.</li>\n  <li><strong>Recalling</strong> means picking neurons one at a time and flipping each to agree with the weighted vote of its neighbors, until nothing changes.</li>\n</ul>\n\n<h2 id=\"the-key-insight-an-energy-that-only-goes-down\">The key insight: an energy that only goes down</h2>\n\n<p>Hopfield’s move was to define an <strong>energy</strong> for the whole network’s state and show that, with symmetric weights and one-at-a-time updates, every update either lowers the energy or leaves it unchanged. The network can’t cycle forever; it has to settle.</p>\n\n<p>Stored patterns sit at the bottoms of valleys in that energy landscape. Start anywhere nearby — a corrupted photo, half a phone number — and the network slides down into the nearest stored memory. That’s <strong>content-addressable memory</strong>: you retrieve by <em>what</em> you remember, not by <em>where</em> it’s stored. It’s also robust: knock out some neurons or connections and recall still mostly works.</p>\n\n<figure class=\"diagram\">\n<svg viewBox=\"0 0 360 478\" role=\"img\" aria-labelledby=\"hop82-dg-title hop82-dg-desc\" xmlns=\"http://www.w3.org/2000/svg\">\n  <title id=\"hop82-dg-title\">Storing and recalling a memory in a Hopfield network</title>\n  <desc id=\"hop82-dg-desc\">Storing: the Hebbian rule strengthens connections between neurons that agree, and several patterns are stored by adding up their contributions. Recalling: start from a noisy cue in which some neurons are flipped; neurons update one at a time to agree with the weighted vote of their neighbors, so the wrong ones flip back until the stored pattern reappears and nothing changes. Underneath, the network's energy only goes down or stays the same with each update, like rolling downhill into a valley, and it settles at the bottom, which is the stored memory.</desc>\n  <defs>\n    <marker id=\"hop82-dg-arrow\" viewBox=\"0 0 10 10\" refX=\"9\" refY=\"5\" markerWidth=\"7\" markerHeight=\"7\" orient=\"auto-start-reverse\">\n      <path class=\"dg-head\" d=\"M0 0 L10 5 L0 10 z\" />\n    </marker>\n  </defs>\n  <rect class=\"dg-pill\" x=\"10\" y=\"8\" width=\"340\" height=\"72\" rx=\"14\" />\n  <text class=\"dg-strong\" x=\"180\" y=\"32\" text-anchor=\"middle\">Store · Hebbian rule</text>\n  <text class=\"dg-body\" x=\"180\" y=\"52\" text-anchor=\"middle\">strengthen links between neurons that agree</text>\n  <text class=\"dg-body\" x=\"180\" y=\"70\" text-anchor=\"middle\">several patterns? add up their weights</text>\n  <line class=\"dg-line\" x1=\"180\" y1=\"82\" x2=\"180\" y2=\"104\" marker-end=\"url(#hop82-dg-arrow)\" />\n  <rect class=\"dg-group\" x=\"4\" y=\"108\" width=\"352\" height=\"196\" rx=\"12\" />\n  <text class=\"dg-label\" x=\"16\" y=\"128\">Recall · neurons are on or off</text>\n  <rect class=\"dg-head\" x=\"14\" y=\"150\" width=\"16\" height=\"16\" rx=\"3\" />\n  <rect class=\"dg-node\" x=\"32\" y=\"150\" width=\"16\" height=\"16\" rx=\"3\" />\n  <rect class=\"dg-head\" x=\"50\" y=\"150\" width=\"16\" height=\"16\" rx=\"3\" />\n  <rect class=\"dg-node\" x=\"68\" y=\"150\" width=\"16\" height=\"16\" rx=\"3\" />\n  <rect class=\"dg-head\" x=\"86\" y=\"150\" width=\"16\" height=\"16\" rx=\"3\" />\n  <rect class=\"dg-head\" x=\"14\" y=\"168\" width=\"16\" height=\"16\" rx=\"3\" />\n  <rect class=\"dg-head\" x=\"32\" y=\"168\" width=\"16\" height=\"16\" rx=\"3\" />\n  <rect class=\"dg-node\" x=\"50\" y=\"168\" width=\"16\" height=\"16\" rx=\"3\" />\n  <rect class=\"dg-node\" x=\"68\" y=\"168\" width=\"16\" height=\"16\" rx=\"3\" />\n  <rect class=\"dg-node\" x=\"86\" y=\"168\" width=\"16\" height=\"16\" rx=\"3\" />\n  <rect class=\"dg-head\" x=\"14\" y=\"186\" width=\"16\" height=\"16\" rx=\"3\" />\n  <rect class=\"dg-head\" x=\"32\" y=\"186\" width=\"16\" height=\"16\" rx=\"3\" />\n  <rect class=\"dg-head\" x=\"50\" y=\"186\" width=\"16\" height=\"16\" rx=\"3\" />\n  <rect class=\"dg-node\" x=\"68\" y=\"186\" width=\"16\" height=\"16\" rx=\"3\" />\n  <rect class=\"dg-head\" x=\"86\" y=\"186\" width=\"16\" height=\"16\" rx=\"3\" />\n  <rect class=\"dg-head\" x=\"14\" y=\"204\" width=\"16\" height=\"16\" rx=\"3\" />\n  <rect class=\"dg-node\" x=\"32\" y=\"204\" width=\"16\" height=\"16\" rx=\"3\" />\n  <rect class=\"dg-node\" x=\"50\" y=\"204\" width=\"16\" height=\"16\" rx=\"3\" />\n  <rect class=\"dg-node\" x=\"68\" y=\"204\" width=\"16\" height=\"16\" rx=\"3\" />\n  <rect class=\"dg-node\" x=\"86\" y=\"204\" width=\"16\" height=\"16\" rx=\"3\" />\n  <rect class=\"dg-head\" x=\"14\" y=\"222\" width=\"16\" height=\"16\" rx=\"3\" />\n  <rect class=\"dg-node\" x=\"32\" y=\"222\" width=\"16\" height=\"16\" rx=\"3\" />\n  <rect class=\"dg-head\" x=\"50\" y=\"222\" width=\"16\" height=\"16\" rx=\"3\" />\n  <rect class=\"dg-node\" x=\"68\" y=\"222\" width=\"16\" height=\"16\" rx=\"3\" />\n  <rect class=\"dg-head\" x=\"86\" y=\"222\" width=\"16\" height=\"16\" rx=\"3\" />\n  <rect class=\"dg-head\" x=\"136\" y=\"150\" width=\"16\" height=\"16\" rx=\"3\" />\n  <rect class=\"dg-node\" x=\"154\" y=\"150\" width=\"16\" height=\"16\" rx=\"3\" />\n  <rect class=\"dg-node\" x=\"172\" y=\"150\" width=\"16\" height=\"16\" rx=\"3\" />\n  <rect class=\"dg-node\" x=\"190\" y=\"150\" width=\"16\" height=\"16\" rx=\"3\" />\n  <rect class=\"dg-head\" x=\"208\" y=\"150\" width=\"16\" height=\"16\" rx=\"3\" />\n  <rect class=\"dg-head\" x=\"136\" y=\"168\" width=\"16\" height=\"16\" rx=\"3\" />\n  <rect class=\"dg-node\" x=\"154\" y=\"168\" width=\"16\" height=\"16\" rx=\"3\" />\n  <rect class=\"dg-node\" x=\"172\" y=\"168\" width=\"16\" height=\"16\" rx=\"3\" />\n  <rect class=\"dg-node\" x=\"190\" y=\"168\" width=\"16\" height=\"16\" rx=\"3\" />\n  <rect class=\"dg-node\" x=\"208\" y=\"168\" width=\"16\" height=\"16\" rx=\"3\" />\n  <rect class=\"dg-head\" x=\"136\" y=\"186\" width=\"16\" height=\"16\" rx=\"3\" />\n  <rect class=\"dg-head\" x=\"154\" y=\"186\" width=\"16\" height=\"16\" rx=\"3\" />\n  <rect class=\"dg-head\" x=\"172\" y=\"186\" width=\"16\" height=\"16\" rx=\"3\" />\n  <rect class=\"dg-head\" x=\"190\" y=\"186\" width=\"16\" height=\"16\" rx=\"3\" />\n  <rect class=\"dg-head\" x=\"208\" y=\"186\" width=\"16\" height=\"16\" rx=\"3\" />\n  <rect class=\"dg-head\" x=\"136\" y=\"204\" width=\"16\" height=\"16\" rx=\"3\" />\n  <rect class=\"dg-node\" x=\"154\" y=\"204\" width=\"16\" height=\"16\" rx=\"3\" />\n  <rect class=\"dg-node\" x=\"172\" y=\"204\" width=\"16\" height=\"16\" rx=\"3\" />\n  <rect class=\"dg-node\" x=\"190\" y=\"204\" width=\"16\" height=\"16\" rx=\"3\" />\n  <rect class=\"dg-head\" x=\"208\" y=\"204\" width=\"16\" height=\"16\" rx=\"3\" />\n  <rect class=\"dg-head\" x=\"136\" y=\"222\" width=\"16\" height=\"16\" rx=\"3\" />\n  <rect class=\"dg-node\" x=\"154\" y=\"222\" width=\"16\" height=\"16\" rx=\"3\" />\n  <rect class=\"dg-head\" x=\"172\" y=\"222\" width=\"16\" height=\"16\" rx=\"3\" />\n  <rect class=\"dg-node\" x=\"190\" y=\"222\" width=\"16\" height=\"16\" rx=\"3\" />\n  <rect class=\"dg-head\" x=\"208\" y=\"222\" width=\"16\" height=\"16\" rx=\"3\" />\n  <rect class=\"dg-head\" x=\"258\" y=\"150\" width=\"16\" height=\"16\" rx=\"3\" />\n  <rect class=\"dg-node\" x=\"276\" y=\"150\" width=\"16\" height=\"16\" rx=\"3\" />\n  <rect class=\"dg-node\" x=\"294\" y=\"150\" width=\"16\" height=\"16\" rx=\"3\" />\n  <rect class=\"dg-node\" x=\"312\" y=\"150\" width=\"16\" height=\"16\" rx=\"3\" />\n  <rect class=\"dg-head\" x=\"330\" y=\"150\" width=\"16\" height=\"16\" rx=\"3\" />\n  <rect class=\"dg-head\" x=\"258\" y=\"168\" width=\"16\" height=\"16\" rx=\"3\" />\n  <rect class=\"dg-node\" x=\"276\" y=\"168\" width=\"16\" height=\"16\" rx=\"3\" />\n  <rect class=\"dg-node\" x=\"294\" y=\"168\" width=\"16\" height=\"16\" rx=\"3\" />\n  <rect class=\"dg-node\" x=\"312\" y=\"168\" width=\"16\" height=\"16\" rx=\"3\" />\n  <rect class=\"dg-head\" x=\"330\" y=\"168\" width=\"16\" height=\"16\" rx=\"3\" />\n  <rect class=\"dg-head\" x=\"258\" y=\"186\" width=\"16\" height=\"16\" rx=\"3\" />\n  <rect class=\"dg-head\" x=\"276\" y=\"186\" width=\"16\" height=\"16\" rx=\"3\" />\n  <rect class=\"dg-head\" x=\"294\" y=\"186\" width=\"16\" height=\"16\" rx=\"3\" />\n  <rect class=\"dg-head\" x=\"312\" y=\"186\" width=\"16\" height=\"16\" rx=\"3\" />\n  <rect class=\"dg-head\" x=\"330\" y=\"186\" width=\"16\" height=\"16\" rx=\"3\" />\n  <rect class=\"dg-head\" x=\"258\" y=\"204\" width=\"16\" height=\"16\" rx=\"3\" />\n  <rect class=\"dg-node\" x=\"276\" y=\"204\" width=\"16\" height=\"16\" rx=\"3\" />\n  <rect class=\"dg-node\" x=\"294\" y=\"204\" width=\"16\" height=\"16\" rx=\"3\" />\n  <rect class=\"dg-node\" x=\"312\" y=\"204\" width=\"16\" height=\"16\" rx=\"3\" />\n  <rect class=\"dg-head\" x=\"330\" y=\"204\" width=\"16\" height=\"16\" rx=\"3\" />\n  <rect class=\"dg-head\" x=\"258\" y=\"222\" width=\"16\" height=\"16\" rx=\"3\" />\n  <rect class=\"dg-node\" x=\"276\" y=\"222\" width=\"16\" height=\"16\" rx=\"3\" />\n  <rect class=\"dg-node\" x=\"294\" y=\"222\" width=\"16\" height=\"16\" rx=\"3\" />\n  <rect class=\"dg-node\" x=\"312\" y=\"222\" width=\"16\" height=\"16\" rx=\"3\" />\n  <rect class=\"dg-head\" x=\"330\" y=\"222\" width=\"16\" height=\"16\" rx=\"3\" />\n  <line class=\"dg-line\" x1=\"106\" y1=\"194\" x2=\"132\" y2=\"194\" marker-end=\"url(#hop82-dg-arrow)\" />\n  <line class=\"dg-line\" x1=\"228\" y1=\"194\" x2=\"254\" y2=\"194\" marker-end=\"url(#hop82-dg-arrow)\" />\n  <text class=\"dg-body\" x=\"58\" y=\"258\" text-anchor=\"middle\">noisy cue</text>\n  <text class=\"dg-body\" x=\"180\" y=\"258\" text-anchor=\"middle\">settling</text>\n  <text class=\"dg-body\" x=\"302\" y=\"258\" text-anchor=\"middle\">memory</text>\n  <text class=\"dg-agent\" x=\"58\" y=\"276\" text-anchor=\"middle\">some bits flipped</text>\n  <text class=\"dg-agent\" x=\"180\" y=\"276\" text-anchor=\"middle\">one flip at a time</text>\n  <text class=\"dg-agent\" x=\"302\" y=\"276\" text-anchor=\"middle\">nothing changes</text>\n  <text class=\"dg-note\" x=\"180\" y=\"295\" text-anchor=\"middle\">each neuron follows the weighted vote of its neighbors</text>\n  <path class=\"dg-axis\" d=\"M40 322 L40 442 L346 442\" />\n  <text class=\"dg-note\" transform=\"translate(30 384) rotate(-90)\" text-anchor=\"middle\">energy</text>\n  <text class=\"dg-note\" x=\"346\" y=\"462\" text-anchor=\"end\">updates →</text>\n  <path class=\"dg-line dg-curve\" d=\"M50 338 L80 338 L80 356 L110 356 L110 370 L140 370 L140 388 L175 388 L175 398 L210 398 L210 408 L340 408\" />\n  <circle class=\"dg-dot\" cx=\"50\" cy=\"338\" r=\"6\" />\n  <circle class=\"dg-dot\" cx=\"340\" cy=\"408\" r=\"6\" />\n  <text class=\"dg-body\" x=\"62\" y=\"330\">cue</text>\n  <text class=\"dg-body\" x=\"330\" y=\"430\" text-anchor=\"end\">valley = stored memory</text>\n  <text class=\"dg-note\" x=\"193\" y=\"352\">every update lowers energy</text>\n  <text class=\"dg-note\" x=\"193\" y=\"368\">or leaves it unchanged</text>\n</svg>\n<figcaption>Illustration with 25 neurons: store with the Hebbian rule, then a noisy cue rolls downhill into the stored pattern.</figcaption>\n</figure>\n\n<h2 id=\"twenty-five-lines-of-numpy\">Twenty-five lines of NumPy</h2>\n\n<p>Store five random patterns in 100 neurons, flip 30% of the bits of one, and let it settle:</p>\n\n<div class=\"language-python highlighter-rouge\"><div class=\"highlight\"><pre class=\"highlight\"><code><span class=\"kn\">import</span> <span class=\"n\">numpy</span> <span class=\"k\">as</span> <span class=\"n\">np</span>\n\n<span class=\"n\">rng</span> <span class=\"o\">=</span> <span class=\"n\">np</span><span class=\"p\">.</span><span class=\"n\">random</span><span class=\"p\">.</span><span class=\"nf\">default_rng</span><span class=\"p\">(</span><span class=\"mi\">1</span><span class=\"p\">)</span>\n<span class=\"n\">N</span><span class=\"p\">,</span> <span class=\"n\">P</span> <span class=\"o\">=</span> <span class=\"mi\">100</span><span class=\"p\">,</span> <span class=\"mi\">5</span>\n<span class=\"n\">patterns</span> <span class=\"o\">=</span> <span class=\"n\">rng</span><span class=\"p\">.</span><span class=\"nf\">choice</span><span class=\"p\">([</span><span class=\"o\">-</span><span class=\"mi\">1</span><span class=\"p\">,</span> <span class=\"mi\">1</span><span class=\"p\">],</span> <span class=\"n\">size</span><span class=\"o\">=</span><span class=\"p\">(</span><span class=\"n\">P</span><span class=\"p\">,</span> <span class=\"n\">N</span><span class=\"p\">))</span>\n<span class=\"n\">W</span> <span class=\"o\">=</span> <span class=\"p\">(</span><span class=\"n\">patterns</span><span class=\"p\">.</span><span class=\"n\">T</span> <span class=\"o\">@</span> <span class=\"n\">patterns</span><span class=\"p\">)</span> <span class=\"o\">/</span> <span class=\"n\">N</span>        <span class=\"c1\"># Hebbian storage\n</span><span class=\"n\">np</span><span class=\"p\">.</span><span class=\"nf\">fill_diagonal</span><span class=\"p\">(</span><span class=\"n\">W</span><span class=\"p\">,</span> <span class=\"mi\">0</span><span class=\"p\">)</span>                 <span class=\"c1\"># no self-connections\n</span>\n<span class=\"k\">def</span> <span class=\"nf\">energy</span><span class=\"p\">(</span><span class=\"n\">s</span><span class=\"p\">):</span>\n    <span class=\"k\">return</span> <span class=\"o\">-</span><span class=\"mf\">0.5</span> <span class=\"o\">*</span> <span class=\"n\">s</span> <span class=\"o\">@</span> <span class=\"n\">W</span> <span class=\"o\">@</span> <span class=\"n\">s</span>\n\n<span class=\"k\">def</span> <span class=\"nf\">recall</span><span class=\"p\">(</span><span class=\"n\">s</span><span class=\"p\">,</span> <span class=\"n\">sweeps</span><span class=\"o\">=</span><span class=\"mi\">5</span><span class=\"p\">):</span>\n    <span class=\"n\">s</span> <span class=\"o\">=</span> <span class=\"n\">s</span><span class=\"p\">.</span><span class=\"nf\">copy</span><span class=\"p\">()</span>\n    <span class=\"k\">for</span> <span class=\"n\">_</span> <span class=\"ow\">in</span> <span class=\"nf\">range</span><span class=\"p\">(</span><span class=\"n\">sweeps</span><span class=\"p\">):</span>\n        <span class=\"k\">for</span> <span class=\"n\">i</span> <span class=\"ow\">in</span> <span class=\"n\">rng</span><span class=\"p\">.</span><span class=\"nf\">permutation</span><span class=\"p\">(</span><span class=\"n\">N</span><span class=\"p\">):</span>   <span class=\"c1\"># one neuron at a time\n</span>            <span class=\"n\">s</span><span class=\"p\">[</span><span class=\"n\">i</span><span class=\"p\">]</span> <span class=\"o\">=</span> <span class=\"mi\">1</span> <span class=\"k\">if</span> <span class=\"n\">W</span><span class=\"p\">[</span><span class=\"n\">i</span><span class=\"p\">]</span> <span class=\"o\">@</span> <span class=\"n\">s</span> <span class=\"o\">&gt;=</span> <span class=\"mi\">0</span> <span class=\"k\">else</span> <span class=\"o\">-</span><span class=\"mi\">1</span>\n    <span class=\"k\">return</span> <span class=\"n\">s</span>\n\n<span class=\"n\">target</span> <span class=\"o\">=</span> <span class=\"n\">patterns</span><span class=\"p\">[</span><span class=\"mi\">0</span><span class=\"p\">]</span>\n<span class=\"n\">noisy</span> <span class=\"o\">=</span> <span class=\"n\">target</span><span class=\"p\">.</span><span class=\"nf\">copy</span><span class=\"p\">()</span>\n<span class=\"n\">noisy</span><span class=\"p\">[</span><span class=\"n\">rng</span><span class=\"p\">.</span><span class=\"nf\">choice</span><span class=\"p\">(</span><span class=\"n\">N</span><span class=\"p\">,</span> <span class=\"mi\">30</span><span class=\"p\">,</span> <span class=\"n\">replace</span><span class=\"o\">=</span><span class=\"bp\">False</span><span class=\"p\">)]</span> <span class=\"o\">*=</span> <span class=\"o\">-</span><span class=\"mi\">1</span>\n\n<span class=\"n\">out</span> <span class=\"o\">=</span> <span class=\"nf\">recall</span><span class=\"p\">(</span><span class=\"n\">noisy</span><span class=\"p\">)</span>\n<span class=\"nf\">print</span><span class=\"p\">((</span><span class=\"n\">noisy</span> <span class=\"o\">==</span> <span class=\"n\">target</span><span class=\"p\">).</span><span class=\"nf\">mean</span><span class=\"p\">(),</span> <span class=\"p\">(</span><span class=\"n\">out</span> <span class=\"o\">==</span> <span class=\"n\">target</span><span class=\"p\">).</span><span class=\"nf\">mean</span><span class=\"p\">())</span>   <span class=\"c1\"># 0.7 -&gt; 1.0\n</span><span class=\"nf\">print</span><span class=\"p\">(</span><span class=\"nf\">round</span><span class=\"p\">(</span><span class=\"nf\">energy</span><span class=\"p\">(</span><span class=\"n\">noisy</span><span class=\"p\">),</span> <span class=\"mi\">1</span><span class=\"p\">),</span> <span class=\"nf\">round</span><span class=\"p\">(</span><span class=\"nf\">energy</span><span class=\"p\">(</span><span class=\"n\">out</span><span class=\"p\">),</span> <span class=\"mi\">1</span><span class=\"p\">))</span>     <span class=\"c1\"># -8.6 -&gt; -50.5\n</span></code></pre></div></div>\n\n<p>70% correct goes to 100% correct, and the energy drops as it does.</p>\n\n<h2 id=\"the-limits\">The limits</h2>\n\n<ul>\n  <li><strong>Capacity is small.</strong> Hopfield found the network reliably holds only about <strong>0.15 memories per neuron</strong>. Push past that and recall falls apart.</li>\n  <li><strong>Spurious memories.</strong> The landscape also grows valleys nobody stored — blends of real memories — and a cue can get stuck in one.</li>\n  <li><strong>Similar memories interfere.</strong> Patterns that overlap a lot blur into each other.</li>\n</ul>\n\n<p>Those limits are exactly what the 2020 follow-up fixes: <a href=\"https://www.avinashgosavi.com/post/paper-tldr-hopfield-networks-is-all-you-need/\">modern Hopfield networks</a> sharpen the valleys so capacity grows exponentially — and their update turns out to be transformer attention.</p>\n\n<h2 id=\"why-it-still-matters-to-me\">Why it still matters to me</h2>\n\n<p>Databases retrieve by key. Vector stores retrieve by nearest neighbor. Hopfield networks retrieve by <strong>settling</strong> — pattern completion from a partial cue, which is much closer to how remembering feels. That idea is the reason <a href=\"https://www.avinashgosavi.com/post/attractor-memory-engine/\">Attractor</a> is called Attractor.</p>\n\n<p>That completes the six papers from <a href=\"https://www.avinashgosavi.com/post/paper-tldr-six-papers-on-memory/\">Part 1</a>. The whole list, with short TL;DRs, lives on my <a href=\"https://www.avinashgosavi.com/research/#papershelf\">papershelf</a>. The series keeps going with <a href=\"https://www.avinashgosavi.com/post/paper-tldr-memgpt/\">MemGPT</a>, which treats an LLM’s context window like an operating system’s memory.</p>\n\n<p>For a one-paragraph definition of <a href=\"https://www.avinashgosavi.com/post/agent-memory-glossary/#associative-memory\"><em>associative (content-addressable) memory</em></a>, see the <a href=\"https://www.avinashgosavi.com/post/agent-memory-glossary/\">agent memory glossary</a>.</p>\n"
    },
    {
      "id": "https://www.avinashgosavi.com/post/paper-tldr-sentence-bert/",
      "url": "https://www.avinashgosavi.com/post/paper-tldr-sentence-bert/",
      "title": "Paper TL;DR: Sentence-BERT — The Quiet Workhorse Behind Semantic Search",
      "summary": "A TL;DR of Sentence-BERT (Reimers & Gurevych, 2019): why BERT is too slow for search, how siamese training fixes it, and why it still powers semantic search.",
      "image": "https://www.avinashgosavi.com/assets/og/paper-tldr-sentence-bert.png",
      "date_published": "2026-10-18T21:00:00+00:00",
      "date_modified": "2026-10-23T01:35:48+00:00",
      "tags": ["Research","Agent memory"],
      "content_html": "<p><strong>Paper:</strong> Nils Reimers &amp; Iryna Gurevych, <a href=\"https://arxiv.org/abs/1908.10084\"><em>Sentence-BERT: Sentence Embeddings using Siamese BERT-Networks</em></a> (EMNLP 2019)</p>\n\n<p><strong>One-line TL;DR:</strong> fine-tune BERT so that every sentence gets its own fixed-size embedding you can compare with cosine similarity — turning semantic search over thousands of sentences from hours into seconds.</p>\n\n<p>It’s the least flashy paper in this series and the one my code depends on most. Every time <a href=\"https://www.avinashgosavi.com/post/attractor-memory-engine/\">Attractor</a>, <a href=\"https://www.avinashgosavi.com/series/#vaultmem\">VaultMem</a> or <a href=\"https://www.avinashgosavi.com/post/daylog-security-architecture/\">daylog</a> “searches by meaning,” it’s using a model descended from this paper.</p>\n\n<h2 id=\"the-problem-bert-is-a-slow-judge\">The problem: BERT is a slow judge</h2>\n\n<p>In 2019, BERT was the best tool for deciding whether two sentences meant the same thing — but only if you fed it <strong>both sentences together</strong>. That works for scoring one pair. It falls apart for search.</p>\n\n<p>The paper’s example: to find the most similar pair in a collection of 10,000 sentences, BERT would need around 50 million pairwise passes — roughly <strong>65 hours</strong> on a GPU. And you can’t precompute anything, because every score depends on both sentences at once.</p>\n\n<p>The obvious shortcut — run each sentence through BERT alone and average its outputs (or take the special <code class=\"language-plaintext highlighter-rouge\">[CLS]</code> token) — produced embeddings that were often <em>worse</em> than old-fashioned averaged word vectors.</p>\n\n<h2 id=\"the-fix-train-it-to-embed\">The fix: train it to embed</h2>\n\n<p>Sentence-BERT puts two copies of the same BERT side by side (a <strong>siamese</strong> network — shared weights), runs one sentence through each, pools each output into a single vector, and trains the whole thing so that:</p>\n\n<ul>\n  <li>sentences that mean similar things land close together, and</li>\n  <li>sentences that don’t land far apart.</li>\n</ul>\n\n<p>They trained on natural-language-inference data (pairs labeled entailment / contradiction / neutral) and on similarity-scored pairs. Afterward you throw away the “pair” setup: each sentence goes through the model <strong>once</strong>, you store its vector, and comparing two sentences is a cheap cosine similarity.</p>\n\n<p>That same 10,000-sentence search drops to about <strong>5 seconds</strong>.</p>\n\n<h2 id=\"why-it-still-matters\">Why it still matters</h2>\n\n<ul>\n  <li><strong>It made vector search practical.</strong> Embed once, store in an index (pgvector, Qdrant, <a href=\"https://www.avinashgosavi.com/post/paper-tldr-hnsw/\">HNSW</a>), and query in milliseconds. That’s the foundation of nearly every <a href=\"https://www.avinashgosavi.com/post/paper-tldr-retrieval-augmented-generation/\">RAG</a> system and agent memory store.</li>\n  <li><strong>The library outlived the paper.</strong> The <code class=\"language-plaintext highlighter-rouge\">sentence-transformers</code> package that came out of this work is still how most people get embedding models running in a few lines.</li>\n  <li><strong>Small models are often enough.</strong> The 384-dimension model I use in <a href=\"https://www.avinashgosavi.com/post/attractor-memory-engine/\">Attractor</a> runs comfortably on a CPU — handy when the whole point is <a href=\"https://www.avinashgosavi.com/uses/\">keeping data on hardware you own</a>.</li>\n</ul>\n\n<h2 id=\"caveats-worth-knowing\">Caveats worth knowing</h2>\n\n<ul>\n  <li>Bi-encoders like SBERT trade a little accuracy for speed. When precision matters, a common pattern is to retrieve with embeddings, then <strong>re-rank</strong> the top results with a slower cross-encoder.</li>\n  <li>“Similar meaning” is whatever the training data said it was. Domain-specific text (logs, configs, medical notes) often needs a domain-tuned model.</li>\n  <li>Embeddings find <em>related</em> text; they don’t reason. Multi-hop questions — the ones <a href=\"https://www.avinashgosavi.com/post/paper-tldr-locomo-long-term-memory-benchmark/\">LoCoMo</a> showed are hardest — need more than nearest neighbors.</li>\n</ul>\n\n<p>Earlier in the series: <a href=\"https://www.avinashgosavi.com/post/paper-tldr-six-papers-on-memory/\">six papers on AI memory</a>, <a href=\"https://www.avinashgosavi.com/post/paper-tldr-hopfield-networks-is-all-you-need/\">Hopfield Networks Is All You Need</a>, <a href=\"https://www.avinashgosavi.com/post/paper-tldr-complementary-learning-systems/\">Complementary Learning Systems</a>, <a href=\"https://www.avinashgosavi.com/post/paper-tldr-locomo-long-term-memory-benchmark/\">LoCoMo</a> and <a href=\"https://www.avinashgosavi.com/post/paper-tldr-memory-as-asset/\">Memory as Asset</a>. Next in the series: <a href=\"https://www.avinashgosavi.com/post/paper-tldr-hopfield-1982/\">Hopfield (1982)</a>, the original associative memory that recalls from a partial cue.</p>\n\n<p><a href=\"https://www.avinashgosavi.com/post/agent-memory-glossary/#embedding\"><em>Embeddings</em></a>, <a href=\"https://www.avinashgosavi.com/post/agent-memory-glossary/#hnsw\"><em>HNSW</em></a> and <a href=\"https://www.avinashgosavi.com/post/agent-memory-glossary/#re-ranking\"><em>re-ranking</em></a> each get a short definition in the <a href=\"https://www.avinashgosavi.com/post/agent-memory-glossary/#storing-and-finding-memories\">agent memory glossary</a>.</p>\n"
    },
    {
      "id": "https://www.avinashgosavi.com/post/paper-tldr-memory-as-asset/",
      "url": "https://www.avinashgosavi.com/post/paper-tldr-memory-as-asset/",
      "title": "Paper TL;DR: Memory as Asset — Whose Memory Is It Anyway?",
      "summary": "A TL;DR of Memory as Asset (Pan, Huang & Yang, 2026): moving AI memory from agent-centric to human-centric, its three principles, and how it shaped VaultMem.",
      "image": "https://www.avinashgosavi.com/assets/og/paper-tldr-memory-as-asset.png",
      "date_published": "2026-10-18T20:00:00+00:00",
      "date_modified": "2026-10-23T01:35:48+00:00",
      "tags": ["Research","Agent memory","Privacy"],
      "content_html": "<p><strong>Paper:</strong> Yanqi Pan, Qinghao Huang &amp; Weihao Yang, <a href=\"https://arxiv.org/abs/2603.14212\"><em>Memory as Asset: From Agent-centric to Human-centric Memory Management</em></a> (arXiv, March 2026)</p>\n\n<p><strong>One-line TL;DR:</strong> the memory an AI keeps about you is designed today as the agent’s internal resource; this paper argues it should be <em>your</em> asset — owned by you, shared only under your permissions, and able to grow over time.</p>\n\n<p>This is the paper that reshaped <a href=\"https://www.avinashgosavi.com/series/#vaultmem\">VaultMem</a>. I found it while I was stuck on a vague feeling that something about agent memory was backwards, and it gave that feeling a name.</p>\n\n<h2 id=\"the-problem-agent-centric-memory\">The problem: agent-centric memory</h2>\n\n<p>Most memory systems treat what an agent remembers about you as part of the agent — stored by the platform, shaped for the platform’s use, and stuck there. The authors call this <strong>agent-centric</strong> memory. The consequences are familiar: you can’t take your history with you, you can’t decide who sees it, and every product rebuilds its own isolated “memory island” about the same person.</p>\n\n<h2 id=\"the-proposal-memory-as-a-personal-asset\">The proposal: memory as a personal asset</h2>\n\n<p>They propose flipping the default, built on three principles:</p>\n\n<ul>\n  <li><strong>Memory in hand</strong> — individuals own and control their memories, not centralized platforms.</li>\n  <li><strong>Memory groups</strong> — memories can be shared and combined, but only through permissions the owner sets.</li>\n  <li><strong>Collective memory evolution</strong> — memory isn’t frozen: it’s recorded, consolidated and abstracted into higher-level knowledge over time, and can be exchanged between groups under control.</li>\n</ul>\n\n<p>Each memory object carries its content, context, an explicit <strong>owner</strong>, an <strong>access policy</strong> and a <strong>version</strong>. Ownership and permissions are part of the data itself, not an afterthought in some admin panel.</p>\n\n<h2 id=\"the-three-layers\">The three layers</h2>\n\n<ol>\n  <li><strong>Personal storage</strong> — fast storage of each person’s private memory objects.</li>\n  <li><strong>Evolution</strong> — updates memory from interactions: recording experiences, merging related memories, abstracting knowledge. It can run locally for privacy or remotely for compute.</li>\n  <li><strong>Memory exchange network</strong> — a decentralized way to discover, authenticate and exchange memory assets.</li>\n</ol>\n\n<p>Notably, the paper doesn’t insist everything stays on your device. If a provider hosts your memory, it should be encrypted so only you can read it.</p>\n\n<h2 id=\"how-it-shaped-what-i-built\">How it shaped what I built</h2>\n\n<p>That last point is where VaultMem lives. I treat it as an implementation of the <strong>first layer</strong>: the platform stores your memories, but they’re encrypted with a key only you hold, so the operator cryptographically can’t read them. Everything in <a href=\"https://www.avinashgosavi.com/series/#vaultmem\">the VaultMem series</a> — client-side encryption, encrypted search, <a href=\"https://www.avinashgosavi.com/post/vaultmem-part-3-pii-sanitizer/\">keeping PII away from the model provider</a> — follows from taking “memory in hand” literally.</p>\n\n<h2 id=\"caveats-worth-knowing\">Caveats worth knowing</h2>\n\n<ul>\n  <li>The authors are upfront that this is an <strong>early draft</strong>: the terms and design are still evolving.</li>\n  <li>It’s a <strong>conceptual and architectural</strong> paper — there are no benchmarks or experiments yet. The claim that existing systems meet at most two of the three principles is an argument, not a measurement.</li>\n  <li>The exchange network raises hard questions (trust, incentives, revocation) that the paper sketches rather than solves.</li>\n</ul>\n\n<p>I still think it’s the right question to ask before building any memory feature: <em>whose memory is this?</em></p>\n\n<p>Earlier in the series: <a href=\"https://www.avinashgosavi.com/post/paper-tldr-six-papers-on-memory/\">six papers on AI memory</a>, <a href=\"https://www.avinashgosavi.com/post/paper-tldr-hopfield-networks-is-all-you-need/\">Hopfield Networks Is All You Need</a>, <a href=\"https://www.avinashgosavi.com/post/paper-tldr-complementary-learning-systems/\">Complementary Learning Systems</a> and <a href=\"https://www.avinashgosavi.com/post/paper-tldr-locomo-long-term-memory-benchmark/\">LoCoMo</a>. Next in the series: <a href=\"https://www.avinashgosavi.com/post/paper-tldr-sentence-bert/\">Sentence-BERT</a>, the sentence embeddings behind most semantic search.</p>\n\n<p><a href=\"https://www.avinashgosavi.com/post/agent-memory-glossary/#zero-knowledge-storage\"><em>Zero-knowledge storage</em></a> and <a href=\"https://www.avinashgosavi.com/post/agent-memory-glossary/#client-side-encryption\"><em>client-side encryption</em></a> are defined in the privacy section of the <a href=\"https://www.avinashgosavi.com/post/agent-memory-glossary/#privacy\">agent memory glossary</a>.</p>\n"
    },
    {
      "id": "https://www.avinashgosavi.com/post/paper-tldr-locomo-long-term-memory-benchmark/",
      "url": "https://www.avinashgosavi.com/post/paper-tldr-locomo-long-term-memory-benchmark/",
      "title": "Paper TL;DR: LoCoMo — How Good Is Your Agent's Memory, Really?",
      "summary": "A TL;DR of the LoCoMo benchmark (Maharana et al., 2024) for very long-term conversational memory — what it tests, what it found, and how I used it on VaultMem.",
      "image": "https://www.avinashgosavi.com/assets/og/paper-tldr-locomo-long-term-memory-benchmark.png",
      "date_published": "2026-10-18T19:00:00+00:00",
      "date_modified": "2026-10-23T01:35:48+00:00",
      "tags": ["Research","Agent memory"],
      "content_html": "<p><strong>Paper:</strong> Maharana et al., <a href=\"https://arxiv.org/abs/2402.17753\"><em>Evaluating Very Long-Term Conversational Memory of LLM Agents</em></a> (ACL 2024)</p>\n\n<p><strong>One-line TL;DR:</strong> LoCoMo is a benchmark of very long, multi-session conversations with questions that probe whether a system really remembers — and even strong long-context models and retrieval setups fall well short of people on it.</p>\n\n<p>If you’re building memory for an agent, this is the paper that keeps you honest.</p>\n\n<h2 id=\"whats-in-the-benchmark\">What’s in the benchmark</h2>\n\n<p>Most chat benchmarks are a few turns long. LoCoMo conversations run for <strong>dozens of sessions and hundreds of turns</strong>, spread over simulated weeks and months. The authors generated them with LLM agents that each had a persona and a timeline of life events, then had people review and fix them so the conversations stay consistent. Some turns include shared images, too.</p>\n\n<p>On top of the conversations they built three tasks: answering questions, summarizing events, and generating responses. The question-answering part is the one I care about, and its categories are what make it useful:</p>\n\n<ul>\n  <li><strong>Single-hop</strong> — one fact from one place in the history.</li>\n  <li><strong>Multi-hop</strong> — needs to connect facts from different sessions.</li>\n  <li><strong>Temporal</strong> — when did something happen, or what happened before or after.</li>\n  <li><strong>Open-domain</strong> — combine the conversation with general knowledge.</li>\n  <li><strong>Adversarial</strong> — questions about things that <em>never</em> happened, where the right answer is “that wasn’t mentioned.”</li>\n</ul>\n\n<figure class=\"diagram\">\n<svg viewBox=\"0 0 360 548\" role=\"img\" aria-labelledby=\"locomo-dg-title locomo-dg-desc\" xmlns=\"http://www.w3.org/2000/svg\">\n  <title id=\"locomo-dg-title\">How the LoCoMo benchmark is built</title>\n  <desc id=\"locomo-dg-desc\">Each LoCoMo conversation averages about 300 turns and 9 thousand tokens, over up to 35 sessions spread across simulated weeks and months. Three tasks are built on top: question answering, event summarization, and multi-modal dialogue generation. Question answering splits into five categories: single-hop, one fact from one place; multi-hop, connecting facts across sessions; temporal, when something happened or what came before or after; open-domain, combining the conversation with general knowledge; and adversarial, questions about things that never happened, where the right answer is that it was not mentioned.</desc>\n  <defs>\n    <marker id=\"locomo-dg-arrow\" viewBox=\"0 0 10 10\" refX=\"9\" refY=\"5\" markerWidth=\"7\" markerHeight=\"7\" orient=\"auto-start-reverse\">\n      <path class=\"dg-head\" d=\"M0 0 L10 5 L0 10 z\" />\n    </marker>\n  </defs>\n  <rect class=\"dg-pill\" x=\"10\" y=\"8\" width=\"340\" height=\"52\" rx=\"26\" />\n  <text class=\"dg-strong\" x=\"180\" y=\"30\" text-anchor=\"middle\">One long conversation</text>\n  <text class=\"dg-body\" x=\"180\" y=\"50\" text-anchor=\"middle\">~300 turns · ~9K tokens on average</text>\n  <rect class=\"dg-node\" x=\"10\" y=\"74\" width=\"60\" height=\"28\" rx=\"6\" />\n  <text class=\"dg-body\" x=\"40\" y=\"93\" text-anchor=\"middle\">S1</text>\n  <rect class=\"dg-node\" x=\"78\" y=\"74\" width=\"60\" height=\"28\" rx=\"6\" />\n  <text class=\"dg-body\" x=\"108\" y=\"93\" text-anchor=\"middle\">S2</text>\n  <rect class=\"dg-node\" x=\"146\" y=\"74\" width=\"60\" height=\"28\" rx=\"6\" />\n  <text class=\"dg-body\" x=\"176\" y=\"93\" text-anchor=\"middle\">S3</text>\n  <text class=\"dg-strong\" x=\"248\" y=\"93\" text-anchor=\"middle\">…</text>\n  <rect class=\"dg-node\" x=\"290\" y=\"74\" width=\"60\" height=\"28\" rx=\"6\" />\n  <text class=\"dg-body\" x=\"320\" y=\"93\" text-anchor=\"middle\">S35</text>\n  <text class=\"dg-note\" x=\"180\" y=\"122\" text-anchor=\"middle\">up to 35 sessions over simulated weeks and months</text>\n  <line class=\"dg-line\" x1=\"180\" y1=\"130\" x2=\"180\" y2=\"150\" marker-end=\"url(#locomo-dg-arrow)\" />\n  <rect class=\"dg-group\" x=\"10\" y=\"152\" width=\"340\" height=\"88\" rx=\"12\" />\n  <text class=\"dg-label\" x=\"22\" y=\"172\">Three tasks</text>\n  <rect class=\"dg-pill\" x=\"20\" y=\"182\" width=\"104\" height=\"48\" rx=\"8\" />\n  <text class=\"dg-body\" x=\"72\" y=\"202\" text-anchor=\"middle\">Question</text>\n  <text class=\"dg-body\" x=\"72\" y=\"220\" text-anchor=\"middle\">answering</text>\n  <rect class=\"dg-box\" x=\"128\" y=\"182\" width=\"104\" height=\"48\" rx=\"8\" />\n  <text class=\"dg-body\" x=\"180\" y=\"202\" text-anchor=\"middle\">Event</text>\n  <text class=\"dg-body\" x=\"180\" y=\"220\" text-anchor=\"middle\">summary</text>\n  <rect class=\"dg-box\" x=\"236\" y=\"182\" width=\"104\" height=\"48\" rx=\"8\" />\n  <text class=\"dg-body\" x=\"288\" y=\"202\" text-anchor=\"middle\">Dialogue</text>\n  <text class=\"dg-body\" x=\"288\" y=\"220\" text-anchor=\"middle\">generation</text>\n  <line class=\"dg-line\" x1=\"72\" y1=\"230\" x2=\"72\" y2=\"266\" marker-end=\"url(#locomo-dg-arrow)\" />\n  <rect class=\"dg-group\" x=\"10\" y=\"268\" width=\"340\" height=\"272\" rx=\"12\" />\n  <text class=\"dg-label\" x=\"22\" y=\"288\">Question categories</text>\n  <rect class=\"dg-box\" x=\"20\" y=\"298\" width=\"320\" height=\"40\" rx=\"8\" />\n  <text class=\"dg-strong\" x=\"34\" y=\"323\">Single-hop</text>\n  <text class=\"dg-note\" x=\"326\" y=\"323\" text-anchor=\"end\">one fact, one place</text>\n  <rect class=\"dg-box\" x=\"20\" y=\"346\" width=\"320\" height=\"40\" rx=\"8\" />\n  <text class=\"dg-strong\" x=\"34\" y=\"371\">Multi-hop</text>\n  <text class=\"dg-note\" x=\"326\" y=\"371\" text-anchor=\"end\">facts across sessions</text>\n  <rect class=\"dg-box\" x=\"20\" y=\"394\" width=\"320\" height=\"40\" rx=\"8\" />\n  <text class=\"dg-strong\" x=\"34\" y=\"419\">Temporal</text>\n  <text class=\"dg-note\" x=\"326\" y=\"419\" text-anchor=\"end\">when · before · after</text>\n  <rect class=\"dg-box\" x=\"20\" y=\"442\" width=\"320\" height=\"40\" rx=\"8\" />\n  <text class=\"dg-strong\" x=\"34\" y=\"467\">Open-domain</text>\n  <text class=\"dg-note\" x=\"326\" y=\"467\" text-anchor=\"end\">plus general knowledge</text>\n  <rect class=\"dg-box dg-muted\" x=\"20\" y=\"490\" width=\"320\" height=\"40\" rx=\"8\" />\n  <text class=\"dg-strong\" x=\"34\" y=\"515\">Adversarial</text>\n  <text class=\"dg-note\" x=\"326\" y=\"515\" text-anchor=\"end\">answer: “not mentioned”</text>\n</svg>\n<figcaption>Scoring each category on its own keeps a strong overall number from hiding weak temporal or multi-hop answers.</figcaption>\n</figure>\n\n<h2 id=\"what-they-found\">What they found</h2>\n\n<p>Even with long context windows or <a href=\"https://www.avinashgosavi.com/post/paper-tldr-retrieval-augmented-generation/\">retrieval-augmented generation</a>, models trail human performance by a wide margin. Retrieval helps, but the hard categories stay hard — especially reasoning about time and resisting the urge to make up answers to adversarial questions. Bigger context alone doesn’t fix it.</p>\n\n<h2 id=\"how-i-used-it\">How I used it</h2>\n\n<p>When I built <a href=\"https://www.avinashgosavi.com/series/#vaultmem\">VaultMem</a>, my first benchmark was 18 synthetic memories I wrote myself. It looked great — which was the problem. LoCoMo gave me a test I didn’t design. <a href=\"https://www.avinashgosavi.com/post/vaultmem-part-2-multimodal-temporal-backends/\">On a sample of its questions</a>, retrieval was strong for single-hop facts and weakest for multi-hop, which needs pieces that weren’t individually asked about. That’s a retrieval problem before it’s a model problem, and it changed what I worked on next.</p>\n\n<h2 id=\"the-takeaway-for-builders\">The takeaway for builders</h2>\n\n<ol>\n  <li><strong>Test against data you didn’t write.</strong> A memory system always looks good on its author’s examples.</li>\n  <li><strong>Score by category, not just overall.</strong> “85% accurate” hides that temporal and multi-hop questions might be failing.</li>\n  <li><strong>Measure the “I don’t know.”</strong> A memory that confidently invents a past is worse than one that admits a gap.</li>\n</ol>\n\n<p>Earlier in the series: <a href=\"https://www.avinashgosavi.com/post/paper-tldr-six-papers-on-memory/\">six papers on AI memory</a>, <a href=\"https://www.avinashgosavi.com/post/paper-tldr-hopfield-networks-is-all-you-need/\">Hopfield Networks Is All You Need</a>, and <a href=\"https://www.avinashgosavi.com/post/paper-tldr-complementary-learning-systems/\">Complementary Learning Systems</a>. Next in the series: <a href=\"https://www.avinashgosavi.com/post/paper-tldr-memory-as-asset/\">Memory as Asset</a>, on who should own an agent’s memory.</p>\n\n<p><a href=\"https://www.avinashgosavi.com/post/agent-memory-glossary/#recall-at-k\"><em>Recall@k</em></a> and <a href=\"https://www.avinashgosavi.com/post/agent-memory-glossary/#mrr\"><em>MRR</em></a>, the usual ways to score retrieval, are explained in the <a href=\"https://www.avinashgosavi.com/post/agent-memory-glossary/#measuring-it\">agent memory glossary</a>.</p>\n"
    },
    {
      "id": "https://www.avinashgosavi.com/post/paper-tldr-complementary-learning-systems/",
      "url": "https://www.avinashgosavi.com/post/paper-tldr-complementary-learning-systems/",
      "title": "Paper TL;DR: Complementary Learning Systems — Why Agents Need Two Kinds of Memory",
      "summary": "A TL;DR of McClelland, McNaughton & O'Reilly (1995): the brain's fast and slow learners, catastrophic interference, and what it suggests for AI agent memory.",
      "image": "https://www.avinashgosavi.com/assets/og/paper-tldr-complementary-learning-systems.png",
      "date_published": "2026-10-18T18:00:00+00:00",
      "date_modified": "2026-10-23T01:35:48+00:00",
      "tags": ["Research","Agent memory"],
      "content_html": "<p><strong>Paper:</strong> McClelland, McNaughton &amp; O’Reilly, <a href=\"https://doi.org/10.1037/0033-295X.102.3.419\"><em>Why there are complementary learning systems in the hippocampus and neocortex</em></a>, Psychological Review (1995)</p>\n\n<p><strong>One-line TL;DR:</strong> the brain learns with two systems on purpose — a fast one that grabs specific experiences and a slow one that gradually extracts general knowledge — because a single system can’t do both without wrecking what it already knows.</p>\n\n<p>It’s a 1995 psychology paper, and it’s one of the most useful things I’ve read about designing memory for AI agents.</p>\n\n<h2 id=\"the-problem-learning-fast-breaks-things\">The problem: learning fast breaks things</h2>\n\n<p>The paper starts from a puzzle that anyone who has fine-tuned a neural network will recognize. If you teach a network something new by adjusting the same shared weights that hold everything else, the new learning can overwrite the old. This is <strong>catastrophic interference</strong>: learn B, forget A.</p>\n\n<p>The fix that works in artificial networks is to learn slowly and <em>interleave</em> — mix new examples with old ones so the weights find a setting that serves both. But that’s slow. You can’t wait for hundreds of repetitions to remember where you parked this morning.</p>\n\n<h2 id=\"the-brains-answer-two-learners\">The brain’s answer: two learners</h2>\n\n<p>The authors argue the brain splits the job:</p>\n\n<ul>\n  <li><strong>Hippocampus — the fast learner.</strong> Stores specific episodes quickly, often after a single exposure, using sparse, separate codes so similar memories don’t collide. Great for “what happened at Tuesday’s meeting.”</li>\n  <li><strong>Neocortex — the slow learner.</strong> Learns gradually, with overlapping representations that capture structure shared across many experiences. Great for “how meetings at my company usually go.”</li>\n</ul>\n\n<p>The bridge between them is <strong>replay</strong>. Over time — notably during sleep — the hippocampus reactivates recent episodes, and the neocortex learns from those replays <em>interleaved</em> with older knowledge. That’s consolidation: specific memories slowly become general knowledge without overwriting what came before.</p>\n\n<figure class=\"diagram\">\n<svg viewBox=\"0 0 360 372\" role=\"img\" aria-labelledby=\"cls-dg-title cls-dg-desc\" xmlns=\"http://www.w3.org/2000/svg\">\n  <title id=\"cls-dg-title\">Two learners connected by replay</title>\n  <desc id=\"cls-dg-desc\">A new experience is written quickly into the hippocampus, the fast learner that stores specific episodes; in an agent this is an episodic log or vector store. Replay, interleaved with older knowledge, slowly teaches the neocortex, the slow learner that extracts general structure; in an agent this is consolidated facts and preferences.</desc>\n  <defs>\n    <marker id=\"cls-dg-arrow\" viewBox=\"0 0 10 10\" refX=\"9\" refY=\"5\" markerWidth=\"7\" markerHeight=\"7\" orient=\"auto-start-reverse\">\n      <path class=\"dg-head\" d=\"M0 0 L10 5 L0 10 z\" />\n    </marker>\n  </defs>\n  <rect class=\"dg-pill\" x=\"105\" y=\"4\" width=\"150\" height=\"36\" rx=\"18\" />\n  <text class=\"dg-strong\" x=\"180\" y=\"27\" text-anchor=\"middle\">New experience</text>\n  <line class=\"dg-line\" x1=\"180\" y1=\"42\" x2=\"180\" y2=\"86\" marker-end=\"url(#cls-dg-arrow)\" />\n  <text class=\"dg-note\" x=\"192\" y=\"69\">written once, fast</text>\n  <rect class=\"dg-box\" x=\"20\" y=\"90\" width=\"320\" height=\"92\" rx=\"10\" />\n  <text class=\"dg-strong\" x=\"180\" y=\"118\" text-anchor=\"middle\">Hippocampus · fast learner</text>\n  <text class=\"dg-body\" x=\"180\" y=\"140\" text-anchor=\"middle\">keeps specific episodes apart</text>\n  <text class=\"dg-agent\" x=\"180\" y=\"166\" text-anchor=\"middle\">agent: episodic log / vector store</text>\n  <line class=\"dg-line dg-dash\" x1=\"180\" y1=\"184\" x2=\"180\" y2=\"254\" marker-end=\"url(#cls-dg-arrow)\" />\n  <text class=\"dg-note\" x=\"192\" y=\"214\">replay, mixed with</text>\n  <text class=\"dg-note\" x=\"192\" y=\"232\">older knowledge</text>\n  <rect class=\"dg-box\" x=\"20\" y=\"258\" width=\"320\" height=\"92\" rx=\"10\" />\n  <text class=\"dg-strong\" x=\"180\" y=\"286\" text-anchor=\"middle\">Neocortex · slow learner</text>\n  <text class=\"dg-body\" x=\"180\" y=\"308\" text-anchor=\"middle\">extracts general structure</text>\n  <text class=\"dg-agent\" x=\"180\" y=\"334\" text-anchor=\"middle\">agent: consolidated facts &amp; preferences</text>\n</svg>\n<figcaption>The two-learner idea from the paper, with the agent-memory equivalent of each part.</figcaption>\n</figure>\n\n<p>The paper backs this with evidence from people with hippocampal damage, who struggle to form new episodic memories yet keep older knowledge, and with network simulations showing why interleaved learning avoids interference.</p>\n\n<h2 id=\"why-it-matters-for-ai-agents\">Why it matters for AI agents</h2>\n\n<p>Read with agents in mind, the paper is basically a design doc:</p>\n\n<ol>\n  <li><strong>Keep an episodic store separate from the model.</strong> Fine-tuning a model on every conversation is the “learn fast in shared weights” mistake. A separate, fast-writing memory — a log or vector store of specific events — is the hippocampus.</li>\n  <li><strong>Consolidate on a schedule, not on every write.</strong> Periodically summarize or distill many episodes into durable facts and preferences, mixing in what’s already known instead of overwriting it.</li>\n  <li><strong>Let memories change over time.</strong> Things that keep getting recalled should get stronger; one-off details can fade. A store that treats every memory as equally permanent forever is missing half the design.</li>\n</ol>\n\n<p>This is the model I borrowed for <a href=\"https://www.avinashgosavi.com/post/attractor-memory-engine/\">Attractor</a>: memories carry the context they were formed in, track how often they’re used, and are meant to strengthen or decay rather than sit frozen.</p>\n\n<h2 id=\"caveats-worth-knowing\">Caveats worth knowing</h2>\n\n<ul>\n  <li>The theory has been extended since 1995 — the neocortex can learn new things quickly when they fit an existing schema — so “neocortex is always slow” is a simplification.</li>\n  <li>Brains aren’t databases; the analogy is a guide for design choices, not a blueprint.</li>\n</ul>\n\n<p>Next in the series: the benchmark that told me how good my own memory system actually was. Earlier parts: <a href=\"https://www.avinashgosavi.com/post/paper-tldr-six-papers-on-memory/\">six papers on AI memory</a> and <a href=\"https://www.avinashgosavi.com/post/paper-tldr-hopfield-networks-is-all-you-need/\">Hopfield Networks Is All You Need</a>.</p>\n\n<p>Quick definitions of <a href=\"https://www.avinashgosavi.com/post/agent-memory-glossary/#consolidation\"><em>consolidation</em></a> and <a href=\"https://www.avinashgosavi.com/post/agent-memory-glossary/#decay\"><em>decay</em></a> are in the <a href=\"https://www.avinashgosavi.com/post/agent-memory-glossary/\">agent memory glossary</a>.</p>\n"
    },
    {
      "id": "https://www.avinashgosavi.com/post/paper-tldr-hopfield-networks-is-all-you-need/",
      "url": "https://www.avinashgosavi.com/post/paper-tldr-hopfield-networks-is-all-you-need/",
      "title": "Paper TL;DR: Hopfield Networks Is All You Need — Attention Is a Memory Lookup",
      "summary": "A walkthrough of Ramsauer et al. (2020): how modern Hopfield networks store exponentially many memories and why their update rule is transformer attention.",
      "image": "https://www.avinashgosavi.com/assets/og/paper-tldr-hopfield-networks-is-all-you-need.png",
      "date_published": "2026-10-18T17:00:00+00:00",
      "date_modified": "2026-10-23T01:35:48+00:00",
      "tags": ["Research","Agent memory"],
      "content_html": "<p><strong>Paper:</strong> Ramsauer, Schäfl, Lehner et al., <a href=\"https://arxiv.org/abs/2008.02217\"><em>Hopfield Networks is All You Need</em></a> (2020)</p>\n\n<p><strong>One-line TL;DR:</strong> a continuous version of the 1982 Hopfield network can store exponentially many memories, usually retrieves one in a single step — and that single step is exactly the attention operation inside every transformer.</p>\n\n<p>This is the paper that made me stop thinking of Hopfield networks as history. Here’s the idea in plain language, plus a tiny demo you can run.</p>\n\n<h2 id=\"the-old-idea-memories-as-valleys\">The old idea: memories as valleys</h2>\n\n<p>In <a href=\"https://www.pnas.org/doi/10.1073/pnas.79.8.2554\">Hopfield’s 1982 network</a>, you store patterns (say, strings of +1 and −1) in the connection weights. The network has an “energy,” and each stored pattern sits at the bottom of a valley. Start from a corrupted pattern, let the network update, and it rolls downhill into the nearest stored memory. That’s <em>content-addressable memory</em>: you find a memory by giving a piece of it, not by knowing its address.</p>\n\n<p>The catch: the classic network only holds about 0.14 patterns per neuron. Store more than that and the valleys blur together into spurious memories.</p>\n\n<h2 id=\"the-new-idea-sharpen-the-valleys\">The new idea: sharpen the valleys</h2>\n\n<p>Ramsauer et al. swap the old quadratic energy for one built on <strong>log-sum-exp</strong> — a smooth version of “max.” Intuitively, each stored pattern now carves a much sharper, deeper valley, so many more of them fit without interfering. The paper shows the capacity grows <em>exponentially</em> with the dimension of the patterns, and that retrieval typically converges after <strong>one</strong> update.</p>\n\n<p>That one update is:</p>\n\n<div class=\"language-plaintext highlighter-rouge\"><div class=\"highlight\"><pre class=\"highlight\"><code>new_state = X · softmax(β · Xᵀ · state)\n</code></pre></div></div>\n\n<p>where the columns of <code class=\"language-plaintext highlighter-rouge\">X</code> are the stored patterns and <code class=\"language-plaintext highlighter-rouge\">β</code> controls how sharp the valleys are.</p>\n\n<h2 id=\"wait--thats-attention\">Wait — that’s attention</h2>\n\n<p>Now look at transformer attention:</p>\n\n<div class=\"language-plaintext highlighter-rouge\"><div class=\"highlight\"><pre class=\"highlight\"><code>output = V · softmax(Kᵀ · q / √d)\n</code></pre></div></div>\n\n<p>Set the <strong>query</strong> <code class=\"language-plaintext highlighter-rouge\">q</code> to the current state, use the stored patterns as both <strong>keys</strong> <code class=\"language-plaintext highlighter-rouge\">K</code> and <strong>values</strong> <code class=\"language-plaintext highlighter-rouge\">V</code>, and pick <code class=\"language-plaintext highlighter-rouge\">β = 1/√d</code>. It’s the same equation. Every attention head in an LLM is doing a one-step associative memory lookup: compare the query against everything stored, softmax the similarities, return a weighted blend that’s usually dominated by the best match.</p>\n\n<p>The paper goes further, showing how different heads and layers behave like different retrieval regimes — some settle on one memory, some average over many — but the core equivalence is the part that stuck with me.</p>\n\n<h2 id=\"twenty-lines-of-numpy\">Twenty lines of NumPy</h2>\n\n<p>Store five random ±1 patterns, flip 40% of the bits in one of them, and do a single update:</p>\n\n<div class=\"language-python highlighter-rouge\"><div class=\"highlight\"><pre class=\"highlight\"><code><span class=\"kn\">import</span> <span class=\"n\">numpy</span> <span class=\"k\">as</span> <span class=\"n\">np</span>\n\n<span class=\"n\">rng</span> <span class=\"o\">=</span> <span class=\"n\">np</span><span class=\"p\">.</span><span class=\"n\">random</span><span class=\"p\">.</span><span class=\"nf\">default_rng</span><span class=\"p\">(</span><span class=\"mi\">0</span><span class=\"p\">)</span>\n<span class=\"n\">d</span><span class=\"p\">,</span> <span class=\"n\">n</span> <span class=\"o\">=</span> <span class=\"mi\">64</span><span class=\"p\">,</span> <span class=\"mi\">5</span>\n<span class=\"n\">X</span> <span class=\"o\">=</span> <span class=\"n\">rng</span><span class=\"p\">.</span><span class=\"nf\">choice</span><span class=\"p\">([</span><span class=\"o\">-</span><span class=\"mf\">1.0</span><span class=\"p\">,</span> <span class=\"mf\">1.0</span><span class=\"p\">],</span> <span class=\"n\">size</span><span class=\"o\">=</span><span class=\"p\">(</span><span class=\"n\">d</span><span class=\"p\">,</span> <span class=\"n\">n</span><span class=\"p\">))</span>   <span class=\"c1\"># stored memories (columns)\n</span>\n<span class=\"k\">def</span> <span class=\"nf\">softmax</span><span class=\"p\">(</span><span class=\"n\">z</span><span class=\"p\">):</span>\n    <span class=\"n\">e</span> <span class=\"o\">=</span> <span class=\"n\">np</span><span class=\"p\">.</span><span class=\"nf\">exp</span><span class=\"p\">(</span><span class=\"n\">z</span> <span class=\"o\">-</span> <span class=\"n\">z</span><span class=\"p\">.</span><span class=\"nf\">max</span><span class=\"p\">())</span>\n    <span class=\"k\">return</span> <span class=\"n\">e</span> <span class=\"o\">/</span> <span class=\"n\">e</span><span class=\"p\">.</span><span class=\"nf\">sum</span><span class=\"p\">()</span>\n\n<span class=\"k\">def</span> <span class=\"nf\">retrieve</span><span class=\"p\">(</span><span class=\"n\">xi</span><span class=\"p\">,</span> <span class=\"n\">beta</span><span class=\"o\">=</span><span class=\"mf\">0.5</span><span class=\"p\">):</span>\n    <span class=\"k\">return</span> <span class=\"n\">X</span> <span class=\"o\">@</span> <span class=\"nf\">softmax</span><span class=\"p\">(</span><span class=\"n\">beta</span> <span class=\"o\">*</span> <span class=\"n\">X</span><span class=\"p\">.</span><span class=\"n\">T</span> <span class=\"o\">@</span> <span class=\"n\">xi</span><span class=\"p\">)</span>      <span class=\"c1\"># one modern-Hopfield update\n</span>\n<span class=\"n\">target</span> <span class=\"o\">=</span> <span class=\"n\">X</span><span class=\"p\">[:,</span> <span class=\"mi\">2</span><span class=\"p\">]</span>\n<span class=\"n\">noisy</span> <span class=\"o\">=</span> <span class=\"n\">target</span><span class=\"p\">.</span><span class=\"nf\">copy</span><span class=\"p\">()</span>\n<span class=\"n\">noisy</span><span class=\"p\">[</span><span class=\"n\">rng</span><span class=\"p\">.</span><span class=\"nf\">choice</span><span class=\"p\">(</span><span class=\"n\">d</span><span class=\"p\">,</span> <span class=\"n\">size</span><span class=\"o\">=</span><span class=\"nf\">int</span><span class=\"p\">(</span><span class=\"mf\">0.4</span> <span class=\"o\">*</span> <span class=\"n\">d</span><span class=\"p\">),</span> <span class=\"n\">replace</span><span class=\"o\">=</span><span class=\"bp\">False</span><span class=\"p\">)]</span> <span class=\"o\">*=</span> <span class=\"o\">-</span><span class=\"mi\">1</span>\n\n<span class=\"n\">out</span> <span class=\"o\">=</span> <span class=\"nf\">retrieve</span><span class=\"p\">(</span><span class=\"n\">noisy</span><span class=\"p\">)</span>\n<span class=\"nf\">print</span><span class=\"p\">(</span><span class=\"sh\">\"</span><span class=\"s\">overlap before:</span><span class=\"sh\">\"</span><span class=\"p\">,</span> <span class=\"nf\">int</span><span class=\"p\">(</span><span class=\"n\">noisy</span> <span class=\"o\">@</span> <span class=\"n\">target</span><span class=\"p\">),</span> <span class=\"sh\">\"</span><span class=\"s\">/</span><span class=\"sh\">\"</span><span class=\"p\">,</span> <span class=\"n\">d</span><span class=\"p\">)</span>       <span class=\"c1\"># 14 / 64\n</span><span class=\"nf\">print</span><span class=\"p\">(</span><span class=\"sh\">\"</span><span class=\"s\">overlap after: </span><span class=\"sh\">\"</span><span class=\"p\">,</span> <span class=\"nf\">int</span><span class=\"p\">(</span><span class=\"n\">np</span><span class=\"p\">.</span><span class=\"nf\">sign</span><span class=\"p\">(</span><span class=\"n\">out</span><span class=\"p\">)</span> <span class=\"o\">@</span> <span class=\"n\">target</span><span class=\"p\">),</span> <span class=\"sh\">\"</span><span class=\"s\">/</span><span class=\"sh\">\"</span><span class=\"p\">,</span> <span class=\"n\">d</span><span class=\"p\">)</span> <span class=\"c1\"># 64 / 64\n</span></code></pre></div></div>\n\n<p>One step takes a pattern that’s only loosely related to the target back to a perfect copy. The softmax weights tell the story: about 0.97 on the right memory and crumbs on the rest — exactly what an attention map looks like.</p>\n\n<h2 id=\"why-i-care-as-someone-building-agent-memory\">Why I care, as someone building agent memory</h2>\n\n<p>Most agent memory today is “embed everything, return the top-k nearest neighbors” (usually through an index like <a href=\"https://www.avinashgosavi.com/post/paper-tldr-hnsw/\">HNSW</a>). This paper reframes that: nearest-neighbor search is a crude, non-differentiable cousin of what attention already does well. It nudged me to build <a href=\"https://www.avinashgosavi.com/post/attractor-memory-engine/\">Attractor</a> around retrieval by partial cues and context, and to think of memory as something that <em>settles</em> rather than something you <em>look up</em>.</p>\n\n<h2 id=\"caveats-worth-knowing\">Caveats worth knowing</h2>\n\n<ul>\n  <li>“Exponential capacity” is a statement about well-separated patterns in high dimensions; real embeddings are messy and correlated.</li>\n  <li>The one-step convergence holds when β is large enough relative to how similar the stored patterns are — similar memories still blur together.</li>\n  <li>Treating attention as memory is a lens, not a full explanation of what transformers learn.</li>\n</ul>\n\n<p>If you read only one modern paper about associative memory, make it this one. Next in the series: <a href=\"https://www.avinashgosavi.com/post/paper-tldr-complementary-learning-systems/\">Complementary Learning Systems</a>, the brain’s split between a fast and a slow learner. The rest of what I’m reading is on my <a href=\"https://www.avinashgosavi.com/research/#papershelf\">papershelf</a>, and <a href=\"https://www.avinashgosavi.com/post/paper-tldr-six-papers-on-memory/\">Part 1 of this series</a> covers the other papers behind my memory projects.</p>\n\n<p>Terms like <a href=\"https://www.avinashgosavi.com/post/agent-memory-glossary/#top-k-retrieval\"><em>top-k retrieval</em></a> and <a href=\"https://www.avinashgosavi.com/post/agent-memory-glossary/#associative-memory\"><em>associative memory</em></a> are defined in plain language in the <a href=\"https://www.avinashgosavi.com/post/agent-memory-glossary/\">agent memory glossary</a>.</p>\n"
    },
    {
      "id": "https://www.avinashgosavi.com/post/paper-tldr-six-papers-on-memory/",
      "url": "https://www.avinashgosavi.com/post/paper-tldr-six-papers-on-memory/",
      "title": "Paper TL;DR: Six Papers That Shaped How I Think About AI Memory",
      "summary": "Short, plain-language summaries of six papers behind my AI memory side projects — from Hopfield's 1982 associative memory to user-owned memory for agents.",
      "image": "https://www.avinashgosavi.com/assets/og/paper-tldr-six-papers-on-memory.png",
      "date_published": "2026-10-18T16:00:00+00:00",
      "date_modified": "2026-10-22T11:35:39+00:00",
      "tags": ["Research","Agent memory"],
      "content_html": "<p>Most of my side projects over the past year have circled one question: <strong>how should an AI system remember things?</strong> Not just “where do we put the vectors,” but how memory gets formed, found again, strengthened or forgotten — and who gets to read it.</p>\n\n<p>I didn’t start with papers. I started building, got stuck, and went looking for how other people had thought about the problem. These six are the ones that actually changed what I built. Here’s each one in a few sentences, in plain language, plus what I took from it. If you only read one, read the first.</p>\n\n<h2 id=\"1-hopfield-1982--memory-as-a-place-you-roll-into\">1. Hopfield (1982) — memory as a place you roll into</h2>\n\n<p><a href=\"https://www.pnas.org/doi/10.1073/pnas.79.8.2554\"><em>Neural networks and physical systems with emergent collective computational abilities</em></a> · PNAS</p>\n\n<p><strong>TL;DR:</strong> Wire up simple on/off neurons with symmetric connections and the network gets an “energy” that can only go down as it updates. Stored memories sit at the bottoms of valleys in that landscape. Give it a partial or noisy pattern and it slides downhill into the full memory. You don’t look memories up by key — you fall into them.</p>\n\n<p><strong>What I took from it:</strong> this was the “oh” moment behind <a href=\"https://www.avinashgosavi.com/post/attractor-memory-engine/\">Attractor</a>. Recall from partial cues feels much closer to how I actually remember things than a database lookup does.</p>\n\n<h2 id=\"2-ramsauer-et-al-2020--attention-is-a-hopfield-update\">2. Ramsauer et al. (2020) — attention is a Hopfield update</h2>\n\n<p><a href=\"https://arxiv.org/abs/2008.02217\"><em>Hopfield Networks is All You Need</em></a> · arXiv</p>\n\n<p><strong>TL;DR:</strong> A “modern” Hopfield network with continuous states can store exponentially many patterns and usually retrieves one in a single step. And that single update step is mathematically the same operation as transformer attention.</p>\n\n<p><strong>What I took from it:</strong> the 1982 idea isn’t a museum piece — it’s sitting inside every LLM. That made me take attractor-style retrieval seriously as a design for agent memory, not just a curiosity.</p>\n\n<h2 id=\"3-mcclelland-mcnaughton--oreilly-1995--two-learners-not-one\">3. McClelland, McNaughton &amp; O’Reilly (1995) — two learners, not one</h2>\n\n<p><a href=\"https://doi.org/10.1037/0033-295X.102.3.419\"><em>Why there are complementary learning systems in the hippocampus and neocortex</em></a> · Psychological Review</p>\n\n<p><strong>TL;DR:</strong> The brain uses a fast learner that grabs specific episodes (hippocampus) and a slow learner that gradually pulls out general structure (neocortex). Replaying episodes into the slow system lets it learn without overwriting what it already knows.</p>\n\n<p><strong>What I took from it:</strong> a vocabulary for something agents get wrong — treating every memory the same forever. It’s why Attractor tracks access and why I think memories should strengthen with use and fade without it.</p>\n\n<h2 id=\"4-reimers--gurevych-2019--embeddings-you-can-actually-search\">4. Reimers &amp; Gurevych (2019) — embeddings you can actually search</h2>\n\n<p><a href=\"https://arxiv.org/abs/1908.10084\"><em>Sentence-BERT: Sentence Embeddings using Siamese BERT-Networks</em></a> · arXiv</p>\n\n<p><strong>TL;DR:</strong> Plain BERT has to compare sentences two at a time, which is painfully slow for search. Training it in a “siamese” setup gives every sentence its own embedding you can compare with cosine similarity, so finding the closest match among thousands takes seconds instead of hours.</p>\n\n<p><strong>What I took from it:</strong> the unglamorous workhorse. The sentence-transformers models behind semantic search in Attractor, VaultMem and <a href=\"https://www.avinashgosavi.com/post/daylog-security-architecture/\">daylog</a> all come from this line of work.</p>\n\n<h2 id=\"5-maharana-et-al-2024--testing-memory-honestly\">5. Maharana et al. (2024) — testing memory honestly</h2>\n\n<p><a href=\"https://arxiv.org/abs/2402.17753\"><em>Evaluating Very Long-Term Conversational Memory of LLM Agents</em> (LoCoMo)</a> · arXiv</p>\n\n<p><strong>TL;DR:</strong> A benchmark of very long, multi-session conversations with questions that need single facts, multi-step reasoning, time awareness, or the sense to say “that never happened.” Even strong long-context models and <a href=\"https://www.avinashgosavi.com/post/agent-memory-glossary/#rag\">RAG</a> setups struggle with it.</p>\n\n<p><strong>What I took from it:</strong> my own little synthetic test was flattering me. Running <a href=\"https://www.avinashgosavi.com/post/vaultmem-part-2-multimodal-temporal-backends/\">VaultMem against LoCoMo</a> gave me numbers I could actually trust — and showed me where retrieval was weak.</p>\n\n<h2 id=\"6-pan-huang--yang-2026--whose-memory-is-it\">6. Pan, Huang &amp; Yang (2026) — whose memory is it?</h2>\n\n<p><a href=\"https://arxiv.org/abs/2603.14212\"><em>Memory as Asset: From Agent-centric to Human-centric Memory Management</em></a> · arXiv</p>\n\n<p><strong>TL;DR:</strong> Argues that agent memory today is built for the agent rather than the person, and proposes treating the memory an AI keeps about you as an asset you own, share under your own permissions, and grow over time.</p>\n\n<p><strong>What I took from it:</strong> this one reshaped <a href=\"https://www.avinashgosavi.com/series/#vaultmem\">VaultMem</a> entirely. It gave words to the feeling that an agent’s memory of your life shouldn’t be readable by whoever runs the agent. VaultMem ended up implementing its first layer.</p>\n\n<h2 id=\"what-ties-them-together\">What ties them together</h2>\n\n<p>Reading these in a row, I keep landing on the same picture: good memory is <strong>found by meaning and context</strong>, <strong>changes over time</strong>, and <strong>belongs to the person it’s about</strong>. Most systems today nail the first, ignore the second, and get the third backwards.</p>\n\n<p>All six live on my <a href=\"https://www.avinashgosavi.com/research/#papershelf\">papershelf</a> with the rest of what I’m reading. If any of the vocabulary here is new, the <a href=\"https://www.avinashgosavi.com/post/agent-memory-glossary/\">agent memory glossary</a> has short definitions. If there’s a paper on memory or agents you think I should read next, I’d genuinely love to hear about it.</p>\n\n<p>Next in the series: <a href=\"https://www.avinashgosavi.com/post/paper-tldr-hopfield-networks-is-all-you-need/\">Hopfield Networks Is All You Need</a>, how modern Hopfield networks store exponentially many memories and why their update rule is transformer attention. Every part is listed on the <a href=\"https://www.avinashgosavi.com/paper-tldr/\">Paper TL;DR</a> page.</p>\n"
    },
    {
      "id": "https://www.avinashgosavi.com/post/doing-the-right-thing-fast/",
      "url": "https://www.avinashgosavi.com/post/doing-the-right-thing-fast/",
      "title": "Doing the Right Thing, Fast",
      "summary": "Cleaning up after deprecating the first Catalyst Cloud Monitoring: splitting it into focused work streams and shipping it all in two weeks with AI help.",
      "image": "https://www.avinashgosavi.com/assets/og/doing-the-right-thing-fast.png",
      "date_published": "2026-04-25T00:00:00+00:00",
      "date_modified": "2026-10-22T00:35:18+00:00",
      "tags": ["Engineering at work","AI tools"],
      "content_html": "<p>We recently deprecated the initial version of Cisco Catalyst Cloud Monitoring. That means the code that drove it — onboarding flows, device-specific paths, integrations that only existed for those devices — needed to go.</p>\n\n<p>In the old world, this kind of cleanup almost never happens on schedule. It’s not that people don’t want to do it. It’s that the cost is high: you’re touching interconnected parts, there’s real risk of breaking something unrelated, and the work has to fit around ongoing feature development. So it gets triaged below the line. Unused code paths don’t disappear — they just quietly accumulate. Every engineer knows the feeling of working around code that should have been deleted six months ago.</p>\n\n<p>This time was different.</p>\n\n<h2 id=\"what-we-actually-did\">What We Actually Did</h2>\n\n<p>I split the cleanup into 20-25 focused work streams. A few people helped with specific areas, but most of it was mine. Over two weeks, every stream went through the same cycle: identify what to remove, make the change, review it carefully, test, and move on.</p>\n\n<p>The Meraki developer tools team’s <strong>jira-to-PR skill</strong> made the pipeline from ticket to code review almost frictionless. I wasn’t fighting process — I was just doing the work.</p>\n\n<p>That work is now fully deployed to production. No errors. The legacy load is gone.</p>\n\n<h2 id=\"where-codex-fit-in\">Where Codex Fit In</h2>\n\n<p>Codex handled the mechanical side of each stream — finding the right call sites, generating the deletions, updating the ripple effects across the codebase. I’ve been here since inception, so I know this code well. That matters. A new person would have needed more time to build the mental model before they could trust what was being removed. My 4.5 years gave me the confidence to move fast without second-guessing.</p>\n\n<p>The way I verify is consistent: I do all my visual inspection in VS Code, diff by diff. When I wasn’t sure what Codex had generated, I asked it to explain the change in plain terms. That became a reliable part of my review loop — not a crutch, just a check.</p>\n\n<p><em>Takeaway: experience and AI compound. The codebase knowledge lets you set the right scope. Codex handles the execution. The combination is what made 2 weeks possible instead of 2 quarters.</em></p>\n\n<h2 id=\"the-shift-i-keep-coming-back-to\">The Shift I Keep Coming Back To</h2>\n\n<p>What I used to put off, I now just do.</p>\n\n<p>Not because the work got trivial — the cleanup was real work, done the right way, with proper reviews and manual testing. But the overhead that used to make it not worth starting has collapsed. The cost of doing the right thing is now low enough that “we’ll do it properly later” is no longer the default answer.</p>\n\n<p>That’s the shift. You can do the right thing <em>and</em> do it fast. Not by cutting corners, but because the slow parts — finding call sites, writing boilerplate, generating consistent changes across files — aren’t slow anymore.</p>\n\n<p>I’m writing less code than I used to. But I’m seeing more of my intentions land exactly as I wanted them to. That’s a trade I’ll take every time.</p>\n\n<p>I’m genuinely excited to see where this goes next.</p>\n\n<hr />\n\n<p><em>Read about how I approach repeatable workflows in Codex: <a href=\"https://www.avinashgosavi.com/post/codex-skills-baking-your-workflow-into-the-tool/\">Codex Skills: Baking Your Workflow Into the Tool</a></em></p>\n\n<p><em>Related: <a href=\"https://www.avinashgosavi.com/post/reviewing-ai-generated-tests-istqb-framework/\">Are These AI-Generated Tests Good?</a> — a repeatable way to review the tests an LLM writes.</em></p>\n"
    },
    {
      "id": "https://www.avinashgosavi.com/post/live-audio-transcription-whisper/",
      "url": "https://www.avinashgosavi.com/post/live-audio-transcription-whisper/",
      "title": "Live Audio Transcription on a $0 Server Using Whisper",
      "summary": "Near real-time audio transcription on my own server with faster-whisper — no third-party APIs — and the open-source service I extracted from daylog.",
      "image": "https://www.avinashgosavi.com/assets/og/live-audio-transcription-whisper.png",
      "date_published": "2026-04-12T00:00:00+00:00",
      "date_modified": "2026-10-23T07:36:47+00:00",
      "tags": ["Local-first","Tutorials"],
      "content_html": "<p>While building <a href=\"https://github.com/aag1091-alt/daylog\">daylog</a> — a private voice diary app — I needed a way to transcribe audio from a phone in near real-time, running entirely on my own server. No OpenAI API, no Google, no third-party services. Just a microphone, a chunk of audio, and text back.</p>\n\n<p>Here’s how I built it, what I learned, and the open-source service I extracted from it.</p>\n\n<hr />\n\n<h2 id=\"the-problem\">The problem</h2>\n\n<p>The browser’s <a href=\"https://developer.mozilla.org/en-US/docs/Web/API/MediaRecorder\">MediaRecorder API</a> lets you record audio in chunks. You can set a timeslice — every N milliseconds, it fires an <code class=\"language-plaintext highlighter-rouge\">ondataavailable</code> event with a blob of audio. Send that blob to a server, get back a transcript, show it to the user. Simple enough in theory.</p>\n\n<p>The tricky parts:</p>\n\n<ol>\n  <li>\n    <p><strong>WebM chunks are not standalone files.</strong> The first chunk from <code class=\"language-plaintext highlighter-rouge\">MediaRecorder</code> contains the WebM file header. Every subsequent chunk is raw audio data with no header. If you send chunk #2 to ffmpeg on its own, it can’t decode it — it has no context for what codec, sample rate, or format to expect. I spent an embarrassing amount of time debugging silent transcriptions before I found this.</p>\n  </li>\n  <li>\n    <p><strong>Whisper is slow on CPU.</strong> The original <code class=\"language-plaintext highlighter-rouge\">openai-whisper</code> package uses PyTorch and takes 15–30 seconds to transcribe a 5-second chunk on a shared CPU VM. That’s not a live transcription experience — that’s watching paint dry.</p>\n  </li>\n  <li>\n    <p><strong>First request is always slow.</strong> Loading a 75MB model from disk takes 10–20 seconds. If you don’t warm it up at startup, the first user request times out.</p>\n  </li>\n</ol>\n\n<hr />\n\n<h2 id=\"the-solutions\">The solutions</h2>\n\n<h3 id=\"webm-header-problem\">WebM header problem</h3>\n\n<p>The fix: don’t send individual chunks. Send the <strong>full accumulated audio</strong> on each event. Every transcription request becomes a valid, self-contained WebM file:</p>\n\n<div class=\"language-js highlighter-rouge\"><div class=\"highlight\"><pre class=\"highlight\"><code><span class=\"kd\">const</span> <span class=\"nx\">chunks</span> <span class=\"o\">=</span> <span class=\"p\">[];</span>\n\n<span class=\"nx\">recorder</span><span class=\"p\">.</span><span class=\"nx\">ondataavailable</span> <span class=\"o\">=</span> <span class=\"k\">async </span><span class=\"p\">(</span><span class=\"nx\">e</span><span class=\"p\">)</span> <span class=\"o\">=&gt;</span> <span class=\"p\">{</span>\n  <span class=\"k\">if </span><span class=\"p\">(</span><span class=\"nx\">e</span><span class=\"p\">.</span><span class=\"nx\">data</span><span class=\"p\">.</span><span class=\"nx\">size</span> <span class=\"o\">&lt;</span> <span class=\"mi\">1000</span><span class=\"p\">)</span> <span class=\"k\">return</span><span class=\"p\">;</span>\n  <span class=\"nx\">chunks</span><span class=\"p\">.</span><span class=\"nf\">push</span><span class=\"p\">(</span><span class=\"nx\">e</span><span class=\"p\">.</span><span class=\"nx\">data</span><span class=\"p\">);</span>\n\n  <span class=\"c1\">// Full accumulated audio = always a valid WebM file</span>\n  <span class=\"kd\">const</span> <span class=\"nx\">snapshot</span> <span class=\"o\">=</span> <span class=\"k\">new</span> <span class=\"nc\">Blob</span><span class=\"p\">(</span><span class=\"nx\">chunks</span><span class=\"p\">,</span> <span class=\"p\">{</span> <span class=\"na\">type</span><span class=\"p\">:</span> <span class=\"nx\">e</span><span class=\"p\">.</span><span class=\"nx\">data</span><span class=\"p\">.</span><span class=\"nx\">type</span> <span class=\"p\">});</span>\n\n  <span class=\"kd\">const</span> <span class=\"nx\">form</span> <span class=\"o\">=</span> <span class=\"k\">new</span> <span class=\"nc\">FormData</span><span class=\"p\">();</span>\n  <span class=\"nx\">form</span><span class=\"p\">.</span><span class=\"nf\">append</span><span class=\"p\">(</span><span class=\"dl\">\"</span><span class=\"s2\">file</span><span class=\"dl\">\"</span><span class=\"p\">,</span> <span class=\"nx\">snapshot</span><span class=\"p\">,</span> <span class=\"dl\">\"</span><span class=\"s2\">audio.webm</span><span class=\"dl\">\"</span><span class=\"p\">);</span>\n  <span class=\"kd\">const</span> <span class=\"nx\">res</span> <span class=\"o\">=</span> <span class=\"k\">await</span> <span class=\"nf\">fetch</span><span class=\"p\">(</span><span class=\"dl\">\"</span><span class=\"s2\">/transcribe</span><span class=\"dl\">\"</span><span class=\"p\">,</span> <span class=\"p\">{</span> <span class=\"na\">method</span><span class=\"p\">:</span> <span class=\"dl\">\"</span><span class=\"s2\">POST</span><span class=\"dl\">\"</span><span class=\"p\">,</span> <span class=\"na\">body</span><span class=\"p\">:</span> <span class=\"nx\">form</span> <span class=\"p\">});</span>\n  <span class=\"kd\">const</span> <span class=\"p\">{</span> <span class=\"nx\">transcript</span> <span class=\"p\">}</span> <span class=\"o\">=</span> <span class=\"k\">await</span> <span class=\"nx\">res</span><span class=\"p\">.</span><span class=\"nf\">json</span><span class=\"p\">();</span>\n  <span class=\"nf\">setLiveTranscript</span><span class=\"p\">(</span><span class=\"nx\">transcript</span><span class=\"p\">);</span> <span class=\"c1\">// replace, don't append</span>\n<span class=\"p\">};</span>\n</code></pre></div></div>\n\n<figure class=\"diagram\">\n<svg viewBox=\"0 0 336 420\" role=\"img\" aria-labelledby=\"wh-wc-title wh-wc-desc\" xmlns=\"http://www.w3.org/2000/svg\">\n  <title id=\"wh-wc-title\">Why single WebM chunks fail and accumulated audio works</title>\n  <desc id=\"wh-wc-desc\">MediaRecorder fires one blob every N milliseconds. Only chunk 1 carries the WebM file header; chunks 2 and 3 are raw audio. Sending chunk 2 on its own fails: ffmpeg has no header, so it can't decode it and the transcript comes back silent. The fix sends everything recorded so far on each event: chunk 1, then chunks 1 and 2, then chunks 1, 2 and 3. Each request starts with the header, so each is a valid WebM file, and each new transcript replaces the last one.</desc>\n  <defs>\n    <marker id=\"wh-wc-arrow\" viewBox=\"0 0 10 10\" refX=\"9\" refY=\"5\" markerWidth=\"7\" markerHeight=\"7\" orient=\"auto-start-reverse\">\n      <path class=\"dg-head\" d=\"M0 0 L10 5 L0 10 z\" />\n    </marker>\n  </defs>\n  <text class=\"dg-note\" x=\"168\" y=\"16\" text-anchor=\"middle\">MediaRecorder: one blob every N ms</text>\n  <rect class=\"dg-box\" x=\"16\" y=\"28\" width=\"120\" height=\"44\" rx=\"6\" />\n  <rect class=\"dg-pill\" x=\"22\" y=\"34\" width=\"58\" height=\"32\" rx=\"4\" />\n  <text class=\"dg-body dg-mono\" x=\"51\" y=\"54\" text-anchor=\"middle\">header</text>\n  <text class=\"dg-body\" x=\"108\" y=\"55\" text-anchor=\"middle\">audio</text>\n  <rect class=\"dg-box\" x=\"144\" y=\"28\" width=\"84\" height=\"44\" rx=\"6\" />\n  <text class=\"dg-body\" x=\"186\" y=\"55\" text-anchor=\"middle\">audio</text>\n  <rect class=\"dg-box\" x=\"236\" y=\"28\" width=\"84\" height=\"44\" rx=\"6\" />\n  <text class=\"dg-body\" x=\"278\" y=\"55\" text-anchor=\"middle\">audio</text>\n  <text class=\"dg-note\" x=\"76\" y=\"90\" text-anchor=\"middle\">chunk 1</text>\n  <text class=\"dg-note\" x=\"186\" y=\"90\" text-anchor=\"middle\">chunk 2</text>\n  <text class=\"dg-note\" x=\"278\" y=\"90\" text-anchor=\"middle\">chunk 3</text>\n\n  <rect class=\"dg-group\" x=\"8\" y=\"104\" width=\"320\" height=\"92\" rx=\"12\" />\n  <text class=\"dg-label\" x=\"24\" y=\"124\">Send one chunk</text>\n  <rect class=\"dg-box dg-muted\" x=\"24\" y=\"138\" width=\"84\" height=\"44\" rx=\"6\" />\n  <text class=\"dg-body\" x=\"66\" y=\"165\" text-anchor=\"middle\">audio</text>\n  <line class=\"dg-line dg-dash\" x1=\"110\" y1=\"160\" x2=\"138\" y2=\"160\" marker-end=\"url(#wh-wc-arrow)\" />\n  <text class=\"dg-body\" x=\"146\" y=\"155\">ffmpeg: no header,</text>\n  <text class=\"dg-body\" x=\"146\" y=\"174\">can't decode → silence</text>\n\n  <rect class=\"dg-group\" x=\"8\" y=\"212\" width=\"320\" height=\"140\" rx=\"12\" />\n  <text class=\"dg-label\" x=\"24\" y=\"232\">Send everything so far</text>\n  <text class=\"dg-note\" x=\"24\" y=\"264\">event 1</text>\n  <rect class=\"dg-pill\" x=\"80\" y=\"246\" width=\"32\" height=\"28\" rx=\"4\" />\n  <text class=\"dg-body\" x=\"96\" y=\"265\" text-anchor=\"middle\">1</text>\n  <text class=\"dg-note\" x=\"24\" y=\"300\">event 2</text>\n  <rect class=\"dg-pill\" x=\"80\" y=\"282\" width=\"32\" height=\"28\" rx=\"4\" />\n  <text class=\"dg-body\" x=\"96\" y=\"301\" text-anchor=\"middle\">1</text>\n  <rect class=\"dg-box\" x=\"116\" y=\"282\" width=\"32\" height=\"28\" rx=\"4\" />\n  <text class=\"dg-body\" x=\"132\" y=\"301\" text-anchor=\"middle\">2</text>\n  <text class=\"dg-note\" x=\"24\" y=\"336\">event 3</text>\n  <rect class=\"dg-pill\" x=\"80\" y=\"318\" width=\"32\" height=\"28\" rx=\"4\" />\n  <text class=\"dg-body\" x=\"96\" y=\"337\" text-anchor=\"middle\">1</text>\n  <rect class=\"dg-box\" x=\"116\" y=\"318\" width=\"32\" height=\"28\" rx=\"4\" />\n  <text class=\"dg-body\" x=\"132\" y=\"337\" text-anchor=\"middle\">2</text>\n  <rect class=\"dg-box\" x=\"152\" y=\"318\" width=\"32\" height=\"28\" rx=\"4\" />\n  <text class=\"dg-body\" x=\"168\" y=\"337\" text-anchor=\"middle\">3</text>\n  <line class=\"dg-line\" x1=\"196\" y1=\"296\" x2=\"222\" y2=\"296\" marker-end=\"url(#wh-wc-arrow)\" />\n  <text class=\"dg-body\" x=\"230\" y=\"292\">valid WebM</text>\n  <text class=\"dg-note\" x=\"230\" y=\"310\">every time</text>\n\n  <line class=\"dg-line\" x1=\"168\" y1=\"354\" x2=\"168\" y2=\"372\" marker-end=\"url(#wh-wc-arrow)\" />\n  <rect class=\"dg-pill\" x=\"24\" y=\"374\" width=\"288\" height=\"40\" rx=\"20\" />\n  <text class=\"dg-body\" x=\"168\" y=\"399\" text-anchor=\"middle\">Each transcript replaces the last</text>\n</svg>\n<figcaption>Only the first chunk carries the WebM header (shaded), so every request resends audio from the start.</figcaption>\n</figure>\n\n<p>The trade-off: each request sends more data than the last. For a 60-second recording with 5-second chunks, the last request sends all 60 seconds. Acceptable for a personal tool, worth optimising for production.</p>\n\n<h3 id=\"whisper-performance\">Whisper performance</h3>\n\n<p>The original <code class=\"language-plaintext highlighter-rouge\">openai-whisper</code> package runs on PyTorch. <a href=\"https://github.com/SYSTRAN/faster-whisper\">faster-whisper</a> uses CTranslate2 as its backend — it’s 2–4x faster on CPU and uses less memory. Combined with the <code class=\"language-plaintext highlighter-rouge\">tiny</code> model and <code class=\"language-plaintext highlighter-rouge\">int8</code> quantization, it transcribes a 5-second chunk in roughly 2–3 seconds on a <code class=\"language-plaintext highlighter-rouge\">shared-cpu-1x</code> Fly.io machine.</p>\n\n<div class=\"language-python highlighter-rouge\"><div class=\"highlight\"><pre class=\"highlight\"><code><span class=\"kn\">from</span> <span class=\"n\">faster_whisper</span> <span class=\"kn\">import</span> <span class=\"n\">WhisperModel</span>\n\n<span class=\"n\">model</span> <span class=\"o\">=</span> <span class=\"nc\">WhisperModel</span><span class=\"p\">(</span><span class=\"sh\">\"</span><span class=\"s\">tiny</span><span class=\"sh\">\"</span><span class=\"p\">,</span> <span class=\"n\">device</span><span class=\"o\">=</span><span class=\"sh\">\"</span><span class=\"s\">cpu</span><span class=\"sh\">\"</span><span class=\"p\">,</span> <span class=\"n\">compute_type</span><span class=\"o\">=</span><span class=\"sh\">\"</span><span class=\"s\">int8</span><span class=\"sh\">\"</span><span class=\"p\">)</span>\n<span class=\"n\">segments</span><span class=\"p\">,</span> <span class=\"n\">_</span> <span class=\"o\">=</span> <span class=\"n\">model</span><span class=\"p\">.</span><span class=\"nf\">transcribe</span><span class=\"p\">(</span><span class=\"sh\">\"</span><span class=\"s\">audio.webm</span><span class=\"sh\">\"</span><span class=\"p\">,</span> <span class=\"n\">beam_size</span><span class=\"o\">=</span><span class=\"mi\">1</span><span class=\"p\">)</span>\n<span class=\"n\">text</span> <span class=\"o\">=</span> <span class=\"sh\">\"</span><span class=\"s\"> </span><span class=\"sh\">\"</span><span class=\"p\">.</span><span class=\"nf\">join</span><span class=\"p\">(</span><span class=\"n\">seg</span><span class=\"p\">.</span><span class=\"n\">text</span><span class=\"p\">.</span><span class=\"nf\">strip</span><span class=\"p\">()</span> <span class=\"k\">for</span> <span class=\"n\">seg</span> <span class=\"ow\">in</span> <span class=\"n\">segments</span><span class=\"p\">).</span><span class=\"nf\">strip</span><span class=\"p\">()</span>\n</code></pre></div></div>\n\n<p><code class=\"language-plaintext highlighter-rouge\">beam_size=1</code> switches from beam search to greedy decoding. For short conversational audio it makes no meaningful difference to accuracy, but it’s noticeably faster.</p>\n\n<h3 id=\"cold-start\">Cold start</h3>\n\n<p>Bake the model into the Docker image. It adds ~75MB to the image size but eliminates the per-deploy download:</p>\n\n<div class=\"language-dockerfile highlighter-rouge\"><div class=\"highlight\"><pre class=\"highlight\"><code><span class=\"k\">RUN </span>python3 <span class=\"nt\">-c</span> <span class=\"s2\">\"from faster_whisper import WhisperModel; WhisperModel('tiny', device='cpu', compute_type='int8')\"</span>\n</code></pre></div></div>\n\n<hr />\n\n<h2 id=\"the-service\">The service</h2>\n\n<p>I extracted this into a standalone microservice: <a href=\"https://github.com/aag1091-alt/whisper-live-transcribe\">whisper-live-transcribe</a>.</p>\n\n<p>It’s a single FastAPI endpoint:</p>\n\n<div class=\"language-plaintext highlighter-rouge\"><div class=\"highlight\"><pre class=\"highlight\"><code>POST /transcribe\nContent-Type: multipart/form-data\n\nfile: &lt;audio file&gt;\n→ { \"transcript\": \"what you said\" }\n</code></pre></div></div>\n\n<p>That’s it. No auth, no database, no state. Drop it behind your existing API or deploy it standalone.</p>\n\n<p>The full <code class=\"language-plaintext highlighter-rouge\">main.py</code> is under 60 lines:</p>\n\n<div class=\"language-python highlighter-rouge\"><div class=\"highlight\"><pre class=\"highlight\"><code><span class=\"nd\">@app.post</span><span class=\"p\">(</span><span class=\"sh\">\"</span><span class=\"s\">/transcribe</span><span class=\"sh\">\"</span><span class=\"p\">)</span>\n<span class=\"k\">async</span> <span class=\"k\">def</span> <span class=\"nf\">transcribe</span><span class=\"p\">(</span><span class=\"nb\">file</span><span class=\"p\">:</span> <span class=\"n\">UploadFile</span> <span class=\"o\">=</span> <span class=\"nc\">File</span><span class=\"p\">(...)):</span>\n    <span class=\"n\">suffix</span> <span class=\"o\">=</span> <span class=\"nc\">Path</span><span class=\"p\">(</span><span class=\"nb\">file</span><span class=\"p\">.</span><span class=\"n\">filename</span> <span class=\"ow\">or</span> <span class=\"sh\">\"</span><span class=\"s\">audio.webm</span><span class=\"sh\">\"</span><span class=\"p\">).</span><span class=\"n\">suffix</span> <span class=\"ow\">or</span> <span class=\"sh\">\"</span><span class=\"s\">.webm</span><span class=\"sh\">\"</span>\n\n    <span class=\"k\">with</span> <span class=\"n\">tempfile</span><span class=\"p\">.</span><span class=\"nc\">NamedTemporaryFile</span><span class=\"p\">(</span><span class=\"n\">suffix</span><span class=\"o\">=</span><span class=\"n\">suffix</span><span class=\"p\">,</span> <span class=\"n\">delete</span><span class=\"o\">=</span><span class=\"bp\">False</span><span class=\"p\">)</span> <span class=\"k\">as</span> <span class=\"n\">tmp</span><span class=\"p\">:</span>\n        <span class=\"n\">tmp</span><span class=\"p\">.</span><span class=\"nf\">write</span><span class=\"p\">(</span><span class=\"k\">await</span> <span class=\"nb\">file</span><span class=\"p\">.</span><span class=\"nf\">read</span><span class=\"p\">())</span>\n        <span class=\"n\">tmp_path</span> <span class=\"o\">=</span> <span class=\"n\">tmp</span><span class=\"p\">.</span><span class=\"n\">name</span>\n\n    <span class=\"k\">def</span> <span class=\"nf\">_run</span><span class=\"p\">():</span>\n        <span class=\"k\">try</span><span class=\"p\">:</span>\n            <span class=\"n\">model</span> <span class=\"o\">=</span> <span class=\"nf\">_get_model</span><span class=\"p\">()</span>\n            <span class=\"n\">segments</span><span class=\"p\">,</span> <span class=\"n\">_</span> <span class=\"o\">=</span> <span class=\"n\">model</span><span class=\"p\">.</span><span class=\"nf\">transcribe</span><span class=\"p\">(</span><span class=\"n\">tmp_path</span><span class=\"p\">,</span> <span class=\"n\">beam_size</span><span class=\"o\">=</span><span class=\"mi\">1</span><span class=\"p\">)</span>\n            <span class=\"k\">return</span> <span class=\"sh\">\"</span><span class=\"s\"> </span><span class=\"sh\">\"</span><span class=\"p\">.</span><span class=\"nf\">join</span><span class=\"p\">(</span><span class=\"n\">seg</span><span class=\"p\">.</span><span class=\"n\">text</span><span class=\"p\">.</span><span class=\"nf\">strip</span><span class=\"p\">()</span> <span class=\"k\">for</span> <span class=\"n\">seg</span> <span class=\"ow\">in</span> <span class=\"n\">segments</span><span class=\"p\">).</span><span class=\"nf\">strip</span><span class=\"p\">()</span>\n        <span class=\"k\">finally</span><span class=\"p\">:</span>\n            <span class=\"nc\">Path</span><span class=\"p\">(</span><span class=\"n\">tmp_path</span><span class=\"p\">).</span><span class=\"nf\">unlink</span><span class=\"p\">(</span><span class=\"n\">missing_ok</span><span class=\"o\">=</span><span class=\"bp\">True</span><span class=\"p\">)</span>\n\n    <span class=\"n\">text</span> <span class=\"o\">=</span> <span class=\"k\">await</span> <span class=\"n\">asyncio</span><span class=\"p\">.</span><span class=\"nf\">to_thread</span><span class=\"p\">(</span><span class=\"n\">_run</span><span class=\"p\">)</span>\n    <span class=\"k\">return</span> <span class=\"p\">{</span><span class=\"sh\">\"</span><span class=\"s\">transcript</span><span class=\"sh\">\"</span><span class=\"p\">:</span> <span class=\"n\">text</span><span class=\"p\">}</span>\n</code></pre></div></div>\n\n<p><code class=\"language-plaintext highlighter-rouge\">asyncio.to_thread</code> keeps Whisper’s CPU-bound work off the event loop so the server stays responsive to other requests while transcribing.</p>\n\n<hr />\n\n<h2 id=\"deploying-to-flyio\">Deploying to Fly.io</h2>\n\n<div class=\"language-bash highlighter-rouge\"><div class=\"highlight\"><pre class=\"highlight\"><code>git clone https://github.com/aag1091-alt/whisper-live-transcribe\n<span class=\"nb\">cd </span>whisper-live-transcribe\nfly launch <span class=\"nt\">--name</span> my-transcribe-service\nfly deploy <span class=\"nt\">--local-only</span>\n</code></pre></div></div>\n\n<p>Total cost: <strong>$0/month</strong> on Fly’s free tier (1 shared-cpu-1x VM). The 1GB RAM config handles the model (~150MB for tiny + runtime overhead) with room to spare.</p>\n\n<hr />\n\n<h2 id=\"performance-numbers\">Performance numbers</h2>\n\n<p>Measured on <code class=\"language-plaintext highlighter-rouge\">shared-cpu-1x</code> with 1GB RAM:</p>\n\n<table>\n  <thead>\n    <tr>\n      <th>Model</th>\n      <th>Size</th>\n      <th>Latency per 5s chunk</th>\n      <th>Notes</th>\n    </tr>\n  </thead>\n  <tbody>\n    <tr>\n      <td>tiny</td>\n      <td>75MB</td>\n      <td>~2–3s</td>\n      <td>Sweet spot for real-time UX</td>\n    </tr>\n    <tr>\n      <td>base</td>\n      <td>145MB</td>\n      <td>~5–8s</td>\n      <td>Better accuracy, noticeable lag</td>\n    </tr>\n    <tr>\n      <td>small</td>\n      <td>460MB</td>\n      <td>~15–20s</td>\n      <td>Needs 2GB RAM</td>\n    </tr>\n  </tbody>\n</table>\n\n<p>For a live transcription experience <code class=\"language-plaintext highlighter-rouge\">tiny</code> is the right call. The accuracy is good enough for clear speech — it occasionally stumbles on names or technical terms, but for conversational audio it’s reliable.</p>\n\n<hr />\n\n<h2 id=\"what-i-ended-up-doing-instead\">What I ended up doing instead</h2>\n\n<p>After shipping this for daylog, I replaced it with the <a href=\"https://developer.mozilla.org/en-US/docs/Web/API/Web_Speech_API\">Web Speech API</a> — <code class=\"language-plaintext highlighter-rouge\">webkitSpeechRecognition</code> on iOS Safari uses Apple’s on-device neural engine. Results are truly instant, word-by-word as you speak, and nothing leaves the device.</p>\n\n<p>But the server-side approach is still the right choice when:</p>\n<ul>\n  <li>You need it to work in non-Safari browsers</li>\n  <li>Privacy requires avoiding Google’s servers (Chrome’s Web Speech API sends audio to Google)</li>\n  <li>You’re building a native app without browser APIs</li>\n  <li>You want to run a different language model or fine-tune for a specific domain</li>\n</ul>\n\n<p>The service is sitting open-source on GitHub if you want to use it. Swap in <code class=\"language-plaintext highlighter-rouge\">base</code> or <code class=\"language-plaintext highlighter-rouge\">small</code> for better accuracy, or point it at a GPU and <code class=\"language-plaintext highlighter-rouge\">large-v3</code> for near-perfect transcription.</p>\n\n<hr />\n\n<p><em><a href=\"https://github.com/aag1091-alt/whisper-live-transcribe\">whisper-live-transcribe on GitHub</a> — PRs welcome.</em></p>\n\n<p><em>Where this fits in daylog: <a href=\"https://www.avinashgosavi.com/post/hobby-project-stack/\">the hobby project stack</a> covers the rest of the setup, and <a href=\"https://www.avinashgosavi.com/post/daylog-security-architecture/\">daylog’s security architecture</a> covers how entries are encrypted once they’re transcribed. For live audio sent to a hosted model instead, <a href=\"https://www.avinashgosavi.com/post/building-rena-ai-health-companion-gemini-live/\">Rena</a> streams 16 kHz PCM over a WebSocket to a backend that passes it to Gemini Live.</em></p>\n"
    },
    {
      "id": "https://www.avinashgosavi.com/post/hobby-project-stack/",
      "url": "https://www.avinashgosavi.com/post/hobby-project-stack/",
      "title": "What Should You Actually Use to Build Your Next Hobby Project?",
      "summary": "What to use for your next hobby project in 2025: the zero-cost stack I chose for daylog — Cloudflare Pages, Fly.io, Neon, Upstash — and why.",
      "image": "https://www.avinashgosavi.com/assets/og/hobby-project-stack.png",
      "date_published": "2026-04-12T00:00:00+00:00",
      "date_modified": "2026-10-20T05:36:08+00:00",
      "tags": ["Tutorials","Infrastructure"],
      "content_html": "<p>I recently built <strong>daylog</strong> — a personal diary app where you speak or type your thoughts and they get encrypted and stored privately, with AI transcription happening server-side using Whisper. The interesting technical core is <a href=\"https://pypi.org/project/vaultmem/\">vaultmem</a>, a Python SDK for building encrypted memory vaults for AI apps. Daylog is essentially a live demo of it.</p>\n\n<p>But the more interesting story — the one I keep getting asked about — is the stack. Because I built this with a hard constraint: <strong>it should cost me nothing to run</strong>, and I should be able to deploy it from my laptop to a URL I can test on my phone in under 10 minutes.</p>\n\n<p>Here’s everything I learned about what’s actually available in 2025, what’s free, and what I’d pick if I were starting fresh today.</p>\n\n<hr />\n\n<h2 id=\"the-constraints-that-shaped-every-decision\">The constraints that shaped every decision</h2>\n\n<p>Before comparing services, the constraints matter:</p>\n\n<ol>\n  <li><strong>Zero fixed cost</strong> — I’m not charging for this. It needs to run on free tiers or scale-to-zero infrastructure.</li>\n  <li><strong>Phone-first</strong> — I needed a real URL on day one, testable on mobile, installable as a PWA.</li>\n  <li><strong>Privacy-first</strong> — Audio diary. Nothing goes to third-party AI APIs. Transcription happens on my own server.</li>\n  <li><strong>Deploy from laptop</strong> — No CI/CD setup in the first week. Just push and it works.</li>\n</ol>\n\n<hr />\n\n<h2 id=\"the-architecture-at-a-glance\">The architecture at a glance</h2>\n\n<p>Before diving into each choice, here’s how all the pieces connect:</p>\n\n<div class=\"mermaid\">\ngraph TD\n    Phone[\"📱 Phone / Browser\\nCloudflare Pages PWA\"]\n    Rails[\"Rails API\\ndaylog-rails-api.fly.dev\\n─────────────\\nAuth · routing · proxy\"]\n    Postgres[(\"Neon Postgres\\nusers · sessions · tokens\")]\n    Vault[\"Vault Service\\nFastAPI + vaultmem\\n─────────────\\ntranscription · search · storage\"]\n    Redis[(\"Upstash Redis\\nvault locks\")]\n    Volume[/\"Fly Volume\\nAES-256 .vmem files\"/]\n    Whisper[\"faster-whisper\\ntiny · int8 · CPU\"]\n    Embed[\"sentence-transformers\\nsemantic search\"]\n\n    Phone --&gt;|\"HTTPS\"| Rails\n    Rails --&gt;|\"SQL\"| Postgres\n    Rails --&gt;|\"Fly internal network\\n:8000 — never public\"| Vault\n    Vault --&gt;|\"distributed lock\"| Redis\n    Vault --&gt;|\"read / write\"| Volume\n    Vault --- Whisper\n    Vault --- Embed\n</div>\n\n<p>The key insight: the vault service is <strong>never exposed to the internet</strong>. Rails acts as the public face — it handles auth, verifies the JWT, and only then forwards requests to the vault service over Fly’s private network. The vault service doesn’t need its own auth layer, rate limiting, or TLS. That’s what makes this setup so lean.</p>\n\n<hr />\n\n<h2 id=\"the-stack-and-why\">The stack, and why</h2>\n\n<h3 id=\"frontend-cloudflare-pages\">Frontend: Cloudflare Pages</h3>\n\n<p><strong>Chose:</strong> Cloudflare Pages\n<strong>Also considered:</strong> Vercel, Netlify, GitHub Pages</p>\n\n<p>Cloudflare Pages is the easy winner for hobby frontends. Free tier includes:</p>\n<ul>\n  <li>Unlimited requests</li>\n  <li>500 builds/month</li>\n  <li>Global CDN</li>\n  <li>Custom domains with auto-SSL</li>\n</ul>\n\n<p>But the real reason I chose it over Vercel: <strong>Cloudflare’s ecosystem</strong>. If you ever need edge functions, object storage (R2), or KV, they’re right there in the same platform. Vercel’s free tier is also generous, but it optimises for Next.js specifically — if you’re not using Next.js (I’m using Vite + React), Cloudflare has less lock-in.</p>\n\n<p><strong>Netlify</strong> is fine but feels like it’s slowly losing the race. Their free tier has a 100GB bandwidth cap and slower build times.</p>\n\n<blockquote>\n  <p><strong>My pick for your next hobby frontend:</strong> Cloudflare Pages, unless you’re building a Next.js app — then Vercel.</p>\n</blockquote>\n\n<hr />\n\n<h3 id=\"backend-flyio\">Backend: Fly.io</h3>\n\n<p><strong>Chose:</strong> Fly.io\n<strong>Also considered:</strong> Railway, Render, Heroku (lol)</p>\n\n<p>For a backend that needs to actually run a persistent server process — not a serverless function — Fly.io is currently the best deal available:</p>\n\n<ul>\n  <li><strong>3 shared-cpu-1x VMs with 256MB RAM</strong> completely free</li>\n  <li><strong>Persistent volumes</strong> (I use this for encrypted vault files)</li>\n  <li><strong>Private networking</strong> between apps via <code class=\"language-plaintext highlighter-rouge\">.internal</code> DNS — no API keys needed between your services</li>\n  <li><strong>Deploy from Docker</strong> — no magic, no platform-specific config</li>\n</ul>\n\n<p>The private networking was the killer feature for daylog. My Rails API talks to the vault service at <code class=\"language-plaintext highlighter-rouge\">http://daylog-vault-service.internal:8000</code>. Zero latency, zero egress cost, zero authentication surface area exposed to the internet.</p>\n\n<p><strong>Railway</strong> is the other serious contender. Their developer plan gives you $5 of free credit per month, which is enough to run a small app. The UX is genuinely nicer than Fly’s. But Railway doesn’t give you persistent volumes on the free tier, and their private networking story is less mature.</p>\n\n<p><strong>Render</strong> has a free tier but web services sleep after 15 minutes of inactivity. If your app gets a request after sleeping, the first one takes 30+ seconds to respond. For a production app this is a dealbreaker. Their paid plans are reasonable.</p>\n\n<p><strong>Heroku</strong> effectively killed their free tier in 2022. Don’t bother.</p>\n\n<blockquote>\n  <p><strong>My pick:</strong> Fly.io if you need persistent state, private networking between services, or Docker deploys. Railway if you want a nicer UI and don’t need volumes.</p>\n</blockquote>\n\n<hr />\n\n<h3 id=\"database-neon-postgres\">Database: Neon Postgres</h3>\n\n<p><strong>Chose:</strong> Neon\n<strong>Also considered:</strong> Supabase, Railway Postgres, PlanetScale</p>\n\n<p>Neon is <strong>serverless Postgres</strong>. It scales to zero when idle, so you’re not paying for compute when nothing’s happening. Free tier:</p>\n<ul>\n  <li>512MB storage</li>\n  <li>1 project, 10 branches</li>\n  <li>Branches are perfect for testing schema changes</li>\n</ul>\n\n<p>The branch feature is genuinely underrated. You can spin up a copy of your database for a feature branch, test a migration, and delete it — all from the CLI.</p>\n\n<p><strong>Supabase</strong> is the obvious alternative and it’s excellent — you get Postgres, auth, real-time, and file storage in one package. Their free tier is a 500MB Postgres database. The catch: free tier projects pause after 7 days of inactivity. Fine if you’re actively building, annoying if you step away for two weeks.</p>\n\n<p><strong>PlanetScale</strong> had the best free tier in the business for a while — then they removed it entirely in 2024. Avoid for hobby projects unless you’re paying.</p>\n\n<p><strong>Railway Postgres</strong> is convenient if you’re already on Railway, but pricier than Neon for the same storage.</p>\n\n<blockquote>\n  <p><strong>My pick:</strong> Neon for pure SQL, Supabase if you also need auth/file storage out of the box.</p>\n</blockquote>\n\n<hr />\n\n<h3 id=\"cache--ephemeral-state-upstash-redis\">Cache / Ephemeral State: Upstash Redis</h3>\n\n<p><strong>Chose:</strong> Upstash Redis\n<strong>Also considered:</strong> Railway Redis, Render Redis</p>\n\n<p>Upstash is serverless Redis. I use it for one thing: a distributed lock that prevents two vault service instances from opening the same encrypted vault simultaneously. Free tier:</p>\n<ul>\n  <li>10,000 commands/day</li>\n  <li>256MB storage</li>\n  <li>Pay-per-request above that</li>\n</ul>\n\n<p>For a hobby app this is effectively infinite. I’ve never come close to the limit.</p>\n\n<p>The key property: <strong>it doesn’t cost you anything when you’re not using it</strong>. Railway and Render both charge you for Redis even when idle. Upstash charges zero for zero activity.</p>\n\n<blockquote>\n  <p><strong>My pick:</strong> Upstash Redis for anything ephemeral, rate-limiting, locking, or session state in a hobby project.</p>\n</blockquote>\n\n<hr />\n\n<h3 id=\"ai--ml-whisper-on-cpu-on-your-own-server\">AI / ML: Whisper on CPU, on your own server</h3>\n\n<p>This is where daylog gets opinionated.</p>\n\n<p>The obvious path for audio transcription is the OpenAI Whisper API — $0.006 per minute, accurate, fast. For a diary app that’s maybe 5 minutes of audio a day, that’s $0.03/day — practically free.</p>\n\n<p>But I’m building a <em>private</em> diary. Audio of your thoughts shouldn’t route through third-party APIs, even cheap ones. So I run Whisper locally on the vault service VM.</p>\n\n<p>The practical setup:</p>\n<ul>\n  <li><strong>faster-whisper</strong> (Python, CTranslate2 backend) — 2-4x faster than the original openai-whisper package</li>\n  <li><strong>tiny model with int8 quantization</strong> — fits in memory, transcribes a 5-second chunk in ~2-3 seconds on a shared CPU</li>\n  <li><strong>Model baked into the Docker image</strong> — no cold download on VM restart</li>\n</ul>\n\n<p>The tradeoff: it’s slower than the API (3-5s per chunk vs instant), and <code class=\"language-plaintext highlighter-rouge\">tiny</code> makes occasional errors. For a private diary that’s fine. For a customer-facing product, you’d probably use the API. (The streaming setup itself is written up in <a href=\"https://www.avinashgosavi.com/post/live-audio-transcription-whisper/\">Live Audio Transcription on a $0 Server Using Whisper</a>.)</p>\n\n<p><strong>What this teaches you:</strong> The “serverless AI API” path is usually the right default for hobby projects — OpenAI, Replicate, Together AI all have generous free tiers or pay-per-use pricing that rounds to zero at hobby scale. Only run models yourself if you have a specific reason (cost at scale, privacy, customisation).</p>\n\n<hr />\n\n<h3 id=\"encrypted-storage-vaultmem\">Encrypted Storage: vaultmem</h3>\n\n<p>This is the part I want to highlight because it’s why daylog exists at all.</p>\n\n<p><a href=\"https://pypi.org/project/vaultmem/\">vaultmem</a> is a Python SDK that gives you an <strong>encrypted, embeddable memory vault</strong> for AI applications. You give it a passphrase and it gives you a session where you can store text atoms, media blobs, and run semantic search — all encrypted at rest.</p>\n\n<p>For daylog, this means:</p>\n<ul>\n  <li>Your diary entries are encrypted on the server with a key derived from your passphrase</li>\n  <li>I (the server operator) can’t read your diary</li>\n  <li>The vault is a single <code class=\"language-plaintext highlighter-rouge\">.vmem</code> file on a Fly volume — no database schema, no migrations</li>\n  <li>Semantic search (“what did I write about three weeks ago?”) works out of the box</li>\n</ul>\n\n<p>The SDK handles all the hard parts: PBKDF2 key derivation, AES-256 encryption, sentence embeddings for search, flush/commit semantics. You write:</p>\n\n<div class=\"language-python highlighter-rouge\"><div class=\"highlight\"><pre class=\"highlight\"><code><span class=\"n\">session</span> <span class=\"o\">=</span> <span class=\"n\">vault</span><span class=\"p\">.</span><span class=\"nf\">open</span><span class=\"p\">(</span><span class=\"n\">user_id</span><span class=\"p\">,</span> <span class=\"n\">passphrase</span><span class=\"p\">)</span>\n<span class=\"n\">session</span><span class=\"p\">.</span><span class=\"nf\">add</span><span class=\"p\">(</span><span class=\"sh\">\"</span><span class=\"s\">Had a great meeting with the team today</span><span class=\"sh\">\"</span><span class=\"p\">)</span>\n<span class=\"n\">session</span><span class=\"p\">.</span><span class=\"nf\">flush</span><span class=\"p\">()</span>\n</code></pre></div></div>\n\n<p>And you get encrypted storage with semantic search for free. (How the encryption and the encrypted search index work is in <a href=\"https://www.avinashgosavi.com/post/daylog-security-architecture/\">the daylog security post</a>; the library itself is explained in <a href=\"https://www.avinashgosavi.com/post/vaultmem-encrypted-memory-for-ai-agents/\">VaultMem Part 1</a>.)</p>\n\n<p>This is what makes the whole stack work at hobby scale — instead of a Postgres schema for diary entries + a vector database for search + an encryption layer + blob storage for audio, you have one SDK and one file.</p>\n\n<hr />\n\n<h2 id=\"what-everything-costs\">What everything costs</h2>\n\n<p>Here’s the actual bill:</p>\n\n<table>\n  <thead>\n    <tr>\n      <th>Service</th>\n      <th>What I use it for</th>\n      <th>Monthly cost</th>\n    </tr>\n  </thead>\n  <tbody>\n    <tr>\n      <td>Cloudflare Pages</td>\n      <td>Web app hosting</td>\n      <td><strong>$0</strong></td>\n    </tr>\n    <tr>\n      <td>Fly.io</td>\n      <td>Rails API + vault service</td>\n      <td><strong>$0</strong> (2 VMs, free tier)</td>\n    </tr>\n    <tr>\n      <td>Neon</td>\n      <td>Postgres (users, sessions)</td>\n      <td><strong>$0</strong> (free tier)</td>\n    </tr>\n    <tr>\n      <td>Upstash</td>\n      <td>Redis (vault locks)</td>\n      <td><strong>$0</strong> (free tier)</td>\n    </tr>\n    <tr>\n      <td><strong>Total</strong></td>\n      <td> </td>\n      <td><strong>$0</strong></td>\n    </tr>\n  </tbody>\n</table>\n\n<p>The only way this crosses zero is if I add a persistent volume larger than 3GB (Fly charges $0.15/GB/month) or if my Neon database exceeds 512MB.</p>\n\n<p>For context: the same app on managed infrastructure — RDS, ElastiCache, EC2, CloudFront — would run $50-100/month minimum.</p>\n\n<hr />\n\n<h2 id=\"my-opinionated-guide-for-your-next-hobby-project\">My opinionated guide for your next hobby project</h2>\n\n<p><strong>Static site / frontend only:</strong>\nCloudflare Pages. Nothing else comes close on the free tier.</p>\n\n<p><strong>Full-stack app, no persistent state:</strong>\nCloudflare Pages (frontend) + Cloudflare Workers (API). Everything runs at the edge, free tier is very generous.</p>\n\n<p><strong>Full-stack app with a database:</strong>\nCloudflare Pages + Fly.io + Neon. This is the setup I’d start with today. All three have genuine free tiers with no sleeping, no credit card traps.</p>\n\n<p><strong>Need auth out of the box:</strong>\nAdd Supabase for the database and use their auth — or keep Neon and use Clerk (generous free tier for up to 10k MAU).</p>\n\n<p><strong>Need background jobs or scheduled tasks:</strong>\nFly.io handles this natively — just run a separate machine. Or use Upstash QStash ($0 for low volume).</p>\n\n<p><strong>AI features:</strong>\nStart with the API (OpenAI, Anthropic, Together, Replicate). Only run models yourself when you have a specific reason. CPU inference on Fly is possible (I’m doing it) but it’s slow — GPU machines are still expensive even at hobby scale.</p>\n\n<p><strong>Encrypted, private AI memory:</strong>\nvaultmem. That’s the pitch.</p>\n\n<hr />\n\n<h2 id=\"the-one-thing-id-do-differently\">The one thing I’d do differently</h2>\n\n<p>I’d set up a simple CI/CD pipeline earlier. Deploying from the terminal (<code class=\"language-plaintext highlighter-rouge\">fly deploy --local-only</code>, <code class=\"language-plaintext highlighter-rouge\">wrangler pages deploy</code>) is fine for the first week, but it gets old fast. GitHub Actions + Fly + Cloudflare deploy actions take maybe 30 minutes to set up and remove a lot of friction.</p>\n\n<hr />\n\n<p>The broader point: the free tier ecosystem for hobby projects has genuinely never been better. In 2025 you can run a full-stack app with a real database, Redis, background jobs, and AI inference for exactly zero dollars a month — and none of these services are traps that will surprise you with a bill when a tweet goes viral.</p>\n\n<p>The days of “I can’t afford to run this side project” are over. The only cost now is your time.</p>\n\n<hr />\n\n<h2 id=\"whats-next-for-daylog\">What’s next for daylog</h2>\n\n<p>I’m putting the finishing touches on a proper demo — the full flow of opening the app, speaking a thought, watching it transcribe in real time, and searching back through weeks of entries with a natural language query. All encrypted, all on-device passphrase, running on the stack described above.</p>\n\n<p>If you want to see what a privacy-first, AI-powered personal knowledge tool looks like when it costs nothing to run — keep an eye out. Demo coming soon.</p>\n\n<p><em>vaultmem is on PyPI. daylog is coming.</em></p>\n\n<script type=\"module\">\n  import mermaid from 'https://cdn.jsdelivr.net/npm/mermaid@11/dist/mermaid.esm.min.mjs';\n  mermaid.initialize({ startOnLoad: true, theme: 'neutral' });\n</script>\n\n"
    },
    {
      "id": "https://www.avinashgosavi.com/post/daylog-security-architecture/",
      "url": "https://www.avinashgosavi.com/post/daylog-security-architecture/",
      "title": "How daylog Keeps Your Diary Private (Even from Itself)",
      "summary": "How daylog, a voice diary, keeps entries and its semantic search index encrypted, with nothing leaving the device to answer a query.",
      "image": "https://www.avinashgosavi.com/assets/og/daylog-security-architecture.png",
      "date_published": "2026-04-12T00:00:00+00:00",
      "date_modified": "2026-10-22T01:36:04+00:00",
      "tags": ["Privacy","Local-first"],
      "content_html": "<p>daylog is a voice diary. You open it, tap the mic, say what’s on your mind, and it saves. You can search your entries semantically — type “frustrated” and it surfaces the day you wrote <em>“couldn’t make progress, everything felt blocked”</em> even though that exact word never appears.</p>\n\n<p>That search working at all, while keeping your diary private, requires a few things to be true simultaneously: the content must be encrypted at rest, the search index must also be encrypted, and nothing should leave the device to answer a query. This post explains how each of those holds.</p>\n\n<hr />\n\n<h2 id=\"how-your-words-travel\">How your words travel</h2>\n\n<p>The diagram traces a single entry from your mouth to disk, and back to your eyes.</p>\n\n<svg viewBox=\"0 0 700 290\" xmlns=\"http://www.w3.org/2000/svg\" style=\"width:100%;max-width:700px;display:block;margin:2rem 0;font-family:system-ui,sans-serif;font-size:12px\">\n  <defs>\n    <marker id=\"arr\" markerWidth=\"8\" markerHeight=\"6\" refX=\"7\" refY=\"3\" orient=\"auto\">\n      <polygon points=\"0 0, 8 3, 0 6\" fill=\"#94a3b8\" />\n    </marker>\n  </defs>\n\n  <!-- SAVING label -->\n  <text x=\"10\" y=\"14\" font-size=\"10\" fill=\"#64748b\" font-weight=\"700\" letter-spacing=\"0.12em\">SAVING AN ENTRY</text>\n\n  <rect x=\"10\" y=\"22\" width=\"120\" height=\"72\" rx=\"7\" fill=\"#dcfce7\" stroke=\"#16a34a\" stroke-width=\"1.5\" />\n  <text x=\"70\" y=\"46\" text-anchor=\"middle\" font-size=\"16\">🎤</text>\n  <text x=\"70\" y=\"63\" text-anchor=\"middle\" font-size=\"10\" fill=\"#15803d\" font-weight=\"600\">You speak</text>\n  <text x=\"70\" y=\"78\" text-anchor=\"middle\" font-size=\"9\" fill=\"#166534\">\"went for a run today\"</text>\n  <line x1=\"130\" y1=\"58\" x2=\"148\" y2=\"58\" stroke=\"#94a3b8\" stroke-width=\"1.5\" marker-end=\"url(#arr)\" />\n\n  <rect x=\"150\" y=\"22\" width=\"120\" height=\"72\" rx=\"7\" fill=\"#dcfce7\" stroke=\"#16a34a\" stroke-width=\"1.5\" />\n  <text x=\"210\" y=\"43\" text-anchor=\"middle\" font-size=\"10\" fill=\"#15803d\" font-weight=\"600\">Browser</text>\n  <text x=\"210\" y=\"57\" text-anchor=\"middle\" font-size=\"9\" fill=\"#166534\">\"went for a run today\"</text>\n  <text x=\"210\" y=\"70\" text-anchor=\"middle\" font-size=\"8\" fill=\"#22c55e\">transcribed on-device</text>\n  <text x=\"210\" y=\"83\" text-anchor=\"middle\" font-size=\"8\" fill=\"#22c55e\">nothing sent yet ✓</text>\n  <line x1=\"270\" y1=\"58\" x2=\"288\" y2=\"58\" stroke=\"#94a3b8\" stroke-width=\"1.5\" marker-end=\"url(#arr)\" />\n\n  <rect x=\"290\" y=\"22\" width=\"120\" height=\"72\" rx=\"7\" fill=\"#dbeafe\" stroke=\"#3b82f6\" stroke-width=\"1.5\" />\n  <text x=\"350\" y=\"46\" text-anchor=\"middle\" font-size=\"10\" fill=\"#1d4ed8\" font-weight=\"600\">HTTPS</text>\n  <text x=\"350\" y=\"60\" text-anchor=\"middle\" font-size=\"9\" fill=\"#1d4ed8\">\"went for a run today\"</text>\n  <text x=\"350\" y=\"74\" text-anchor=\"middle\" font-size=\"8\" fill=\"#3b82f6\">content encrypted</text>\n  <text x=\"350\" y=\"86\" text-anchor=\"middle\" font-size=\"8\" fill=\"#3b82f6\">in transit by TLS</text>\n  <line x1=\"410\" y1=\"58\" x2=\"428\" y2=\"58\" stroke=\"#94a3b8\" stroke-width=\"1.5\" marker-end=\"url(#arr)\" />\n\n  <rect x=\"430\" y=\"22\" width=\"120\" height=\"72\" rx=\"7\" fill=\"#fef9c3\" stroke=\"#ca8a04\" stroke-width=\"1.5\" />\n  <text x=\"490\" y=\"43\" text-anchor=\"middle\" font-size=\"10\" fill=\"#854d0e\" font-weight=\"600\">Vault encrypts</text>\n  <text x=\"490\" y=\"57\" text-anchor=\"middle\" font-size=\"8\" fill=\"#92400e\">embed → AES-256-GCM</text>\n  <text x=\"490\" y=\"70\" text-anchor=\"middle\" font-size=\"8\" fill=\"#92400e\">🔑 using your key</text>\n  <text x=\"490\" y=\"83\" text-anchor=\"middle\" font-size=\"8\" fill=\"#92400e\">text + vector encrypted</text>\n  <line x1=\"550\" y1=\"58\" x2=\"568\" y2=\"58\" stroke=\"#94a3b8\" stroke-width=\"1.5\" marker-end=\"url(#arr)\" />\n\n  <rect x=\"570\" y=\"22\" width=\"120\" height=\"72\" rx=\"7\" fill=\"#fee2e2\" stroke=\"#dc2626\" stroke-width=\"1.5\" />\n  <text x=\"630\" y=\"43\" text-anchor=\"middle\" font-size=\"14\">💾</text>\n  <text x=\"630\" y=\"59\" text-anchor=\"middle\" font-size=\"10\" fill=\"#991b1b\" font-weight=\"600\">Stored on disk</text>\n  <text x=\"630\" y=\"73\" text-anchor=\"middle\" font-size=\"9\" fill=\"#b91c1c\">7f3a·a4c8·e12d·...</text>\n  <text x=\"630\" y=\"86\" text-anchor=\"middle\" font-size=\"8\" fill=\"#dc2626\">ciphertext — unreadable</text>\n\n  <!-- Divider -->\n  <line x1=\"10\" y1=\"112\" x2=\"690\" y2=\"112\" stroke=\"#e2e8f0\" stroke-width=\"1\" stroke-dasharray=\"5,4\" />\n\n  <!-- READING label -->\n  <text x=\"10\" y=\"130\" font-size=\"10\" fill=\"#64748b\" font-weight=\"700\" letter-spacing=\"0.12em\">READING YOUR ENTRIES</text>\n\n  <rect x=\"10\" y=\"138\" width=\"120\" height=\"72\" rx=\"7\" fill=\"#fee2e2\" stroke=\"#dc2626\" stroke-width=\"1.5\" />\n  <text x=\"70\" y=\"157\" text-anchor=\"middle\" font-size=\"14\">💾</text>\n  <text x=\"70\" y=\"174\" text-anchor=\"middle\" font-size=\"10\" fill=\"#991b1b\" font-weight=\"600\">Stored on disk</text>\n  <text x=\"70\" y=\"188\" text-anchor=\"middle\" font-size=\"9\" fill=\"#b91c1c\">7f3a·a4c8·e12d·...</text>\n  <text x=\"70\" y=\"201\" text-anchor=\"middle\" font-size=\"8\" fill=\"#dc2626\">ciphertext</text>\n  <line x1=\"130\" y1=\"174\" x2=\"148\" y2=\"174\" stroke=\"#94a3b8\" stroke-width=\"1.5\" marker-end=\"url(#arr)\" />\n\n  <rect x=\"150\" y=\"138\" width=\"120\" height=\"72\" rx=\"7\" fill=\"#fef9c3\" stroke=\"#ca8a04\" stroke-width=\"1.5\" />\n  <text x=\"210\" y=\"159\" text-anchor=\"middle\" font-size=\"10\" fill=\"#854d0e\" font-weight=\"600\">Vault decrypts</text>\n  <text x=\"210\" y=\"173\" text-anchor=\"middle\" font-size=\"9\" fill=\"#166534\">\"went for a run today\"</text>\n  <text x=\"210\" y=\"186\" text-anchor=\"middle\" font-size=\"8\" fill=\"#92400e\">🔑 your key only</text>\n  <text x=\"210\" y=\"199\" text-anchor=\"middle\" font-size=\"8\" fill=\"#92400e\">in memory, not logged</text>\n  <line x1=\"270\" y1=\"174\" x2=\"288\" y2=\"174\" stroke=\"#94a3b8\" stroke-width=\"1.5\" marker-end=\"url(#arr)\" />\n\n  <rect x=\"290\" y=\"138\" width=\"120\" height=\"72\" rx=\"7\" fill=\"#dbeafe\" stroke=\"#3b82f6\" stroke-width=\"1.5\" />\n  <text x=\"350\" y=\"162\" text-anchor=\"middle\" font-size=\"10\" fill=\"#1d4ed8\" font-weight=\"600\">HTTPS</text>\n  <text x=\"350\" y=\"176\" text-anchor=\"middle\" font-size=\"9\" fill=\"#166534\">\"went for a run today\"</text>\n  <text x=\"350\" y=\"190\" text-anchor=\"middle\" font-size=\"8\" fill=\"#3b82f6\">content encrypted</text>\n  <text x=\"350\" y=\"202\" text-anchor=\"middle\" font-size=\"8\" fill=\"#3b82f6\">in transit by TLS</text>\n  <line x1=\"410\" y1=\"174\" x2=\"428\" y2=\"174\" stroke=\"#94a3b8\" stroke-width=\"1.5\" marker-end=\"url(#arr)\" />\n\n  <rect x=\"430\" y=\"138\" width=\"120\" height=\"72\" rx=\"7\" fill=\"#dcfce7\" stroke=\"#16a34a\" stroke-width=\"1.5\" />\n  <text x=\"490\" y=\"159\" text-anchor=\"middle\" font-size=\"10\" fill=\"#15803d\" font-weight=\"600\">Browser</text>\n  <text x=\"490\" y=\"173\" text-anchor=\"middle\" font-size=\"9\" fill=\"#166534\">\"went for a run today\"</text>\n  <text x=\"490\" y=\"186\" text-anchor=\"middle\" font-size=\"8\" fill=\"#22c55e\">plain text ✓</text>\n  <line x1=\"550\" y1=\"174\" x2=\"568\" y2=\"174\" stroke=\"#94a3b8\" stroke-width=\"1.5\" marker-end=\"url(#arr)\" />\n\n  <rect x=\"570\" y=\"138\" width=\"120\" height=\"72\" rx=\"7\" fill=\"#dcfce7\" stroke=\"#16a34a\" stroke-width=\"1.5\" />\n  <text x=\"630\" y=\"159\" text-anchor=\"middle\" font-size=\"14\">📱</text>\n  <text x=\"630\" y=\"175\" text-anchor=\"middle\" font-size=\"10\" fill=\"#15803d\" font-weight=\"600\">You see it</text>\n  <text x=\"630\" y=\"189\" text-anchor=\"middle\" font-size=\"9\" fill=\"#166534\">\"went for a run today\"</text>\n\n  <!-- Legend -->\n  <rect x=\"10\" y=\"230\" width=\"11\" height=\"11\" fill=\"#dcfce7\" stroke=\"#16a34a\" stroke-width=\"1.5\" rx=\"2\" />\n  <text x=\"25\" y=\"240\" font-size=\"9\" fill=\"#64748b\">Readable text</text>\n  <rect x=\"120\" y=\"230\" width=\"11\" height=\"11\" fill=\"#fee2e2\" stroke=\"#dc2626\" stroke-width=\"1.5\" rx=\"2\" />\n  <text x=\"135\" y=\"240\" font-size=\"9\" fill=\"#64748b\">Encrypted — unreadable</text>\n  <rect x=\"278\" y=\"230\" width=\"11\" height=\"11\" fill=\"#dbeafe\" stroke=\"#3b82f6\" stroke-width=\"1.5\" rx=\"2\" />\n  <text x=\"293\" y=\"240\" font-size=\"9\" fill=\"#64748b\">Secured in transit (TLS)</text>\n  <rect x=\"445\" y=\"230\" width=\"11\" height=\"11\" fill=\"#fef9c3\" stroke=\"#ca8a04\" stroke-width=\"1.5\" rx=\"2\" />\n  <text x=\"460\" y=\"240\" font-size=\"9\" fill=\"#64748b\">Encryption / decryption point</text>\n</svg>\n\n<p>Your words are readable on your device and readable back in your browser. Everything in between — transit, storage — is ciphertext. Notice the saving path says <em>“text + vector encrypted”</em>: the search index is encrypted too, not just the content. More on that below.</p>\n\n<hr />\n\n<h2 id=\"what-vaultmem-actually-does\">What vaultmem actually does</h2>\n\n<p>The vault service uses <a href=\"https://www.avinashgosavi.com/post/vaultmem-encrypted-memory-for-ai-agents/\">vaultmem</a> — a library I built for giving AI agents a private, searchable long-term memory. In daylog it does three things every time you save an entry:</p>\n\n<p><strong>1. Classifies the entry automatically.</strong></p>\n\n<p>Every entry is run through a fast, rule-based classifier (no LLM involved) that assigns a memory type:</p>\n\n<ul>\n  <li><strong>EPISODIC</strong> — a time-anchored personal experience. <em>“Yesterday I went for a run.”</em> Has a temporal marker + first-person reference.</li>\n  <li><strong>PERSONA</strong> — a habitual trait or preference. <em>“I usually start work before 8am.”</em> Has a frequency word + first-person framing.</li>\n  <li><strong>PROCEDURAL</strong> — a how-to or sequence. <em>“First compile, then run tests.”</em> Has sequential markers.</li>\n  <li><strong>SEMANTIC</strong> — a timeless fact. Everything else.</li>\n</ul>\n\n<p>Most diary entries classify as EPISODIC. The type determines how the memory is weighted and how it ages — episodic memories have a natural recency decay, semantic ones don’t.</p>\n\n<p><strong>2. Embeds the entry as a 384-dimensional vector.</strong></p>\n\n<p>Before the entry is encrypted, it is run through <code class=\"language-plaintext highlighter-rouge\">sentence-transformers/all-MiniLM-L6-v2</code> — a 22M parameter model running locally inside the vault container. The model converts your text into a 384-float vector that captures the semantic meaning of what you wrote. (<code class=\"language-plaintext highlighter-rouge\">sentence-transformers</code> is the library that came out of the <a href=\"https://www.avinashgosavi.com/post/paper-tldr-sentence-bert/\">Sentence-BERT paper</a>.)</p>\n\n<p><em>“went for a run today”</em> → <code class=\"language-plaintext highlighter-rouge\">[0.312, -0.087, 0.441, ..., 0.198]</code> (384 values)</p>\n\n<p>This vector is how search finds relevant entries later. It is stored alongside the text.</p>\n\n<p><strong>3. Encrypts text + vector together and writes to disk.</strong></p>\n\n<p>The entry object — text, classification, timestamp, and embedding vector — is serialised to JSON, compressed with zlib, and encrypted with AES-256-GCM using the master encryption key (MEK). The result is appended to a <code class=\"language-plaintext highlighter-rouge\">.vmem</code> file on disk as <code class=\"language-plaintext highlighter-rouge\">IV + length + ciphertext + GCM tag</code>.</p>\n\n<p><strong>The embedding is encrypted.</strong> The search index — the thing that makes semantic search work — is not sitting in a plaintext index somewhere. It is inside the same ciphertext block as your diary entry. Nobody can search your vault without your key.</p>\n\n<hr />\n\n<h2 id=\"how-search-works-end-to-end\">How search works end to end</h2>\n\n<p>When you type “frustrated” into the search bar:</p>\n\n<p><strong>1. The vault service embeds your query.</strong></p>\n\n<p>“frustrated” → <code class=\"language-plaintext highlighter-rouge\">[0.289, -0.041, 0.509, ...]</code> (the same 384-dim model, same vector space as your stored entries).</p>\n\n<p><strong>2. It decrypts all stored embeddings from disk.</strong></p>\n\n<p>Every <code class=\"language-plaintext highlighter-rouge\">.vmem</code> entry is decrypted in RAM. The vault service now holds a matrix of shape <code class=\"language-plaintext highlighter-rouge\">(N entries) × 384</code>.</p>\n\n<p><strong>3. It runs a batched dot product.</strong></p>\n\n<p>Since both vectors are unit-normalised, dot product = cosine similarity. One matrix multiply gives a relevance score for every entry simultaneously.</p>\n\n<p><strong>4. It filters, ranks, and returns.</strong></p>\n\n<p>Entries with score below 0.3 are dropped. The rest are sorted and the top 8 returned — with their decrypted text.</p>\n\n<p>The decrypted entries exist only in RAM during this step. They are never written back to disk.</p>\n\n<p><strong>Why this finds meaning, not just keywords.</strong></p>\n\n<p>Two sentences with similar meaning will have similar vectors even if they share no words. “couldn’t make any progress today, everything felt blocked” scores ~0.62 against “frustrated” because the model has learned that these concepts occupy the same region of the vector space.</p>\n\n<p>This is the same reason “angry” might surface an entry about a deploy failing at midnight, or “energised” might surface one about a flow state — the words you actually wrote don’t have to match the words you search for.</p>\n\n<hr />\n\n<h2 id=\"the-memory-hierarchy\">The memory hierarchy</h2>\n\n<p>vaultmem stores entries at three granularity levels:</p>\n\n<ul>\n  <li><strong>ATOM</strong> — a single diary entry. This is what you write.</li>\n  <li><strong>COMPOSITE</strong> — an auto-generated aggregation of related atoms. A weekly summary, or a cluster of entries about the same topic. Not yet used in daylog but the foundation is there.</li>\n  <li><strong>AFFINITY</strong> — a detected standing pattern. If the vault notices you mention sleep problems every week, it could surface that as a persistent affinity. This is where it starts to feel less like a diary and more like a memory that knows you.</li>\n</ul>\n\n<p>Search queries all three tiers in priority order — AFFINITY first, then COMPOSITE, then ATOM — so a standing pattern surfaces above any individual entry that mentions the same thing.</p>\n\n<hr />\n\n<h2 id=\"the-one-trade-off-ai-summarise\">The one trade-off: AI summarise</h2>\n\n<p>When you tap <strong>Summarise this day</strong>, the browser’s already-decrypted entries for that day are sent to <a href=\"https://groq.com\">Groq</a> (Llama 3.3 70B) via the Rails API. What Groq receives:</p>\n\n<div class=\"language-plaintext highlighter-rouge\"><div class=\"highlight\"><pre class=\"highlight\"><code>09:10 AM: Went for a run today.\n08:30 PM: Good session, finally fixed that bug.\n</code></pre></div></div>\n\n<p>Time + text for one day. No name, no metadata, nothing from other days. The summary is returned and displayed — it’s never saved back into the vault.</p>\n\n<p>Everything else — adding entries, browsing, searching — is vault-only. No external API. Summarise is the one feature that trades privacy for convenience, and it’s a button you tap deliberately.</p>\n\n<p>If that trade-off isn’t right for you, remove <code class=\"language-plaintext highlighter-rouge\">GROK_API_KEY</code> from the Rails API config and everything else works unchanged.</p>\n\n<hr />\n\n<h2 id=\"data-at-rest\">Data at rest</h2>\n\n<table>\n  <thead>\n    <tr>\n      <th>Where</th>\n      <th>What</th>\n      <th>Status</th>\n    </tr>\n  </thead>\n  <tbody>\n    <tr>\n      <td>Fly.io volume (<code class=\"language-plaintext highlighter-rouge\">/data/vaults/</code>)</td>\n      <td><code class=\"language-plaintext highlighter-rouge\">.vmem</code> — text + embedding vectors, all encrypted</td>\n      <td>AES-256-GCM</td>\n    </tr>\n    <tr>\n      <td>Postgres (Neon)</td>\n      <td>email, bcrypt password hash</td>\n      <td>Passwords hashed; DB encrypted at rest</td>\n    </tr>\n    <tr>\n      <td>Browser sessionStorage</td>\n      <td>session_id, derived passphrase</td>\n      <td>Cleared on tab close</td>\n    </tr>\n    <tr>\n      <td>Browser localStorage</td>\n      <td>auth JWT</td>\n      <td>Cleared on sign out</td>\n    </tr>\n    <tr>\n      <td>Vault service RAM</td>\n      <td>MEK, decrypted atoms during active session</td>\n      <td>Never written to disk</td>\n    </tr>\n  </tbody>\n</table>\n\n<hr />\n\n<h2 id=\"the-short-version\">The short version</h2>\n\n<ul>\n  <li><strong>Voice input</strong>: transcribed on-device by Apple’s neural engine. Nothing leaves your phone at this step.</li>\n  <li><strong>Saving</strong>: entry is classified, embedded, then encrypted. Text and search vector hit disk together, both as ciphertext.</li>\n  <li><strong>Searching</strong>: vault decrypts embeddings in RAM, runs cosine similarity, returns matching plaintext. No external API.</li>\n  <li><strong>Summarising</strong>: sends that day’s entries to Groq. Optional, explicit, one day at a time.</li>\n</ul>\n\n<p>The encrypted search index is the part most people don’t expect. Normally if you want full-text or semantic search, you need a plaintext index somewhere — Elasticsearch, pgvector, whatever. vaultmem stores the vectors encrypted alongside the content, so you get semantic search without giving the index to anyone.</p>\n\n<hr />\n\n<h2 id=\"try-it-yourself\">Try it yourself</h2>\n\n<p>The app is live. You can log in with the demo account and poke around — all entries are pre-seeded across the last two months.</p>\n\n<p><strong><a href=\"https://daylog-ds3.pages.dev\">daylog-ds3.pages.dev</a></strong></p>\n\n<div class=\"language-plaintext highlighter-rouge\"><div class=\"highlight\"><pre class=\"highlight\"><code>email:    demo@daylog.app\npassword: demo1234\n</code></pre></div></div>\n\n<p>Things worth trying:</p>\n\n<ul>\n  <li><strong>Search</strong> (bottom nav → Search tab): type <code class=\"language-plaintext highlighter-rouge\">frustrated</code>, <code class=\"language-plaintext highlighter-rouge\">flow state</code>, <code class=\"language-plaintext highlighter-rouge\">couldn't sleep</code>, or <code class=\"language-plaintext highlighter-rouge\">good run</code> — these find entries by meaning, not exact words</li>\n  <li><strong>Summarise</strong>: navigate to any day that has entries, scroll past the list, tap <strong>Summarize this day</strong> — it reads everything you logged that day and reflects back themes and mood</li>\n  <li><strong>Voice</strong>: tap the mic, say something, watch the live transcription appear word by word</li>\n</ul>\n\n<p><em>The rest of the stack (hosting, transcription, costs) is in <a href=\"https://www.avinashgosavi.com/post/hobby-project-stack/\">my hobby project stack</a>, and the open-source Whisper transcription service I extracted from daylog has <a href=\"https://www.avinashgosavi.com/post/live-audio-transcription-whisper/\">its own post</a>. <a href=\"https://www.avinashgosavi.com/post/agent-memory-glossary/#episodic-memory\">Episodic</a>, <a href=\"https://www.avinashgosavi.com/post/agent-memory-glossary/#semantic-memory\">semantic</a>, <a href=\"https://www.avinashgosavi.com/post/agent-memory-glossary/#procedural-memory\">procedural</a> and <a href=\"https://www.avinashgosavi.com/post/agent-memory-glossary/#persona-memory\">persona</a> memory, <a href=\"https://www.avinashgosavi.com/post/agent-memory-glossary/#embedding\">embeddings</a>, <a href=\"https://www.avinashgosavi.com/post/agent-memory-glossary/#semantic-search\">semantic search</a> and <a href=\"https://www.avinashgosavi.com/post/agent-memory-glossary/#decay\">decay</a> each get a short definition in the <a href=\"https://www.avinashgosavi.com/post/agent-memory-glossary/\">agent memory glossary</a>.</em></p>\n"
    },
    {
      "id": "https://www.avinashgosavi.com/post/attractor-memory-engine/",
      "url": "https://www.avinashgosavi.com/post/attractor-memory-engine/",
      "title": "Attractor: Building a Memory Engine Inspired by How the Brain Works",
      "summary": "Attractor, a memory engine inspired by how brains remember — Hopfield-style retrieval by partial cues and context instead of keys or nearest neighbors.",
      "image": "https://www.avinashgosavi.com/assets/og/attractor-memory-engine.png",
      "date_published": "2026-04-05T00:00:00+00:00",
      "date_modified": "2026-10-21T17:36:35+00:00",
      "tags": ["Agent memory","Research"],
      "content_html": "<p>I’ve been thinking about memory — not human memory, but how machines store and retrieve it. Most systems today are either key-value stores (fast, exact, dumb) or vector databases (semantic, but still just nearest-neighbour). Neither of them works the way a brain does.</p>\n\n<p>So I started building something different. I called it <strong>Attractor</strong>.</p>\n\n<h2 id=\"the-problem-with-how-machines-remember\">The Problem with How Machines Remember</h2>\n\n<p>When you search a database, you give it a key and it returns a value. When you search a vector database, you give it a query and it returns the closest vectors by distance (usually via an approximate index like <a href=\"https://www.avinashgosavi.com/post/paper-tldr-hnsw/\">HNSW</a>). Both are useful. But neither captures something important about how biological memory works — <strong>context</strong>.</p>\n\n<p>Think about how you remember things. You don’t retrieve memories by key. You retrieve them by <em>partial cues</em> — a smell, a phrase, a situation. And the retrieval is shaped by context: what you were doing, who you were with, what problem you were trying to solve. Two memories with identical content mean different things depending on when and why they were formed.</p>\n\n<p>Machines don’t do this. And for AI agents that need to reason across long conversations or accumulated knowledge, this is a real limitation.</p>\n\n<h2 id=\"hopfield-networks--the-intriguing-part\">Hopfield Networks — The Intriguing Part</h2>\n\n<p>In 1982, John Hopfield described a type of recurrent neural network that stores memories as <strong>energy minima</strong> <a href=\"#references\">[1]</a>. The idea is elegant: each memory is a stable state — a basin of attraction — in an energy landscape. When you give the network a partial or noisy input, it doesn’t search for the closest match. It <em>converges</em> — like a ball rolling downhill — to the nearest stable state.</p>\n\n<p>This is called <strong>content-addressable memory</strong>. You don’t need the exact key. You need something close enough to fall into the right basin.</p>\n\n<figure class=\"diagram\">\n<svg viewBox=\"0 0 360 282\" role=\"img\" aria-labelledby=\"attractor-el-title attractor-el-desc\" xmlns=\"http://www.w3.org/2000/svg\">\n  <title id=\"attractor-el-title\">Energy landscape: memories as basins of attraction</title>\n  <desc id=\"attractor-el-desc\">A qualitative sketch. The vertical axis is energy and the horizontal axis is the space of possible states. The curve has two valleys. Memory A, \"switched from JWT to session tokens\", sits at the bottom of the first valley; Memory B, \"auth tokens should never be stored in localStorage\", sits at the bottom of the second. A query partway up the slope of the first valley rolls downhill and settles on Memory A, without needing an exact match.</desc>\n  <defs>\n    <marker id=\"attractor-el-arrow\" viewBox=\"0 0 10 10\" refX=\"9\" refY=\"5\" markerWidth=\"7\" markerHeight=\"7\" orient=\"auto-start-reverse\">\n      <path class=\"dg-head\" d=\"M0 0 L10 5 L0 10 z\" />\n    </marker>\n  </defs>\n  <text class=\"dg-label\" x=\"40\" y=\"18\">Energy</text>\n  <line class=\"dg-axis\" x1=\"40\" y1=\"28\" x2=\"40\" y2=\"200\" />\n  <line class=\"dg-axis\" x1=\"40\" y1=\"200\" x2=\"348\" y2=\"200\" />\n  <path class=\"dg-line dg-curve\" d=\"M52 48 C80 48 95 165 125 165 C155 165 170 80 195 80 C220 80 240 160 270 160 C300 160 315 58 340 55\" />\n  <line class=\"dg-edge dg-dash\" x1=\"125\" y1=\"172\" x2=\"125\" y2=\"200\" />\n  <line class=\"dg-edge dg-dash\" x1=\"270\" y1=\"167\" x2=\"270\" y2=\"200\" />\n  <circle class=\"dg-dot\" cx=\"125\" cy=\"165\" r=\"6\" />\n  <circle class=\"dg-dot\" cx=\"270\" cy=\"160\" r=\"6\" />\n  <circle class=\"dg-query\" cx=\"160\" cy=\"111\" r=\"7\" />\n  <path class=\"dg-line dg-dash\" d=\"M151 119 Q140 146 133 156\" marker-end=\"url(#attractor-el-arrow)\" />\n  <text class=\"dg-agent\" x=\"172\" y=\"138\">query</text>\n  <text class=\"dg-strong\" x=\"125\" y=\"224\" text-anchor=\"middle\">Memory A</text>\n  <text class=\"dg-note\" x=\"125\" y=\"242\" text-anchor=\"middle\">JWT → session tokens</text>\n  <text class=\"dg-strong\" x=\"270\" y=\"224\" text-anchor=\"middle\">Memory B</text>\n  <text class=\"dg-note\" x=\"270\" y=\"242\" text-anchor=\"middle\">no localStorage tokens</text>\n  <text class=\"dg-label\" x=\"194\" y=\"274\" text-anchor=\"middle\">Possible states</text>\n</svg>\n<figcaption>Memories sit at the bottom of basins. A partial cue doesn't need an exact match: it rolls downhill into the nearest one.</figcaption>\n</figure>\n\n<p>The name Attractor comes directly from this. In dynamical systems, an attractor is a state that a system converges to over time. Hopfield memories are attractors. Retrieval is the process of finding which attractor your input belongs to.</p>\n\n<p>What makes this intriguing is that it mirrors something real about biological memory. Your brain isn’t doing a database lookup when you remember something. It’s doing something closer to energy minimisation — pattern completion from partial cues.</p>\n\n<p>What’s also interesting: in 2020, researchers showed that modern Hopfield networks with continuous states have exponentially higher storage capacity than the original binary version <a href=\"#references\">[2]</a> — and that the attention mechanism in transformers is mathematically equivalent to a single Hopfield update step. The architecture powering every LLM today is, at its core, a Hopfield retrieval operation.</p>\n\n<h2 id=\"what-i-built--phase-1\">What I Built — Phase 1</h2>\n\n<p>Attractor is a standalone memory service. Right now it’s Phase 1: a working foundation that stores memories with context and retrieves them by meaning.</p>\n\n<figure class=\"diagram\">\n<svg viewBox=\"0 0 360 522\" role=\"img\" aria-labelledby=\"attractor-arch-title attractor-arch-desc\" xmlns=\"http://www.w3.org/2000/svg\">\n  <title id=\"attractor-arch-title\">Attractor Phase 1 architecture</title>\n  <desc id=\"attractor-arch-desc\">A client — memlog, an agent, or any HTTP client — calls a FastAPI service over REST. FastAPI exposes POST /memories, POST /memories/search, GET /memories/:id and DELETE /memories/:id. It uses an encoder (sentence-transformers all-MiniLM-L6-v2, producing a 384-dimensional vector) and PgStore (an asyncpg connection pool for write, get, search and delete). Both lead to PostgreSQL with pgvector, which stores content as TEXT, context and tags as JSONB, vector as vector(384), access_count as INT and created_at as TIMESTAMPTZ. The service runs on port 7747, \"loci\" on a phone keypad.</desc>\n  <defs>\n    <marker id=\"attractor-arch-arrow\" viewBox=\"0 0 10 10\" refX=\"9\" refY=\"5\" markerWidth=\"7\" markerHeight=\"7\" orient=\"auto-start-reverse\">\n      <path class=\"dg-head\" d=\"M0 0 L10 5 L0 10 z\" />\n    </marker>\n  </defs>\n\n  <rect class=\"dg-box\" x=\"40\" y=\"6\" width=\"280\" height=\"52\" rx=\"26\" />\n  <text class=\"dg-strong\" x=\"180\" y=\"28\" text-anchor=\"middle\">Client</text>\n  <text class=\"dg-note\" x=\"180\" y=\"47\" text-anchor=\"middle\">memlog, an agent, or any HTTP client</text>\n\n  <line class=\"dg-line\" x1=\"180\" y1=\"58\" x2=\"180\" y2=\"88\" marker-end=\"url(#attractor-arch-arrow)\" />\n  <text class=\"dg-note\" x=\"192\" y=\"78\">REST</text>\n\n  <rect class=\"dg-box\" x=\"6\" y=\"92\" width=\"348\" height=\"112\" rx=\"8\" />\n  <text class=\"dg-strong\" x=\"22\" y=\"116\">FastAPI</text>\n  <text class=\"dg-body dg-mono\" x=\"22\" y=\"140\">POST   /memories</text>\n  <text class=\"dg-body dg-mono\" x=\"22\" y=\"158\">POST   /memories/search</text>\n  <text class=\"dg-body dg-mono\" x=\"22\" y=\"176\">GET    /memories/:id</text>\n  <text class=\"dg-body dg-mono\" x=\"22\" y=\"194\">DELETE /memories/:id</text>\n\n  <line class=\"dg-line\" x1=\"130\" y1=\"204\" x2=\"92\" y2=\"234\" marker-end=\"url(#attractor-arch-arrow)\" />\n  <line class=\"dg-line\" x1=\"230\" y1=\"204\" x2=\"268\" y2=\"234\" marker-end=\"url(#attractor-arch-arrow)\" />\n\n  <rect class=\"dg-box\" x=\"6\" y=\"238\" width=\"170\" height=\"88\" rx=\"8\" />\n  <text class=\"dg-strong\" x=\"20\" y=\"261\">Encoder</text>\n  <text class=\"dg-note\" x=\"20\" y=\"281\">sentence-transformers</text>\n  <text class=\"dg-note dg-mono\" x=\"20\" y=\"299\">all-MiniLM-L6-v2</text>\n  <text class=\"dg-note\" x=\"20\" y=\"317\">→ 384-dim vector</text>\n\n  <rect class=\"dg-box\" x=\"184\" y=\"238\" width=\"170\" height=\"88\" rx=\"8\" />\n  <text class=\"dg-strong\" x=\"198\" y=\"261\">PgStore</text>\n  <text class=\"dg-note\" x=\"198\" y=\"281\">asyncpg connection pool</text>\n  <text class=\"dg-note\" x=\"198\" y=\"299\">write / get /</text>\n  <text class=\"dg-note\" x=\"198\" y=\"317\">search / delete</text>\n\n  <line class=\"dg-line\" x1=\"92\" y1=\"326\" x2=\"140\" y2=\"358\" marker-end=\"url(#attractor-arch-arrow)\" />\n  <line class=\"dg-line\" x1=\"268\" y1=\"326\" x2=\"220\" y2=\"358\" marker-end=\"url(#attractor-arch-arrow)\" />\n\n  <rect class=\"dg-box\" x=\"6\" y=\"362\" width=\"348\" height=\"126\" rx=\"8\" />\n  <text class=\"dg-strong\" x=\"22\" y=\"386\">PostgreSQL + pgvector</text>\n  <line class=\"dg-edge\" x1=\"22\" y1=\"398\" x2=\"338\" y2=\"398\" />\n  <text class=\"dg-note dg-mono\" x=\"22\" y=\"420\">content TEXT</text>\n  <text class=\"dg-note dg-mono\" x=\"22\" y=\"439\">context JSONB</text>\n  <text class=\"dg-note dg-mono\" x=\"22\" y=\"458\">tags JSONB</text>\n  <text class=\"dg-note dg-mono\" x=\"22\" y=\"477\">created_at TIMESTAMPTZ</text>\n  <text class=\"dg-note dg-mono\" x=\"200\" y=\"420\">vector vector(384)</text>\n  <text class=\"dg-note dg-mono\" x=\"200\" y=\"439\">access_count INT</text>\n\n  <text class=\"dg-note\" x=\"180\" y=\"512\" text-anchor=\"middle\">Service runs on port 7747 — “loci” on a phone keypad</text>\n</svg>\n<figcaption>Phase 1: a FastAPI service that embeds each memory and stores it, with its context, in Postgres + pgvector.</figcaption>\n</figure>\n\n<p><strong>The data model</strong> is built around something I call a <code class=\"language-plaintext highlighter-rouge\">ContextFrame</code> — the situational state at the time a memory was formed:</p>\n\n<div class=\"language-python highlighter-rouge\"><div class=\"highlight\"><pre class=\"highlight\"><code><span class=\"k\">class</span> <span class=\"nc\">ContextFrame</span><span class=\"p\">(</span><span class=\"n\">BaseModel</span><span class=\"p\">):</span>\n    <span class=\"n\">topic</span><span class=\"p\">:</span> <span class=\"nb\">str</span> <span class=\"o\">|</span> <span class=\"bp\">None</span>      <span class=\"c1\"># what was being discussed\n</span>    <span class=\"n\">entities</span><span class=\"p\">:</span> <span class=\"nb\">list</span><span class=\"p\">[</span><span class=\"nb\">str</span><span class=\"p\">]</span>    <span class=\"c1\"># who or what was involved\n</span>    <span class=\"n\">task</span><span class=\"p\">:</span> <span class=\"nb\">str</span> <span class=\"o\">|</span> <span class=\"bp\">None</span>       <span class=\"c1\"># what was being done\n</span>    <span class=\"n\">session_id</span><span class=\"p\">:</span> <span class=\"nb\">str</span> <span class=\"o\">|</span> <span class=\"bp\">None</span> <span class=\"c1\"># which session it came from\n</span></code></pre></div></div>\n\n<p>This isn’t metadata. It’s a retrieval key. When you search, you can filter by context — “find memories from this topic” or “find memories from this session” — and combine that with semantic search over the content itself.</p>\n\n<p><strong>The storage layer</strong> uses PostgreSQL with pgvector <a href=\"#references\">[3]</a>. Each memory stores its content, context frame, tags, timestamps, access count, and a 384-dimensional embedding from a sentence-transformers model <a href=\"#references\">[4]</a>. The embedding enables semantic search — retrieving by meaning rather than exact words.</p>\n\n<p><strong>The API</strong> is simple:</p>\n\n<div class=\"language-plaintext highlighter-rouge\"><div class=\"highlight\"><pre class=\"highlight\"><code>POST /memories          — store a memory with context\nGET  /memories/:id      — retrieve by ID\nPOST /memories/search   — search by content + context filters\nDELETE /memories/:id    — remove a memory\n</code></pre></div></div>\n\n<p><strong>The CLI</strong> wraps the service:</p>\n\n<div class=\"language-bash highlighter-rouge\"><div class=\"highlight\"><pre class=\"highlight\"><code>attractor start    <span class=\"c\"># starts the service on port 7747</span>\nattractor connect  <span class=\"c\"># verifies it's running</span>\n</code></pre></div></div>\n\n<p>Port 7747 is “loci” on a phone keypad — a nod to the method of loci, the memory palace technique <a href=\"#references\">[5]</a>, which was the project’s original name before I found a better one.</p>\n\n<h2 id=\"a-concrete-example\">A Concrete Example</h2>\n\n<p>Here’s what makes it different from a plain vector search. Say you store these two memories:</p>\n\n<div class=\"language-plaintext highlighter-rouge\"><div class=\"highlight\"><pre class=\"highlight\"><code>\"auth tokens should never be stored in localStorage\"\n  context: { topic: \"security review\", task: \"reviewing PR #42\" }\n\n\"switched from JWT to session tokens — stateless was causing issues\"\n  context: { topic: \"auth refactor\", task: \"architecture decision\" }\n</code></pre></div></div>\n\n<p>Now you search: <code class=\"language-plaintext highlighter-rouge\">\"why did we change the auth approach\"</code> with <code class=\"language-plaintext highlighter-rouge\">context_filter: { topic: \"auth refactor\" }</code>.</p>\n\n<figure class=\"diagram\">\n<svg viewBox=\"0 0 360 476\" role=\"img\" aria-labelledby=\"attractor-cf-title attractor-cf-desc\" xmlns=\"http://www.w3.org/2000/svg\">\n  <title id=\"attractor-cf-title\">Context filtering: same query, different results</title>\n  <desc id=\"attractor-cf-desc\">The query \"why did we change the auth approach\" is embedded as a 384-dimensional vector. With vector search only, both memories surface: \"switched from JWT to session tokens\" and \"auth tokens should never be stored in localStorage\". Running the same query with the context filter topic equals \"auth refactor\" surfaces only the session-tokens memory; the localStorage memory, formed during a security review, is filtered out.</desc>\n  <defs>\n    <marker id=\"attractor-cf-arrow\" viewBox=\"0 0 10 10\" refX=\"9\" refY=\"5\" markerWidth=\"7\" markerHeight=\"7\" orient=\"auto-start-reverse\">\n      <path class=\"dg-head\" d=\"M0 0 L10 5 L0 10 z\" />\n    </marker>\n  </defs>\n\n  <rect class=\"dg-pill\" x=\"22\" y=\"6\" width=\"316\" height=\"54\" rx=\"27\" />\n  <text class=\"dg-body\" x=\"180\" y=\"29\" text-anchor=\"middle\">“why did we change the auth approach”</text>\n  <text class=\"dg-body\" x=\"180\" y=\"48\" text-anchor=\"middle\">query + 384-dim embedding</text>\n\n  <line class=\"dg-line\" x1=\"180\" y1=\"60\" x2=\"180\" y2=\"90\" marker-end=\"url(#attractor-cf-arrow)\" />\n\n  <rect class=\"dg-group\" x=\"6\" y=\"94\" width=\"348\" height=\"146\" rx=\"12\" />\n  <text class=\"dg-label\" x=\"22\" y=\"116\">Vector search only</text>\n  <rect class=\"dg-box\" x=\"22\" y=\"128\" width=\"316\" height=\"36\" rx=\"6\" />\n  <text class=\"dg-body\" x=\"36\" y=\"151\">“switched from JWT to session tokens…”</text>\n  <rect class=\"dg-box\" x=\"22\" y=\"172\" width=\"316\" height=\"36\" rx=\"6\" />\n  <text class=\"dg-body\" x=\"36\" y=\"195\">“auth tokens should never be stored in…”</text>\n  <text class=\"dg-note\" x=\"180\" y=\"228\" text-anchor=\"middle\">both surface: semantic match, no context</text>\n\n  <line class=\"dg-line dg-dash\" x1=\"60\" y1=\"240\" x2=\"60\" y2=\"298\" marker-end=\"url(#attractor-cf-arrow)\" />\n  <text class=\"dg-note\" x=\"74\" y=\"264\">same query, plus</text>\n  <text class=\"dg-note\" x=\"74\" y=\"282\">context_filter: topic = “auth refactor”</text>\n\n  <rect class=\"dg-group\" x=\"6\" y=\"302\" width=\"348\" height=\"168\" rx=\"12\" />\n  <text class=\"dg-label\" x=\"22\" y=\"324\">With the context filter</text>\n  <rect class=\"dg-pill\" x=\"22\" y=\"336\" width=\"316\" height=\"36\" rx=\"6\" />\n  <text class=\"dg-body\" x=\"36\" y=\"359\">“switched from JWT to session tokens…”</text>\n  <rect class=\"dg-box dg-muted\" x=\"22\" y=\"380\" width=\"316\" height=\"36\" rx=\"6\" />\n  <text class=\"dg-note\" x=\"36\" y=\"403\">“auth tokens should never be stored in…”</text>\n  <text class=\"dg-note\" x=\"180\" y=\"438\" text-anchor=\"middle\">filtered out: formed in a security review</text>\n  <text class=\"dg-note\" x=\"180\" y=\"458\" text-anchor=\"middle\">semantic match + context = precision</text>\n</svg>\n<figcaption>Same query, two searches. Vector search alone returns both auth memories; adding the context filter keeps only the one formed during the auth refactor.</figcaption>\n</figure>\n\n<p>The second memory surfaces — not because the words match, but because:</p>\n<ol>\n  <li>The embedding is semantically close to the query</li>\n  <li>The context filter narrows to the right topic</li>\n</ol>\n\n<p>Without the context filter, both memories are relevant. With it, you get the one that was formed in the right situation. That’s the difference.</p>\n\n<h2 id=\"the-roadmap\">The Roadmap</h2>\n\n<p>Phase 1 is the foundation. The more interesting work is ahead:</p>\n\n<p><strong>Phase 2 — Association graph</strong>: Memories aren’t just points in a vector space. They’re nodes in a graph with typed edges — <code class=\"language-plaintext highlighter-rouge\">supports</code>, <code class=\"language-plaintext highlighter-rouge\">contradicts</code>, <code class=\"language-plaintext highlighter-rouge\">caused_by</code>, <code class=\"language-plaintext highlighter-rouge\">followed_by</code>. Retrieval triggers spreading activation: surface the memory you asked for, and the ones connected to it.</p>\n\n<p><strong>Phase 3 — Decay and consolidation</strong>: Not all memories are equally important. The <code class=\"language-plaintext highlighter-rouge\">access_count</code> field is already tracking how often each memory is retrieved. Phase 3 uses this to implement decay — memories that are never accessed fade, memories that are frequently retrieved strengthen. This mirrors how biological memory consolidation works <a href=\"#references\">[6]</a>.</p>\n\n<p><strong>Phase 4 — Hopfield retrieval</strong>: Replace vector similarity with a proper Hopfield weight matrix. Retrieval becomes pattern completion — give the network a partial cue and it converges to the full memory. This is the step that moves from “semantic search with context” to something genuinely brain-inspired.</p>\n\n<h2 id=\"isnt-this-already-being-done\">Isn’t This Already Being Done?</h2>\n\n<p>Honestly — yes, in parts. Vector search with metadata filtering exists in Pinecone, Weaviate, and Qdrant. Memory layers for agents exist in LangChain and LlamaIndex. And the “Hopfield Networks is All You Need” paper already showed that transformer attention <em>is mathematically a Hopfield update</em> — so in a sense, every LLM is already doing content-addressable retrieval implicitly.</p>\n\n<p>Phase 1 of Attractor is not novel. It’s a clean, focused implementation of something that exists in various forms.</p>\n\n<p>The bet is on Phases 3 and 4. Most agent memory systems don’t implement decay — memories don’t weaken over time unless reinforced, they don’t consolidate the way biological memory does, and retrieval is always ranking rather than convergence. That’s the gap Attractor is trying to close.</p>\n\n<p>Whether that gap matters in practice is an open question — and the honest answer is that it depends entirely on whether the difference shows up in real use. That’s why I’m building <strong>memlog</strong> on top of Attractor as a daily-use testbed: log thoughts, decisions, and notes every day, and see whether context-aware retrieval with decay actually surfaces better results than a plain vector search would. If it does, the model is right. If it doesn’t, the model needs revision.</p>\n\n<p>Theory is cheap. The feedback loop is the thing.</p>\n\n<h2 id=\"why-this-matters-for-ai-agents\">Why This Matters for AI Agents</h2>\n\n<p>Most AI agents today are stateless between sessions or rely on crude context windows. The ones that do have memory bolt it on as an afterthought — store the last N messages, embed them, retrieve by cosine similarity.</p>\n\n<p>Attractor is an attempt to do it properly. Not because the engineering is especially hard, but because the <em>model</em> of memory matters. If you treat memory as a lookup table, you get lookup-table behaviour. If you treat it as an energy landscape with context and decay, you get something closer to how intelligence actually works.</p>\n\n<p>Phase 1 is live. <code class=\"language-plaintext highlighter-rouge\">pip install attractor-engine</code> — start storing memories with context, retrieve by meaning.</p>\n\n<p>The interesting phases are coming.</p>\n\n<hr />\n\n<h2 id=\"references\">References</h2>\n\n<p><a name=\"references\"></a></p>\n\n<ol>\n  <li>\n    <p>Hopfield, J.J. (1982). <a href=\"https://www.pnas.org/doi/10.1073/pnas.79.8.2554\">Neural networks and physical systems with emergent collective computational abilities</a>. <em>Proceedings of the National Academy of Sciences</em>, 79(8), 2554–2558. Plain-language summary: <a href=\"https://www.avinashgosavi.com/post/paper-tldr-hopfield-1982/\">Paper TL;DR</a>.</p>\n  </li>\n  <li>\n    <p>Ramsauer, H., Schäfl, B., Lehner, J., et al. (2020). <a href=\"https://arxiv.org/abs/2008.02217\">Hopfield Networks is All You Need</a>. <em>arXiv:2008.02217</em>. <em>(Shows modern Hopfield networks have exponential capacity and that transformer attention is a Hopfield update.)</em> Plain-language summary: <a href=\"https://www.avinashgosavi.com/post/paper-tldr-hopfield-networks-is-all-you-need/\">Paper TL;DR</a>.</p>\n  </li>\n  <li>\n    <p>pgvector. (2021). <a href=\"https://github.com/pgvector/pgvector\">Open-source vector similarity search for Postgres</a>. GitHub.</p>\n  </li>\n  <li>\n    <p>Reimers, N. &amp; Gurevych, I. (2019). <a href=\"https://arxiv.org/abs/1908.10084\">Sentence-BERT: Sentence Embeddings using Siamese BERT-Networks</a>. <em>arXiv:1908.10084</em>. Plain-language summary: <a href=\"https://www.avinashgosavi.com/post/paper-tldr-sentence-bert/\">Paper TL;DR</a>.</p>\n  </li>\n  <li>\n    <p>Yates, F.A. (1966). <em>The Art of Memory</em>. University of Chicago Press. <em>(The definitive account of the method of loci — the memory palace technique.)</em></p>\n  </li>\n  <li>\n    <p>McClelland, J.L., McNaughton, B.L., &amp; O’Reilly, R.C. (1995). <a href=\"https://doi.org/10.1037/0033-295X.102.3.419\">Why there are complementary learning systems in the hippocampus and neocortex</a>. <em>Psychological Review</em>, 102(3), 419–457. <em>(The foundational paper on memory consolidation and why some memories strengthen while others decay.)</em> Plain-language summary: <a href=\"https://www.avinashgosavi.com/post/paper-tldr-complementary-learning-systems/\">Paper TL;DR</a>.</p>\n  </li>\n</ol>\n\n<hr />\n\n<p><em>Source: <a href=\"https://github.com/aag1091-alt/attractor\">github.com/aag1091-alt/attractor</a></em></p>\n\n<p><em>Context frames, consolidation, decay and associative memory are each defined in a sentence or two in the <a href=\"https://www.avinashgosavi.com/post/agent-memory-glossary/\">agent memory glossary</a>.</em></p>\n"
    },
    {
      "id": "https://www.avinashgosavi.com/post/vaultmem-part-3-pii-sanitizer/",
      "url": "https://www.avinashgosavi.com/post/vaultmem-part-3-pii-sanitizer/",
      "title": "VaultMem Part 3 — Hiding PII from Your LLM Provider",
      "summary": "VaultMem v0.3: a PII sanitizer that keeps personal details away from the LLM provider — the gap that encryption at rest doesn't close.",
      "image": "https://www.avinashgosavi.com/assets/og/vaultmem-part-3-pii-sanitizer.png",
      "date_published": "2026-04-01T00:00:00+00:00",
      "date_modified": "2026-10-22T17:36:32+00:00",
      "tags": ["Agent memory","Privacy"],
      "content_html": "<p><a href=\"https://www.avinashgosavi.com/post/vaultmem-encrypted-memory-for-ai-agents/\">Part 1</a> covered the core encryption model. <a href=\"https://www.avinashgosavi.com/post/vaultmem-part-2-multimodal-temporal-backends/\">Part 2</a> covered multi-modal ingestion and pluggable backends. This post covers v0.3.x — a new layer of privacy that addresses a problem encryption at rest cannot solve.</p>\n\n<blockquote>\n  <p><strong>Install:</strong> <code class=\"language-plaintext highlighter-rouge\">pip install 'vaultmem[spacy]'</code> · <strong>Code:</strong> <a href=\"https://github.com/aag1091-alt/vaultmem-sdk\">github.com/aag1091-alt/vaultmem-sdk</a> · <strong>Live demo:</strong> <a href=\"https://vaultmem-demo.streamlit.app\">vaultmem-demo.streamlit.app</a></p>\n</blockquote>\n\n<hr />\n\n<h2 id=\"the-gap-encryption-at-rest-doesnt-close\">The gap encryption at rest doesn’t close</h2>\n\n<p>VaultMem stores every memory atom encrypted with AES-256-GCM. Your passphrase never leaves your device. The platform operator cryptographically cannot read your vault. The threat model comes from the Memory-as-Asset framework (Pan, Huang &amp; Yang, <a href=\"https://arxiv.org/abs/2603.14212\">arXiv:2603.14212</a>; <a href=\"https://www.avinashgosavi.com/post/paper-tldr-memory-as-asset/\">plain-language TL;DR</a>): memory is a personal asset, not a platform resource.</p>\n\n<p>But there is a second exposure point I hadn’t fully addressed: the moment you decrypt a memory and inject it into an LLM prompt.</p>\n\n<p>Say your vault contains:</p>\n\n<div class=\"language-plaintext highlighter-rouge\"><div class=\"highlight\"><pre class=\"highlight\"><code>I met Sarah Chen at the NeurIPS conference. She works on ML infra at Google.\nMy accountant James Park can be reached at james@parkaccounting.com.\n</code></pre></div></div>\n\n<p>When you ask your assistant “what do I know about Sarah?”, VaultMem retrieves those memories, decrypts them, and sends them as context to the LLM provider — Groq, OpenAI, Anthropic, whoever you’re using. At that point, Sarah’s name, her employer, and James’s email address are all sitting in plaintext in an HTTP request to a third-party cloud.</p>\n\n<p>Encryption at rest doesn’t help here. The data is decrypted before it goes out the door.</p>\n\n<hr />\n\n<h2 id=\"the-solution-strip-pii-before-injection-restore-it-after\">The solution: strip PII before injection, restore it after</h2>\n\n<p>VaultMem v0.3.0 ships a <code class=\"language-plaintext highlighter-rouge\">Sanitizer</code> class that sits between your vault and your LLM call.</p>\n\n<div class=\"language-python highlighter-rouge\"><div class=\"highlight\"><pre class=\"highlight\"><code><span class=\"kn\">from</span> <span class=\"n\">vaultmem</span> <span class=\"kn\">import</span> <span class=\"n\">VaultSession</span><span class=\"p\">,</span> <span class=\"n\">Sanitizer</span>\n\n<span class=\"n\">san</span> <span class=\"o\">=</span> <span class=\"nc\">Sanitizer</span><span class=\"p\">(</span><span class=\"n\">backend</span><span class=\"o\">=</span><span class=\"sh\">\"</span><span class=\"s\">spacy</span><span class=\"sh\">\"</span><span class=\"p\">)</span>   <span class=\"c1\"># one instance per session\n</span>\n<span class=\"k\">with</span> <span class=\"n\">VaultSession</span><span class=\"p\">.</span><span class=\"nf\">open</span><span class=\"p\">(</span><span class=\"sh\">\"</span><span class=\"s\">./my_vault</span><span class=\"sh\">\"</span><span class=\"p\">,</span> <span class=\"n\">passphrase</span><span class=\"o\">=</span><span class=\"sh\">\"</span><span class=\"s\">...</span><span class=\"sh\">\"</span><span class=\"p\">)</span> <span class=\"k\">as</span> <span class=\"n\">s</span><span class=\"p\">:</span>\n    <span class=\"n\">memories</span> <span class=\"o\">=</span> <span class=\"n\">s</span><span class=\"p\">.</span><span class=\"nf\">search</span><span class=\"p\">(</span><span class=\"sh\">\"</span><span class=\"s\">sarah chen</span><span class=\"sh\">\"</span><span class=\"p\">,</span> <span class=\"n\">top_k</span><span class=\"o\">=</span><span class=\"mi\">5</span><span class=\"p\">)</span>\n    <span class=\"n\">context</span> <span class=\"o\">=</span> <span class=\"sh\">\"</span><span class=\"se\">\\n</span><span class=\"sh\">\"</span><span class=\"p\">.</span><span class=\"nf\">join</span><span class=\"p\">(</span><span class=\"sa\">f</span><span class=\"sh\">\"</span><span class=\"s\">- </span><span class=\"si\">{</span><span class=\"n\">m</span><span class=\"p\">.</span><span class=\"n\">atom</span><span class=\"p\">.</span><span class=\"n\">content</span><span class=\"si\">}</span><span class=\"sh\">\"</span> <span class=\"k\">for</span> <span class=\"n\">m</span> <span class=\"ow\">in</span> <span class=\"n\">memories</span><span class=\"p\">)</span>\n\n    <span class=\"c1\"># Sanitize context AND query with the same instance\n</span>    <span class=\"n\">sanitized_context</span><span class=\"p\">,</span> <span class=\"n\">rmap</span> <span class=\"o\">=</span> <span class=\"n\">san</span><span class=\"p\">.</span><span class=\"nf\">sanitize</span><span class=\"p\">(</span><span class=\"n\">context</span><span class=\"p\">)</span>\n    <span class=\"n\">sanitized_query</span><span class=\"p\">,</span> <span class=\"n\">rmap</span>   <span class=\"o\">=</span> <span class=\"n\">san</span><span class=\"p\">.</span><span class=\"nf\">sanitize</span><span class=\"p\">(</span><span class=\"n\">user_query</span><span class=\"p\">)</span>\n\n    <span class=\"n\">raw_reply</span> <span class=\"o\">=</span> <span class=\"n\">llm</span><span class=\"p\">.</span><span class=\"nf\">chat</span><span class=\"p\">(</span><span class=\"n\">sanitized_query</span><span class=\"p\">,</span> <span class=\"n\">context</span><span class=\"o\">=</span><span class=\"n\">sanitized_context</span><span class=\"p\">)</span>\n\n    <span class=\"c1\"># Restore real values in the LLM's response\n</span>    <span class=\"n\">final_reply</span> <span class=\"o\">=</span> <span class=\"n\">san</span><span class=\"p\">.</span><span class=\"nf\">restore</span><span class=\"p\">(</span><span class=\"n\">raw_reply</span><span class=\"p\">,</span> <span class=\"n\">rmap</span><span class=\"p\">)</span>\n</code></pre></div></div>\n\n<p>The LLM provider receives:</p>\n\n<div class=\"language-plaintext highlighter-rouge\"><div class=\"highlight\"><pre class=\"highlight\"><code>I met Jordan at the NeurIPS conference. She works on ML infra at Globex.\nMy accountant Casey can be reached at [EMAIL_1].\n</code></pre></div></div>\n\n<p>It reasons about this correctly, responds naturally, and VaultMem restores the real values client-side before the answer is shown to the user.</p>\n\n<p>The provider never sees the real names, companies, or contact details.</p>\n\n<figure class=\"diagram\">\n<svg viewBox=\"0 0 336 462\" role=\"img\" aria-labelledby=\"vm-san-title vm-san-desc\" xmlns=\"http://www.w3.org/2000/svg\">\n  <title id=\"vm-san-title\">How the VaultMem sanitizer round-trips a prompt</title>\n  <desc id=\"vm-san-desc\">On your device, memories are decrypted from the vault, for example Sarah Chen, who works at Google, and James's email address. Sanitizer.sanitize swaps them for a pseudonym, a fictional company and a typed token: Jordan, Globex and EMAIL_1, using one mapping for the whole session. Only that sanitized text goes to the LLM provider. The provider's reply mentions Jordan. On your device again, Sanitizer.restore maps Jordan back to Sarah Chen, so the answer you read uses the real names.</desc>\n  <defs>\n    <marker id=\"vm-san-arrow\" viewBox=\"0 0 10 10\" refX=\"9\" refY=\"5\" markerWidth=\"7\" markerHeight=\"7\" orient=\"auto-start-reverse\">\n      <path class=\"dg-head\" d=\"M0 0 L10 5 L0 10 z\" />\n    </marker>\n  </defs>\n  <rect class=\"dg-group\" x=\"8\" y=\"8\" width=\"320\" height=\"170\" rx=\"12\" />\n  <text class=\"dg-label\" x=\"24\" y=\"28\">On your device</text>\n  <rect class=\"dg-box\" x=\"24\" y=\"38\" width=\"288\" height=\"52\" rx=\"8\" />\n  <text class=\"dg-strong\" x=\"168\" y=\"59\" text-anchor=\"middle\">Vault memories, decrypted</text>\n  <text class=\"dg-note dg-mono\" x=\"168\" y=\"79\" text-anchor=\"middle\">Sarah Chen · Google · james@…</text>\n  <line class=\"dg-line\" x1=\"168\" y1=\"90\" x2=\"168\" y2=\"110\" marker-end=\"url(#vm-san-arrow)\" />\n  <rect class=\"dg-box\" x=\"24\" y=\"112\" width=\"288\" height=\"54\" rx=\"8\" />\n  <text class=\"dg-strong\" x=\"168\" y=\"134\" text-anchor=\"middle\">Sanitizer.sanitize()</text>\n  <text class=\"dg-note\" x=\"168\" y=\"155\" text-anchor=\"middle\">one stable mapping for the session</text>\n  <line class=\"dg-line\" x1=\"168\" y1=\"166\" x2=\"168\" y2=\"220\" marker-end=\"url(#vm-san-arrow)\" />\n  <text class=\"dg-note\" x=\"178\" y=\"196\">sends Jordan · Globex</text>\n  <text class=\"dg-note\" x=\"178\" y=\"212\">and [EMAIL_1]</text>\n  <rect class=\"dg-box dg-muted\" x=\"24\" y=\"222\" width=\"288\" height=\"54\" rx=\"8\" />\n  <text class=\"dg-strong\" x=\"168\" y=\"244\" text-anchor=\"middle\">LLM provider</text>\n  <text class=\"dg-note\" x=\"168\" y=\"265\" text-anchor=\"middle\">sees pseudonyms and tokens only</text>\n  <line class=\"dg-line\" x1=\"168\" y1=\"276\" x2=\"168\" y2=\"328\" marker-end=\"url(#vm-san-arrow)\" />\n  <text class=\"dg-note\" x=\"178\" y=\"293\">reply mentions Jordan</text>\n  <rect class=\"dg-group\" x=\"8\" y=\"302\" width=\"320\" height=\"152\" rx=\"12\" />\n  <text class=\"dg-label\" x=\"24\" y=\"322\">On your device</text>\n  <rect class=\"dg-box\" x=\"24\" y=\"330\" width=\"288\" height=\"54\" rx=\"8\" />\n  <text class=\"dg-strong\" x=\"168\" y=\"352\" text-anchor=\"middle\">Sanitizer.restore()</text>\n  <text class=\"dg-note\" x=\"168\" y=\"373\" text-anchor=\"middle\">Jordan → Sarah Chen</text>\n  <line class=\"dg-line\" x1=\"168\" y1=\"384\" x2=\"168\" y2=\"402\" marker-end=\"url(#vm-san-arrow)\" />\n  <rect class=\"dg-pill\" x=\"24\" y=\"404\" width=\"288\" height=\"40\" rx=\"20\" />\n  <text class=\"dg-body\" x=\"168\" y=\"429\" text-anchor=\"middle\">The answer you read uses real names</text>\n</svg>\n<figcaption>Real names and contact details stay on your device; the provider only ever reasons about Jordan, Globex and [EMAIL_1].</figcaption>\n</figure>\n\n<hr />\n\n<h2 id=\"pseudonyms-not-tokens\">Pseudonyms, not tokens</h2>\n\n<p>Person names are replaced with natural-sounding pseudonyms — Jordan, Casey, Morgan, Riley, Avery — rather than opaque tokens like <code class=\"language-plaintext highlighter-rouge\">[PERSON_1]</code>.</p>\n\n<p>The reason is fluency. If the LLM sees <code class=\"language-plaintext highlighter-rouge\">[PERSON_1]</code> in context and addresses the user with it, the response reads awkwardly. With a pseudonym, it can write “Based on what Jordan told you at NeurIPS…” and after restoration the user reads “Based on what Sarah Chen told you at NeurIPS…” — completely natural.</p>\n\n<p>Organisations get fictional company names (Acme, Globex, Initech, Pied Piper). Structured PII — emails, phone numbers, SSNs, credit cards, IPs — gets typed tokens (<code class=\"language-plaintext highlighter-rouge\">[EMAIL_1]</code>, <code class=\"language-plaintext highlighter-rouge\">[PHONE_2]</code>) because there’s no natural-sounding substitute for those.</p>\n\n<p>The mapping is <strong>session-scoped and stable</strong>: if “Sarah Chen” appears in five different memories, she is always Jordan in that session. The LLM can reason about her as a single consistent person across all memories.</p>\n\n<hr />\n\n<h2 id=\"two-backends\">Two backends</h2>\n\n<p>Presidio supports different NLP backends. VaultMem exposes both:</p>\n\n<div class=\"language-python highlighter-rouge\"><div class=\"highlight\"><pre class=\"highlight\"><code><span class=\"c1\"># Lightweight — ~12 MB spaCy model, no PyTorch dependency\n# Works on Streamlit Cloud, CI, anywhere torch is unavailable\n</span><span class=\"n\">san</span> <span class=\"o\">=</span> <span class=\"nc\">Sanitizer</span><span class=\"p\">(</span><span class=\"n\">backend</span><span class=\"o\">=</span><span class=\"sh\">\"</span><span class=\"s\">spacy</span><span class=\"sh\">\"</span><span class=\"p\">)</span>\n\n<span class=\"c1\"># High-accuracy — ~400 MB transformers model, requires PyTorch\n# Best for local deployments or privacy-critical production use\n</span><span class=\"n\">san</span> <span class=\"o\">=</span> <span class=\"nc\">Sanitizer</span><span class=\"p\">(</span><span class=\"n\">backend</span><span class=\"o\">=</span><span class=\"sh\">\"</span><span class=\"s\">transformers</span><span class=\"sh\">\"</span><span class=\"p\">)</span>\n</code></pre></div></div>\n\n<p>The spaCy backend (<code class=\"language-plaintext highlighter-rouge\">en_core_web_sm</code>) is the default. It handles the common cases — person names, organisations, locations, emails, phone numbers — and is small enough to deploy anywhere without a GPU or a multi-gigabyte dependency install.</p>\n\n<div class=\"language-bash highlighter-rouge\"><div class=\"highlight\"><pre class=\"highlight\"><code><span class=\"c\"># spaCy (default)</span>\npip <span class=\"nb\">install</span> <span class=\"s1\">'vaultmem[spacy]'</span>\npython <span class=\"nt\">-m</span> spacy download en_core_web_sm\n\n<span class=\"c\"># transformers (high accuracy)</span>\npip <span class=\"nb\">install</span> <span class=\"s1\">'vaultmem[presidio]'</span>\n</code></pre></div></div>\n\n<hr />\n\n<h2 id=\"what-gets-skipped\">What gets skipped</h2>\n\n<p>Not everything Presidio detects is PII. Temporal expressions (<code class=\"language-plaintext highlighter-rouge\">DATE_TIME</code>), cardinal numbers, percentages, monetary values, and quantities are identifying in context but not on their own — and redacting them breaks the coherence of the context significantly.</p>\n\n<p>VaultMem v0.3.2 introduced a skip list:</p>\n\n<div class=\"language-python highlighter-rouge\"><div class=\"highlight\"><pre class=\"highlight\"><code><span class=\"n\">_SKIP_ENTITY_TYPES</span> <span class=\"o\">=</span> <span class=\"nf\">frozenset</span><span class=\"p\">({</span>\n    <span class=\"sh\">\"</span><span class=\"s\">DATE_TIME</span><span class=\"sh\">\"</span><span class=\"p\">,</span> <span class=\"sh\">\"</span><span class=\"s\">CARDINAL</span><span class=\"sh\">\"</span><span class=\"p\">,</span> <span class=\"sh\">\"</span><span class=\"s\">ORDINAL</span><span class=\"sh\">\"</span><span class=\"p\">,</span>\n    <span class=\"sh\">\"</span><span class=\"s\">PERCENT</span><span class=\"sh\">\"</span><span class=\"p\">,</span> <span class=\"sh\">\"</span><span class=\"s\">MONEY</span><span class=\"sh\">\"</span><span class=\"p\">,</span> <span class=\"sh\">\"</span><span class=\"s\">QUANTITY</span><span class=\"sh\">\"</span><span class=\"p\">,</span> <span class=\"sh\">\"</span><span class=\"s\">LANGUAGE</span><span class=\"sh\">\"</span><span class=\"p\">,</span>\n<span class=\"p\">})</span>\n</code></pre></div></div>\n\n<p>Before this, a memory like “Every Friday at 4pm I do a weekly review — takes 20 minutes” was returned to the LLM as “Every [DATE_TIME_3] at [DATE_TIME_4] I do a weekly review — takes [DATE_TIME_5]”. Correct in theory, unreadable in practice. The skip list fixes this.</p>\n\n<p>The same release fixed two spaCy-specific label mismatches: spaCy reports organisations as <code class=\"language-plaintext highlighter-rouge\">ORGANIZATION</code> (not <code class=\"language-plaintext highlighter-rouge\">ORG</code>) and geopolitical entities as <code class=\"language-plaintext highlighter-rouge\">GPE</code> (not <code class=\"language-plaintext highlighter-rouge\">LOCATION</code>). Before the fix these fell through to typed tokens instead of receiving pseudonyms from the pools.</p>\n\n<hr />\n\n<h2 id=\"a-subtle-bug-case-insensitive-deduplication\">A subtle bug: case-insensitive deduplication</h2>\n\n<p>During testing I hit a bug that’s easy to miss until you actually run the system with real queries.</p>\n\n<p>The memory stored <code class=\"language-plaintext highlighter-rouge\">\"Sarah Chen\"</code> (title case). The user asked <code class=\"language-plaintext highlighter-rouge\">\"who is sarah chen?\"</code> (lowercase). Presidio detected <code class=\"language-plaintext highlighter-rouge\">\"sarah chen\"</code> as a PERSON — but the forward map looked it up with an exact string match. <code class=\"language-plaintext highlighter-rouge\">\"sarah chen\" != \"Sarah Chen\"</code>, so it was treated as a new entity and assigned a fresh pseudonym: Casey.</p>\n\n<p>The LLM then received context about Jordan but a query about Casey. Its response: <em>“There is no information about Casey.”</em> Technically accurate. Completely useless.</p>\n\n<p>The fix in v0.3.3: all forward-map keys are normalised to lowercase. A separate canonical map preserves the first-seen capitalisation so restoration returns <code class=\"language-plaintext highlighter-rouge\">\"Sarah Chen\"</code>, not <code class=\"language-plaintext highlighter-rouge\">\"sarah chen\"</code>. The variant seen first wins on casing; all subsequent variants map to the same pseudonym.</p>\n\n<div class=\"language-python highlighter-rouge\"><div class=\"highlight\"><pre class=\"highlight\"><code><span class=\"k\">def</span> <span class=\"nf\">_assign</span><span class=\"p\">(</span><span class=\"n\">self</span><span class=\"p\">,</span> <span class=\"n\">real</span><span class=\"p\">:</span> <span class=\"nb\">str</span><span class=\"p\">,</span> <span class=\"n\">entity_type</span><span class=\"p\">:</span> <span class=\"nb\">str</span><span class=\"p\">)</span> <span class=\"o\">-&gt;</span> <span class=\"nb\">str</span><span class=\"p\">:</span>\n    <span class=\"n\">key</span> <span class=\"o\">=</span> <span class=\"n\">real</span><span class=\"p\">.</span><span class=\"nf\">lower</span><span class=\"p\">()</span>\n    <span class=\"k\">if</span> <span class=\"n\">key</span> <span class=\"ow\">in</span> <span class=\"n\">self</span><span class=\"p\">.</span><span class=\"n\">_forward</span><span class=\"p\">:</span>\n        <span class=\"k\">return</span> <span class=\"n\">self</span><span class=\"p\">.</span><span class=\"n\">_forward</span><span class=\"p\">[</span><span class=\"n\">key</span><span class=\"p\">]</span>          <span class=\"c1\"># \"sarah chen\" hits \"Sarah Chen\"'s entry\n</span>    <span class=\"n\">self</span><span class=\"p\">.</span><span class=\"n\">_canonical</span><span class=\"p\">[</span><span class=\"n\">key</span><span class=\"p\">]</span> <span class=\"o\">=</span> <span class=\"n\">real</span>            <span class=\"c1\"># first-seen casing for restoration\n</span>    <span class=\"bp\">...</span>\n</code></pre></div></div>\n\n<hr />\n\n<h2 id=\"what-this-doesnt-solve\">What this doesn’t solve</h2>\n\n<p>The Sanitizer addresses what leaves your device. It doesn’t address what the LLM infers or reconstructs. If your memory says “I grew up two blocks from the Eiffel Tower”, stripping names doesn’t prevent the LLM from inferring something about where you’re from.</p>\n\n<p>The complete solution is TEE-based inference — running the model inside a trusted execution environment so the provider’s infrastructure never sees plaintext at all. That’s the direction the research is heading (see the arXiv paper above), but it’s not production-ready for general-purpose models yet.</p>\n\n<p>PII stripping is the practical near-term answer. It’s not perfect, but it meaningfully shrinks the surface area of personal data your LLM provider sees on every query.</p>\n\n<hr />\n\n<h2 id=\"changelog\">Changelog</h2>\n\n<ul>\n  <li><strong>v0.3.0</strong> — <code class=\"language-plaintext highlighter-rouge\">Sanitizer</code> class, transformers backend, pseudonym pools, restoration map</li>\n  <li><strong>v0.3.1</strong> — spaCy backend (<code class=\"language-plaintext highlighter-rouge\">en_core_web_sm</code>), new <code class=\"language-plaintext highlighter-rouge\">[spacy]</code> optional extra, <code class=\"language-plaintext highlighter-rouge\">backend=</code> parameter</li>\n  <li><strong>v0.3.2</strong> — <code class=\"language-plaintext highlighter-rouge\">DATE_TIME</code> skip list, <code class=\"language-plaintext highlighter-rouge\">ORGANIZATION</code>/<code class=\"language-plaintext highlighter-rouge\">GPE</code> label fixes for spaCy</li>\n  <li><strong>v0.3.3</strong> — case-insensitive entity deduplication</li>\n</ul>\n\n<p><code class=\"language-plaintext highlighter-rouge\">pip install 'vaultmem[spacy]'</code> gets you the latest.</p>\n\n<p>If you’re building an AI agent that touches personal data, I’d love to hear what you’re working on.</p>\n\n<p><em>For short definitions of PII sanitization, zero-knowledge storage and the other terms in this series, see the <a href=\"https://www.avinashgosavi.com/post/agent-memory-glossary/\">agent memory glossary</a>.</em></p>\n"
    },
    {
      "id": "https://www.avinashgosavi.com/post/vaultmem-your-ai-agent-shouldnt-read-your-diary/",
      "url": "https://www.avinashgosavi.com/post/vaultmem-your-ai-agent-shouldnt-read-your-diary/",
      "title": "Your AI Agent Shouldn't Be Able to Read Your Diary",
      "summary": "A plain-language case for AI memory the platform can't read: if your assistant remembers your life, who else gets to see it?",
      "image": "https://www.avinashgosavi.com/assets/og/vaultmem-your-ai-agent-shouldnt-read-your-diary.png",
      "date_published": "2026-03-28T00:00:00+00:00",
      "date_modified": "2026-10-21T21:35:32+00:00",
      "tags": ["Agent memory","Privacy"],
      "content_html": "<p>Imagine you have a personal AI assistant. Over months, you tell it everything — your health worries, your relationship friction, your career anxieties, the habits you’re trying to break. It remembers. It helps.</p>\n\n<p>Now ask yourself: who else can read that?</p>\n\n<p>The honest answer, with every AI memory product that exists today, is: the company running it.</p>\n\n<hr />\n\n<h2 id=\"the-problem-isnt-the-ai-its-where-the-memory-lives\">The problem isn’t the AI. It’s where the memory lives.</h2>\n\n<p>When you use an AI agent with persistent memory — whether that’s a health companion, a journaling assistant, a personal coach, or a productivity tool — your conversations get stored somewhere. That “somewhere” is a server owned by the platform. And the platform can read it.</p>\n\n<p>This is true even when there’s a privacy policy. A privacy policy is a promise. It can be broken, subpoenaed, acquired, or quietly changed. A cryptographic guarantee cannot.</p>\n\n<p>I spent a few weeks building something that fixes this at the foundation:</p>\n\n<p><strong><a href=\"https://github.com/aag1091-alt/vaultmem-sdk\">VaultMem</a></strong> — a Python library that gives AI agents encrypted, persistent, portable memory where the platform operator mathematically cannot read what users store.</p>\n\n<p>Not “we promise not to look.” Not “we anonymize your data.” The key never leaves your session. The math makes it true regardless of what the policy says.</p>\n\n<hr />\n\n<h2 id=\"a-real-world-example-the-health-companion\">A real-world example: the health companion</h2>\n\n<p>Say you’re building a personal health AI — the kind of agent a person with a chronic condition might use to track symptoms, medications, and how they’re feeling day to day.</p>\n\n<p>Without VaultMem, your architecture probably looks like this:</p>\n\n<ul>\n  <li>User tells agent “my migraines have been worse since I started the new medication”</li>\n  <li>Agent stores that in a database you control</li>\n  <li>You — the developer — can query that database</li>\n  <li>So can anyone who compromises it, or a legal subpoena, or your future acquirer</li>\n</ul>\n\n<p>With VaultMem, it looks like this:</p>\n\n<div class=\"language-python highlighter-rouge\"><div class=\"highlight\"><pre class=\"highlight\"><code><span class=\"kn\">from</span> <span class=\"n\">vaultmem</span> <span class=\"kn\">import</span> <span class=\"n\">VaultSession</span><span class=\"p\">,</span> <span class=\"n\">LocalEmbedder</span>\n\n<span class=\"k\">with</span> <span class=\"n\">VaultSession</span><span class=\"p\">.</span><span class=\"nf\">open</span><span class=\"p\">(</span><span class=\"sh\">\"</span><span class=\"s\">./alice_vault</span><span class=\"sh\">\"</span><span class=\"p\">,</span> <span class=\"n\">passphrase</span><span class=\"o\">=</span><span class=\"sh\">\"</span><span class=\"s\">alice_secret</span><span class=\"sh\">\"</span><span class=\"p\">,</span>\n                        <span class=\"n\">embedder</span><span class=\"o\">=</span><span class=\"nc\">LocalEmbedder</span><span class=\"p\">())</span> <span class=\"k\">as</span> <span class=\"n\">s</span><span class=\"p\">:</span>\n    <span class=\"n\">s</span><span class=\"p\">.</span><span class=\"nf\">add</span><span class=\"p\">(</span><span class=\"sh\">\"</span><span class=\"s\">Migraines worse since starting new medication on March 15th</span><span class=\"sh\">\"</span><span class=\"p\">)</span>\n    <span class=\"n\">s</span><span class=\"p\">.</span><span class=\"nf\">add</span><span class=\"p\">(</span><span class=\"sh\">\"</span><span class=\"s\">Blood pressure 120/80 at checkup — within normal range</span><span class=\"sh\">\"</span><span class=\"p\">)</span>\n    <span class=\"n\">s</span><span class=\"p\">.</span><span class=\"nf\">add</span><span class=\"p\">(</span><span class=\"sh\">\"</span><span class=\"s\">Feeling anxious about the scan results, haven</span><span class=\"sh\">'</span><span class=\"s\">t slept well</span><span class=\"sh\">\"</span><span class=\"p\">)</span>\n\n    <span class=\"n\">results</span> <span class=\"o\">=</span> <span class=\"n\">s</span><span class=\"p\">.</span><span class=\"nf\">search</span><span class=\"p\">(</span><span class=\"sh\">\"</span><span class=\"s\">recent health concerns</span><span class=\"sh\">\"</span><span class=\"p\">,</span> <span class=\"n\">top_k</span><span class=\"o\">=</span><span class=\"mi\">3</span><span class=\"p\">)</span>\n</code></pre></div></div>\n\n<p>You — the developer — receive an encrypted blob you cannot open. Alice holds the key. Her memories are hers.</p>\n\n<p>The same principle applies to a therapist AI, a relationship coach, a financial planner, a personal journal. Anywhere the content is deeply personal and the user has good reason not to trust a third party with it.</p>\n\n<hr />\n\n<h2 id=\"what-it-does\">What it does</h2>\n\n<div style=\"text-align:center; margin: 2rem 0;\">\n  <picture>\n    <source type=\"image/webp\" sizes=\"(max-width: 42rem) 100vw, 42rem\" srcset=\"/assets/images/vaultmem-explainer-720.webp 720w, /assets/images/vaultmem-explainer-1400.webp 1400w\" />\n    <img width=\"1400\" height=\"746\" loading=\"lazy\" decoding=\"async\" src=\"https://www.avinashgosavi.com/assets/images/vaultmem-explainer.jpg\" alt=\"VaultMem: your memories stay yours\" style=\"width:100%; max-width:860px; border-radius:12px; box-shadow: 0 2px 16px rgba(0,0,0,0.08);\" />\n  </picture>\n</div>\n\n<p>The library handles the full memory lifecycle — not just encryption:</p>\n\n<ul>\n  <li><strong>Four memory types</strong> auto-classified from content: episodic (events), semantic (facts), persona (preferences), procedural (how-tos)</li>\n  <li><strong>Smart retrieval</strong> that surfaces recurring habits and patterns above one-off mentions — so “my morning coffee” surfaces a pattern summary, not just the last time you mentioned coffee</li>\n  <li><strong>Time-aware search</strong> — ask “what was I doing last summer?” and it searches by when events actually happened, not when you typed them in</li>\n  <li><strong>Multi-modal</strong> — photos, voice notes, PDFs ingested as encrypted memory atoms with EXIF dates and GPS stripped from metadata</li>\n  <li><strong>Portable</strong> — the <code class=\"language-plaintext highlighter-rouge\">.vmem</code> format is open and self-describing; your vault can move between apps</li>\n  <li><strong>Production backends</strong> — S3 for storage, Postgres for indexing, HNSW for fast search at scale</li>\n</ul>\n\n<p>Three core dependencies. No mandatory cloud service. No phone-home.</p>\n\n<hr />\n\n<h2 id=\"its-early--and-id-love-feedback\">It’s early — and I’d love feedback</h2>\n\n<p>This is a work in progress. The library is usable and the core is solid, but I’m still building toward the vision.</p>\n\n<p><strong>Try the interactive demo</strong> (no install needed): <a href=\"https://vaultmem-demo.streamlit.app\">vaultmem-demo.streamlit.app</a></p>\n\n<p>It runs a pre-seeded vault with 36 example memories and shows you both sides — what you see (meaningful memories retrieved by your question) and what the platform sees (encrypted bytes that reveal nothing).</p>\n\n<p><strong>Install the SDK:</strong></p>\n<div class=\"language-bash highlighter-rouge\"><div class=\"highlight\"><pre class=\"highlight\"><code>pip <span class=\"nb\">install </span>vaultmem\n</code></pre></div></div>\n\n<p><strong>Read the technical writeups:</strong></p>\n<ul>\n  <li><a href=\"https://www.avinashgosavi.com/post/vaultmem-encrypted-memory-for-ai-agents/\">Part 1 — the encryption model, memory types, and retrieval</a></li>\n  <li><a href=\"https://www.avinashgosavi.com/post/vaultmem-part-2-multimodal-temporal-backends/\">Part 2 — multi-modal, temporal search, and production backends</a></li>\n  <li><a href=\"https://www.avinashgosavi.com/post/vaultmem-part-3-pii-sanitizer/\">Part 3 — hiding PII from your LLM provider</a></li>\n</ul>\n\n<p><strong>The research:</strong> VaultMem implements Layer 1 of the <a href=\"https://arxiv.org/abs/2603.14212\">Memory-as-Asset framework</a> — a formal model for user-owned AI memory (<a href=\"https://www.avinashgosavi.com/post/paper-tldr-memory-as-asset/\">plain-language TL;DR</a>). The preprint is on <a href=\"https://doi.org/10.5281/zenodo.19154079\">Zenodo</a>.</p>\n\n<hr />\n\n<p>If you’re building an AI agent and want to give your users memory that’s genuinely theirs — or if you have thoughts on where this should go — I’d love to hear from you.</p>\n"
    }
  ]
}
