2026-08-03

Empty and never-looked are the same JSON

A tool call comes back. content: [], isError: false, HTTP 200. The span closes green. The model reads it as there is nothing there and writes the next paragraph on top of that.

There were three possible worlds behind that response and the wire can only carry two of them:

Two and three are byte-identical. Not similar — identical. There is no field to inspect, no attribute to assert on, nothing an observability layer can surface, because the correct output and the broken output are the same artifact. No amount of looking finds it.

I went and checked instead of assuming

I expected to find the field and to have missed it. I did not find the field.

So this is a convention gap, not a capability gap. _meta permits the fix today. Nobody uses it for this.

What it looks like in something you have probably integrated

SAP SuccessFactors returns 200 {"d":{"results":[]}} when role-based permissions deny you the records. Not a 403. A well-formed empty result set, in the same shape as a successful query that genuinely matched nothing.

Every client I have read does .get("d",{}).get("results",[]) straight through it. The denial and the empty are the same list, and the agent above reports no records found with total confidence.

Three of mine, because I would rather show my own

Why it survives for years in real code

It can only ever fail upward. A parse failure inflates the score. An empty result reads as a confident does not exist. A silent auth scope-down looks like a clean small answer.

Nothing downstream is motivated to catch it, because there is no angry user filing a bug that says their model scored too well. The asymmetry is the whole reason this class outlives the people who wrote it.

The fix is a denominator, not a status

_meta: { searched: 1200, matched: 0, truncated: false }

Then "0 of 1200 rows, and I looked at all of them" and "0 of 0, because permissions handed me an empty index" stop being the same sentence to the model reading it.

The honest caveat: an unenforced optional convention is historically a weak thing to bet on. CONSORT worked in clinical trials because journal editors reject papers without a participant flow diagram — there was an enforcement layer. There is no equivalent here yet. So I would not tell you this gets adopted. I would tell you that you can put it in your own server this afternoon and stop lying to your own agent.

How to check yours

Grep your tool handlers for a return that produces the same shape on the empty path and on the denied-or-failed path. Mine had four. It takes about twenty minutes and the answer is usually uncomfortable.


I do this for money — one day, together, on your codebase, going after exactly this class of thing. What that looks like. And if you would rather just have the answer: send me one repo and I will run the check over it and mail you what it finds, free, once. It found nothing is the more common result and I will tell you that plainly.

This is one of fourteen. The survey collects every case I have read where a failed check and a passing check produce the same result — named, quoted, with line numbers, including the ones that turned out to be my own mistake.