<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Jonathan Wilcox</title>
    <description>The latest articles on DEV Community by Jonathan Wilcox (@wang1051992187).</description>
    <link>https://dev.to/wang1051992187</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4061620%2F98287c3a-e61d-4854-8ca8-e770630fb88a.jpg</url>
      <title>DEV Community: Jonathan Wilcox</title>
      <link>https://dev.to/wang1051992187</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/wang1051992187"/>
    <language>en</language>
    <item>
      <title>CLAUDE.md vs. a memory MCP: what actually goes where</title>
      <dc:creator>Jonathan Wilcox</dc:creator>
      <pubDate>Tue, 04 Aug 2026 06:00:58 +0000</pubDate>
      <link>https://dev.to/wang1051992187/claudemd-vs-a-memory-mcp-what-actually-goes-where-438n</link>
      <guid>https://dev.to/wang1051992187/claudemd-vs-a-memory-mcp-what-actually-goes-where-438n</guid>
      <description>&lt;p&gt;If you use Claude Code or Cursor for real work, you already have a &lt;code&gt;CLAUDE.md&lt;/code&gt; (or an &lt;code&gt;AGENTS.md&lt;/code&gt;, or a &lt;code&gt;.cursorrules&lt;/code&gt;). You've written down the build command, the "we use pnpm, not npm," the folder layout, the one weird thing about the auth flow. And it works. The agent reads it at the start of every session and stops making the same three mistakes.&lt;/p&gt;

&lt;p&gt;So when someone shows up saying "add a memory MCP server to your agent," the honest first reaction is: &lt;em&gt;why? I already have a file the agent reads.&lt;/em&gt; That's a fair question. This post answers it properly — including the cases where the honest answer is "you don't need one yet."&lt;/p&gt;

&lt;p&gt;Disclosure up front: I build one of these (Kireo — more at the bottom). So read this as a builder trying not to oversell, not a neutral survey. But the framework below is the one I actually use to decide what goes in a markdown file and what goes in a memory store, and it holds no matter whose store you use.&lt;/p&gt;

&lt;h2&gt;
  
  
  The one distinction that matters: who writes it, and how often it changes
&lt;/h2&gt;

&lt;p&gt;Here's the split that has survived contact with real projects:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;CLAUDE.md&lt;/th&gt;
&lt;th&gt;Memory MCP&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Who writes it&lt;/td&gt;
&lt;td&gt;You, by hand&lt;/td&gt;
&lt;td&gt;The agent, while working&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;What it holds&lt;/td&gt;
&lt;td&gt;Stable facts&lt;/td&gt;
&lt;td&gt;Accumulated decisions, dead ends, gotchas&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;How it changes&lt;/td&gt;
&lt;td&gt;You edit it when it drifts&lt;/td&gt;
&lt;td&gt;It grows every session&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;How the agent uses it&lt;/td&gt;
&lt;td&gt;Loaded into context every time&lt;/td&gt;
&lt;td&gt;Queried on demand&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;code&gt;CLAUDE.md&lt;/code&gt; is for &lt;strong&gt;stable facts a human curates.&lt;/strong&gt; "Run &lt;code&gt;pnpm --filter api test&lt;/code&gt;. The API talks to Postgres and Redis. Don't touch &lt;code&gt;generated/&lt;/code&gt;." You typed those. You re-read them when they drift. They change maybe once a month.&lt;/p&gt;

&lt;p&gt;A memory store is for &lt;strong&gt;dynamic facts an agent accumulates.&lt;/strong&gt; "Tried batching the embedding upload at 500 symbols per request — the embedding service chokes on batches that large, so I capped it at 100." Nobody hand-edits that into &lt;code&gt;CLAUDE.md&lt;/code&gt;. The agent hit it on Tuesday, and by Thursday, in a fresh session, it's just gone — unless something wrote it down.&lt;/p&gt;

&lt;p&gt;The test I use: &lt;em&gt;would I open the file and type this by hand?&lt;/em&gt; If yes, it's a &lt;code&gt;CLAUDE.md&lt;/code&gt; fact. If it's a byproduct of doing the work — a decision, a dead end, a "we tried X and it broke Y" — it wants a memory store, because you are never going back to curate it into markdown.&lt;/p&gt;

&lt;h2&gt;
  
  
  You don't need a memory MCP yet if…
&lt;/h2&gt;

&lt;p&gt;I would rather you not bolt on a server you don't need, so here's the honest checklist. If all of these are true, a well-kept &lt;code&gt;CLAUDE.md&lt;/code&gt; is enough, and adding infrastructure is just overhead:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;One project.&lt;/strong&gt; Your context still fits, and one file describes it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;One tool.&lt;/strong&gt; You live in Claude Code, or only in Cursor — not both.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;One machine.&lt;/strong&gt; Same laptop, same setup, every day.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Small, curatable volume.&lt;/strong&gt; The stable facts are few enough that you're happy maintaining them by hand, and you rarely hit "the agent solved this exact thing last week and I can't get it back."&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If that's you, close this tab and go keep a good &lt;code&gt;CLAUDE.md&lt;/code&gt;. Most solo, single-repo projects live here, and there's nothing to fix. A memory store is a tool for a problem you may not have yet.&lt;/p&gt;

&lt;h2&gt;
  
  
  When the file stops being enough
&lt;/h2&gt;

&lt;p&gt;Four triggers move the needle. When you hit them, a static file starts losing:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. Multiple tools.&lt;/strong&gt; You start a feature in Claude Code, then jump to Cursor for the UI. Two tools, two separate &lt;code&gt;CLAUDE.md&lt;/code&gt;-style files, and the context you built up in one is invisible to the other. Because MCP is an open protocol, a memory server both tools speak means a single store: written by whichever tool you were in, readable from the other.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. Multiple machines.&lt;/strong&gt; Laptop plus desktop plus a cloud dev box. A file per machine drifts out of sync fast. A store is one place, reachable from all of them.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. Team-shared context.&lt;/strong&gt; A &lt;code&gt;CLAUDE.md&lt;/code&gt; in git &lt;em&gt;is&lt;/em&gt; shared — but it shares &lt;em&gt;stable&lt;/em&gt; facts you agreed to commit. The accumulated "we tried X, it broke Y" is exactly what a new teammate's agent would benefit from, and exactly what nobody writes into the committed markdown. A shared namespace in a store carries the long tail that never makes it into the file.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;4. Volume past hand-curation.&lt;/strong&gt; Beyond a certain size, &lt;code&gt;CLAUDE.md&lt;/code&gt; becomes a wall of text that eats context on every session whether or not any line is relevant today. A store flips that: nothing loads until the agent runs a search and pulls back the top few results it actually needs. If it never searches, that costs zero context.&lt;/p&gt;

&lt;p&gt;That last point is the real mechanical difference. &lt;strong&gt;A file is loaded. A store is queried.&lt;/strong&gt; One is always in your context budget; the other is there only when the agent asks for it.&lt;/p&gt;

&lt;h2&gt;
  
  
  "But Claude Code already has auto memory"
&lt;/h2&gt;

&lt;p&gt;It does, and it's genuinely useful — I use it. But it has a shape worth understanding before you assume it covers the same ground.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;It's selective by design.&lt;/strong&gt; The agent decides what's worth persisting. That's the right default — you don't want every stray thought in there — but it means the memory is only ever a subset of what happened. And you can't search what it chose not to write down. If it never recorded the OOM ceiling, there's nothing to recall later; you rediscover it the hard way.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;It's per-tool.&lt;/strong&gt; Claude Code's memory belongs to Claude Code. Cursor doesn't read it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;It's a file that loads into context, not a query.&lt;/strong&gt; Mechanically it's closer to &lt;code&gt;CLAUDE.md&lt;/code&gt; than to a searchable store: present whether relevant or not, rather than pulled on demand.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;None of that makes it bad. It makes it the &lt;em&gt;stable-facts-plus-a-bit&lt;/em&gt; layer, not the searchable-accumulation-across-tools layer. The two coexist happily: let Claude Code keep managing its file, and let a memory store hold the searchable long tail that spans your tools and machines.&lt;/p&gt;

&lt;h2&gt;
  
  
  If you decide you want the searchable layer
&lt;/h2&gt;

&lt;p&gt;Here's what it looks like in practice, kept short and factual rather than pitched.&lt;/p&gt;

&lt;p&gt;The agent gets a &lt;code&gt;memory_save&lt;/code&gt; and a &lt;code&gt;memory_search&lt;/code&gt;, plus &lt;code&gt;memory_recall&lt;/code&gt;, &lt;code&gt;memory_get&lt;/code&gt;, &lt;code&gt;memory_update&lt;/code&gt;, &lt;code&gt;memory_delete&lt;/code&gt;, &lt;code&gt;memory_list_namespaces&lt;/code&gt;, and &lt;code&gt;memory_health&lt;/code&gt; — eight tools total. It saves a decision or a gotcha when it judges one worth keeping; later, in any MCP client, it searches and gets the top few results back. Because it's an MCP server, "any client" includes both Claude Code and Cursor reading and writing the same store.&lt;/p&gt;

&lt;p&gt;Two things I insisted on, because they're the parts that make me nervous as a user of &lt;em&gt;other&lt;/em&gt; people's memory tools:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Everything is visible.&lt;/strong&gt; There's a web dashboard where you can read, edit, and delete every memory. It is not a black box quietly accumulating who-knows-what.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Everything exports.&lt;/strong&gt; Full JSON dump anytime — no lock-in. If the tool disappoints you, you leave with all your data.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;On privacy, since it's usually the next question: the store keeps the memory text your tools explicitly send plus its metadata (namespace, tags, timestamps); the code index keeps derived embeddings and file paths, never your source. The full write-up is here: &lt;a href="https://app.kireo.app/legal/privacy#data-storage" rel="noopener noreferrer"&gt;https://app.kireo.app/legal/privacy#data-storage&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Install is one line (free API key at &lt;a href="https://app.kireo.app/api-keys" rel="noopener noreferrer"&gt;https://app.kireo.app/api-keys&lt;/a&gt;):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;claude mcp add kireo &lt;span class="nt"&gt;--scope&lt;/span&gt; user &lt;span class="nt"&gt;--env&lt;/span&gt; &lt;span class="nv"&gt;KIREO_API_KEY&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;ki_sk_xxx &lt;span class="nt"&gt;--&lt;/span&gt; npx &lt;span class="nt"&gt;-y&lt;/span&gt; &lt;span class="nt"&gt;--package&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;@kireo/mcp-server kireo-mcp
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  The takeaway
&lt;/h2&gt;

&lt;p&gt;Put stable, human-curated facts in a file the agent reads. Put agent-accumulated, searchable facts in a store the agent queries. If you only have the first kind of fact, you only need the first kind of tool — and that's a fine place to stop.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Disclosure: I build Kireo, the memory MCP server used in the examples above. It's a free beta right now — generous limits, no card required. If the honest read of your setup is "a good &lt;code&gt;CLAUDE.md&lt;/code&gt; is enough," that's a completely legitimate place to land, and I'd rather you stop there than pay a complexity tax for a problem you don't have. The framework holds regardless of whose store you use.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;*Repo: &lt;a href="https://github.com/wang1051992187/kireo-mcp-server" rel="noopener noreferrer"&gt;github.com/wang1051992187/kireo-mcp-server&lt;/a&gt; · npm: &lt;a href="https://www.npmjs.com/package/@kireo/mcp-server" rel="noopener noreferrer"&gt;@kireo/mcp-server&lt;/a&gt; · site: &lt;a href="https://kireo.app/?utm_source=devto&amp;amp;utm_medium=blog&amp;amp;utm_campaign=launch-202608" rel="noopener noreferrer"&gt;https://kireo.app/?utm_source=devto&amp;amp;utm_medium=blog&amp;amp;utm_campaign=launch-202608&lt;/a&gt;&lt;/p&gt;

</description>
      <category>agents</category>
      <category>claude</category>
      <category>llm</category>
      <category>mcp</category>
    </item>
    <item>
      <title>Building low-latency semantic memory for coding agents with LanceDB</title>
      <dc:creator>Jonathan Wilcox</dc:creator>
      <pubDate>Tue, 04 Aug 2026 06:00:49 +0000</pubDate>
      <link>https://dev.to/wang1051992187/building-low-latency-semantic-memory-for-coding-agents-with-lancedb-144k</link>
      <guid>https://dev.to/wang1051992187/building-low-latency-semantic-memory-for-coding-agents-with-lancedb-144k</guid>
      <description>&lt;p&gt;A coding agent starts each session cold. It has no idea what you decided last Tuesday, why you rejected the obvious approach, or which config value burned an afternoon. You can paste the context back in every time, or you can give the agent a memory it can query.&lt;/p&gt;

&lt;p&gt;The second option sounds simple until you write it down as an engineering problem, because it has two constraints that pull against each other:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Retrieval has to be fast.&lt;/strong&gt; Memory lookup sits inside the agent's tool loop. If a recall call adds a noticeable stall on top of everything else the agent is doing, people stop using it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Retrieval has to be token-cheap.&lt;/strong&gt; If pulling context back in costs more tokens than just re-explaining, the memory layer is a net negative. It has to earn its place in the prompt.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;This post is about how we built that layer for Kireo, and — more usefully — the things that broke along the way. Honest framing up front: this isn't a scale-bragging post. It runs on one small VM, and the interesting part is how far a modest box goes when the retrieval path stays lean.&lt;/p&gt;

&lt;h2&gt;
  
  
  The token objection, head-on
&lt;/h2&gt;

&lt;p&gt;The most common pushback on any agent-memory idea is: &lt;em&gt;isn't this just going to bloat every prompt?&lt;/em&gt; Fair worry, and the answer is entirely in the design.&lt;/p&gt;

&lt;p&gt;Memory is an &lt;strong&gt;MCP tool the agent calls on demand&lt;/strong&gt;, not a blob injected into every turn. When the agent needs prior context it calls &lt;code&gt;memory_search&lt;/code&gt;, gets back a small ranked set of hits (default 10, hard cap 50), and spends tokens only on those. Nothing is prepended to the system prompt; nothing runs on turns where the agent doesn't ask.&lt;/p&gt;

&lt;p&gt;That distinction — pull, not push — is the whole reason the token math works. A per-turn injection scheme pays for memory on every message whether it helps or not. An on-demand top-k tool pays only when the model judges the context worth retrieving, and the budget is bounded by &lt;code&gt;limit&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The server ships eight tools over MCP stdio — &lt;code&gt;memory_save&lt;/code&gt;, &lt;code&gt;memory_search&lt;/code&gt;, &lt;code&gt;memory_recall&lt;/code&gt;, &lt;code&gt;memory_get&lt;/code&gt;, &lt;code&gt;memory_update&lt;/code&gt;, &lt;code&gt;memory_delete&lt;/code&gt;, &lt;code&gt;memory_list_namespaces&lt;/code&gt;, and &lt;code&gt;memory_health&lt;/code&gt; — and every MCP client (Claude Code, Cursor, and others) sees the same set. But &lt;code&gt;memory_search&lt;/code&gt; is the one that has to be both fast and frugal, so that's where the engineering went.&lt;/p&gt;

&lt;h2&gt;
  
  
  The stack: small on purpose
&lt;/h2&gt;

&lt;p&gt;The whole retrieval path:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;LanceDB&lt;/strong&gt; — an embedded columnar vector store. No separate database server; it's a library that reads Lance-format files off a disk volume mounted into the API and worker containers. Memory &lt;em&gt;content&lt;/em&gt; lives here.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Neon Postgres&lt;/strong&gt; — holds metadata only (row bookkeeping, embedding status). It never stores the memory body. That split matters later.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Self-hosted embeddings&lt;/strong&gt; — HuggingFace's Text Embeddings Inference (TEI) running &lt;code&gt;intfloat/multilingual-e5-small&lt;/code&gt;, a 384-dimension model, on CPU. It speaks an OpenAI-compatible &lt;code&gt;/v1/embeddings&lt;/code&gt; endpoint on the internal network.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Two of those choices are the load-bearing ones.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;LanceDB is embedded, not a service.&lt;/strong&gt; For a workload this size, a managed vector database is overkill. LanceDB is columnar on disk, the query path is a library call, no network hop. The vector column is an Arrow &lt;code&gt;FixedSizeList(dim)&lt;/code&gt; — remember that, it comes back to bite us.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;384 dimensions on CPU is enough, and it's cheap.&lt;/strong&gt; Memory snippets are short — a decision, a gotcha, a config note. You don't need a 1536- or 3072-dim frontier model to separate "we chose Postgres row-level security over app-layer checks" from "the CI cache key needs the lockfile hash." Smaller vectors mean cheaper ANN and less storage, and a small e5 model does short-text semantic matching well on CPU. The one catch: e5-family models want asymmetric prefixes — &lt;code&gt;passage:&lt;/code&gt; for stored documents, &lt;code&gt;query:&lt;/code&gt; for search queries — supplied from config and empty for OpenAI-style models. (That innocuous trailing space caused a real bug; more below.)&lt;/p&gt;

&lt;p&gt;A side effect of TEI's OpenAI-compatible endpoint: the same client code talks to the self-hosted model by pointing a base URL at the internal service — swapping providers was a config change, not a rewrite. The cache and rate-limit layers use the same trick: a self-hosted Redis fronted by a shim speaking the Upstash REST API, so the REST client needs no managed account.&lt;/p&gt;

&lt;h2&gt;
  
  
  Hybrid search, with a graceful-degradation ladder
&lt;/h2&gt;

&lt;p&gt;Pure vector search misses exact-match cases (a specific error code, a function name). Pure keyword search misses paraphrase. So &lt;code&gt;memory_search&lt;/code&gt; runs both and fuses them.&lt;/p&gt;

&lt;p&gt;The flow:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Embed the query (cached, so repeated searches don't re-embed).&lt;/li&gt;
&lt;li&gt;In parallel, run an &lt;strong&gt;ANN search&lt;/strong&gt; over the vector column (top 50) and a &lt;strong&gt;native full-text search&lt;/strong&gt; (top 50), both scoped to the caller's tenant.&lt;/li&gt;
&lt;li&gt;Fuse the two ranked lists with &lt;strong&gt;reciprocal rank fusion&lt;/strong&gt; and slice to the requested limit.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;RRF is deliberately boring: it combines rankings by &lt;code&gt;1/(k + rank)&lt;/code&gt; without needing the two scoring systems to share a scale — cosine distance and BM25 don't. Robust and cheap, which is what you want in the hot path.&lt;/p&gt;

&lt;p&gt;The interesting part is the &lt;strong&gt;degradation ladder&lt;/strong&gt;: in a lean stack any dependency can be briefly unavailable, and search still has to return &lt;em&gt;something&lt;/em&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Query embedding fails (provider hiccup, timeout)? Log a degraded event so it alerts, drop the vector arm, search keyword-only.&lt;/li&gt;
&lt;li&gt;No FTS index (common in a fresh dev environment)? Fall back to a bounded case-insensitive substring scan ranked by importance.&lt;/li&gt;
&lt;li&gt;Both arms empty? Return an empty result, not a 500.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;None of that is glamorous, but it's the difference between "memory occasionally returns fewer hits" and "memory throws inside the agent's tool loop."&lt;/p&gt;

&lt;h3&gt;
  
  
  Namespaces and tenant isolation
&lt;/h3&gt;

&lt;p&gt;Every row carries a &lt;code&gt;user_id&lt;/code&gt; and a &lt;code&gt;namespace&lt;/code&gt; (e.g. &lt;code&gt;code-my-app&lt;/code&gt; for an indexed repo). Isolation is a &lt;code&gt;user_id&lt;/code&gt; predicate pushed into every LanceDB query, plus a belt-and-suspenders assertion after results return: if any row's &lt;code&gt;user_id&lt;/code&gt; doesn't match the caller, the code throws instead of leaking it. The filter should never be wrong — so we check anyway, on every read path. Cross-tenant leakage is the one bug you never want to ship, and a three-line assertion is cheap insurance.&lt;/p&gt;

&lt;h2&gt;
  
  
  War story 1: the embedding-dimension migration
&lt;/h2&gt;

&lt;p&gt;The stack didn't start at 384 dimensions. It started at 1536 against a hosted model; moving to the self-hosted 384-dim e5 model meant every stored vector was now the wrong length.&lt;/p&gt;

&lt;p&gt;Here's where LanceDB's &lt;code&gt;FixedSizeList(dim)&lt;/code&gt; schema stops being an implementation detail. The vector column's dimension is baked into the table schema, and in the version we run there is &lt;strong&gt;no in-place column resize and no table rename&lt;/strong&gt;. Once the dimension changes, every insert fails against the old table, and you can't quietly widen the column.&lt;/p&gt;

&lt;p&gt;The migration is dump-drop-recreate:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Read every row out of the &lt;code&gt;memories&lt;/code&gt; table.&lt;/li&gt;
&lt;li&gt;Write a &lt;strong&gt;durable JSON dump to disk before anything destructive&lt;/strong&gt; — memory content lives &lt;em&gt;only&lt;/em&gt; in LanceDB (Postgres has metadata only), so if the recreate dies halfway, that dump is the only copy. Old vectors are dropped on reload anyway, so the dump excludes them.&lt;/li&gt;
&lt;li&gt;Drop the table, recreate it empty at the new dimension.&lt;/li&gt;
&lt;li&gt;Re-insert every row in batches with &lt;code&gt;embedding = null&lt;/code&gt; and &lt;code&gt;embedding_status = 'queued'&lt;/code&gt;, then reset the Postgres status rows to &lt;code&gt;queued&lt;/code&gt; too so the backfill job re-embeds everything with the new model.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The migration is idempotent — if the table is already at the target dimension it's a no-op — which matters when you're running it by hand on a live box, unsure whether the last attempt finished.&lt;/p&gt;

&lt;p&gt;And there's a subtle second-order bug this exposed. The embedding cache is keyed by &lt;em&gt;model + content hash&lt;/em&gt; — but &lt;strong&gt;not&lt;/strong&gt; by dimension. So after a same-model endpoint change that alters the vector length, a stale cached vector of the &lt;em&gt;old&lt;/em&gt; length would sail past the cache lookup and get written into the freshly recreated table, breaking the insert. The fix is a dimension guard at two layers: the embed client asserts &lt;code&gt;vector.length === EMBEDDING_DIM&lt;/code&gt; before returning (a wrong-length embed fails and degrades to the queue instead of corrupting the table), and the cache read treats a length mismatch as a miss. The rule: &lt;em&gt;never let a wrong-dimension vector reach the table&lt;/em&gt;, enforced at every point one could enter.&lt;/p&gt;

&lt;h2&gt;
  
  
  War story 2: making batch writes idempotent
&lt;/h2&gt;

&lt;p&gt;Indexing a repo uploads symbols in batches — up to 100 per request. Batches time out sometimes, and the obvious retry (re-send the batch) creates duplicates unless the write path is idempotent.&lt;/p&gt;

&lt;p&gt;The fix is content-hash dedup as a &lt;strong&gt;single set query, not N point lookups&lt;/strong&gt;. Before inserting a batch, one query fetches the active (non-deleted) rows whose &lt;code&gt;content_hash&lt;/code&gt; is in the batch's hashes — &lt;code&gt;content_hash IN (...)&lt;/code&gt; for the whole batch — and skips them. Re-running the same upload is safe: identical content hashes to identical rows, retries don't multiply.&lt;/p&gt;

&lt;p&gt;Two things I like here. First, one &lt;code&gt;IN&lt;/code&gt; query keeps dedup off the per-item hot path — one query per batch, not one per symbol. Second, it's the same guarantee surfaced in the CLI docs: &lt;em&gt;if a batch upload times out, re-running the same command is safe.&lt;/em&gt; Not an internal nicety but a documented contract — the person hitting the timeout is the one who needs to trust the retry.&lt;/p&gt;

&lt;h2&gt;
  
  
  War story 3: what "trash" actually means
&lt;/h2&gt;

&lt;p&gt;Delete is where naive implementations quietly lose data or lie about counts. Our fixes here were all, at heart, about &lt;em&gt;semantics&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;Deletes are &lt;strong&gt;soft&lt;/strong&gt; by default: a delete stamps &lt;code&gt;deleted_at&lt;/code&gt; and sets a 30-day &lt;code&gt;expires_at&lt;/code&gt; restore window. The row stays in the table, filtered out of normal reads by &lt;code&gt;deleted_at IS NULL&lt;/code&gt;. Restore checks the window and refuses if it's expired; a TTL sweep physically removes rows past &lt;code&gt;expires_at&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;That created three follow-on requirements that each needed explicit handling:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;A trash view needs the &lt;em&gt;inverse&lt;/em&gt; filter.&lt;/strong&gt; Listing deleted items isn't "include deleted" — it's "&lt;em&gt;only&lt;/em&gt; deleted." Different queries, and "only deleted" has to win when both flags are set. Get it subtly wrong and the user sees an empty or wrong trash.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Counts have to match the view.&lt;/strong&gt; The "trashed" count is its own query, not &lt;code&gt;total − active&lt;/code&gt;, because off-by-a-little count math is exactly what users notice and stop trusting.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Updates are in-place, never delete-then-add.&lt;/strong&gt; LanceDB will let you delete and re-add a row, but a crash between those calls permanently loses it while its Postgres metadata survives — a torn write. Every mutation (edit, soft-delete, restore, namespace rename) is an in-place &lt;code&gt;update&lt;/code&gt;, so there's no window where the row doesn't exist.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;One more LanceDB-shaped wrinkle: a plain scan has no &lt;code&gt;ORDER BY&lt;/code&gt;. To list newest-first without materializing a million rows, the list path streams every matching batch, re-sorting as they arrive and truncating to &lt;code&gt;limit + 1&lt;/code&gt; so only the current top page stays in memory. It's more code than &lt;code&gt;ORDER BY ... LIMIT&lt;/code&gt;, but it's what the storage engine actually supports.&lt;/p&gt;

&lt;h2&gt;
  
  
  The unglamorous infrastructure footnotes
&lt;/h2&gt;

&lt;p&gt;Three more that belong in any honest "one VM" story:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;A single log file once grew to 62 GB.&lt;/strong&gt; A worker got stuck in an error loop against an exhausted upstream and its JSON logs ate the disk. The fix is boring and permanent: every container caps log rotation at 10 MB × 3 files. On a small box, an unbounded log is a time bomb.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The embedding image is pinned by digest.&lt;/strong&gt; An earlier CPU tag of TEI bundled a dependency version that failed model download with a cryptic &lt;em&gt;"relative URL without a base."&lt;/em&gt; It's now pinned to a known-good digest, with a comment saying exactly why, so an innocent &lt;code&gt;:latest&lt;/code&gt; pull can't resurrect the bug.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Config whitespace is load-bearing.&lt;/strong&gt; Docker's dotenv parsing trims trailing whitespace, which silently ate the trailing space in the e5 &lt;code&gt;passage:&lt;/code&gt; prefix and quietly degraded recall until the value was quoted. The kind of bug that has no stack trace.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;None of these are clever. They're the tax for running a real workload on modest hardware — and writing them down means the next person (often me, three months later) skips the debugging.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where this runs
&lt;/h2&gt;

&lt;p&gt;This is the memory layer behind &lt;strong&gt;Kireo&lt;/strong&gt;, an MCP server that gives Claude Code, Cursor, and any MCP client one shared long-term memory — save decisions and gotchas as you work, recall them from any tool, and browse, edit, or delete everything in a web dashboard, with full JSON export if you ever want to walk away with your data. It's in free beta right now with generous limits and no card required.&lt;/p&gt;

&lt;p&gt;Install is one line:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;claude mcp add kireo &lt;span class="nt"&gt;--scope&lt;/span&gt; user &lt;span class="nt"&gt;--env&lt;/span&gt; &lt;span class="nv"&gt;KIREO_API_KEY&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;ki_sk_xxx &lt;span class="nt"&gt;--&lt;/span&gt; npx &lt;span class="nt"&gt;-y&lt;/span&gt; &lt;span class="nt"&gt;--package&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;@kireo/mcp-server kireo-mcp
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Grab a key and read the docs at &lt;a href="https://kireo.app/?utm_source=devto&amp;amp;utm_medium=blog&amp;amp;utm_campaign=launch-202608" rel="noopener noreferrer"&gt;kireo.app&lt;/a&gt;. On privacy: the server only sends what you explicitly pass to &lt;code&gt;memory_save&lt;/code&gt;; it never reads your code, and code indexing stores derived embeddings and file paths, not your source — see the &lt;a href="https://app.kireo.app/legal/privacy#data-storage" rel="noopener noreferrer"&gt;data-storage policy&lt;/a&gt;. The MCP server is on &lt;a href="https://www.npmjs.com/package/@kireo/mcp-server" rel="noopener noreferrer"&gt;npm&lt;/a&gt; and &lt;a href="https://github.com/wang1051992187/kireo-mcp-server" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;I've kept latency claims out on purpose — I'd rather ship measured numbers than round ones, and a follow-up profiling the search path is on the list. If you build agent memory on a small stack and hit a different set of walls, I'd like to hear which ones.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>database</category>
      <category>llm</category>
      <category>rag</category>
    </item>
  </channel>
</rss>
