building vodou.

25 budgets declared, 3 enforced: the fallback was Infinity

A prompt assembler resolved undeclared lanes to Infinity, so a 600-token cap in the registry never fired once. The fix is a test that joins the two lists.

Chad Priest / / 3 min read

Our gateway has a registry, lanes.toml, that declares every block of text allowed into a model’s prompt: 25 stanzas, 14 of them carrying a numeric token cap. The prompt assembler that actually packs the request knows three of those caps. Nothing compares the two lists. That is the class: a config declares resource budgets, a runtime component enforces its own hardcoded subset, and the gap is invisible because no test joins them.

Here is the specific one. lanes.toml line 134 declares the workbench lane (operator standing instructions, injected on the user side) at budget = 600, with the note “was UNCAPPED operator free text appended verbatim.” The enforcement, in MCP-servers/Vodou-Console/src/llm.ts:

const b = laneBudgetTok('workbench');
const over = tokensOf(wbBlock) > b;
if (over) console.error(`[Context] budget: workbench instructions are ${tokensOf(wbBlock)} tok over ${b} tok ...`);

And the resolver it calls:

const CONTEXT_BUDGET_DEFAULT_TOK: Record<string, number> = {
  memory: 2000, tool_results: 1500, skill: 3000,
};
return CONTEXT_BUDGET_DEFAULT_TOK[lane] ?? Infinity;

b is Infinity. over is always false. That log line has never printed, the receipt can never carry state: over_budget, and the free text still goes in whole, exactly as it did before anyone wrote 600 down. The override path was empty too:

sqlite> SELECT COUNT(*) FROM gateway_settings WHERE key LIKE 'context.budget%';
0

I wrote that ?? Infinity, and I defended it in a comment: defaulting an unknown lane to 0 would evict a whole lane the moment someone added one and forgot the table. That reasoning is still correct. I was wrong that it was the end of the job. Fail-open is the right default and a silent one, so it needs a reconciler or it eats every ceiling you write down.

Fail-open caps, two lists, and nobody joining them

The same shape shows up in API gateways with declared versus enforced rate limits, MCP servers with per-tool result caps, and any RAG context builder whose config.yaml names buckets the packer does not.

Every key that declares a limit must resolve to a finite limit at the site that enforces it, and the set difference between declared keys and enforced keys must be asserted empty by a test.

The geodocs context-budget spec says to declare per-bucket caps in a runtime config with an overflow policy. Good advice, and it does not cover the case where the config is not the object the packer reads. Ethos does cover it, almost by accident: its resultBudgetChars: 80_000 lives in one object that ToolRegistry.executeParallel divides. One list, one owner, nothing to drift.

Paste this at the end of your assembler and send one real request:

for (const name of Object.keys(sections)) {
  console.error(`[budget] ${name} cap=${budgetFor(name)} used=${countTokens(sections[name])}`);
}

Passing looks like [budget] history cap=4000 used=3110, two finite numbers on every line. Failing looks like [budget] tool_schemas cap=Infinity used=4812, or cap=undefined, or a cap so large nothing could ever reach it. Then make it permanent:

test('every declared cap resolves at the enforcement site', () => {
  for (const [name, cap] of Object.entries(loadBudgetConfig())) {
    expect(budgetFor(name)).toBe(cap);   // not: expect(cap).toBeDefined()
  }
});

That comment is not rhetorical. Our test asserted /budget\s*=/ against the registry, so it passed on a stanza whose number no code ever read. If your budget file has more keys than your resolver, you do not have budgets. You have documentation with units.