<?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: Chad Priest</title>
    <description>The latest articles on DEV Community by Chad Priest (@chad_priest).</description>
    <link>https://dev.to/chad_priest</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%2F4096055%2F384a64d5-a3c5-4814-bacf-2d46f9d42ed9.png</url>
      <title>DEV Community: Chad Priest</title>
      <link>https://dev.to/chad_priest</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/chad_priest"/>
    <language>en</language>
    <item>
      <title>Your MEMORY.md is a file nobody writes to. Render it instead.</title>
      <dc:creator>Chad Priest</dc:creator>
      <pubDate>Thu, 27 Aug 2026 06:57:50 +0000</pubDate>
      <link>https://dev.to/chad_priest/your-memorymd-is-a-file-nobody-writes-to-render-it-instead-4f5c</link>
      <guid>https://dev.to/chad_priest/your-memorymd-is-a-file-nobody-writes-to-render-it-instead-4f5c</guid>
      <description>&lt;p&gt;Every agent harness injects a memory file at session start. Claude Code has &lt;code&gt;MEMORY.md&lt;/code&gt;. OpenClaw has a workspace &lt;code&gt;MEMORY.md&lt;/code&gt;. Starpod keeps one per user. The file is the one thing the model reads on every turn, so it is the highest-leverage artifact in the whole memory stack. And in most systems it is hand-edited, which means it is maintained by whoever remembers to maintain it, which means nobody.&lt;/p&gt;

&lt;p&gt;I found this out by counting. My memory store held 42,000 chunks. The machine-managed zone of the injected file had received one bullet since 2026-04-08. Four months. One bullet. The retrieval pipeline was working; the artifact the model actually saw was frozen.&lt;/p&gt;

&lt;h2&gt;
  
  
  One bullet in four months against 42k chunks in the store
&lt;/h2&gt;

&lt;p&gt;The fix was to stop treating the file as a source and start treating it as a view. &lt;code&gt;MEMORY.md&lt;/code&gt; is now rendered from the database at session start, per session and per project, by a deterministic renderer.&lt;/p&gt;

&lt;p&gt;The rendering has three tiers. Pinned chunks go first, under Identity, Preferences, Decisions and Notes, and they appear in every project. Then the memories tagged with the project the session is running in, resolved from the working directory by the same longest-root-path rule the prompt hook already used. Then global memories fill the remainder of a 6 KB budget (later raised to 8 KB), restricted to durable tags or importance 7 and above. Imports and raw captures are excluded from the render entirely; more on why below.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fhettawhfqe5ricjpvmwa.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fhettawhfqe5ricjpvmwa.png" alt="Session start asks the daemon to render MEMORY.md from memory.db; if the daemon is down a 60-second-old snapshot on disk stands in" width="800" height="318"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;The file is a view, not a source&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;The file on disk still exists, because two readers (the workspace loader and the gateway) read it without talking to the daemon. The daemon rewrites it every 60 seconds, atomically, and the write is a no-op modulo the timestamp line if nothing changed. If the daemon is down at session start, the snapshot stands. If it is up, the hook gets a per-project rendering and splices it into the injected section.&lt;/p&gt;

&lt;p&gt;For a person using it, the change is one rule: never edit the file. To change what every session sees, pin a chunk or pin a sentence into a section. Everything else in the file is earned by importance, tag and recency, and it moves.&lt;/p&gt;

&lt;h2&gt;
  
  
  74 of 42k chunks carried a project id, so the project tier was empty
&lt;/h2&gt;

&lt;p&gt;The renderer worked on day one and the project tier was thin to the point of uselessness. The reason was not the renderer. Only 74 of 42,000 chunks had a &lt;code&gt;project_id&lt;/code&gt;. The tier had nothing to select.&lt;/p&gt;

&lt;p&gt;The evidence to fix that had been on disk the whole time. Every Claude Code transcript line carries &lt;code&gt;cwd&lt;/code&gt;. Nothing had ever read it. So the IDE capture path now resolves a transcript's dominant &lt;code&gt;cwd&lt;/code&gt; to a project and stamps the conversation, and the extractor, which already wrote &lt;code&gt;project:&amp;lt;id&amp;gt;&lt;/code&gt; on bullets from tagged conversations, needed no change. A backfill pass over 489 transcripts on disk matched 341; three were tagged to a real project and 338 resolved to the install root, which is global by design. Historical chunks were not retagged, because there is no chunk-to-conversation link, and I rejected keyword tagging outright: a wrong project tag hides a memory from every other project's recall, which is worse than no tag.&lt;/p&gt;

&lt;p&gt;Then the stamp itself went wrong. &lt;code&gt;extract_project&lt;/code&gt; trusted any &lt;code&gt;project:&lt;/code&gt; token found in text, so a plan document that merely mentioned the token got stamped with whatever followed it. Seven live chunks carried ids like &lt;code&gt;&amp;lt;id&amp;gt;&lt;/code&gt;, &lt;code&gt;changed&lt;/code&gt; and &lt;code&gt;\S+\s+)?&lt;/code&gt;. A junk stamp is strictly worse than none: a non-NULL &lt;code&gt;project_id&lt;/code&gt; is neither global nor in any real project, so the chunk falls out of the rendered file, out of the project boost in search, and out of every vault carrying a project rule. My first guard pinned the id to exactly eight hex characters, and three existing tests failed because they use &lt;code&gt;proj_abc&lt;/code&gt; fixtures. That was the useful failure. An over-strict validator drops a legitimate stamp, which does the identical harm to the junk it stops. The shipped guard checks shape (the &lt;code&gt;proj_&lt;/code&gt; prefix), which all seven junk values fail on their own.&lt;/p&gt;

&lt;h2&gt;
  
  
  Zero of 42,798 chunks had a valid_at, and imports outranked native facts
&lt;/h2&gt;

&lt;p&gt;The bigger dead end was time. The renderer and the search ranker both sort on recency. Recency was &lt;code&gt;created_at&lt;/code&gt;. &lt;code&gt;created_at&lt;/code&gt; answers when a row was written, not when the fact was true, and my temporal-memory plan had specified a &lt;code&gt;valid_at&lt;/code&gt; column for exactly that distinction. The column shipped. The writer never did. I audited on 2026-08-16: 0 of 42,798 chunks had one.&lt;/p&gt;

&lt;p&gt;Two defects had been hiding inside that zero. First, import collapse: an archive is written all at once, so all 3,403 ChatGPT import chunks, three and a half years of history, landed inside one 24-hour window. Second, re-sync re-dating: re-indexing a file deletes and reinserts its chunks, and &lt;code&gt;created_at&lt;/code&gt; was not in the preserve map, so every chunk of &lt;code&gt;memory/2026-05-01.md&lt;/code&gt; read 2026-05-02 03:56, the following night's pass. No chunk's date matched its own filename.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fo8wm09xsudoq1vamjf1m.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fo8wm09xsudoq1vamjf1m.png" alt="Chunks with a valid_at before the writer existed: 0 of 42,798; ChatGPT import chunks collapsed into one 24-hour window: 3,403" width="800" height="199"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;What the audit found on 2026-08-16&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;The live cost was measurable: 87 rows where an import chunk held canonical over a native one, because the inject tie-break ("yesterday's correction beats last year's stale fact") was reading last year's fact with yesterday's timestamp. This is why imports and captures are excluded from the rendered file. They are not less true; their clocks were lying.&lt;/p&gt;

&lt;p&gt;Writing &lt;code&gt;valid_at&lt;/code&gt; was not enough on its own. The reconciler, which decides whether a new fact supersedes an old one, already had a deterministic guard that refuses to move memory backwards in time. The guard stopped the damage but taught the model nothing: the LLM kept proposing updates the guard silently refused, and both facts stayed standing side by side. The prompt showed no dates, so "NEW" read as "newer" when it only meant "newly ingested". Each fact in the prompt now renders a &lt;code&gt;[YYYY-MM-DD]&lt;/code&gt; from &lt;code&gt;valid_at&lt;/code&gt; (else &lt;code&gt;created_at&lt;/code&gt;), an explicit &lt;code&gt;[undated]&lt;/code&gt; where there is none rather than an empty bracket the model could read as a parse failure, and the instruction says to choose UPDATE only when the new fact's date is the later one.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fxpym8uzj7mi7jgy3jjyj.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fxpym8uzj7mi7jgy3jjyj.png" alt="Renderer shipped, then the empty project tier, then the junk project stamps, then the valid_at audit, then the dated reconcile prompt" width="800" height="173"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The invariant: every column a ranker sorts on has exactly one writer that means it
&lt;/h2&gt;

&lt;p&gt;Here is the class, stated so you can check it. For every column that participates in ranking, selection or supersession, there must be a writer whose semantics match the column's name, and the writer must run on every ingest path. &lt;code&gt;valid_at&lt;/code&gt; failed the first half (no writer). &lt;code&gt;project_id&lt;/code&gt; failed the second half (one path wrote it, one path wrote garbage, most paths wrote nothing). In both cases the ranker was correct and the column was hollow, and a hollow column does not error. It sorts. It just sorts on the wrong thing, and the artifact the model reads looks fine.&lt;/p&gt;

&lt;p&gt;The corollary is that the injected file cannot be a source of truth, because a file has exactly one writer (a human) and that writer's semantics are "when I remembered to". Make it a view and the columns become the only thing you have to get right.&lt;/p&gt;

&lt;h2&gt;
  
  
  Three queries to run on your own store
&lt;/h2&gt;

&lt;p&gt;You need nothing from Vodou for this. Point them at whatever SQLite or Postgres table holds your memories.&lt;/p&gt;

&lt;p&gt;First, does your ranking column have a writer? Substitute your event-time column for &lt;code&gt;valid_at&lt;/code&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="k"&gt;COUNT&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;total&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
       &lt;span class="k"&gt;SUM&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;CASE&lt;/span&gt; &lt;span class="k"&gt;WHEN&lt;/span&gt; &lt;span class="n"&gt;valid_at&lt;/span&gt; &lt;span class="k"&gt;IS&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt; &lt;span class="k"&gt;THEN&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="k"&gt;ELSE&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt; &lt;span class="k"&gt;END&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;missing&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;chunks&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Passing looks like &lt;code&gt;total=42798, missing=120&lt;/code&gt;, a small residue you can explain. Failing looks like &lt;code&gt;missing&lt;/code&gt; equal to &lt;code&gt;total&lt;/code&gt;. If it does, every "recent" boost in your ranker is a write-time boost, and anything you bulk-imported is winning ties it should lose.&lt;/p&gt;

&lt;p&gt;Second, are your creation timestamps clustered where they should be spread?&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="nb"&gt;date&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;created_at&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="k"&gt;day&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;COUNT&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;n&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;chunks&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="k"&gt;source&lt;/span&gt; &lt;span class="k"&gt;LIKE&lt;/span&gt; &lt;span class="s1"&gt;'import:%'&lt;/span&gt;
&lt;span class="k"&gt;GROUP&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="k"&gt;day&lt;/span&gt; &lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;n&lt;/span&gt; &lt;span class="k"&gt;DESC&lt;/span&gt; &lt;span class="k"&gt;LIMIT&lt;/span&gt; &lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Passing is a spread across many days. Failing is one row holding most of the count. For me it was 3,403 chunks on one day.&lt;/p&gt;

&lt;p&gt;Third, is your scoping column populated, and populated with values that resolve?&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;project_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;COUNT&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;chunks&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;project_id&lt;/span&gt; &lt;span class="k"&gt;IS&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;
  &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;project_id&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;IN&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;projects&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;GROUP&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;project_id&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Passing is zero rows. Failing is any row: those chunks are in no scope at all, invisible to global and to every project. Then check the ratio of non-NULL to total; mine was 74 over 42,000, which is not a scoping feature, it is a scoping placebo.&lt;/p&gt;

&lt;p&gt;And one check for the file itself, if you inject one. Diff its machine-managed zone against a copy from a month ago. If the diff is empty and your store grew, the file is not a memory. It is a fossil.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the memory architecture guides skip: the file is the read side
&lt;/h2&gt;

&lt;p&gt;The published patterns are good about layers and poor about the artifact the model actually reads. The &lt;a href="https://geodocs.dev/ai-agents/agent-memory-pattern-spec" rel="noopener noreferrer"&gt;agent memory pattern spec&lt;/a&gt; says to score retrieval by recency, frequency and relevance, and never says which timestamp recency means; mine was the wrong one for a year. The &lt;a href="https://engineering-playbook.vercel.app/agentic/agent-memory" rel="noopener noreferrer"&gt;engineering playbook&lt;/a&gt; splits long-term memory into episodic, semantic and procedural, which is a fine taxonomy for storage and silent on how any of it reaches the context window on turn one. &lt;a href="https://github.com/INOSX/agent-memory/blob/HEAD/docs/memory-system.md" rel="noopener noreferrer"&gt;INOSX's five-layer design&lt;/a&gt; is the closest, with an explicit "read-time assembly" plane, which is the right framing: the injected file is an assembly, and assemblies are rendered.&lt;/p&gt;

&lt;p&gt;The tools that do keep a &lt;code&gt;MEMORY.md&lt;/code&gt; mostly index it. &lt;a href="https://github.com/vibeputin/gigabrain" rel="noopener noreferrer"&gt;Gigabrain&lt;/a&gt; syncs the workspace &lt;code&gt;MEMORY.md&lt;/code&gt; alongside its registry for unified recall, and &lt;a href="https://github.com/Simple4uhq/simple4u-memory" rel="noopener noreferrer"&gt;sae4u-memory&lt;/a&gt; searches SQLite facts and the markdown auto-memory dirs in one call. Both treat the file as a second corpus to read from. I went the other way: the file is an output of the database, never an input. &lt;a href="https://pypi.org/project/hermes-memory/" rel="noopener noreferrer"&gt;hermes-memory&lt;/a&gt; is nearest in spirit, keeping the hot injection under about 180 tokens and migrating accumulated facts out of the file into the DB, which is the same instinct with a smaller budget. My hot tier is 6 to 8 KB because the pinned identity and preference facts are the part that must never be lost to retrieval scoring.&lt;/p&gt;

&lt;h2&gt;
  
  
  Still open: historical chunks keep their project blindness
&lt;/h2&gt;

&lt;p&gt;The backfill stamped conversations, not chunks. The 42,000 chunks that existed before 2026-08-16 have no link back to the conversation they came from, so they will never move into a project tier unless they are re-extracted. I chose that over keyword tagging because a wrong tag is a hidden memory, but the honest state is that the project tier is accurate only for memory captured after the change. Whether the store's older half is worth re-extracting against the corrected paths is a measurement I have not taken.&lt;/p&gt;




&lt;p&gt;Source: &lt;a href="https://blog.vodou.ai/render-memory-md-from-database/" rel="noopener noreferrer"&gt;Your MEMORY.md is a file nobody writes to. Render it instead.&lt;/a&gt; by Chad Priest, from Building Vodou in Public.&lt;/p&gt;

</description>
      <category>aiagents</category>
      <category>memory</category>
      <category>llm</category>
      <category>architecture</category>
    </item>
    <item>
      <title>A 400-byte cap crashed my memory daemon on one emoji</title>
      <dc:creator>Chad Priest</dc:creator>
      <pubDate>Thu, 27 Aug 2026 06:51:48 +0000</pubDate>
      <link>https://dev.to/chad_priest/a-400-byte-cap-crashed-my-memory-daemon-on-one-emoji-1anb</link>
      <guid>https://dev.to/chad_priest/a-400-byte-cap-crashed-my-memory-daemon-on-one-emoji-1anb</guid>
      <description>&lt;p&gt;Every agent memory system trims text somewhere. Facts get capped before they go into a prompt. Summaries get cut to fit a budget. Recall blocks get sliced to a length. The cut is usually one line, written months ago, and it takes a number that somebody assumed was characters. It was bytes. Or UTF-16 code units. The first user who puts an em dash or a party emoji near the limit finds out which.&lt;/p&gt;

&lt;p&gt;That is one of two bugs I hit this week while moving Vodou's memory graph into the console so that memory is one store served by one process. The other one is about a guard that ran perfectly and left no trace. They have the same shape, and that shape is the point of this post.&lt;/p&gt;

&lt;h2&gt;
  
  
  The move: one graph, one process, and 49,338 chunks that had never been deduplicated
&lt;/h2&gt;

&lt;p&gt;The capability itself is unglamorous. The brain graph (the map of facts, their groups, and which twin retired which) used to live in a separate process with its own database handle. It now lives inside the console, reading through the same API as the memory list, the vault rules and the document library. The public side is in &lt;code&gt;MCP-servers/Vodou-Console/src/brain/queries.ts&lt;/code&gt; and &lt;code&gt;src/api/brain.ts&lt;/code&gt;, with a drift test in &lt;code&gt;src/__tests__/brain-queries-drift.test.ts&lt;/code&gt; so the graph queries and the list queries cannot quietly disagree about what a live chunk is.&lt;/p&gt;

&lt;p&gt;For a person using it: one search box, one theme, one process to restart, and a map whose filter searches memories instead of filenames (that one was a bug too). The engine underneath it gained fact groups and a reconcile step that decides, when two facts say the same thing, which one lives.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F2ulsbch09km8hne1r4d7.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F2ulsbch09km8hne1r4d7.png" alt="Extraction path: transcript to extractor to an independent verifier to reconcile to the memory store, with the console reading one store" width="800" height="260"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Where a fact goes before it is a fact&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Getting the graph to tell the truth meant fixing what it displayed. Three of those fixes are worth your time.&lt;/p&gt;

&lt;h2&gt;
  
  
  Exit 99, and the hook said "memory DEGRADED" instead of "the daemon is dead"
&lt;/h2&gt;

&lt;p&gt;Reconcile runs over every fact it compares, and it trims long facts to 400 for display in the prompt. The cap was written as a byte length, and the standard library truncate in that language asserts when the index lands inside a multi-byte character. A fact with an accent, an em dash or an emoji near position 400 took the daemon down with exit 99.&lt;/p&gt;

&lt;p&gt;What made it expensive was not the panic. It was the symptom. The worker process survived, so from the outside memory looked empty rather than crashed. The prompt hook reported DEGRADED and connection refused. I spent time on sockets while &lt;code&gt;assertion failed: is_char_boundary&lt;/code&gt; sat in the system log. The bug had been latent since 2026-07-16.&lt;/p&gt;

&lt;p&gt;The fix is one line: cut on a character boundary, and make the cap 400 characters, which is what the call site always meant. It was trimming for display, not enforcing a byte budget. The regression test fills the string with 2-byte and 4-byte characters, both of which panic under the old code.&lt;/p&gt;

&lt;p&gt;This is not a Vodou bug. It is the industry's bug this year. OpenFang hit it &lt;a href="https://github.com/RightNow-AI/openfang/issues/104" rel="noopener noreferrer"&gt;at a fixed byte offset with an em dash&lt;/a&gt;. AionUi hit it &lt;a href="https://github.com/iOfficeAI/aionrs/pull/248" rel="noopener noreferrer"&gt;on a 25,000-byte MEMORY.md cap with CJK text&lt;/a&gt;, where the panic killed the agent at session start and every later start too. Mastra hit the UTF-16 version: &lt;a href="https://github.com/mastra-ai/mastra/issues/15573" rel="noopener noreferrer"&gt;truncation split a surrogate pair and Anthropic's JSON parser rejected the whole request&lt;/a&gt;. OpenClaw &lt;a href="https://github.com/openclaw/openclaw/pull/98711" rel="noopener noreferrer"&gt;shipped the same fix&lt;/a&gt; for recall summaries prepended to the prompt. Four projects, four months, one line each.&lt;/p&gt;

&lt;h2&gt;
  
  
  2,437 backwards clocks, and the reconciler that kept the poorer twin
&lt;/h2&gt;

&lt;p&gt;The second fix started as a user-visible failure: Vodou had learned my sons' names four times and thrown them away four times, keeping "married, two sons" every time. Chasing it found two unrelated bugs wearing one symptom.&lt;/p&gt;

&lt;p&gt;The first was inert. 2,437 of 3,281 retired chunks had a retirement time earlier than their creation time. Re-syncing a memory file deletes and re-inserts its chunks, so creation is re-stamped from the file while retirement is carried over from the deleted row. Two columns, two sources, no invariant. I backfilled them and then checked all 33 readers of that column: every one tests for NULL, none compares it as a time. So it changed nothing anyone saw. I recorded it anyway, so nobody hunts for a symptom that does not exist.&lt;/p&gt;

&lt;p&gt;The second one bit. Reconcile's duplicate arm was first-writer-wins. The incumbent survived, the newcomer was hidden. A later, richer phrasing lost purely for arriving second. The fix retires the fact that says less, not the one that arrived later.&lt;/p&gt;

&lt;p&gt;Underneath both sat a third problem: near-duplicate demotion had never completed on this corpus at all. The scan is O(n²) over 49,338 chunks, and the CLI watchdog (progress-based since 2026-08-22, so slow-but-advancing work survives) killed it every run with "no progress since start, 90s". A lane that never beats is indistinguishable from a wedged one. The fix was to beat every 512 chunks, not to add a 17th exemption. First completed scan: 428,484 pairs over cosine, 8,407 passing overlap, 980 groups, 3,292 demoted, 20 value conflicts queued, 147 seconds. Retrieval-bench moved with it.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F7a1d684idfi1vloem60p.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F7a1d684idfi1vloem60p.png" alt="Retrieval-bench above-floor rate rose from 91 percent to 93 percent after the first completed dedup scan" width="800" height="153"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;retrieval-bench above-floor, before and after the first finished dedup&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Zero verifier lines after 120 facts, and both explanations fit
&lt;/h2&gt;

&lt;p&gt;The bridge's 4B extractor turned a turn that said "just saying hi" into a preference fact about reply length, and wrote it. One model read the transcript, asserted what it meant, and its assertion was the verification. Extraction now runs each candidate fact past a separate completion that sees only the source and the claim and answers one question: does the source say this. Not "is this plausible". Plausible-given-the-source is exactly the trap.&lt;/p&gt;

&lt;p&gt;Then I went to measure the rejection rate. Five conversations, 120 facts written, zero verifier log lines. I spent several steps hunting a wiring failure that did not exist. The verifier had run the whole time. It only logged on rejection, so "ran and approved everything" and "never ran" produced byte-identical evidence: nothing.&lt;/p&gt;

&lt;p&gt;I had built, into the instrument whose purpose is catching unchecked claims, a log that could not distinguish success from absence. It now emits one line per batch, unconditionally:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;[extract-verify] mode=Log checked=4 kept=4 unsupported=0 unknown=0
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;An absent line now means the verifier did not run. &lt;code&gt;unsupported&lt;/code&gt; and &lt;code&gt;unknown&lt;/code&gt; are separate numbers on purpose: a high &lt;code&gt;unknown&lt;/code&gt; means the checker could not reach its provider, and must never be read as "the facts were clean". First real rate from a live turn: 8 checked, 0 unsupported, 0 unknown.&lt;/p&gt;

&lt;h2&gt;
  
  
  The class: a cap in the wrong unit, and a guard whose silence means two things
&lt;/h2&gt;

&lt;p&gt;Both bugs are checkable properties, not advice.&lt;/p&gt;

&lt;p&gt;First: every truncation applied to user text must count in the unit the consumer of the text counts in. A prompt-display cap is characters. A storage cap is bytes. A JSON-over-HTTP cap must never split a UTF-16 surrogate. If the cap's unit and the cut's unit differ, the code is wrong regardless of whether it has fired yet.&lt;/p&gt;

&lt;p&gt;Second: a guard that gates writes must produce evidence on every invocation, including approvals. If the set of observable outputs for "ran and passed everything" equals the set for "did not run", the guard is unobservable and its rejection rate is unmeasurable. This is the same property as a health check that returns 200 from a stranger's process.&lt;/p&gt;

&lt;h2&gt;
  
  
  Run this on your own store: an emoji at the cap, and a grep for silent guards
&lt;/h2&gt;

&lt;p&gt;Find your cuts. In a Rust, Go, or C tree:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-rnE&lt;/span&gt; &lt;span class="s1"&gt;'\.truncate\(|\[\.\.[A-Za-z_0-9]+\]|\[:[A-Za-z_0-9]+\]'&lt;/span&gt; &lt;span class="nt"&gt;--include&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;'*.rs'&lt;/span&gt; &lt;span class="nt"&gt;--include&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;'*.go'&lt;/span&gt; src/ | &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-v&lt;/span&gt; &lt;span class="nb"&gt;test&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In a TypeScript or Python tree:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-rnE&lt;/span&gt; &lt;span class="s1"&gt;'\.slice\(0, ?[A-Za-z_0-9]+\)|\.substring\(0|\[:[A-Za-z_0-9]+\]'&lt;/span&gt; &lt;span class="nt"&gt;--include&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;'*.ts'&lt;/span&gt; &lt;span class="nt"&gt;--include&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;'*.js'&lt;/span&gt; &lt;span class="nt"&gt;--include&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;'*.py'&lt;/span&gt; src/ | &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-v&lt;/span&gt; &lt;span class="nb"&gt;test&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Each hit is a cap. For each one, ask what unit the number is in and what unit the slice is in. Then feed the function a string that puts a 4-byte character exactly on the boundary. In Node, this is the whole test:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;cap&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;s&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;abcd🔥ef&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;cut&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;s&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;slice&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;cap&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stringify&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;cut&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="nx"&gt;cut&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;isWellFormed&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Failing output is &lt;code&gt;"abcd\ud83d" false&lt;/code&gt;: a lone high surrogate that &lt;code&gt;JSON.stringify&lt;/code&gt; will happily emit and a strict server-side parser will reject. Passing output ends in a whole character and prints &lt;code&gt;true&lt;/code&gt;. In Python, &lt;code&gt;s.encode()[:cap].decode()&lt;/code&gt; raises &lt;code&gt;UnicodeDecodeError&lt;/code&gt; on a failing cap and returns cleanly on a passing one. If your code path is Rust, use the char-boundary-aware cut and write the test with &lt;code&gt;é&lt;/code&gt; and &lt;code&gt;🙂&lt;/code&gt; fills; both panic under a byte cut.&lt;/p&gt;

&lt;p&gt;For the second property, take your write-side guard (the schema validator, the PII redactor, the fact verifier, whatever stands between a model's assertion and your store) and run this against its log after real traffic:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-c&lt;/span&gt; &lt;span class="s1"&gt;'verify'&lt;/span&gt; app.log&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-c&lt;/span&gt; &lt;span class="s1"&gt;'INSERT INTO facts'&lt;/span&gt; app.log
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If the second number is large and the first is zero, you have one of two situations and cannot tell which. Passing looks like a guard line count that is at least the batch count, with an explicit approved count in each line. Failing looks like silence, and silence is what you saw before you looked.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the memory guides say about consolidation, and the two things they skip
&lt;/h2&gt;

&lt;p&gt;The memory-architecture literature is good on tiers. The &lt;a href="https://geodocs.dev/ai-agents/agent-memory-pattern-spec" rel="noopener noreferrer"&gt;Geodocs pattern spec&lt;/a&gt; lays out working, episodic, semantic and procedural memory with consolidation and eviction rules. The &lt;a href="https://engineering-playbook.vercel.app/agentic/agent-memory" rel="noopener noreferrer"&gt;Engineering Playbook&lt;/a&gt; maps the same three flavours onto storage choices. &lt;a href="https://pockit.tools/blog/ai-agent-memory-architecture-production-guide/" rel="noopener noreferrer"&gt;Pockit's production guide&lt;/a&gt; is right that context windows are not memory.&lt;/p&gt;

&lt;p&gt;None of them say who verifies the extractor. Consolidation is described as a step, not as an assertion that needs a second reader. And none mention that the prompt you consolidate into gets truncated somewhere, and that the truncation is where four projects fell over this year. Tiers are the easy part. The seam between "the model said this" and "the store now holds this" is where memory quality is decided, and it needs both a checker and a log of the checker.&lt;/p&gt;

&lt;h2&gt;
  
  
  Still open: the checker is graded on four cases, not on a corpus
&lt;/h2&gt;

&lt;p&gt;The verifier has been checked against four discriminating cases and one live turn of eight facts. That is enough to know it runs and to know its log is honest. It is not enough to know its false-approval rate on a month of real transcripts, and "0 unsupported" on eight facts is not a rate. Until a proper labelled set exists, the number to watch is &lt;code&gt;unknown&lt;/code&gt;, because that one tells me when the checker stopped being able to check.&lt;/p&gt;




&lt;p&gt;Source: &lt;a href="https://blog.vodou.ai/memory-truncation-verifier-absence-shaped/" rel="noopener noreferrer"&gt;A 400-byte cap crashed my memory daemon on one emoji&lt;/a&gt; by Chad Priest, from Building Vodou in Public.&lt;/p&gt;

</description>
      <category>memory</category>
      <category>aiagents</category>
      <category>llm</category>
      <category>observability</category>
    </item>
    <item>
      <title>Your document chunker is a memory chunker, and it quadruples your store</title>
      <dc:creator>Chad Priest</dc:creator>
      <pubDate>Thu, 27 Aug 2026 06:44:01 +0000</pubDate>
      <link>https://dev.to/chad_priest/your-document-chunker-is-a-memory-chunker-and-it-quadruples-your-store-3j1b</link>
      <guid>https://dev.to/chad_priest/your-document-chunker-is-a-memory-chunker-and-it-quadruples-your-store-3j1b</guid>
      <description>&lt;p&gt;Every memory system I have looked at, mine included, grew a document ingest path by pointing the existing chunker at a folder. It splits on bullets, it caps at a few hundred characters, it has worked for months. Then a real directory goes in and the store gets four times denser, the router starts ranking a 0.000 above a 0.746, and nobody can say which of two scorers produced the number on screen. None of that is a model problem. All of it is a seam between two things that were quietly sharing one rule.&lt;/p&gt;

&lt;p&gt;This is what shipped over eight days in Vodou's library lane, and more usefully, what was wrong first.&lt;/p&gt;

&lt;h2&gt;
  
  
  A library is not a memory: whole files, one card each, two ways to match
&lt;/h2&gt;

&lt;p&gt;The capability is plain. You add a document from the Library page (a file, a folder, or a URL), it is extracted and indexed as a whole document, it gets a one-row "card" summarising what it is about, and it becomes addressable in any chat as an &lt;code&gt;@doc:&lt;/code&gt; token. Two matching lanes answer two different questions. The subject lane asks "is this document what you are asking ABOUT" and scores with a cross-encoder. The topic lane asks "does this document DISCUSS what you asked" by finding the best passage in the chunk index and mapping it back to its file. The two are never sorted against each other, because a cross-encoder probability and a raw cosine are not the same kind of number.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F69dqd3c1kuh41ljzz9kh.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F69dqd3c1kuh41ljzz9kh.png" alt="A query fans into a subject lane scored by cross-encoder and a topic lane scored by passage cosine, merged by survival not by rank" width="798" height="154"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Two lanes, two scales, never one sort&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;The public half is &lt;code&gt;MCP-servers/Vodou-Console/src/api/library.ts&lt;/code&gt; (the routes), &lt;code&gt;extension/Store-vodou-bridge/background.js&lt;/code&gt; (the URL and page-text lanes) and &lt;code&gt;MCP-servers/Vodou-Console/src/__tests__/library-e2e.test.ts&lt;/code&gt;, which is the harness that should have existed first.&lt;/p&gt;

&lt;h2&gt;
  
  
  181 chunks from one 15,869-character file
&lt;/h2&gt;

&lt;p&gt;The directory ingest command already existed. Its first real run turned a 15,869-character plan file into 181 chunks, about 88 characters each, against a 600-character ceiling. The reason was correct behaviour in the wrong place. For memory files, a bullet is a fact: "- The user's name is Chad." has to be its own retrieval unit. For a document, "- [ ] Run &lt;code&gt;npm run build&lt;/code&gt;" was becoming its own embedding row. Across the 643 files I actually wanted in, that density would have roughly quadrupled a 38k-chunk store.&lt;/p&gt;

&lt;p&gt;The engine now carries a chunk mode, memory or document, and the same file went from 181 to 50 chunks with nothing dropped. One test pins the difference between modes rather than either mode alone.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fd1tce5qb441syk0nz6tl.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fd1tce5qb441syk0nz6tl.png" alt="Chunks produced from one 15,869 character file: 181 in memory mode, 50 in document mode" width="800" height="153"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;One plan file, two chunk modes&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;The first fix changed nothing, and that miss is worth keeping. I patched the function that applies the ceiling, which is the probe. The writer re-chunked in memory mode on its own. Probe and writer had two copies of the decision, so fixing one left the store exactly as dense as before.&lt;/p&gt;

&lt;h2&gt;
  
  
  0.70 was calibrated on cosine and applied to a cross-encoder
&lt;/h2&gt;

&lt;p&gt;Then the router. The card lane had a 0.68 floor justified by a code comment citing one pair of observations. Measured against a 22-query labelled set, the relevant population (0.553 to 0.845) and the ordinary-browsing population (0.455 to 0.686) overlap outright. "Sourdough Starter Recipe" scored 0.686, above a genuine contract query, because a bi-encoder rewards surface overlap between a short title and a long summary. No constant separates those two populations. Switching the card scorer to the cross-encoder collapsed every noise query to about 0.000 and routed all 8 relevant queries to the correct document. It also took match from 0.36s to 8.3s cold, so a cosine gate below every floor skips the model for ordinary browsing.&lt;/p&gt;

&lt;p&gt;That gate created the next bug. The &lt;code&gt;score&lt;/code&gt; field was overwritten with a cross-encoder probability when the reranker ran and left as a cosine when the gate skipped it. The 0.70 floor was calibrated on cosine. So whether a document was eligible depended on whether a latency gate happened to fire. Three queries, each ranking the right document first under both scorers, each rejected under only one:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;query                       cosine   cross-encoder
launching on hacker news     0.703       0.000
console redesign             0.720       0.001
go to market launch plan     0.676       0.052
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Meanwhile sub-floor subject hits were never dropped, and because topic hits are appended after subject hits, a 0.000 non-answer printed above the 0.746 right answer. I first filtered them out, which turned "go to market launch plan" (right document, 0.052) into "no documents matched". Losing the answer is worse than mis-ordering it, so sub-floor hits now survive as a tagged &lt;code&gt;weak&lt;/code&gt; fallback used only when neither lane produced anything real.&lt;/p&gt;

&lt;p&gt;The split that stuck: cosine decides eligibility and is the number shown; the cross-encoder decides order among the eligible; &lt;code&gt;score&lt;/code&gt; is restored to the cosine after reranking. Cosine alone gets "go to market launch plan" wrong (0.687 for the wrong plan). The cross-encoder alone rejects it. Each does the one thing it is calibrated for.&lt;/p&gt;

&lt;p&gt;There was a third leak under load. The reranker had four early-return paths and only one was safe. When the model failed to load under memory pressure, raw cosine stood and "Getting Started with React" cleared a floor that means nothing on a cosine. Cosine ordering is a weaker signal; cosine thresholding is a broken one. The result now carries an explicit calibrated-or-not marker, and an uncalibrated result is suppressed rather than answered. On a machine with no inference runtime the lane goes silent and says so. All 11 noise queries: 0 leaks, from 1 to 2 under load.&lt;/p&gt;

&lt;h2&gt;
  
  
  The failure class: one field, two producers, one floor
&lt;/h2&gt;

&lt;p&gt;Two invariants fell out, and both are checkable against a codebase rather than advice.&lt;/p&gt;

&lt;p&gt;First: the chunk-size decision has exactly one owner, and the probe that predicts the chunk count and the writer that produces it call it. If they can disagree, the ceiling trims the wrong amount and a test on the probe passes while the store fills.&lt;/p&gt;

&lt;p&gt;Second: a threshold is bound to the distribution it was calibrated on, so any field a floor reads must have exactly one producer or a calibration tag. If a rerank step can be skipped, disabled, or fail, and on every one of those paths the same field holds a different scorer's output, then eligibility is a function of latency and memory pressure, not of relevance.&lt;/p&gt;

&lt;h2&gt;
  
  
  Two queries to run on your own store tonight
&lt;/h2&gt;

&lt;p&gt;Both take five minutes on any SQLite or Postgres chunk table. First, chunk density per source. Documents and facts should not share a distribution.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;source_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
       &lt;span class="k"&gt;COUNT&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;                          &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;chunks&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
       &lt;span class="k"&gt;SUM&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;LENGTH&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;content&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;              &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;chars&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
       &lt;span class="k"&gt;SUM&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;LENGTH&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;content&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="k"&gt;COUNT&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;   &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;avg_chunk&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;chunks&lt;/span&gt;
&lt;span class="k"&gt;GROUP&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;source_id&lt;/span&gt;
&lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;chunks&lt;/span&gt; &lt;span class="k"&gt;DESC&lt;/span&gt;
&lt;span class="k"&gt;LIMIT&lt;/span&gt; &lt;span class="mi"&gt;20&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Passing: your long documents sit near your configured ceiling (a 600 ceiling should show 400 to 600). Failing looks like mine did, &lt;code&gt;181 | 15869 | 87&lt;/code&gt; for a single file. If your average chunk is a fifth of your ceiling, your document chunker is a fact chunker.&lt;/p&gt;

&lt;p&gt;Second, the scale check. Log the retriever's final score alongside which scorer produced it, then look at the floor's neighbourhood.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;scorer&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;COUNT&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
       &lt;span class="k"&gt;MIN&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;score&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="k"&gt;AVG&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;score&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="k"&gt;MAX&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;score&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;retrieval_log&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;score&lt;/span&gt; &lt;span class="k"&gt;BETWEEN&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;60&lt;/span&gt; &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;80&lt;/span&gt;
&lt;span class="k"&gt;GROUP&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;scorer&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Passing: one row, or two rows whose ranges do not both straddle your floor. Failing: &lt;code&gt;cosine&lt;/code&gt; averaging 0.71 and &lt;code&gt;cross_encoder&lt;/code&gt; averaging 0.03 in the same band, which means the same floor is admitting one and rejecting the other by accident. If you have no &lt;code&gt;scorer&lt;/code&gt; column, that is the finding: add it before you tune the threshold again.&lt;/p&gt;

&lt;h2&gt;
  
  
  The retrieval-quality posts say measure recall; the seam is inside one score
&lt;/h2&gt;

&lt;p&gt;The current writing is right that context-building fails before generation does. The New Stack's piece on &lt;a href="https://thenewstack.io/retrieval-ai-agent-architecture/" rel="noopener noreferrer"&gt;retrieval as the defining bottleneck&lt;/a&gt; and Weaviate's &lt;a href="https://weaviate.io/blog/retrieval-quality-rag-overview" rel="noopener noreferrer"&gt;retrieval-quality overview&lt;/a&gt; both land on the same point: when retrieval breaks, the model extrapolates with full fluency. Shaped's &lt;a href="https://www.shaped.ai/blog/the-stochastic-parrot-why-your-ai-agent-is-only-as-good-as-its-retrieval" rel="noopener noreferrer"&gt;stochastic parrot post&lt;/a&gt; names the semantic trap where a vector store finds fifty near-misses. What none of them say is that the trap can be a data-shape bug: a single &lt;code&gt;score&lt;/code&gt; column with two producers, or a chunker with two callers. Recall benchmarks report the symptom. The &lt;a href="https://mem.nowledge.co/docs/library" rel="noopener noreferrer"&gt;mem.nowledge library docs&lt;/a&gt; get the product split right (documents stay whole, memories are takeaways), and that split has to reach the chunker and the scorer too, not stop at the UI.&lt;/p&gt;

&lt;h2&gt;
  
  
  Still open: the topic lane leaks on vocabulary, and there is no number
&lt;/h2&gt;

&lt;p&gt;The topic lane matched react.dev pages to implementation plans and doc.rust-lang.org to anything with a Rust paragraph. Two threshold calibrations and a cross-encoder swap failed: true interior hits sat at 0.702 to 0.741, false ones at 0.703 to 0.743, interleaved point for point. The fix that held is a document-level veto (the cited passage's heading must mention what was asked), 3 of 3 false positives rejected, 0 of 4 true hits killed. But that labelled set is 22 queries on one library, and the subject lane still scores real paraphrases near 0.0, so &lt;code&gt;weak&lt;/code&gt; ordering is barely better than chance. The e2e suite reports the topic leak count on every run instead of thresholding it, because a cap calibrated on a live library breaks within hours. I have the number for mine. I do not yet have one I would trust on yours.&lt;/p&gt;




&lt;p&gt;Source: &lt;a href="https://blog.vodou.ai/document-chunking-scoring-floor-two-scales/" rel="noopener noreferrer"&gt;Your document chunker is a memory chunker, and it quadruples your store&lt;/a&gt; by Chad Priest, from Building Vodou in Public.&lt;/p&gt;

</description>
      <category>retrieval</category>
      <category>aiagents</category>
      <category>memory</category>
      <category>architecture</category>
    </item>
    <item>
      <title>Per-site memory off switches only work if every reader asks the same authority</title>
      <dc:creator>Chad Priest</dc:creator>
      <pubDate>Thu, 27 Aug 2026 06:36:19 +0000</pubDate>
      <link>https://dev.to/chad_priest/per-site-memory-off-switches-only-work-if-every-reader-asks-the-same-authority-15po</link>
      <guid>https://dev.to/chad_priest/per-site-memory-off-switches-only-work-if-every-reader-asks-the-same-authority-15po</guid>
      <description>&lt;p&gt;If your agent reads the browser tab, you already have a policy problem, whether or not you have written the policy. The extension sees a bank login page the same way it sees a Wikipedia article. The memory store does not know which facts came from a health portal. And the day a user asks "forget everything you learned from that site", you find out how many code paths actually read that data, because each one needs to honor the answer.&lt;/p&gt;

&lt;p&gt;I shipped the governance layer for Vodou's page memory across two days in August. This is what it took, including the parts the tests said were fine and were not.&lt;/p&gt;

&lt;h2&gt;
  
  
  Off, suggest, collect: the mode lives in the gateway, not the extension
&lt;/h2&gt;

&lt;p&gt;The capability is small to describe. Every host gets a mode: off, suggest, or collect. Banks, health portals, tax sites and sign-in hosts default to off, which means the extension never reads the tab there until the user flips it. Everything else collects. Precedence is one line: a user rule beats the sensitive default, which beats the global default, and a rule covers subdomains.&lt;/p&gt;

&lt;p&gt;The part that matters for your stack is where the mode is decided. It is not in the extension. The gateway is the authority, exposed as &lt;code&gt;GET/PUT /api/page-match/site-mode&lt;/code&gt; and &lt;code&gt;GET /api/page-match/site-modes&lt;/code&gt; in &lt;code&gt;MCP-servers/Vodou-Console/src/page-site-mode.ts&lt;/code&gt;. The extension asks before it does anything with a page. Typing suggestions ask the mode first and send nothing until it answers. That ordering is the whole design: the client is not trusted to remember the policy, it is required to fetch it.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fy1zmx35s0116swp0zpug.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fy1zmx35s0116swp0zpug.png" alt="Three read paths for page memory, each gated by one site-mode lookup in the gateway before any daemon call" width="799" height="297"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;One authority, three enforcement points&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Three paths read page memory: the match endpoint that fills the side panel, the probe that colors the toolbar icon, and the note and link routes that write a fact with a page attached. All three now check the mode. Off means the match returns empty without asking the daemon at all, the probe returns no icon, and a write returns 403 unless the mode is collect. Those are three separate checks because they are three separate readers, and I tested them separately: four tests for the mode resolution, four for enforcement, in &lt;code&gt;MCP-servers/Vodou-Console/src/__tests__/page-site-mode.test.ts&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Forget a host: soft delete, dry run first, confirm within eight seconds
&lt;/h2&gt;

&lt;p&gt;The second half of governance is retraction. &lt;code&gt;mem forget --host &amp;lt;h&amp;gt;&lt;/code&gt; sets &lt;code&gt;invalid_at&lt;/code&gt; on every fact whose source host is the host or a subdomain of it. It does not delete rows. Library documents saved from that host are counted and reported, not removed, because a saved article is something the user chose deliberately and I did not want one command to take two kinds of data.&lt;/p&gt;

&lt;p&gt;The panel exposes it as "Forget site…" with a two-click shape: the first click runs a dry run and shows the count, the second click within 8 seconds confirms. &lt;code&gt;--undo&lt;/code&gt; clears the &lt;code&gt;invalid_at&lt;/code&gt; stamps back. I verified the loop live on w3schools.com: forget hid one fact, the count read 1, undo brought it back, the count read 0.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fdo59et2499lgqswjt0gu.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fdo59et2499lgqswjt0gu.png" alt="Before: a page fact could only be removed by a hard delete with no preview. After: forget by host is a soft invalidation with a dry run, a count, a confirm window and an undo" width="800" height="255"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Soft delete has a cost that a hard delete does not: every reader must filter on &lt;code&gt;invalid_at&lt;/code&gt;. That is the same shape as the site-mode problem. A governance decision is only as good as the number of readers that honor it.&lt;/p&gt;

&lt;h2&gt;
  
  
  The unit suites were green and the live page found four defects
&lt;/h2&gt;

&lt;p&gt;Before P4 landed I loaded build .74 unpacked and ran it against real pages: ChatGPT with the consent gate open in DevTools, a Wikipedia clip, a saved article, the Ctrl+Shift+B regression. Every unit suite had passed. Four things were wrong anyway.&lt;/p&gt;

&lt;p&gt;A fact I had stored 30 minutes earlier displayed as "18h" old. The by_page query returns &lt;code&gt;COALESCE(valid_at, created_at)&lt;/code&gt;, and a fact that came out of a daily log has its &lt;code&gt;valid_at&lt;/code&gt; set to the log day at local midnight. The age math was correct and the input was a day-granular instant. Day-granular instants now render as today, yesterday, or N days.&lt;/p&gt;

&lt;p&gt;"Send selection to Vodou memory" sent no URL. The two Wikipedia clips I made landed with an empty &lt;code&gt;source_url&lt;/code&gt;, which means they could never be found from that page again. The selection was the only content the handler passed along. It now sends the tab's URL and title as well.&lt;/p&gt;

&lt;p&gt;The lane the "Add this page to Vodou Library" action actually uses, &lt;code&gt;add_text&lt;/code&gt;, never wrote &lt;code&gt;memory_sources.source_url&lt;/code&gt; and never stamped its chunks. Only &lt;code&gt;add_url&lt;/code&gt; did. Two entry points to the same table, one of which stamped provenance. I mirrored the behavior and stamped the one existing document by hand so it matched.&lt;/p&gt;

&lt;p&gt;And once that article was stamped, its 111 chunks flooded the "From this page" list as raw &lt;code&gt;# Title\n\nSource:&lt;/code&gt; fragments and buried the four facts I had clipped. The fact tiers now exclude &lt;code&gt;doc:%&lt;/code&gt; sources and a separate &lt;code&gt;docs_by_page&lt;/code&gt; lists each document once.&lt;/p&gt;

&lt;p&gt;None of these four are exotic. Each one is a reader or a writer that did not participate in the provenance contract the others followed. The tests were green because each test exercised the path that did participate.&lt;/p&gt;

&lt;h2&gt;
  
  
  The icon took four rejected drafts in one evening
&lt;/h2&gt;

&lt;p&gt;The smaller build story is the toolbar icon, which turns green when the current page has memory from it. The first draft was a text badge; Chrome's fixed badge overlay ate most of a 16px icon. The second was a corner dot. The third was a pulse, which reads as an alert that never resolves. The fourth, the one that shipped, redraws the icon on an OffscreenCanvas from our own artwork, hue-shifting only the brand-blue pixels so the white eyes survive. A flat &lt;code&gt;source-atop&lt;/code&gt; fill had painted over them.&lt;/p&gt;

&lt;p&gt;The decision about which signal drives the color was harder than the drawing. Counting site-tier matches swallowed the icon on every page of a site. Counting title-only semantic hits dotted every Google results page. The icon goes green for exact facts and documents saved from this page, nothing broader. The probe result is cached for 30 seconds per page key, so a burst of tab switches is one round trip. That logic is in &lt;code&gt;MCP-servers/Vodou-Console/src/vbb/page-probe.ts&lt;/code&gt; with five tests.&lt;/p&gt;

&lt;h2&gt;
  
  
  The invariant: readers of a governed column equal checks of its policy
&lt;/h2&gt;

&lt;p&gt;Here is the failure class, stated so you can check it rather than nod at it.&lt;/p&gt;

&lt;p&gt;For any column that a policy governs (&lt;code&gt;source_host&lt;/code&gt; under a site mode, &lt;code&gt;invalid_at&lt;/code&gt; under a soft delete), the set of code paths that read or write that column must equal the set of code paths that consult the policy. If there are N readers and N minus one policy checks, the policy is decorative on exactly one path, and that path is the one the user will hit.&lt;/p&gt;

&lt;p&gt;The four defects in .74 all violate a sibling of this: two writers of &lt;code&gt;memory_sources&lt;/code&gt;, one stamping provenance. Three readers of page memory, before P4, zero consulting a mode. The property is countable.&lt;/p&gt;

&lt;h2&gt;
  
  
  Run this against your own store before you trust your off switch
&lt;/h2&gt;

&lt;p&gt;Take five minutes and count. First, find every place that reads the governed column, and every place that reads the policy. Substitute your own names.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# readers of the governed axis&lt;/span&gt;
&lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-rn&lt;/span&gt; &lt;span class="s2"&gt;"source_host&lt;/span&gt;&lt;span class="se"&gt;\|&lt;/span&gt;&lt;span class="s2"&gt;source_url"&lt;/span&gt; src/ &lt;span class="nt"&gt;--include&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;'*.ts'&lt;/span&gt; &lt;span class="nt"&gt;--include&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;'*.py'&lt;/span&gt; | &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-v&lt;/span&gt; &lt;span class="nb"&gt;test&lt;/span&gt; | &lt;span class="nb"&gt;wc&lt;/span&gt; &lt;span class="nt"&gt;-l&lt;/span&gt;
&lt;span class="c"&gt;# places that consult the policy&lt;/span&gt;
&lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-rn&lt;/span&gt; &lt;span class="s2"&gt;"siteMode&lt;/span&gt;&lt;span class="se"&gt;\|&lt;/span&gt;&lt;span class="s2"&gt;site_mode&lt;/span&gt;&lt;span class="se"&gt;\|&lt;/span&gt;&lt;span class="s2"&gt;isHostAllowed"&lt;/span&gt; src/ &lt;span class="nt"&gt;--include&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;'*.ts'&lt;/span&gt; &lt;span class="nt"&gt;--include&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;'*.py'&lt;/span&gt; | &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-v&lt;/span&gt; &lt;span class="nb"&gt;test&lt;/span&gt; | &lt;span class="nb"&gt;wc&lt;/span&gt; &lt;span class="nt"&gt;-l&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Passing looks like the second number being close to the first, and a quick read of the diff between the two lists showing only helpers. Failing looks like &lt;code&gt;14&lt;/code&gt; and &lt;code&gt;2&lt;/code&gt;, which is roughly what I would have seen before P4.&lt;/p&gt;

&lt;p&gt;Second, check whether your writers agree on provenance:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;source_kind&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;COUNT&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="k"&gt;rows&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
       &lt;span class="k"&gt;SUM&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;CASE&lt;/span&gt; &lt;span class="k"&gt;WHEN&lt;/span&gt; &lt;span class="n"&gt;source_url&lt;/span&gt; &lt;span class="k"&gt;IS&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt; &lt;span class="k"&gt;OR&lt;/span&gt; &lt;span class="n"&gt;source_url&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;''&lt;/span&gt; &lt;span class="k"&gt;THEN&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="k"&gt;ELSE&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt; &lt;span class="k"&gt;END&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;no_url&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;memory_sources&lt;/span&gt;
&lt;span class="k"&gt;GROUP&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;source_kind&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Passing is a &lt;code&gt;no_url&lt;/code&gt; of 0 for every kind that came from a page. Failing is one kind with rows and no URLs, which tells you which writer skipped the stamp. That query is how I would have caught &lt;code&gt;add_text&lt;/code&gt; in seconds instead of on a live Wikipedia tab.&lt;/p&gt;

&lt;p&gt;Third, if you soft delete, prove every reader filters:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;UPDATE&lt;/span&gt; &lt;span class="n"&gt;memories&lt;/span&gt; &lt;span class="k"&gt;SET&lt;/span&gt; &lt;span class="n"&gt;invalid_at&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;datetime&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'now'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;source_host&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'example.com'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then hit each of your read endpoints for that host with curl and look for a nonzero count. Any endpoint that still returns those rows is a reader that did not get the memo. Undo with &lt;code&gt;SET invalid_at = NULL&lt;/code&gt; on the same rows.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the memory-pattern guides leave out
&lt;/h2&gt;

&lt;p&gt;The published specs describe memory in tiers. The &lt;a href="https://geodocs.dev/ai-agents/agent-memory-pattern-spec" rel="noopener noreferrer"&gt;agent memory pattern spec&lt;/a&gt; names working, episodic, semantic and procedural, and its privacy rule is to redact PII at write time. The &lt;a href="https://engineering-playbook.vercel.app/agentic/agent-memory" rel="noopener noreferrer"&gt;engineering playbook&lt;/a&gt; splits the same space into episodic, semantic and procedural. Both are right and neither says where the fact came from. A site mode is a rule about the source, not the tier, and write-time redaction cannot fire on a page the extension should never have read in the first place. The off mode has to sit in front of the read, which is a different enforcement point than the redactor.&lt;/p&gt;

&lt;p&gt;Retraction is under-described too. The &lt;a href="https://arxiv.org/html/2608.10502" rel="noopener noreferrer"&gt;dependency-guided rollback paper&lt;/a&gt; makes the point that deleting a faulty memory leaves everything derived from it active. My &lt;code&gt;forget --host&lt;/code&gt; has the same limit: it invalidates the source facts and does nothing about a fill plan or an answer that already used them. That is a real gap, not a footnote, and the paper is the clearest statement of it I have found.&lt;/p&gt;

&lt;h2&gt;
  
  
  Still open: a chunk that names its own page can undo a manual link
&lt;/h2&gt;

&lt;p&gt;One limitation is live. &lt;code&gt;mem page-link&lt;/code&gt; stamps a memory with a page by writing the column directly, and sync's &lt;code&gt;COALESCE&lt;/code&gt; keeps that value on re-index. But a chunk whose own text carries a &lt;code&gt;page:&lt;/code&gt; token reverts to that token's page on the next sync, overriding the manual link. Two writers of &lt;code&gt;source_url&lt;/code&gt; again, with a precedence rule that favors the wrong one for this case. I noted it in the P2 commit and did not fix it in P4. If you count your writers with the query above, that is the kind of pair it will show you.&lt;/p&gt;




&lt;p&gt;Source: &lt;a href="https://blog.vodou.ai/per-site-memory-policy-enforcement/" rel="noopener noreferrer"&gt;Per-site memory off switches only work if every reader asks the same authority&lt;/a&gt; by Chad Priest, from Building Vodou in Public.&lt;/p&gt;

</description>
      <category>aiagents</category>
      <category>memory</category>
      <category>architecture</category>
      <category>privacy</category>
    </item>
    <item>
      <title>Your Agent's Memory Has No Idea Where It Was</title>
      <dc:creator>Chad Priest</dc:creator>
      <pubDate>Wed, 26 Aug 2026 17:35:14 +0000</pubDate>
      <link>https://dev.to/chad_priest/your-agents-memory-has-no-idea-where-it-was-3pad</link>
      <guid>https://dev.to/chad_priest/your-agents-memory-has-no-idea-where-it-was-3pad</guid>
      <description>&lt;p&gt;Every agent memory system I have read stores &lt;em&gt;what&lt;/em&gt; was said and &lt;em&gt;when&lt;/em&gt;. Almost none store &lt;em&gt;where&lt;/em&gt;. The chunk knows its text, its embedding, a created_at, maybe a session id. It does not know that you learned it on a Wikipedia article, or typed it into a vendor onboarding form, or that it came out of a ChatGPT conversation you had on a specific page.&lt;/p&gt;

&lt;p&gt;That missing axis costs you more than it looks like it should. Without it you cannot answer "what do I already know about the thing in front of me," you cannot forget one source without hunting chunk ids, and you cannot tell a user which of your stored facts came from their bank's website, which, the moment you have a browser extension, is the question that decides whether they trust you at all.&lt;/p&gt;

&lt;p&gt;I spent two days adding that axis to Vodou's memory store. Here is what it actually took, including the four defects that every unit test in the repo said did not exist.&lt;/p&gt;

&lt;h2&gt;
  
  
  Three columns and a match endpoint that tiers by URL
&lt;/h2&gt;

&lt;p&gt;Three columns on the memory chunk: &lt;code&gt;source_url&lt;/code&gt;, &lt;code&gt;source_host&lt;/code&gt;, &lt;code&gt;source_ref&lt;/code&gt;. Then a match endpoint that, given a URL, returns what memory holds about it in four tiers.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// MCP-servers/Vodou-Console/src/api/page-match.ts — the tiers&lt;/span&gt;
&lt;span class="c1"&gt;// T1  exact page   facts whose source_url matches this URL&lt;/span&gt;
&lt;span class="c1"&gt;// T2  this site    facts from the same host&lt;/span&gt;
&lt;span class="c1"&gt;// T3  semantic     facts that match the page's title, not its URL&lt;/span&gt;
&lt;span class="c1"&gt;// T4  documents    Library docs saved from this page&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;On top of that: a browser extension that colors its own toolbar icon green when the current page has memory &lt;em&gt;from&lt;/em&gt; it, a "Note about this page" box, insert-anywhere, typing suggestions, form fill from memory with a review card, and a per-site governance model that decides whether Vodou is allowed to look at the tab at all.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fcl4ixpiqvm0eahixhinq.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fcl4ixpiqvm0eahixhinq.png" alt="A URL enters the gateway, is checked against the per-site mode, then fans out to four memory tiers and back to the panel"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;What happens when you switch tabs&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;The interesting design constraint is the last edge. The icon is green for T1 and T4 only. That was not the first thing I built.&lt;/p&gt;

&lt;h2&gt;
  
  
  Everything green, then four defects in build .74
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Everything green, four defects
&lt;/h3&gt;

&lt;p&gt;I finished the unit suites. &lt;code&gt;page-match.test.ts&lt;/code&gt;, &lt;code&gt;page-probe.test.ts&lt;/code&gt;, &lt;code&gt;page-site-mode.test.ts&lt;/code&gt;: all passing, plus four new route tests for the note and link endpoints. Then I loaded build .74 as an unpacked extension with the DevTools Network tab open and used it like a person for twenty minutes: a real ChatGPT capture, a Wikipedia clip, a saved article, the keyboard shortcut.&lt;/p&gt;

&lt;p&gt;Four defects. None of them visible to any test I had written.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;A thirty-minute-old fact rendered as "18h old."&lt;/strong&gt; The by-page query returned &lt;code&gt;COALESCE(valid_at, created_at)&lt;/code&gt;, and for a fact extracted out of a daily log, &lt;code&gt;valid_at&lt;/code&gt; is the log &lt;em&gt;day&lt;/em&gt; at local midnight. So a fact written at 12:30pm carried an instant of 00:00:00 and the age formatter did the arithmetic correctly on a value that was never meant to be read at that precision. The fix is not in the query, it is in the renderer: day-granular instants now render "today" / "yesterday" / "3d", never hours. A timestamp with a granularity is not a timestamp.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Two Wikipedia clips landed with an empty &lt;code&gt;source_url&lt;/code&gt;.&lt;/strong&gt; The right-click "Send selection to Vodou memory" handler sent the selected text and nothing else. The selection &lt;em&gt;is&lt;/em&gt; the content, so the handler looked complete. It had no tab metadata, so both clips were stored with no page: findable by search, invisible forever from the page they came from. The whole feature, silently not working, on the exact path a user would try first.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The Library lane never stamped anything.&lt;/strong&gt; There are two ways a page becomes a document: &lt;code&gt;add_url&lt;/code&gt; and &lt;code&gt;add_text&lt;/code&gt;. &lt;code&gt;add_url&lt;/code&gt; wrote &lt;code&gt;memory_sources.source_url&lt;/code&gt; and stamped its chunks. &lt;code&gt;add_text&lt;/code&gt; did not. "Add this page to Vodou Library", the button an actual person clicks, routes through &lt;code&gt;add_text&lt;/code&gt;. I had implemented the axis on the lane nobody uses. I mirrored it and hand-stamped doc #225 to match.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Then the fix broke the feature.&lt;/strong&gt; Once that 111-chunk article was correctly stamped, it flooded "From this page." The panel filled with &lt;code&gt;# Title\n\nSource:&lt;/code&gt; fragments, raw chunk headers, 111 of them, and buried the four facts I had actually clipped. Correct data, useless surface. Fact tiers now exclude &lt;code&gt;doc:%&lt;/code&gt; entirely, and documents get their own list that shows each document once instead of each of its chunks.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Flzyax7hnqlzj33fqc8ux.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Flzyax7hnqlzj33fqc8ux.png" alt="Before: green unit suites hid four defects. After: live unpacked load surfaced all four."&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Every one of these lived in the gap between "the function returns the right value" and "a person got what they came for." The tests asserted the first. Nothing asserted the second.&lt;/p&gt;

&lt;h3&gt;
  
  
  Four indicators in one evening, three rejected on sight
&lt;/h3&gt;

&lt;p&gt;The icon was supposed to be the easy part. Show the user that this page has memory. I built four versions in one evening and threw away three.&lt;/p&gt;

&lt;p&gt;A &lt;strong&gt;text badge&lt;/strong&gt; with a count: Chrome's badge overlay is fixed-size and ate a 16px icon. Unreadable. A &lt;strong&gt;corner dot&lt;/strong&gt;: too small to notice, and it does not say what it means. A &lt;strong&gt;pulse&lt;/strong&gt;: motion in a browser toolbar reads as an unending alert. You notice it once and then it is a thing you are ignoring forever. The one that shipped is the icon itself drawn green, redrawn per tab on a transparent OffscreenCanvas from our own artwork.&lt;/p&gt;

&lt;p&gt;That last one had its own trap. Hue-shifting the brand-blue pixels works. A flat &lt;code&gt;source-atop&lt;/code&gt; fill does not: it paints over the white eyes in the logo and you get a green blob. &lt;code&gt;MARK_MODE&lt;/code&gt; still keeps &lt;code&gt;dot&lt;/code&gt; and &lt;code&gt;pulse&lt;/code&gt; one line away, because I do not trust that I am done being wrong about this.&lt;/p&gt;

&lt;p&gt;The harder question was not &lt;em&gt;how&lt;/em&gt; to draw it but &lt;em&gt;when&lt;/em&gt;. My first cut lit the icon for any tier hit. Two failures, both obvious the moment I used it:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Include the site tier (T2), and the count swallows the icon on any site you have ever stored anything from.&lt;/li&gt;
&lt;li&gt;Include the title-only semantic tier (T3), and every Google results page lights up, because a results page's title contains your query, which is what you were searching your memory for.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Green means T1 and T4 only: facts and documents saved &lt;em&gt;from this page&lt;/em&gt;. That is a claim narrow enough to always be true. An indicator that fires on a weak match is worse than no indicator, because a user calibrates on it once and then never looks again.&lt;/p&gt;

&lt;h3&gt;
  
  
  The permission model was the actual feature
&lt;/h3&gt;

&lt;p&gt;The part I underestimated by the widest margin: this is a browser extension that reads the URL of every tab you open. That is not a feature detail. That is the whole trust question.&lt;/p&gt;

&lt;p&gt;The gateway is the authority, not the extension. Site mode is &lt;code&gt;off&lt;/code&gt; / &lt;code&gt;suggest&lt;/code&gt; / &lt;code&gt;collect&lt;/code&gt;. Banks, health portals, tax sites and sign-in hosts default to &lt;strong&gt;off&lt;/strong&gt;: Vodou does not read the tab there until the user says otherwise. Resolution order is user rule, then sensitive default, then global default, and a rule covers subdomains.&lt;/p&gt;

&lt;p&gt;Off is enforced in four places, not one: &lt;code&gt;page-match&lt;/code&gt; returns empty without asking the daemon, &lt;code&gt;page_probe&lt;/code&gt; returns no icon, and &lt;code&gt;note&lt;/code&gt; and &lt;code&gt;link&lt;/code&gt; both 403 unless the mode is &lt;code&gt;collect&lt;/code&gt;. The extension-side typing suggestions ask the mode &lt;em&gt;first&lt;/em&gt; and send nothing until it answers. Four tests on the resolver, four on enforcement.&lt;/p&gt;

&lt;p&gt;Then &lt;code&gt;mem forget --host&lt;/code&gt;, which is soft (sets &lt;code&gt;invalid_at&lt;/code&gt;), covers subdomains, and counts Library documents rather than removing them. First panel click is a dry run that shows you the count; a second click within 8 seconds confirms. Verified live on w3schools.com: hide, 1, undo, 0.&lt;/p&gt;

&lt;p&gt;The QA plan for all of this was two-phase and network-observable, which is the only kind of privacy test I now believe:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Phase A  page memory OFF  → switch tabs → assert ZERO page-match requests
Phase B  page memory ON   → accept disclosure → assert requests appear,
                             and the fact walnut-lantern-4417 shows under
                             "From this page" in the ChatGPT tab
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Phase A is the important one. It does not test that the feature works. It tests that the feature is &lt;em&gt;absent&lt;/em&gt;, from the network panel, where a skeptical user would look.&lt;/p&gt;

&lt;h2&gt;
  
  
  Test the privacy guarantee on the wire, not the return value
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Your privacy guarantee is a claim about the wire, so test it on the wire.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Nearly every agent system I have seen tests its permission model by asserting the return value of a permission function. That is testing your intent. The user's actual question is "did you send my data anywhere," and the answer to that lives in the network panel, not in your unit suite. Write the test that asserts zero requests. It is a strange test to write, most test frameworks make asserting a &lt;em&gt;non-event&lt;/em&gt; awkward, and it is the only one that answers the question that was asked.&lt;/p&gt;

&lt;p&gt;Two corollaries, both of which cost me a defect each:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;When you add a field, find every write path, not the one you were looking at.&lt;/strong&gt; &lt;code&gt;add_url&lt;/code&gt; and &lt;code&gt;add_text&lt;/code&gt; both create documents. I stamped one. The one I missed was the one behind the button. Grep for every writer of the table, not every caller of the function you happened to open.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;A stored timestamp has a granularity, and the granularity does not travel with it.&lt;/strong&gt; A &lt;code&gt;datetime&lt;/code&gt; column looks second-precise. A value derived from a log &lt;em&gt;day&lt;/em&gt; is not. If you render it at a precision the data never had, you produce a confident lie: "18h ago" for something thirty minutes old. Carry the granularity, or render at the coarsest one you can guarantee.&lt;/p&gt;

&lt;h2&gt;
  
  
  Agent memory tiers say nothing about which page you were on
&lt;/h2&gt;

&lt;p&gt;The public discussion of agent memory is almost entirely about &lt;em&gt;tiers&lt;/em&gt;. The &lt;a href="https://engineering-playbook.vercel.app/agentic/agent-memory" rel="noopener noreferrer"&gt;engineering playbook's agent-memory writeup&lt;/a&gt; splits long-term memory into episodic, semantic and procedural. &lt;a href="https://geodocs.dev/ai-agents/agent-memory-pattern-spec" rel="noopener noreferrer"&gt;Geodocs' pattern spec&lt;/a&gt; adds working memory on top and specifies consolidation, scoring, eviction and write-time PII redaction. Both are good. Both are about &lt;em&gt;what kind of thing&lt;/em&gt; a memory is.&lt;/p&gt;

&lt;p&gt;Neither has an axis for &lt;em&gt;where the user was standing when it entered the store&lt;/em&gt;. That is not an oversight in the taxonomy so much as an artifact of the assumption underneath it: that memory arrives through your chat interface. Once memory can be captured from any page in a browser, provenance stops being metadata and becomes the primary key for two operations you cannot otherwise perform: "show me what you know about this" and "forget this source."&lt;/p&gt;

&lt;p&gt;The &lt;a href="https://pockit.tools/blog/ai-agent-memory-architecture-production-guide/" rel="noopener noreferrer"&gt;Pockit production-memory guide&lt;/a&gt; is right that context windows are not memory and that persistence is the wall everyone hits. But durability cuts both ways, which is the point of &lt;a href="https://arxiv.org/html/2608.10502" rel="noopener noreferrer"&gt;"From Faulty Memories to Corrected Actions"&lt;/a&gt;: persistence makes errors durable too, and deleting a bad memory leaves everything already derived from it in place. Their answer is dependency-guided rollback. My cheaper, dumber version of the same insight is that if every chunk carries its origin, "forget everything that came from this host" is one soft update instead of an archaeology project. Provenance is not just retrieval: it is the handle you need to undo.&lt;/p&gt;

&lt;p&gt;Worth reading alongside these: &lt;a href="https://github.com/rohitg00/agentmemory/pull/648" rel="noopener noreferrer"&gt;agentmemory#648&lt;/a&gt;, a stability pass where two separate bugs turned out to share one root cause, a daemon wrapper surviving &lt;code&gt;stop&lt;/code&gt;, and the &lt;a href="https://github.com/cacheplane/dawnai/commit/4e3e020c969dd9b1ad9f10c374d2dfd5105e82c0" rel="noopener noreferrer"&gt;dawnai memory-tooling commit&lt;/a&gt;, which adds a gated live-smoke suite for exactly the reason I am writing this post: some behaviors only appear against a real backend, so they gate a separate suite on a real key and keep it out of CI.&lt;/p&gt;

&lt;h2&gt;
  
  
  COALESCE lets a chunk's own page: token overwrite mem page-link
&lt;/h2&gt;

&lt;p&gt;The stamping path has a known conflict. &lt;code&gt;mem page-link&lt;/code&gt; writes the column directly, but re-indexing uses a &lt;code&gt;COALESCE&lt;/code&gt; that prefers a &lt;code&gt;page:&lt;/code&gt; token found in the chunk's own text. So a chunk whose text carries its own page token will revert to that token on the next sync, overriding the link. I know about it, it is in the plan, it is not fixed.&lt;/p&gt;

&lt;p&gt;Form fill is two lanes and only one of them is deterministic. Learn-back, answers you accepted on this page before, or on this site, is exact and ordered, latest wins. Everything past that is one LLM call with a hard "only what memory supports, never invent" rule, which is a rule you write down and hope for. The review card exists because I do not trust that lane. Nothing is written to the page until you tick it, and it never submits the form.&lt;/p&gt;

&lt;p&gt;Sensitive-host defaults are a list. Lists are wrong at the edges. A bank I have never heard of collects by default until somebody notices, and "somebody notices" is not a security model.&lt;/p&gt;

&lt;p&gt;And the whole thing rests on one number I cannot measure from here: how many of those four live-only defects still exist in the paths I did not manually click for twenty minutes. Every unit suite in this repo is green. That was true before I opened the browser, too.&lt;/p&gt;




&lt;p&gt;Source: &lt;a href="https://blog.vodou.ai/page-axis-agent-memory/" rel="noopener noreferrer"&gt;Your Agent's Memory Has No Idea Where It Was&lt;/a&gt; by Chad Priest, from Building Vodou in Public.&lt;/p&gt;

</description>
      <category>aiagents</category>
      <category>memory</category>
      <category>retrieval</category>
      <category>architecture</category>
    </item>
    <item>
      <title>Your retired button is still mounted, still handling clicks</title>
      <dc:creator>Chad Priest</dc:creator>
      <pubDate>Wed, 26 Aug 2026 17:26:21 +0000</pubDate>
      <link>https://dev.to/chad_priest/your-retired-button-is-still-mounted-still-handling-clicks-56ng</link>
      <guid>https://dev.to/chad_priest/your-retired-button-is-still-mounted-still-handling-clicks-56ng</guid>
      <description>&lt;p&gt;The button did nothing. No console error, no failed request, no red toast. I clicked it, and the page went on being a page.&lt;/p&gt;

&lt;p&gt;I had spent the afternoon inside &lt;code&gt;runInject()&lt;/code&gt;: the function that pulls a context block from the local gateway and drops it into whatever composer the host site is using. Edit, reload the extension, reload the tab, click. Nothing. On the third round I put a &lt;code&gt;console.log&lt;/code&gt; on the first line of &lt;code&gt;runInject&lt;/code&gt;. It never fired.&lt;/p&gt;

&lt;p&gt;The button I was clicking was not wired to &lt;code&gt;runInject&lt;/code&gt;. It was &lt;code&gt;cbtn&lt;/code&gt;, the retired "🧠 My context" control from the previous design, and it called &lt;code&gt;requestContext&lt;/code&gt;: the old in-page picker overlay. It had been superseded weeks earlier. It was still mounting on every page load, still painted with the same mark as its replacement, and still perfectly happy to handle a click.&lt;/p&gt;

&lt;h2&gt;
  
  
  Two entry points, one scope, no error
&lt;/h2&gt;

&lt;p&gt;The shape that made it invisible:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// content.js — one IIFE, two mount paths&lt;/span&gt;
&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;mountContextButton&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;window&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;__vodouContextButtonMounted&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="nx"&gt;MOUNT_TOKEN&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nb"&gt;window&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;__vodouContextButtonMounted&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;MOUNT_TOKEN&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;runInject&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;site&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;forceComposer&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;composer&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;onDone&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;manual&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;ctl&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="cm"&gt;/* current */&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;mountFab&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;                       &lt;span class="c1"&gt;// the NEW control&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getElementById&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;vodou-fab-wrap&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="c1"&gt;// ...&lt;/span&gt;
    &lt;span class="nf"&gt;run&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;report&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nf"&gt;runInject&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;site&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nf"&gt;findComposer&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="kc"&gt;undefined&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="nf"&gt;mountFab&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;// ...elsewhere in the same IIFE, outside that closure:&lt;/span&gt;
&lt;span class="nf"&gt;mountCbtn&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;     &lt;span class="c1"&gt;// the OLD control — cannot see runInject, calls requestContext&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;runInject&lt;/code&gt; lives inside &lt;code&gt;mountContextButton()&lt;/code&gt;'s scope. Nothing outside that closure can call it. So the retired control &lt;em&gt;could not&lt;/em&gt; have been calling the current path even if I'd rewired it, and that's the part that took me embarrassingly long: the scope was the proof, sitting right there, and I read it three times as a detail instead of as an answer.&lt;/p&gt;

&lt;p&gt;The mount guards made it worse by working. Ours are versioned on purpose: &lt;code&gt;MOUNT_TOKEN&lt;/code&gt; is the manifest version, so reloading the extension re-arms the guard in already-open tabs instead of hitting a &lt;code&gt;=== true&lt;/code&gt; and returning early. The new control's guard is keyed on its own &lt;code&gt;id&lt;/code&gt;. The old control had its own guard, keyed on its own &lt;code&gt;id&lt;/code&gt;. Both cleared. Both mounted. Neither knew the other existed.&lt;/p&gt;

&lt;h2&gt;
  
  
  The standard advice is about orphaning. This wasn't orphaning.
&lt;/h2&gt;

&lt;p&gt;Every result you get searching for this is about &lt;em&gt;context invalidation&lt;/em&gt;. The classic &lt;a href="https://stackoverflow.com/questions/57468219/how-to-remove-orphaned-script-after-chrome-extension-update" rel="noopener noreferrer"&gt;Stack Overflow thread on orphaned content scripts&lt;/a&gt; and &lt;a href="https://github.com/samber/cc-skills/blob/main/skills/chrome-extension/references/content-scripts.md" rel="noopener noreferrer"&gt;samber/cc-skills' content-scripts reference&lt;/a&gt; both cover it well: after an update the old script keeps running, loses &lt;code&gt;chrome.runtime&lt;/code&gt;, every message throws, so you guard with &lt;code&gt;isExtensionAlive()&lt;/code&gt; and show a refresh banner. &lt;a href="https://auditbuffet.com/patterns/ab-001334" rel="noopener noreferrer"&gt;AuditBuffet's pattern&lt;/a&gt; names the visible symptom exactly right: "ghost UI that does nothing when clicked."&lt;/p&gt;

&lt;p&gt;None of it covers my case. My extension context was fine. &lt;code&gt;chrome.runtime.id&lt;/code&gt; was there. &lt;code&gt;isExtensionAlive()&lt;/code&gt; returned true, messaging worked, and the ghost button was shipping in the &lt;em&gt;current&lt;/em&gt; bundle. There is no invalidation event to hang cleanup off when the thing that retired your code path was a refactor.&lt;/p&gt;

&lt;h2&gt;
  
  
  The class: a retired entry point that still boots
&lt;/h2&gt;

&lt;p&gt;This is not a Chrome bug. It's what happens whenever a new implementation is added beside an old one in overlapping scopes and the old mount is left in the boot sequence: React components whose replacement ships but whose old &lt;code&gt;addEventListener&lt;/code&gt; still runs from a stale &lt;code&gt;useEffect&lt;/code&gt;, embedded JS SDKs (Stripe/Intercom-shaped) where v1's mount call survives in the host's snippet next to v2's, any server-rendered page that emits two &lt;code&gt;&amp;lt;script&amp;gt;&lt;/code&gt; tags for the same widget.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Retiring a code path means deleting its mount call, not deleting its callers: an entry point that still executes at boot is live code, and if its handler is in a different scope from your current implementation, it is by construction calling something else.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Here's the check, in your own console, on your own page. Chrome DevTools only, since &lt;code&gt;getEventListeners&lt;/code&gt; is a console builtin:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;for &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;el&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;querySelectorAll&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;[id*="myprefix"], [class*="myprefix"]&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;el&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nx"&gt;el&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;className&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nf"&gt;getEventListeners&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;el&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Passing looks like one node per control, one &lt;code&gt;click&lt;/code&gt; entry each. Failing looks like two nodes you only rendered once, or one node with two &lt;code&gt;click&lt;/code&gt; listeners. Then expand the listener, click the source link, and read &lt;em&gt;which file and line&lt;/em&gt; it lands on. If that line is inside code you believed you deleted, you just found it. Sixty seconds.&lt;/p&gt;

&lt;h2&gt;
  
  
  The backtick that eats the stylesheet
&lt;/h2&gt;

&lt;p&gt;Second half of the same day, same file. The injected &lt;code&gt;&amp;lt;style&amp;gt;&lt;/code&gt; is a 103-line template literal, &lt;code&gt;content.js:1325-1428&lt;/code&gt;, with ten prose comments inside it explaining why each rule exists. 327 lines earlier there's a JS comment that reads &lt;code&gt;// `manual` = a user-initiated trigger&lt;/code&gt;. Same writing habit, different container. A backtick inside that CSS comment terminates the template early, everything after it parses as JavaScript, and the file fails to parse, so nothing mounts at all, on all 22 hosts. You get &lt;code&gt;Uncaught SyntaxError: Unexpected identifier&lt;/code&gt; pointing at a line nowhere near the backtick you typed.&lt;/p&gt;

&lt;p&gt;The check is a shell loop, and it works on any repo that emits markup from template strings:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="k"&gt;for &lt;/span&gt;f &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="si"&gt;$(&lt;/span&gt;git ls-files &lt;span class="s1"&gt;'*.js'&lt;/span&gt; &lt;span class="s1"&gt;'*.mjs'&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;do &lt;/span&gt;node &lt;span class="nt"&gt;--check&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$f&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"BROKEN: &lt;/span&gt;&lt;span class="nv"&gt;$f&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;done&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Passing output is silence. Failing output is a filename and a line number that will not be where the mistake is: that's the tell, and it's why this takes an hour by eye and four seconds by parser.&lt;/p&gt;

&lt;p&gt;Two rules out of one day. Grep your boot sequence for mount calls before you grep for handlers. And whatever delimiter your template literal uses, it is banned inside that literal, including in the comments, especially in the comments, because comments are where prose goes and prose is where backticks live.&lt;/p&gt;




&lt;p&gt;Source: &lt;a href="https://blog.vodou.ai/your-retired-button-is-still-mounted-still-handling-clicks/" rel="noopener noreferrer"&gt;Your retired button is still mounted, still handling clicks&lt;/a&gt; by Chad Priest, from Building Vodou in Public.&lt;/p&gt;

</description>
      <category>javascript</category>
      <category>chrome</category>
      <category>debugging</category>
      <category>webdev</category>
    </item>
    <item>
      <title>Your isolated test lab isn't isolated: 3 rows, 8 daemons</title>
      <dc:creator>Chad Priest</dc:creator>
      <pubDate>Wed, 26 Aug 2026 17:25:39 +0000</pubDate>
      <link>https://dev.to/chad_priest/your-isolated-test-lab-isnt-isolated-3-rows-8-daemons-4bfp</link>
      <guid>https://dev.to/chad_priest/your-isolated-test-lab-isnt-isolated-3-rows-8-daemons-4bfp</guid>
      <description>&lt;p&gt;I wrote a script whose entire job is to break my system on purpose in a sandbox and print what every surface says. &lt;code&gt;scripts/broken-lab.sh&lt;/code&gt;, 516 lines, committed as &lt;code&gt;ca66f6c8&lt;/code&gt;. It found three real defects in its first hour, which felt great until I looked closer and found that three of the bugs it found were its own.&lt;/p&gt;

&lt;p&gt;It wrote three rows into the live &lt;code&gt;gateway.db&lt;/code&gt;. It health-checked a different process than the one it started. And it leaked eight daemons into a machine-wide process limit that the production stack shares.&lt;/p&gt;

&lt;p&gt;The system under test was fine. The instrument was lying.&lt;/p&gt;

&lt;h2&gt;
  
  
  The harness wrote to production because the app resolved its own path
&lt;/h2&gt;

&lt;p&gt;The gateway reads &lt;code&gt;VODOU_PROJECT_PATH&lt;/code&gt; to decide where its SQLite file lives. What I did not know is that &lt;code&gt;db.ts&lt;/code&gt; only &lt;em&gt;trusts&lt;/em&gt; that variable if the directory already contains &lt;code&gt;vodou-core.db&lt;/code&gt;, and it resolves the root once at module load. My lab set the env var, skipped the baseline step that seeds that file, and the gateway silently fell back to the repo root. Then it staged a graph fan and recorded three runs in the live database: the one file this script promises in its header never to touch.&lt;/p&gt;

&lt;p&gt;Nothing errored. The harness passed. I deleted the rows by hand.&lt;/p&gt;

&lt;p&gt;The fix is not "set the variable more carefully." The fix is that the harness stops believing it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="k"&gt;if &lt;/span&gt;port_is_taken&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;then&lt;/span&gt;
  &lt;span class="c"&gt;# Isolation is asserted, never assumed. If the gateway created its DB&lt;/span&gt;
  &lt;span class="c"&gt;# anywhere but the lab, every later reading describes the live system.&lt;/span&gt;
  &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;[&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt; &lt;span class="nt"&gt;-f&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$LAB&lt;/span&gt;&lt;span class="s2"&gt;/MCP-servers/Vodou-Console/gateway.db"&lt;/span&gt; &lt;span class="o"&gt;]&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;then
    &lt;/span&gt;say &lt;span class="s2"&gt;"ISOLATION FAILED — the lab gateway did not create &lt;/span&gt;&lt;span class="nv"&gt;$LAB&lt;/span&gt;&lt;span class="s2"&gt;/MCP-servers/Vodou-Console/gateway.db."&lt;/span&gt;
    say &lt;span class="s2"&gt;"  It is writing to the LIVE databases. Stopping before anything else runs."&lt;/span&gt;
    lab_gateway_kill
    &lt;span class="k"&gt;return &lt;/span&gt;1
  &lt;span class="k"&gt;fi
  return &lt;/span&gt;0
&lt;span class="k"&gt;fi&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  A 200 on your port is not proof that it's your process
&lt;/h2&gt;

&lt;p&gt;Second bug, and this one cost a full debugging cycle. Startup waited for &lt;code&gt;/api/health&lt;/code&gt; to return 200 and called that success. But a leftover gateway from an earlier lab run still held &lt;code&gt;:8791&lt;/code&gt;, so my new process logged &lt;code&gt;Refusing to start a second instance&lt;/code&gt;, exited, and the health probe cheerfully passed: against the stranger. Every subsequent request hit another lab's database. The new lab's &lt;code&gt;gateway.db&lt;/code&gt; was never created. Then the kill test SIGKILLed a pid that had already died and reported a clean recovery.&lt;/p&gt;

&lt;p&gt;Now the loop checks &lt;code&gt;kill -0 "$pid"&lt;/code&gt; on the pid it spawned before it believes any 200, and aborts outright if the port is taken before boot.&lt;/p&gt;

&lt;p&gt;The third bug: the lab gateway spawns its daemon and worker &lt;em&gt;detached&lt;/em&gt;, from the repo binary, so &lt;code&gt;kill -9 -PGID&lt;/code&gt; misses them and &lt;code&gt;pkill -f "$LAB/vodou-core"&lt;/code&gt; can never match. The lab's path isn't in their argv. Eight orphans accumulated in one afternoon, and since &lt;code&gt;VODOU_MAX_PROCESSES&lt;/code&gt; counts machine-wide, the next run was refused with &lt;code&gt;6 vodou-core processes are already running (limit 5)&lt;/code&gt;. The harness had started starving the machine it promises not to touch. The only place the lab's identity survives is the environment, so that's what the reaper matches now: &lt;code&gt;ps -E -o pid=,command= | grep -F "VODOU_PROJECT_PATH=$LAB"&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Any harness that discovers its dependencies like production will grade production
&lt;/h2&gt;

&lt;p&gt;The class: &lt;strong&gt;any harness that discovers its dependencies the same way production does will eventually grade production.&lt;/strong&gt; Not "uses the same database". &lt;em&gt;discovers&lt;/em&gt; the same way. Config file, env var with a fallback, service discovery, a well-known port, a shared process pool. It shows up as pytest fixtures against the shared dev Postgres, LangChain eval suites pointed at the production vector store by an unset &lt;code&gt;INDEX_NAME&lt;/code&gt;, Compose suites reusing host ports, and any MCP or daemon test that spawns subprocesses with no cleanup barrier.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://raju-shanigarapu.vercel.app/blog/2026-06-18-testcontainers-your-isolated-tests-are-still-lying-to-you" rel="noopener noreferrer"&gt;Testcontainers: Your Isolated Tests Are Still Lying To You&lt;/a&gt; is right that a shared dev database is a solved problem and half-isolation is the real trap. What it misses is my case: a throwaway container guarantees a clean database exists, not that the app under test &lt;em&gt;used&lt;/em&gt; it. My isolation was correct in the harness and discarded by the callee. Closer is &lt;a href="https://dev.to/andreas_huttenrauch/your-test-suite-is-lying-to-you-about-which-process-its-in-39l0"&gt;Your test suite is lying to you about which process it's in&lt;/a&gt;, where a correct guard ran in the wrong process. Same family. My version is worse in one way: I didn't have a guard that ran in the wrong place, I had a probe that couldn't tell processes apart at all.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;A harness must assert on every run that the process answering it is the pid it spawned, and that the writes it made landed under the path it created. Both are resolved by the system under test, not by the harness.&lt;/strong&gt; That's checkable: grep your test setup for a readiness probe with no pid comparison, and for a teardown that trusts argv.&lt;/p&gt;

&lt;p&gt;Five-minute check against your own stack, no Vodou anything:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# 1. Does your suite touch production? Fingerprint before and after.&lt;/span&gt;
psql &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$PROD_URL&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="nt"&gt;-tAc&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="s2"&gt;"select relname, n_live_tup from pg_stat_user_tables order by 1"&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; /tmp/before
pytest &lt;span class="nt"&gt;-q&lt;/span&gt;            &lt;span class="c"&gt;# or npm test, go test ./...&lt;/span&gt;
psql &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$PROD_URL&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="nt"&gt;-tAc&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="s2"&gt;"select relname, n_live_tup from pg_stat_user_tables order by 1"&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; /tmp/after
diff /tmp/before /tmp/after &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;echo &lt;/span&gt;ISOLATED &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"YOUR SUITE WRITES TO PROD"&lt;/span&gt;

&lt;span class="c"&gt;# 2. Is the thing answering your health check the thing you started?&lt;/span&gt;
./run-test-server &amp;amp; &lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="nv"&gt;$!&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; /tmp/mine.pid
curl &lt;span class="nt"&gt;-sf&lt;/span&gt; localhost:8080/health &lt;span class="o"&gt;&amp;gt;&lt;/span&gt;/dev/null &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"health says 200"&lt;/span&gt;
lsof &lt;span class="nt"&gt;-ti&lt;/span&gt; tcp:8080 &lt;span class="nt"&gt;-sTCP&lt;/span&gt;:LISTEN &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; /tmp/listening
&lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-qx&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;cat&lt;/span&gt; /tmp/mine.pid&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; /tmp/listening &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"same process"&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"TESTING A STRANGER: &lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;cat&lt;/span&gt; /tmp/listening&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;

&lt;span class="c"&gt;# 3. Do you leak? Count before and after.&lt;/span&gt;
pgrep &lt;span class="nt"&gt;-fc&lt;/span&gt; myservice   &lt;span class="c"&gt;# run this before the suite, and again after&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Passing looks like: empty diff, "same process", identical counts. Failing looks like a table that grew by three rows nobody can explain, a pid in &lt;code&gt;/tmp/listening&lt;/code&gt; that isn't yours, and a count that climbs one per run until something unrelated starts getting refused. (&lt;code&gt;n_live_tup&lt;/code&gt; is an estimate; for small tables use exact &lt;code&gt;count(*)&lt;/code&gt; per table.)&lt;/p&gt;

&lt;p&gt;The rule I'd give a stranger: a test that cannot prove which process it talked to and which file it wrote has not produced a result, it has produced a sentence. Make isolation an assertion with an abort, not a configuration you set once and trust.&lt;/p&gt;




&lt;p&gt;Source: &lt;a href="https://blog.vodou.ai/your-isolated-test-lab-isnt-isolated/" rel="noopener noreferrer"&gt;Your isolated test lab isn't isolated: 3 rows, 8 daemons&lt;/a&gt; by Chad Priest, from Building Vodou in Public.&lt;/p&gt;

</description>
      <category>testing</category>
      <category>debugging</category>
      <category>sqlite</category>
      <category>devops</category>
    </item>
    <item>
      <title>Why My LLM Agent Fabricated Numbers From Stale Context</title>
      <dc:creator>Chad Priest</dc:creator>
      <pubDate>Wed, 26 Aug 2026 17:19:58 +0000</pubDate>
      <link>https://dev.to/chad_priest/why-my-llm-agent-fabricated-numbers-from-stale-context-561m</link>
      <guid>https://dev.to/chad_priest/why-my-llm-agent-fabricated-numbers-from-stale-context-561m</guid>
      <description>&lt;p&gt;I asked my own system what my CPU was doing. It told me, confidently, with a number. The number was ten minutes old. Nothing had measured anything on that turn.&lt;/p&gt;

&lt;p&gt;Then it did it again in a different shape: it produced a timing table for MCP tool calls that it had reasoned out of the source code, and a set of memory statistics carried forward from an earlier run where &lt;code&gt;get_memory_info&lt;/code&gt; had actually executed. Both were presented as measurements. Neither was measured.&lt;/p&gt;

&lt;p&gt;That is the worst class of bug in an agent product. A crash is honest. This was the system lying to me in my own voice, about my own machine, using data it had legitimately collected at some point in the past.&lt;/p&gt;

&lt;h2&gt;
  
  
  The symptom: replayed tool output presented as a live measurement
&lt;/h2&gt;

&lt;p&gt;Vodou's gateway (&lt;code&gt;MCP-servers/Vodou-Console/&lt;/code&gt;) assembles context for every turn: memory recall from a SQLite store, plus tool output when a query routes to an MCP tool. Tool output goes to the model inside an &lt;code&gt;&amp;lt;active_context&amp;gt;&lt;/code&gt; block.&lt;/p&gt;

&lt;p&gt;There is a deliberate replay path. If you ask "what's my CPU at," get an answer, and then say "huh, that seems high," the second message doesn't route to a tool. It's conversational. So the gateway re-injects the previous turn's tool output so the model can talk about what it just said. Reasonable feature. Here it is, &lt;code&gt;llm.ts:3195&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// BrainLoader skipped (conversational message) — still re-inject stored context if available&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;stored&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;_lastOiContext&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;conversationId&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;stored&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;now&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nx"&gt;stored&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;timestamp&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="mi"&gt;600&lt;/span&gt;&lt;span class="nx"&gt;_000&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;oiResults&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;stored&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;oiResults&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nx"&gt;stored&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;timestamp&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;now&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`[BrainLoader] skipped (conversational) — reusing stored context (&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;oiResults&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; chars)`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If you grep your own logs for &lt;code&gt;reusing stored context&lt;/code&gt;, that line is where I started.&lt;/p&gt;

&lt;h2&gt;
  
  
  Three days blaming the TTL
&lt;/h2&gt;

&lt;p&gt;Look at line four of that block. &lt;code&gt;stored.timestamp = Date.now()&lt;/code&gt;: the sibling site at &lt;code&gt;llm.ts:3099&lt;/code&gt; even labels it &lt;code&gt;// refresh TTL on use&lt;/code&gt;. Touch the cache and its age resets. A ten-minute window that refreshes on every read is not a ten-minute window; in a long conversation it never expires. Stale context reads as fresh forever.&lt;/p&gt;

&lt;p&gt;I was sure that was it. I wrote up a fix: stop refreshing on read, and stamp the replayed block so the model knows what it's holding.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight xml"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;active_context&lt;/span&gt; &lt;span class="na"&gt;fresh=&lt;/span&gt;&lt;span class="s"&gt;"false"&lt;/span&gt; &lt;span class="na"&gt;age_s=&lt;/span&gt;&lt;span class="s"&gt;"412"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Clean. Honest. The model gets provenance, decides for itself, and the whole thing is about fifteen lines.&lt;/p&gt;

&lt;p&gt;I was wrong for three days, and the tell was sitting in the transcript the whole time: on the turn where Vodou reported the stale CPU number, the daemon &lt;em&gt;had&lt;/em&gt; run the tool. Fresh output existed. The unbounded TTL explains how stale data survives. It does not explain how stale data beats fresh data that is present in the same request.&lt;/p&gt;

&lt;p&gt;I also chased a second wrong theory in parallel: that this was ordinary model hallucination and the answer was a stronger anti-hallucination anchor in the system prompt. That turned out to be a real finding but a different bug. Three of the five provider functions get no ground-truth block at all, so BYOK OpenAI and Ollama users are running a materially different product than the managed tier and nothing in the UI says so. Worth fixing. Not this.&lt;/p&gt;

&lt;h2&gt;
  
  
  The measurement that cracked it: count the writers, not the readers
&lt;/h2&gt;

&lt;p&gt;I stopped reading the consumers and enumerated the &lt;em&gt;producers&lt;/em&gt;. Every code path in the entire system that can write text into a model request. One row per site, file and line.&lt;/p&gt;

&lt;p&gt;The plan I was working from budgeted for five. There were seven.&lt;/p&gt;

&lt;p&gt;Number six was the one that mattered. The Rust daemon has its own tool-output channel that I had forgotten about, and it does not write into &lt;code&gt;&amp;lt;active_context&amp;gt;&lt;/code&gt; at all. It writes a &lt;code&gt;### Vodou Tool Results (auto-routed)&lt;/code&gt; heading into the &lt;em&gt;memory&lt;/em&gt; lane, and it has a second cached variant that does the same. Two independent producers, two different lanes, one buffer at the far end.&lt;/p&gt;

&lt;p&gt;(Number seven was the SessionStart hook's workspace bootstrap: AGENTS.md plus MEMORY.md, 25,293 bytes, consistent across sampled sessions, counted by nothing.)&lt;/p&gt;

&lt;p&gt;Once both writers were on the same page, the bug was obvious.&lt;/p&gt;

&lt;h2&gt;
  
  
  Root cause: a guard that tested truthiness instead of provenance
&lt;/h2&gt;

&lt;p&gt;Five sites in &lt;code&gt;llm.ts&lt;/code&gt;, 5437, 5994, 6200, 7099, 7609, one per provider family, contain this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Fix 2: strip tool results block from system prompt — it's already in &amp;lt;active_context&amp;gt; via oiResults&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;memoryForSystem&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;oiResults&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nx"&gt;memoryContext&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="nx"&gt;memoryContext&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;replace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sr"&gt;/### Vodou Tool Results&lt;/span&gt;&lt;span class="se"&gt;[\s\S]&lt;/span&gt;&lt;span class="sr"&gt;+/&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;''&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;trim&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
  &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;memoryContext&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The intent is deduplication. If tool output is already going to the model in &lt;code&gt;&amp;lt;active_context&amp;gt;&lt;/code&gt;, don't also ship the daemon's copy in the system prompt.&lt;/p&gt;

&lt;p&gt;The guard is &lt;code&gt;oiResults &amp;amp;&amp;amp;&lt;/code&gt;. It asks whether cached tool output &lt;em&gt;exists&lt;/em&gt;. It never asks whether that output came from &lt;em&gt;this turn&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;So: conversational message, BrainLoader skipped, replay fills &lt;code&gt;oiResults&lt;/code&gt; from ten minutes ago. Meanwhile the daemon auto-routes the query and puts genuinely fresh tool output into &lt;code&gt;memoryContext&lt;/code&gt;. The strip fires, because &lt;code&gt;oiResults&lt;/code&gt; is a non-empty string. &lt;code&gt;[\s\S]+&lt;/code&gt; eats the fresh block and everything after it. The model receives the ten-minute-old reading and nothing else.&lt;/p&gt;

&lt;p&gt;The comment on that line asserts the deleted content is safely present elsewhere. It isn't. A different, older thing is. The code was working exactly as commented and the comment was describing a state that didn't exist.&lt;/p&gt;

&lt;p&gt;That's the whole bug. Fresh output deleted to make room for stale output, five times, in a line whose comment claims the opposite.&lt;/p&gt;

&lt;h2&gt;
  
  
  The obvious fix is wrong on the OpenAI-compat path
&lt;/h2&gt;

&lt;p&gt;My first instinct was to concatenate the fresh block back onto the system prompt when the guard rejects the replay. On three of the five provider paths, that works.&lt;/p&gt;

&lt;p&gt;On &lt;code&gt;chatWithOpenAICompat&lt;/code&gt; it silently breaks something else, and this is the part I'd have shipped if I hadn't gone reading. That function has a stable-prefix mode where the system prompt is &lt;em&gt;deliberately frozen&lt;/em&gt;:&lt;/p&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;STABLE_PREFIX&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;VODOU_COMPAT_STABLE_PREFIX&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;
  &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;VODOU_COMPAT_STABLE_PREFIX&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;1&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
  &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;currentProvider&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;vodou&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="c1"&gt;// ...&lt;/span&gt;
&lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;STABLE_PREFIX&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;systemPrompt&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;staticParts&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;                  &lt;span class="c1"&gt;// frozen → cacheable prefix&lt;/span&gt;
  &lt;span class="nx"&gt;lateContextBlock&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;memoryForSystem&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="dl"&gt;''&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;    &lt;span class="c1"&gt;// volatile → relocated to a late turn&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The system prompt is &lt;code&gt;staticParts&lt;/code&gt; and nothing else, on purpose, because byte-identical request fronts are what make the provider's prompt cache hit: 87–97% on warm turns with a roughly two-turn warm-up. Anything query-dependent gets relocated into the message array instead, spliced in as a late &lt;code&gt;system&lt;/code&gt; turn just before the current user turn:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;STABLE_PREFIX&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nx"&gt;lateContextBlock&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;insertAt&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;max&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;m&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// before the trailing current-user turn&lt;/span&gt;
  &lt;span class="nx"&gt;m&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;splice&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;insertAt&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;role&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;system&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;content&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;### Relevant context for this turn&lt;/span&gt;&lt;span class="se"&gt;\n\n&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;lateContextBlock&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Append fresh per-turn context to &lt;code&gt;systemPrompt&lt;/code&gt; on that path and you don't lose the context. You lose the cache, on the tier where cost actually matters, and the only symptom is a bill. &lt;code&gt;systemPromptStaticPrefix()&lt;/code&gt; also has two arities across the provider families, which is the second way that "just concatenate it" patch fails to compile in one place and compile-but-misbehave in another.&lt;/p&gt;

&lt;p&gt;That is why the repair isn't merged yet. Five nearly-identical strip sites across five provider functions is roughly 27 edit points for the full set of changes, and I'm not hand-editing 27 sites in a file this hot. The single context-assembly seam gets built first; the strip repair, the replay deletion, and a regression test land in it.&lt;/p&gt;

&lt;p&gt;The regression test is the part I'd write first if I were you: on a turn where the daemon auto-routes but the gateway's own router does not match, assert that the fresh &lt;code&gt;### Vodou Tool Results&lt;/code&gt; block reaches the model. That assertion fails today.&lt;/p&gt;

&lt;p&gt;The policy change that came out of it is one line: &lt;strong&gt;replay instructions, never replay measurements.&lt;/strong&gt; Skill text, menus, operating rules. Safe to re-inject, they don't decay. A CPU reading, a row count, a disk figure: those are true at an instant and lies forever after. That distinction also killed 341 entries in my conflicts queue that turned out to be CPU readings from different runs disagreeing with each other, which is not a contradiction, just two clocks.&lt;/p&gt;

&lt;p&gt;Here's the rule I'd hand to a stranger. &lt;strong&gt;If two code paths can write to the same buffer, every dedupe guard between them must key on provenance, not existence.&lt;/strong&gt; &lt;code&gt;if (cached)&lt;/code&gt; is not a freshness check. &lt;code&gt;if (cached.turnId === currentTurnId)&lt;/code&gt; is. And before you tune any of the readers, enumerate the writers with file and line numbers, because the number is higher than your architecture diagram says. Mine said five.&lt;/p&gt;




&lt;p&gt;Source: &lt;a href="https://blog.vodou.ai/why-my-llm-agent-fabricated-numbers-from-stale-context/" rel="noopener noreferrer"&gt;Why My LLM Agent Fabricated Numbers From Stale Context&lt;/a&gt; by Chad Priest, from Building Vodou in Public.&lt;/p&gt;

</description>
      <category>typescript</category>
      <category>llm</category>
      <category>debugging</category>
      <category>caching</category>
    </item>
    <item>
      <title>Every copy of the rule agreed. That was the bug.</title>
      <dc:creator>Chad Priest</dc:creator>
      <pubDate>Wed, 26 Aug 2026 17:19:17 +0000</pubDate>
      <link>https://dev.to/chad_priest/every-copy-of-the-rule-agreed-that-was-the-bug-560b</link>
      <guid>https://dev.to/chad_priest/every-copy-of-the-rule-agreed-that-was-the-bug-560b</guid>
      <description>&lt;p&gt;Any AI system past a certain size has the same rule written down in more than one place. A slug format. A status word. A receipt string. A retry policy. You wrote it twice because the two surfaces are in different languages, or different processes, or one of them ships to a browser and the other doesn't. You left a comment saying &lt;code&gt;MUST match&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Both copies agree today. That is not a contract. That is a coincidence you have not audited yet.&lt;/p&gt;

&lt;p&gt;I spent three days in August auditing every one of those coincidences in Vodou. Thirty-two commits, eighty-seven files. Almost none of it was new features. The interesting part is what the audit found, and how much of it no test could have caught.&lt;/p&gt;

&lt;h2&gt;
  
  
  A finding is a number in FINDINGS.md, and walks are immutable
&lt;/h2&gt;

&lt;p&gt;The unit of work is a &lt;em&gt;finding&lt;/em&gt;. Someone walks a flow end to end as a user would, writes down everything that contradicts something else, and the contradictions land in a &lt;code&gt;FINDINGS.md&lt;/code&gt; with a number. Walks are immutable once written. Fixes cite the number in the commit subject: &lt;code&gt;fix(coherence): F13 — the @doc: token is minted once, by the code that resolves it&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;There is no coherence module. No &lt;code&gt;vodou-core coherence&lt;/code&gt; command. No scheduled job. It is a documents-and-commits discipline, and the only executable parts are at the two ends: a lab that breaks the product on purpose, and a commit guard that refuses the specific defects the audit proved were real.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Faebzhf82sn03z0dvuuoe.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Faebzhf82sn03z0dvuuoe.png" alt="A coherence finding moves from a walk through findings, fix, and either a guard rule or an instrument" width="800" height="269"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;What happens to a finding&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The slug rule was written four times, the fifth copy was the test
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;The slug rule was written four times, and the fifth copy was the test.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Vodou resolves &lt;code&gt;@doc:contract-2026&lt;/code&gt; tokens to attached documents. The slug rule, how a filename becomes a token, lived in the resolver, in the library page, and twice inline in the browser panel, once for the documents lane and once for the page-memory lane. I had expected two copies. There were four.&lt;/p&gt;

&lt;p&gt;The fifth was in the end-to-end test, in a comment that read &lt;em&gt;verbatim from sidepanel.js&lt;/em&gt;. That test could only ever prove that two copies of one expression agree with each other. It had passed every day of its life and was structurally incapable of failing for the reason it existed.&lt;/p&gt;

&lt;p&gt;All four product copies agreed. That is the whole problem. The day one drifts, the failure is silent and total: a token minted one way and resolved another names a document that does not exist, so the system tells you it attached your contract and then answers about something else.&lt;/p&gt;

&lt;p&gt;Synchronising the copies would have been the wrong fix. I deleted the deciders instead. &lt;code&gt;/api/library&lt;/code&gt;, &lt;code&gt;/api/library/match&lt;/code&gt; and &lt;code&gt;/api/page-match&lt;/code&gt; now mint the slug with the resolver's own function and ship it on the row. The surfaces paste what they were handed. Where a client needs a fallback, it falls back to the id, which the resolver already accepts.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;A test that goes red because the code improved is measuring the wrong thing.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The same week I found three byte-identical copies of the phrase &lt;code&gt;4 memories · 2 tools · 1 skill&lt;/code&gt;: one in the content script injected into ChatGPT, one in the panel, one in Console Two. Each built the counting and the pluralisation itself.&lt;/p&gt;

&lt;p&gt;There was already a parity test for this. It grepped the panel's render function for three ternaries. When I moved those rules into a shared &lt;code&gt;receipt.js&lt;/code&gt;, the test went red, not because the receipt was wrong, but because the ternaries had moved somewhere better. I had written a test that asserted the location of a bug's workaround. It had to be replaced, not repaired.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Ftfedcu1qohok6zxmn651.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Ftfedcu1qohok6zxmn651.png" alt="Copies of a single rule found in the codebase: four for the doc slug, three for the receipt phrase" width="800" height="153"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Independent implementations of one rule&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The lab I built to test broken states was not isolated.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Phase 0 was &lt;code&gt;scripts/broken-lab.sh&lt;/code&gt;: induce &lt;code&gt;daemon-down&lt;/code&gt;, &lt;code&gt;empty-account&lt;/code&gt;, &lt;code&gt;unreadable-db&lt;/code&gt;, &lt;code&gt;no-memory&lt;/code&gt; in a throwaway instance, then print what every surface says in each, verbatim and side by side. It found three real defects in the first hour. The best one: a status command was reporting the health of a &lt;em&gt;different installation&lt;/em&gt; entirely, so it answered "healthy" for a stack that was face down.&lt;/p&gt;

&lt;p&gt;The first version of the lab also wrote to the live database, health-checked a stranger, and leaked eight daemons. A harness that claims isolation has to prove it on every run, not in its README. It now owns its project root, its DBs and its socket, calls no start or stop script, and kills no pid it did not spawn.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;One word meant five things, and two of my renames were wrong.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The vocabulary pass was the one I thought would be mechanical. It was not. Two of the renames changed a word that was correct in its context, and I had to back them out. A separate finding, F43, turned out to be false: the contract I claimed was missing already existed, and what had actually failed was the guard's ability to see it. Another was already fixed months earlier, and the only real defect left was a report implying otherwise.&lt;/p&gt;

&lt;p&gt;Roughly one finding in six did not survive contact with the code. If your audit has a 100% confirmation rate, you are writing down conclusions, not observations.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Two questions rode one exit code.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;code&gt;runtime-status&lt;/code&gt; printed &lt;code&gt;Overall: down&lt;/code&gt; and exited 0. &lt;code&gt;mem search&lt;/code&gt; exited 1 for the same condition. So &lt;code&gt;vodou-core runtime-status &amp;amp;&amp;amp; start_work&lt;/code&gt; walked straight into a dead stack.&lt;/p&gt;

&lt;p&gt;They disagreed because the exit code was carrying two different questions, &lt;em&gt;did the command work&lt;/em&gt; and &lt;em&gt;is the subject healthy&lt;/em&gt;, which are not the same answer for a reporting command. They now get separate codes, the way systemctl has always done it.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# 0 = healthy   2 = reported, and unhealthy   1 = the command itself failed&lt;/span&gt;
vodou-core runtime-status&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="nv"&gt;$?&lt;/span&gt; &lt;span class="k"&gt;in
  &lt;/span&gt;0&lt;span class="p"&gt;)&lt;/span&gt; start_work &lt;span class="p"&gt;;;&lt;/span&gt;
  2&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"stack is down; the report is trustworthy"&lt;/span&gt; &lt;span class="p"&gt;;;&lt;/span&gt;
  1&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"could not determine anything"&lt;/span&gt; &lt;span class="p"&gt;;;&lt;/span&gt;
&lt;span class="k"&gt;esac&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;An unknown verdict is never healthy. Finding the two existing callers and teaching them this was half the work: both read any non-zero as "the probe is broken", so one printed a fallback one-liner &lt;em&gt;underneath&lt;/em&gt; a complete report, and the other discarded the payload and said "unavailable" for a stack that had just described itself in detail.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The version string was never going to answer "which build is this".&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;We once lost 38 hours to a daemon quietly running the previous build. Both sides reported the same version. Three of our extension folders still do. What actually discriminates a build is the file it was loaded from, so that is what every long-lived process now reports: path, size, mtime, and whether debug assertions are compiled in. One command cross-checks them.&lt;/p&gt;

&lt;p&gt;The ordering is load-bearing and easy to get backwards. The binary swap does &lt;code&gt;rm&lt;/code&gt; then &lt;code&gt;cp&lt;/code&gt;, so an identity read lazily at status time would describe the &lt;em&gt;replacement&lt;/em&gt; and report a stale process as current. It has to be stamped at process start or it is worse than nothing.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Frk5tmbwr3thhs7zf6hum.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Frk5tmbwr3thhs7zf6hum.png" alt="Three days of the audit, including a dead end and two wrong findings" width="800" height="156"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  More than one implementation is an outage with a delay fuse
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;A rule with more than one implementation is an outage with a delay fuse.&lt;/strong&gt; The copies agree right up until someone improves one of them, and improvement is exactly what your review process rewards. So the fix is never "sync the copies". It is: delete every decider but one, and have the others paste what they were handed.&lt;/p&gt;

&lt;p&gt;Where you genuinely cannot share code, different language, different runtime, ships to a different place, you do not get a contract, so buy the next best thing: a test that reads &lt;em&gt;both&lt;/em&gt; implementations from their real locations and asserts they produce the same output for the same input. Not one that greps one of them.&lt;/p&gt;

&lt;p&gt;And check what your parity tests actually compare. If a test's fixture was copy-pasted from the implementation, it is a mirror, not a check.&lt;/p&gt;

&lt;h2&gt;
  
  
  Four slug copies lived under a comment saying MUST match
&lt;/h2&gt;

&lt;p&gt;The instinct is to solve this with review prose: a &lt;code&gt;CONVENTIONS.md&lt;/code&gt;, a comment reading &lt;code&gt;MUST match&lt;/code&gt;, a checklist item. We had all three. The four slug copies were written &lt;em&gt;under&lt;/em&gt; a comment saying they must match.&lt;/p&gt;

&lt;p&gt;The working idiom in this repo is commit guards, and the audit's mechanical rules became one: &lt;code&gt;scripts/coherence-guard.py&lt;/code&gt;, with &lt;code&gt;scripts/test-coherence-guard.sh&lt;/code&gt; covering eleven hermetic cases. It reads staged added lines on shipping surfaces only, and blocks three things the audit proved were real: an internal enum reaching &lt;code&gt;textContent&lt;/code&gt; untranslated; a &lt;code&gt;*_count&lt;/code&gt; that is displayed but that nothing ever writes; an iframe framing one of our own surfaces with no declared min-width. When a flag is deliberate you say so in the diff, in a comment the guard reads.&lt;/p&gt;

&lt;p&gt;The second half is measurement, because some rules are not statically checkable. Grading has to compare two records of one event: capture-to-searchable latency per lane, a receipt's claimed count against the ids it actually recorded, &lt;code&gt;last_run_at&lt;/code&gt; against the newest run row. A grader with no evidence answers &lt;code&gt;unknown&lt;/code&gt;, never &lt;code&gt;ok&lt;/code&gt;. That distinction is the whole point. "Nobody looked" and "fine" are different states, and only one of them is safe to ship on.&lt;/p&gt;

&lt;h2&gt;
  
  
  Console Two cannot import the shared receipt module
&lt;/h2&gt;

&lt;p&gt;Console Two is a separate codebase and cannot import the shared receipt module, so that phrase still has two homes and a parity test standing between them.&lt;/p&gt;

&lt;p&gt;The guard only sees staged added lines on four directory trees. Move a violation into an unlisted path and it passes.&lt;/p&gt;

&lt;p&gt;Committed source and the committed binary in this repo drift apart. A fix can be at HEAD and still not be what the machine runs. Build identity makes that visible now; it does not make it stop.&lt;/p&gt;

&lt;p&gt;And one measurement I would rather not have: an extraction rule nobody had ever checked is holding at 30%. It is written down, numbered, and open.&lt;/p&gt;




&lt;p&gt;Source: &lt;a href="https://blog.vodou.ai/vocabulary-drift-in-agent-systems/" rel="noopener noreferrer"&gt;Every copy of the rule agreed. That was the bug.&lt;/a&gt; by Chad Priest, from Building Vodou in Public.&lt;/p&gt;

</description>
      <category>aiagents</category>
      <category>architecture</category>
      <category>observability</category>
      <category>mcp</category>
    </item>
    <item>
      <title>SQLite FTS5 quietly ANDs your search terms</title>
      <dc:creator>Chad Priest</dc:creator>
      <pubDate>Wed, 26 Aug 2026 17:13:35 +0000</pubDate>
      <link>https://dev.to/chad_priest/sqlite-fts5-quietly-ands-your-search-terms-dko</link>
      <guid>https://dev.to/chad_priest/sqlite-fts5-quietly-ands-your-search-terms-dko</guid>
      <description>&lt;p&gt;Someone asked my search box a normal human question and got back &lt;code&gt;{"results":[]}&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Not an error. Not a timeout. An empty array, in 8ms, with a 200. The corpus it&lt;br&gt;
searched has 52,058 chunks in it and definitely contains the answer. I know it&lt;br&gt;
does, because a two-word version of the same question returned twenty hits.&lt;/p&gt;

&lt;p&gt;I spent the first twenty minutes assuming this was a ranking problem, because&lt;br&gt;
that's the interesting kind of problem and I wanted to have it. It was not a&lt;br&gt;
ranking problem. It was one space character.&lt;/p&gt;
&lt;h2&gt;
  
  
  The symptom: results decay to zero as the query gets longer
&lt;/h2&gt;

&lt;p&gt;Here's the thing that made it obvious, once I stopped theorizing and just ran&lt;br&gt;
the same query at increasing lengths:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Query&lt;/th&gt;
&lt;th&gt;Hits&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;daemon&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;1,521&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;daemon socket&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;162&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;daemon socket memory&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;153&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;daemon socket memory search&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;27&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;daemon socket memory search Tuesday&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;0&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Monotonic decay. Every word you add can only ever remove results. That is not&lt;br&gt;
what relevance looks like: relevance is noisy, it goes up and down. A clean&lt;br&gt;
staircase to zero is a &lt;em&gt;set intersection&lt;/em&gt;, and set intersections come from&lt;br&gt;
&lt;code&gt;AND&lt;/code&gt;.&lt;/p&gt;
&lt;h2&gt;
  
  
  The cause: space-joined tokens are an implicit AND
&lt;/h2&gt;

&lt;p&gt;The query builder was doing something that reads as completely reasonable:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;tokens&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;q&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;split&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sr"&gt;/&lt;/span&gt;&lt;span class="se"&gt;\s&lt;/span&gt;&lt;span class="sr"&gt;+/&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;t&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;t&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;replace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sr"&gt;/&lt;/span&gt;&lt;span class="se"&gt;[^\w&lt;/span&gt;&lt;span class="sr"&gt;'-&lt;/span&gt;&lt;span class="se"&gt;]&lt;/span&gt;&lt;span class="sr"&gt;/g&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;''&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
  &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;filter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;Boolean&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;t&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s2"&gt;`"&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;t&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;// ...&lt;/span&gt;
&lt;span class="nx"&gt;WHERE&lt;/span&gt; &lt;span class="nx"&gt;memory_fts&lt;/span&gt; &lt;span class="nx"&gt;MATCH&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt;   &lt;span class="c1"&gt;// tokens.join(' ')&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Split the query, strip punctuation so it can't blow up the parser, quote each&lt;br&gt;
token so a stray &lt;code&gt;OR&lt;/code&gt; or &lt;code&gt;NEAR&lt;/code&gt; in user text is treated as a literal, join it&lt;br&gt;
back together. Four defensive decisions, all individually correct.&lt;/p&gt;

&lt;p&gt;The problem is the last one. In FTS5, whitespace between two terms is the&lt;br&gt;
&lt;strong&gt;implicit AND operator&lt;/strong&gt;. &lt;code&gt;"daemon" "socket"&lt;/code&gt; doesn't mean "look for these two&lt;br&gt;
words." It means "return only rows containing both." That's &lt;a href="https://www.sqlite.org/fts5.html#full_text_query_syntax" rel="noopener noreferrer"&gt;documented FTS5&lt;br&gt;
behavior&lt;/a&gt; and it's the&lt;br&gt;
right default for a query language. It is an absolutely terrible default for a&lt;br&gt;
search box, because a search box receives &lt;em&gt;sentences&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;Do the math on a real query. Someone types:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;I was thinking about the deploy pipeline yesterday and also need to remember&lt;br&gt;
to renew the domain on Tuesday, plus what was that socket timeout we hit, and&lt;br&gt;
can you check the graph frontend branch&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;That's 33 tokens. For that to return anything, one single document has to&lt;br&gt;
contain all 33 of those words. &lt;code&gt;deploy&lt;/code&gt; and &lt;code&gt;domain&lt;/code&gt; and &lt;code&gt;socket&lt;/code&gt; and&lt;br&gt;
&lt;code&gt;branch&lt;/code&gt;, in one chunk. No such document exists in any corpus, which means this&lt;br&gt;
query returns zero results, and so does every other query of that shape. The&lt;br&gt;
failure rate isn't "sometimes": it's &lt;strong&gt;100% of prose queries&lt;/strong&gt;, forever, with&lt;br&gt;
no error message.&lt;/p&gt;

&lt;p&gt;The reason it survived so long is that it works beautifully for the case you&lt;br&gt;
test it on. You type &lt;code&gt;daemon&lt;/code&gt; and get results. You type &lt;code&gt;dogs name&lt;/code&gt; and get&lt;br&gt;
one stray hit, and you assume the data is missing.&lt;/p&gt;
&lt;h2&gt;
  
  
  The second bug hiding under the first: &lt;code&gt;dogs&lt;/code&gt; is not &lt;code&gt;dog's&lt;/code&gt;
&lt;/h2&gt;

&lt;p&gt;While measuring the above, I hit a separate cliff:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;dog          → 94 hits
dog's name   → 23 hits
dogs name    →  1 hit
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The sanitizer keeps apostrophes (&lt;code&gt;[^\w'-]&lt;/code&gt;), which is deliberate: you want&lt;br&gt;
&lt;code&gt;don't&lt;/code&gt; to survive as one token. But that means &lt;code&gt;dogs&lt;/code&gt; and &lt;code&gt;dog's&lt;/code&gt; are two&lt;br&gt;
different tokens, and only one of them is meaningfully in the index. Nobody types the&lt;br&gt;
apostrophe. Every real user query is the broken spelling.&lt;/p&gt;

&lt;p&gt;Two independent near-zero paths, both invisible, both looking exactly like&lt;br&gt;
"we never stored that."&lt;/p&gt;
&lt;h2&gt;
  
  
  The debugging lesson: measure the shape, not the instance
&lt;/h2&gt;

&lt;p&gt;The thing that actually cracked this wasn't reading code. I read that code twice&lt;br&gt;
and it looked fine both times, because each line &lt;em&gt;is&lt;/em&gt; fine.&lt;/p&gt;

&lt;p&gt;What cracked it was running the query five times at five lengths and looking at&lt;br&gt;
the shape of the curve. A ranking bug produces noise. A tokenizer bug produces a&lt;br&gt;
cliff. An AND bug produces a staircase. You can identify the class of failure&lt;br&gt;
from the shape before you know anything about the cause, and that tells you&lt;br&gt;
which file to open.&lt;/p&gt;

&lt;p&gt;I'd generalize it like this: &lt;strong&gt;when a query returns nothing, don't debug the&lt;br&gt;
query: debug the family it belongs to.&lt;/strong&gt; Vary one dimension (length, casing,&lt;br&gt;
punctuation, term rarity) and plot it. Five curl calls got me to the right line&lt;br&gt;
faster than an hour of reading would have.&lt;/p&gt;
&lt;h2&gt;
  
  
  The fix: tokens.join(' OR ') alone trades zero hits for garbage
&lt;/h2&gt;

&lt;p&gt;The obvious patch is &lt;code&gt;tokens.join(' OR ')&lt;/code&gt;. Don't ship only that: you'll trade&lt;br&gt;
zero results for garbage, because now a stopword like &lt;code&gt;the&lt;/code&gt; pulls in half the&lt;br&gt;
corpus and the top hit is whatever document says "the" the most.&lt;/p&gt;

&lt;p&gt;What you want is a tiered fallback:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// 1. Try AND — if the user typed 2-3 precise terms, this is the best answer.&lt;/span&gt;
&lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;rows&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;search&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;tokens&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt; &lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;

&lt;span class="c1"&gt;// 2. Zero hits? Fall back to OR, but cap tokens and drop stopwords first,&lt;/span&gt;
&lt;span class="c1"&gt;//    so ranking has something to work with instead of everything.&lt;/span&gt;
&lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;rows&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;terms&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;tokens&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;filter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;t&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;STOP&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;has&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;t&lt;/span&gt;&lt;span class="p"&gt;)).&lt;/span&gt;&lt;span class="nf"&gt;slice&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;8&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nx"&gt;rows&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;search&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;terms&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt; OR &lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;AND-first preserves precision for the short queries where precision is what you&lt;br&gt;
want. OR-fallback means a sentence degrades into &lt;em&gt;worse ranking&lt;/em&gt; instead of&lt;br&gt;
&lt;em&gt;no results&lt;/em&gt;. Those two failure modes feel completely different to a user: one&lt;br&gt;
is a search engine having an off day, the other is a broken product.&lt;/p&gt;

&lt;p&gt;And normalize possessives at index and query time both, or you will keep&lt;br&gt;
shipping the &lt;code&gt;dogs&lt;/code&gt; bug in new places.&lt;/p&gt;

&lt;h2&gt;
  
  
  Zero results as the query grows means intersection, not search
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;If a search interface returns zero results more often as the input gets&lt;br&gt;
longer, it is doing set intersection and calling it search.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Any search box wired directly to &lt;code&gt;MATCH&lt;/code&gt; has this bug. It will pass every test&lt;br&gt;
you write, because you write tests with keywords, and it will fail every query&lt;br&gt;
a human types, because humans type sentences. Go paste a full sentence into your&lt;br&gt;
own search box right now. If you get an empty state, you know what to look for.&lt;/p&gt;




&lt;p&gt;Source: &lt;a href="https://blog.vodou.ai/sqlite-fts5-ands-your-search-terms/" rel="noopener noreferrer"&gt;SQLite FTS5 quietly ANDs your search terms&lt;/a&gt; by Chad Priest, from Building Vodou in Public.&lt;/p&gt;

</description>
      <category>sqlite</category>
      <category>search</category>
      <category>debugging</category>
      <category>javascript</category>
    </item>
    <item>
      <title>A silent failure looks exactly like a feature you never built</title>
      <dc:creator>Chad Priest</dc:creator>
      <pubDate>Wed, 26 Aug 2026 17:12:54 +0000</pubDate>
      <link>https://dev.to/chad_priest/a-silent-failure-looks-exactly-like-a-feature-you-never-built-23n2</link>
      <guid>https://dev.to/chad_priest/a-silent-failure-looks-exactly-like-a-feature-you-never-built-23n2</guid>
      <description>&lt;p&gt;Your system has memory. Your user is typing into someone else's chat box.&lt;/p&gt;

&lt;p&gt;That gap is the whole problem, and it is not a retrieval problem. The place your context needs to arrive is a page you do not control, rendered by a company that has no interest in your product, whose DOM changes on their schedule. Every design you can reach for there is bad in a different way. A browser popup closes the instant the user clicks back into the page, which is the exact moment they need it. In-page injection means you are a permanent guest, mounting your UI into a tree the host owns and re-mounting it every time the host re-renders.&lt;/p&gt;

&lt;p&gt;We ran the second option for months. The memory picker was injected in-page, with a 3-second remount timer, on all 22 chat hosts we support. It worked. It also meant that every user with the extension installed had a timer firing four times a minute on every AI site they visited, forever, to fight a re-render that usually never came.&lt;/p&gt;

&lt;p&gt;Between 2026-07-29 and 2026-08-02 I moved the whole surface into Chrome's side panel and retired the popup. 45 commits, 43 files. Most of them were not the feature.&lt;/p&gt;

&lt;h2&gt;
  
  
  The popup is gone: one panel holds Memory, Activity, Settings
&lt;/h2&gt;

&lt;p&gt;The toolbar icon now opens a side panel instead of a popup. The panel holds three views: Memory, Activity, Settings. The memory picker lives there, so the 22-host timer is gone. Pairing, connection state, gateway config and import all moved in, so there is one surface instead of a popup that could only be six inches tall and an options page nobody found.&lt;/p&gt;

&lt;p&gt;The panel persists while you type. That is the entire reason it exists.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fjdw35z4zeupzajerlplv.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fjdw35z4zeupzajerlplv.png" alt="Before: popup closes on blur and the picker is injected into every host page with a 3-second remount timer. After: a side panel that stays open while you type, with no in-page timer." width="800" height="233"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The content script still has to exist, because inserting text into the composer and reading the reply out of it can only happen in the page. But it no longer owns any UI worth keeping alive.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F0il3mc67yr7lv6mfjqae.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F0il3mc67yr7lv6mfjqae.png" alt="Toolbar click or keyboard command goes to the background service worker, which opens the side panel; the panel reads storage and messages the worker, which holds the socket to the local engine; a separate thin content script runs inside the host chat page" width="800" height="221"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Where the surface sits&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The spike: can chrome.sidePanel.open() fire from a commands handler
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F4u0menuaiqxhba5o4ndj.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F4u0menuaiqxhba5o4ndj.png" alt="Five days of work: a spike, a gesture bug, a Mac shortcut that was bound to nothing, a version collision across three builds, an audit that found leaks and dead code, and a 22-site selector sweep" width="800" height="173"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The spike existed to make an unknown observable.&lt;/strong&gt; Before designing anything around the side panel, I needed one fact: does &lt;code&gt;chrome.sidePanel.open()&lt;/code&gt; work from a &lt;code&gt;commands&lt;/code&gt; keyboard handler, or does Chrome insist on a click? I wired both paths in a throwaway build and made both fail loudly, because a silent failure there is indistinguishable from "the shortcut isn't bound," and we would have re-derived the same question the next session. That instinct was right, and it is the only reason the next bug took an hour instead of a day.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The gesture does not survive an await.&lt;/strong&gt; &lt;code&gt;sidePanel.open()&lt;/code&gt; may only be called in response to a user action, and Chrome's user-gesture flag is consumed the moment you yield. My first version awaited &lt;code&gt;setOptions()&lt;/code&gt; and then called &lt;code&gt;open()&lt;/code&gt;. It threw every single time. There were two awaits in the way, not one: the command listener was &lt;code&gt;async&lt;/code&gt; and fell back to &lt;code&gt;await chrome.tabs.query(...)&lt;/code&gt; when it had no tab.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// wrong: the gesture is spent before open() runs&lt;/span&gt;
&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;openPanel&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;tabId&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;chrome&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;sidePanel&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setOptions&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;tabId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;path&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;sidepanel.html&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;chrome&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;sidePanel&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;open&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;tabId&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;   &lt;span class="c1"&gt;// "may only be called in response to a user action"&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;// right: privileged call first, bookkeeping after&lt;/span&gt;
&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;openPanel&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;tabId&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;chrome&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;sidePanel&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;open&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;tabId&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;                 &lt;span class="c1"&gt;// synchronous entry, first async thing&lt;/span&gt;
  &lt;span class="nx"&gt;chrome&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;sidePanel&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setOptions&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;tabId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;path&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;sidepanel.html&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;catch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;err&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;[vodou] setOptions&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The command listener is now synchronous and uses the tab the event hands it. If a tab is ever missing it logs loudly instead of awaiting a query, because the await is precisely what breaks it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;A shortcut can be bound to nothing and look bound.&lt;/strong&gt; I shipped &lt;code&gt;"mac": "Ctrl+Shift+M"&lt;/code&gt;. Chrome converts a bare &lt;code&gt;Ctrl&lt;/code&gt; in a mac &lt;code&gt;suggested_key&lt;/code&gt; into Command. So the toggle became Cmd+Shift+M, which is Chrome's own profile switcher and cannot be overridden. Nothing was bound to literal Control+Shift+M, so pressing it did exactly nothing. No error, no log line, no entry anywhere a user could see. The second one was worse: &lt;code&gt;inject-context&lt;/code&gt; became Cmd+B, which is bold on all 22 sites, and &lt;code&gt;content.js&lt;/code&gt; had carried &lt;code&gt;e.ctrlKey &amp;amp;&amp;amp; !e.metaKey&lt;/code&gt; for months with the comment &lt;em&gt;"Cmd+B stays the site's bold on macOS."&lt;/em&gt; I shipped a manifest that contradicted my own code's comment. Had Chrome actually bound it, Ctrl+B users would have kept working while bold quietly stopped functioning everywhere.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"commands"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"toggle-side-panel"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"suggested_key"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"default"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Ctrl+Shift+M"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"mac"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"MacCtrl+Shift+M"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;MacCtrl&lt;/code&gt; is the literal Control key, which is what the content-script gate tests for. A test now rejects a bare &lt;code&gt;Ctrl&lt;/code&gt; in a mac key.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Three builds claimed one version line.&lt;/strong&gt; We ship a store build, a sideload build, and a sideload-only build. Each got bumped by whatever work happened to touch it, so they had drifted to .10, .9 and .8. The spike landed in sideload at .9 while the store build sat at .10 without the side panel, which means the higher number was the older code. That is backwards for the one thing a version is for. Worse, &lt;code&gt;dist/&lt;/code&gt; zips are named by version, and a &lt;code&gt;0.5.97.9-store.zip&lt;/code&gt; already existed from a different package. One version string naming two artifacts is how you upload the wrong zip to Google. The fix was not a numbering convention. It was porting the panel into all three builds so that equal versions mean equal features, then setting all three to 0.5.97.11.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Then the stupid ones, which cost the most wall-clock.&lt;/strong&gt; A backtick inside a CSS comment killed the entire content script, because the stylesheet lived in a template literal. The resting state of our mark rendered as a 42x40 oval, twice, because BIMI's viewBox cropped it. &lt;code&gt;content.js&lt;/code&gt; kept throwing after every extension reload, because it was still evaluated in a dead extension context. We sent on a socket that had closed during a &lt;code&gt;chrome.tabs.query&lt;/code&gt;. One out-of-order &lt;code&gt;const&lt;/code&gt; removed every in-page button on every site.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The audit found what the feature work hid.&lt;/strong&gt; Before submission I read the diff cold and found two leaks, a keystroke hijack and around 450 dead lines left over from the popup era. The copy said "your chats never leave your computer," which was false as written, so it changed. And in the same sweep I discovered I had deleted the activity log's only write. The Activity view looked completely healthy the whole time, because it was rendering rows written before the deletion.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Capture across 22 sites is a fixture farm.&lt;/strong&gt; One parameterized extractor covers 20 of them, with per-site save selectors verified one at a time. A Kimi reply was stored as fragments while the console reported success. Auto-attach at send missed Perplexity's send button, then Kimi's, then Kimi's disabled state, then needed a per-site override for Manus, and finally a guarded positional fallback for buttons with no stable marker. NotebookLM shipped with an invalid Chrome match pattern. OpenRouter needed incremental scroll-and-collect before it worked at all. That got us to 20 of 22.&lt;/p&gt;

&lt;h2&gt;
  
  
  The lesson: a silent failure is indistinguishable from a missing feature
&lt;/h2&gt;

&lt;p&gt;Every expensive bug on this list has one shape. Something did not happen, and nothing said so.&lt;/p&gt;

&lt;p&gt;The shortcut fired into an unbound key. The panel refused to open with an error only the console saw. The activity feed rendered stale rows after its writer was deleted. Kimi's capture reported success and stored splinters. The version number said the store build had the newer code.&lt;/p&gt;

&lt;p&gt;This is the dominant failure class in agent systems, not just browser extensions. A tool call that returns 200 and writes nothing. A retrieval that returns an empty list because the index is empty, which is byte-identical to a retrieval that correctly found no matches. A guard that fails open on timeout. In all of them the system's own report is consistent, and the only way to know is to compare two independent records of the same event.&lt;/p&gt;

&lt;p&gt;Three things you can do today, on any stack:&lt;/p&gt;

&lt;p&gt;Assert the effect, not the call. After a tool run, read back the row you claim to have written and compare it to what you sent. We now grade capture by comparing a receipt's count against the ids it actually recorded, because a count is written by the same code path that is lying to you.&lt;/p&gt;

&lt;p&gt;Make absence a distinct state from failure. &lt;code&gt;unknown&lt;/code&gt; is a valid answer and it is not &lt;code&gt;ok&lt;/code&gt;. A health check with no evidence should report that it had no evidence.&lt;/p&gt;

&lt;p&gt;Spend a half-day making a gate observable before you build on it. That is what the spike was. It cost four hours and it is the reason the gesture bug was diagnosable from my own code rather than from a user report three weeks later.&lt;/p&gt;

&lt;p&gt;The narrower lesson, which generalizes further than it looks: authority is time-scoped. Chrome grants the gesture for the synchronous duration of your handler and &lt;code&gt;await&lt;/code&gt; spends it. The same shape shows up in approval windows, short-lived tokens and transaction contexts. Do the privileged thing first. Do the bookkeeping after, with its own error handler.&lt;/p&gt;

&lt;h2&gt;
  
  
  Building Effective AI Agents is about the loop, not the browser surface
&lt;/h2&gt;

&lt;p&gt;The good guidance on agent design is about the loop. Anthropic's &lt;a href="https://www.anthropic.com/engineering/building-effective-agents" rel="noopener noreferrer"&gt;Building Effective AI Agents&lt;/a&gt; is right that the successful implementations use simple composable patterns rather than frameworks, and OpenAI's &lt;a href="https://cdn.openai.com/business-guides-and-resources/a-practical-guide-to-building-agents.pdf" rel="noopener noreferrer"&gt;practical guide to building agents&lt;/a&gt; is right about orchestration and guardrails. Neither has much to say about the surface, and the surface is where an agent that lives alongside a user actually fails.&lt;/p&gt;

&lt;p&gt;Two projects are working the same seam. The claude_codex_bridge &lt;a href="https://github.com/SeemSeam/claude_codex_bridge/tree/main/docs/plantree/plans/sidebar-provider-activity" rel="noopener noreferrer"&gt;sidebar provider activity plan&lt;/a&gt; states the rule I arrived at by breaking it: the sidebar remains a client of one authority and must not become a separate authority for identity or state. Our panel reads storage and messages the worker; the worker owns the socket. When I let three build artifacts each own their own version number, I got the thing that plan is written to prevent.&lt;/p&gt;

&lt;p&gt;And Cordum's &lt;a href="https://github.com/cordum-io/cordum/blob/main/docs/adr/010-edge-p0-architecture-decisions.md" rel="noopener noreferrer"&gt;ADR-010&lt;/a&gt; contains the cleanest statement of the failure class I know, about a Claude Code &lt;code&gt;PreToolUse&lt;/code&gt; deny hook: HTTP hooks can deny with a 2xx JSON response, but connection failures, non-2xx responses and timeouts are non-blocking. A guard whose failure mode is "allow" is not a guard. That is my unbound Mac shortcut with higher stakes.&lt;/p&gt;

&lt;p&gt;For the vocabulary of the problem, &lt;a href="https://agentsurface.dev/" rel="noopener noreferrer"&gt;Agent Surface&lt;/a&gt; is worth reading. Making software legible to agents is the mirror image of what we did here, which was making an agent legible inside software that has never heard of it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Two of 22 sites still have no verified save path
&lt;/h2&gt;

&lt;p&gt;Two of 22 sites have no verified save path. I know which two.&lt;/p&gt;

&lt;p&gt;The positional fallback for unmarked send buttons is a guess with a guard on it. It will break, and when it breaks the symptom will be an auto-attach that silently does nothing, which is the exact failure class this post is about. It is the piece I trust least.&lt;/p&gt;

&lt;p&gt;Per-site selectors are fixtures against DOMs owned by other people. There is no version of this that stops needing maintenance. We test them, which turns a break into a red suite instead of a support ticket, and that is the whole of the mitigation.&lt;/p&gt;

&lt;p&gt;Three build folders still exist with three copies of &lt;code&gt;sidepanel.js&lt;/code&gt;. One version line makes them honest. It does not make them one file.&lt;/p&gt;

&lt;p&gt;The keyboard shortcut is discoverable now, because it is a real &lt;code&gt;commands&lt;/code&gt; entry that appears in &lt;code&gt;chrome://extensions/shortcuts&lt;/code&gt; and can be rebound. For its first several months it was a keydown listener in the content script, which meant it was invisible, unbindable, and dead on any host where injection failed. Nobody reported it. That is the point.&lt;/p&gt;




&lt;p&gt;Source: &lt;a href="https://blog.vodou.ai/silent-failures-in-agent-surfaces/" rel="noopener noreferrer"&gt;A silent failure looks exactly like a feature you never built&lt;/a&gt; by Chad Priest, from Building Vodou in Public.&lt;/p&gt;

</description>
      <category>aiagents</category>
      <category>observability</category>
      <category>architecture</category>
      <category>browserextensions</category>
    </item>
    <item>
      <title>Node execFile was 2.7x slower: the spawn cost nothing</title>
      <dc:creator>Chad Priest</dc:creator>
      <pubDate>Wed, 26 Aug 2026 17:06:32 +0000</pubDate>
      <link>https://dev.to/chad_priest/node-execfile-was-27x-slower-the-spawn-cost-nothing-4134</link>
      <guid>https://dev.to/chad_priest/node-execfile-was-27x-slower-the-spawn-cost-nothing-4134</guid>
      <description>&lt;p&gt;Same machine, same daemon, same question. The gateway got its memory context in &lt;strong&gt;1495ms&lt;/strong&gt;. The MCP server got it in &lt;strong&gt;4041ms&lt;/strong&gt;. Both of them are code I wrote, in the same repo, for the same operation.&lt;/p&gt;

&lt;h2&gt;
  
  
  Unix socket in llm.ts, execFile in vodou-memory/index.js:38
&lt;/h2&gt;

&lt;p&gt;The gateway talks to the Rust daemon over a Unix socket, one round trip, in &lt;code&gt;MCP-servers/Vodou-Console/src/llm.ts&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;request&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stringify&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;cmd&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;prompt&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;hook_json&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;...&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;client&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;net&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;createConnection&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;path&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;sockConnectTarget&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;sockPath&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;write&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;request&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nx"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;end&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The MCP server shells out to the CLI instead, in &lt;code&gt;MCP-servers/vodou-memory/index.js:38&lt;/code&gt;:&lt;/p&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;core&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;args&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;timeoutMs&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;20000&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Promise&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;resolve&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;reject&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nf"&gt;execFile&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;VODOU_CORE&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;args&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;cwd&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;PROJECT_ROOT&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;timeout&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;timeoutMs&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;maxBuffer&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;4&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;1024&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;1024&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="p"&gt;...);&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;VODOU_CORE&lt;/code&gt; is a 33MB Rust binary. So my diagnosis wrote itself: fork, exec, page in 33 megabytes, parse argv, tear it down. Of course that's slower. Delete the second implementation, point it at the socket, ship.&lt;/p&gt;

&lt;h2&gt;
  
  
  Forking the 33MB binary measured zero milliseconds, twice
&lt;/h2&gt;

&lt;p&gt;Before deleting anything I measured the thing I was about to blame:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;/usr/bin/time &lt;span class="nt"&gt;-p&lt;/span&gt; ./vodou-core &lt;span class="nt"&gt;--help&lt;/span&gt;
&lt;span class="go"&gt;real 0.00
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Zero. Twice. Fork/exec of a 33MB binary on an M-series Mac is free at this resolution, and I'd have known that a year ago if I'd ever checked instead of assuming.&lt;/p&gt;

&lt;p&gt;The real answer was already sitting in the JSON I'd captured, because the CLI reports its own timing:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="nl"&gt;"timing"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"search_ms"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;3998&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"selected_ms"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"total_ms"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;4041&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;3998 of 4041ms happened &lt;strong&gt;inside the daemon&lt;/strong&gt;, on the other side of the socket. The subprocess accounted for 43ms of a 4-second call. The two implementations weren't a fast transport and a slow transport. They were two different questions asked of the same backend: &lt;code&gt;mem context&lt;/code&gt; over-fetches to the 50-cap, resolves vault membership, then runs the cross-encoder rerank, and &lt;code&gt;cmd:'prompt'&lt;/code&gt; does not.&lt;/p&gt;

&lt;p&gt;Two other things fell out of measuring instead of theorizing. &lt;code&gt;mem context&lt;/code&gt; without a vault fails closed and loud: &lt;code&gt;error: the following required arguments were not provided: --vault &amp;lt;VAULT&amp;gt;&lt;/code&gt;. And an empty query dies loudly inside the engine with &lt;code&gt;empty query — pass a non-empty search string&lt;/code&gt; rather than quietly returning the whole store.&lt;/p&gt;

&lt;h2&gt;
  
  
  Instrument under both paths before deleting the execFile call
&lt;/h2&gt;

&lt;p&gt;When you find two implementations of one operation and one is slower, the transport is the decoy. Instrument the layer &lt;em&gt;underneath&lt;/em&gt; both before you delete either: the subprocess, the HTTP hop, the extra serialization are all things you can see, which is exactly why they attract blame.&lt;/p&gt;

&lt;p&gt;If the shared backend already emits timing, read it before you write a benchmark. Mine did.&lt;/p&gt;




&lt;p&gt;Source: &lt;a href="https://blog.vodou.ai/node-execfile-was-2-7x-slower-the-spawn-cost-nothing/" rel="noopener noreferrer"&gt;Node execFile was 2.7x slower: the spawn cost nothing&lt;/a&gt; by Chad Priest, from Building Vodou in Public.&lt;/p&gt;

</description>
      <category>node</category>
      <category>performance</category>
      <category>debugging</category>
      <category>mcp</category>
    </item>
  </channel>
</rss>
