DEV Community

Meronq
Meronq

Posted on Originally published at meronq.pages.dev

We built a product around context cost and never measured our own tools

Meronq is an engineering-intelligence layer: it keeps a model of your project — components, decisions, evidence — and serves it to AI assistants over MCP. Context cost is the whole premise. We have an ADR that prices understanding against tokens. Our handshake tool sends a vitality-focused slice, 21 entities out of 176, precisely so a session does not pay for the entire graph.

Then we measured what the other tools returned.

memory_sync — the tool whose useful answer is "synced, here is what changed" — was returning 84,484 bytes on a real project. The entire Canonical Engineering Model plus the entire project scan. About 21,000 tokens to confirm a sync.

project_list, which answers "which projects can I work on", returned 46,190 bytes. The local registry had grown to 240 entries; 227 of them pointed at temp directories that no longer existed. Every call shipped all of them.

That is the uncomfortable part: none of this was discovered by a user complaint or a benchmark. It was sitting in our own instrumentation the whole time — access_events.response_bytes, a column we wrote and never read.

The fix is deliberately boring

Return counts by default. memory_sync, project_scan and github_sync share one code path. It now returns graph counts, what the sync changed, warnings, and a hint. The full payload is one argument away:

before after
memory_sync (Unity project) 85,061 B 3,528 B
memory_sync (our monorepo) 137,358 B 1,521 B
project_list 46,190 B 2,377 B

Nothing is lost. The graph is in SQLite either way, semantic search reaches it, and detail="full" still returns everything.

Put a ceiling on the shared response helper. Every result now passes a 32,000-byte limit. One detail mattered more than the number: an oversized object is replaced by a notice carrying its actual size, not truncated. Half a JSON object is not JSON, and handing an agent an unparseable fragment is worse than telling it plainly that the response was too large and how to narrow it. Oversized strings keep their head, which is usually the useful part of a log.

The ceiling also has an escape hatch. A caller who explicitly asks for full detail raises it rather than being overruled. A limit that argues with an explicit request is a bug, not a safeguard.

Stop shipping dead rows. project_list lists what exists on disk and counts the rest. The cause of the pile-up turned out to be our own test suite: project initialization registered into the real ~/.meronq/projects.json whenever no session was passed, so every full test run left rows behind in the developer's own registry. It takes an isolated registry home now.

What did not ship, and why

We bundle a multilingual e5-small model for offline semantic search. The obvious next step is to use it for selection — pick the entities that best cover the graph instead of the hottest ones. A maximal-marginal-relevance pass over the stored vectors already does better on paper: seven entity types in a twenty-item budget instead of three.

It is not enabled, because of what we feed the model. The embedded text is name + summary + type + layer, and component summaries are formulaic — "Package: packages/x". The model reads the template, not the meaning. Clustering at cosine 0.95 cheerfully merged 64 distinct Unity modules into one group, and four unrelated packages of ours into another. Fix the input before trusting the output.

One instrumentation gap is also still open: rows written outside the MCP dispatcher — our handshake's own presence row, and every desktop sidecar call — carry no response size. That is how 970 handshake rows ended up with 11 measurements, and why this took so long to notice.

The lesson we actually take from this

Having the right principle, the right ADR and the right instrumentation is not the same as looking at the number. We had all three for months. The tool that violated the principle hardest was the one nobody thought to check, because it "just syncs".

If your product has a cost you care about, put one measurement of it somewhere you cannot avoid reading.


Read the full post on meronq.pages.dev · Source on GitHub

Top comments (0)