DEV Community

Cover image for 9 of my 27 declared lanes logged nothing, for three different reasons
Chad Priest
Chad Priest

Posted on Originally published at blog.vodou.ai

9 of my 27 declared lanes logged nothing, for three different reasons

If your agent builds prompts from more than two sources, you probably have a list somewhere of what those sources are: memory, tool results, the skill that matched, the page the user pasted, a rolling summary. You may call them context sources or injectors. I call them lanes. The list is supposed to be the truth about what can reach the model.

Mine wasn't. Nine of the 27 lanes in my registry had zero rows in the event log. The registry said all 27 were live. Nothing in the build could tell me which of the nine were broken and which just hadn't seen any traffic yet.

Each lane entry now says where it lands: log, receipt or none

I built this into Vodou, a local-first system that assembles context from a person's own memory, their skills and their tools before every model call. The lane registry is a TOML file, lanes.toml, and it's public. Each stanza already declared a name, the injector that assembles the text, a token budget and a trust label. A commit guard already refused any lane name literal in the code that had no stanza.

The change adds one field. emits says where the lane actually ends up:

# lanes.toml, paraphrased as YAML for readability
- name: skill
  emits: receipt   # reaches the per-turn receipt the user sees, never the event log
  budget: 3000
- name: rolling_summary
  emits: none      # declared, written by nothing
- name: page_context
  # emits absent => "log": an emitter exists; zero rows means no traffic
Enter fullscreen mode Exit fullscreen mode

There are two places a lane can show up. turn_events is the append-only log I debug from. turn_receipts.lanes is what the person sees on each answer: "this reply used memory, a skill, your pasted page." When a lane is in the receipt but not in the log, the user can see something the log can't. That's how I ended up here.

A test, lanes-emitters.test.ts, checks every claim against the source tree. It checks both ways, which is the point.

The registry declares lanes; the gate checks each emits claim against producers found in both the TypeScript gateway and the daemon source, failing in either direction

No stanza without a producer, no producer without a stanza

Zero rows had three causes, and lumping them together was my first mistake

My first reading of "nine lanes, zero rows" was nine bugs. That was wrong, and fixing them as nine bugs would have made things worse.

Three of them, hook_intent, lenses and rolling_summary, appeared nowhere in the source. Someone declared them and nothing ever wrote them. They're dead.

Two, skill and automation, did reach the model. They were pushed into the receipt's lane list during context assembly and never emitted to the log. I had proven this earlier for skill: a skill turn goes to the model and logs no lane. That's a real bug, but a different one. The fix is to wire an event, not delete a stanza.

Four, page_context, doc_attach, api_tool and api_message, had working emitters. The console has a noteUserBodyLane helper, and the API-family requests go through a mapper. They had zero rows because nobody had pasted a page or made an API-family call since the log shipped. Nothing was wrong with them.

Before: nine lanes all look the same with zero rows. After: three dead, two receipt-only, four unexercised, each with a different fix

If I'd treated all nine as broken, I'd have ripped out four working emitters' lanes or wired fake traffic to make them look alive. If I'd treated all nine as unexercised, skill would still be invisible in the log. Having one number for three different states is the bug.

The first gate flagged hook_memory, which the daemon writes in Rust

The first draft of the test scanned TypeScript only. It reported hook_memory as a lane with no producer.

hook_memory is one of the busiest lanes in the system. The daemon writes it, in a different language, in a different process. The gate could see half the producers, and it reported the half it couldn't see as a defect. That's the exact failure it was built to catch: a claim about the system based on less than the whole system.

So now it reads the daemon source too and matches the other language's spelling of the append call. Then I tested it by lying to it both ways. Claiming a dead lane reaches the log produced 2 failures. Marking a live lane dead produced 2 failures. Restoring the file gave 4 passes. The full console suite stayed green, 1,322 tests across 137 files, and the existing coherence guard was clean.

The set of known-dead lanes is pinned by name in the test. Wiring one of them fails the build until someone updates that list, which is what I want. Coming back to life should be a deliberate act, not quiet drift.

Invariant: every registry entry names its producer, and the check scans every language that can produce

Stated so you can test it against a codebase: for every entry in a declarative registry of things the system emits, there's at least one producer in the source, found by a scan that covers every language and process allowed to emit. A second check runs the other way: every entry marked as having no producer really has none.

Most codebases only enforce the other half ("no literal without a declaration"). That half catches typos. It doesn't catch a registry that has turned into a wish list. And a one-way gate that only scans one language produces false positives, which teaches people to ignore the gate.

Run this against your own event log before you trust your registry

You need three things: your registry (whatever file lists your context sources, event types or metric names), your event table, and your source tree. Put every language you ship into the grep.

# 1. Declared names. Adjust the extractor to your registry format.
grep -oE 'name *[:=] *"?[a-z_]+' registry.toml | sed -E 's/.*[:=] *"?//' | sort -u > declared.txt

# 2. For each name, is there a producer anywhere, in ANY language you ship?
while read n; do
  hits=$(grep -rIl --include='*.ts' --include='*.py' --include='*.rs' --include='*.go' \
         -E "[\"']$n[\"']" src/ services/ 2>/dev/null | grep -v -E 'test|spec' | wc -l)
  echo "$n $hits"
done < declared.txt | awk '$2==0 {print "NO PRODUCER:", $1}'
Enter fullscreen mode Exit fullscreen mode
-- 3. Declared names with no rows in the last 30 days (SQLite; adapt the date function).
SELECT d.name
FROM declared d
LEFT JOIN events e
  ON e.lane = d.name AND e.created_at > datetime('now', '-30 days')
GROUP BY d.name
HAVING COUNT(e.lane) = 0;
Enter fullscreen mode Exit fullscreen mode

Cross the two outputs. If a name has no producer and no rows, it's dead. If it has a producer and no rows, it's either unexercised or emitting somewhere else. Grep for it next to your user-facing receipt or response metadata. If it shows up there and not in the log, you have the skill bug. A clean result is empty output from step 2 and only names you can explain from step 3. If step 2 lists a name you know is busy, your scan is missing a language. That's my hook_memory mistake, and it means your scan is wrong, not your registry.

Lane registries elsewhere check the manifest's shape, not whether anything writes to it

This isn't a new idea. The Selective Intelligence lane kernel defines a JSON schema for lane manifests and rejects a bad manifest with 16 concrete errors. LaneGate re-runs its guards against the merged result instead of the branch, which is a sharp idea I'd like more tools to copy. Open Agent Architecture makes verification gates and chained receipts part of the runtime.

All three validate what a declaration says. None of them I read checks that the declaration has a writer. A well-formed manifest for a lane nothing produces passes every schema. Anthropic's advice to find the simplest solution possible is right, and here's what it means in practice: a registry is only simpler than no registry if it stays true. The research-to-deployment gap survey talks about the distance between how agents are described and how they run. A registry with dead entries is that gap in one file.

Still open: three dead lanes stay dead, and "unexercised" is still a guess

This commit wired nothing. hook_intent, lenses and rolling_summary are now honestly labelled none, and skill and automation are honestly labelled receipt. Whether to wire or delete each one is a separate decision. The registry just can't lie about it anymore. The gate also works from text. A lane name built from string concatenation would get past it. For the four unexercised lanes, "an emitter exists" is weaker than "the emitter works." I won't know that until someone pastes a page.

Why a memory system needs a registry that can't lie

I care about this because of what Vodou is for. Every lane is something that gets spent from a context budget on the way to the model: your memory, your skill, the page you pasted. When the receipt under an answer says "used your memory," that has to be true, and it has to be traceable in the log. Otherwise nobody, including me, can debug why the model said what it said.

Vodou keeps your memory on your machine, in a local database you own. It extracts facts from your conversations and sessions on its own, so you don't keep notes for it. The same memory goes into Claude Code, Cursor and Claude Desktop through MCP and hooks, and across the browser boundary into ChatGPT, Claude and Gemini through the Vodou Bridge extension. Retrieval combines vector and keyword search with a cross-encoder reranker. It has a precision floor, so when nothing is relevant it injects nothing instead of noise.

The same discipline as this gate runs through the rest of it. Proactive loops tell you when capture goes quiet or extraction stops, before you notice missing memory. A scheduler runs work while you're away. This blog post was mined from my memory, drafted, graded by a rubric, checked by a redaction gate and deployed on Vodou's own scheduler. And it's yours to extend: skills, MCP servers, scripts and schedules are all files you can add and change. The client side, including lanes.toml and the test described here, is open source.

It's for engineers who are tired of re-explaining themselves to every AI tool they open, and who want to see how their context is assembled instead of trusting it. Start at vodou.ai.

If you want your own memory assembled into every model call through lanes that are declared, budgeted and checked against the code that writes them, get it at vodou.ai.


Source: 9 of my 27 declared lanes logged nothing, for three different reasons by Chad Priest, from Building Vodou in Public.

Top comments (0)