Found while implementing #8798 (removing the discarded historyMetaItem round trip in diffMetaItem). Filed rather than fixed — the catch below predates that card and deciding what a diff should answer during a history outage is a contract question, not a cleanup. Not assigned; recording, not starting.
What happens
ObjectStackProtocolImplementation.diffMetaItem (packages/metadata-protocol/src/protocol.ts) reads the history rows inside a try whose catch is empty apart from a comment:
} catch {
// history table unavailable — fall through with empty list
}
histRows then stays empty, so the function returns a well-formed, successful response with added: [], removed: [], changed: [] and fromVersion / toVersion of null or the caller's requested numbers. GET /api/v1/meta/:type/:name/diff answers 200.
Why it is worth a card
There is no field, status or header that distinguishes these two answers:
- the item genuinely did not change between the two versions, and
- the history table could not be read at all.
An operator comparing two versions before a rollback sees "no differences" in both cases. That is the failure shape Route & surface ownership §3 (Absence must be loud) and §4 (machine-readable surfaces must not lie) are written against: a verifier that silently degrades is worse than none, because it reports success. It is also the harder half of the AI-authoring axis — a client, SDK or agent reading this endpoint cannot see the degradation at all, so it will act on "unchanged" with full confidence.
Measured on origin/main with a stub engine whose find on sys_metadata_history throws: diffMetaItem({ type: 'view', name: 'grid', fromVersion: 1, toVersion: 2 }) resolves to { added: [], removed: [], changed: [] }. No log line is emitted either, so the outage leaves no trace on any surface.
Scope note relative to #8798
Before #8798 this swallow was reached inconsistently: the discarded historyMetaItem call above it was unguarded, so the same outage threw for a type passing isOverlayAllowed / isRuntimeCreateAllowed and was swallowed for the five types taking that gate's early return (field, job, api, capability, agent). #8798 removes the discarded call, so every type now reaches the catch uniformly. That change makes the behaviour consistent and is pinned there; it deliberately does not decide whether swallowing is correct. This card is that decision. #8798 remains open on its own PR and is not addressed here.
Options, for whoever picks this up
- A — propagate. Let the read throw; the endpoint answers 5xx. Loudest, and matches "prefer failing to falling back". Costs: a diff on an item whose history table is genuinely absent in a minimal deployment starts erroring where it used to answer.
- B — answer 200 but declare the degradation. Keep serving the diff, add an explicit field (e.g.
historyUnavailable: true) so clients and the UI can tell the two cases apart. Additive to the response contract; needs a spec change and a renderer that reads it. - C — distinguish "table missing" from "read failed". Swallow only the schema-absent case, propagate real read errors. Most precise, most engine-coupling.
No recommendation attached — this is exactly the kind of response-contract question that should be ruled on rather than guessed.
Not a duplicate
Searched open issues before filing by keyword (diffMetaItem, sys_metadata_history, outage, swallowed, silent, empty diff, history unavailable) and by file path. The only match is #8798 itself, which concerns the discarded round trip rather than the catch.
Generated by Claude Code
Found while implementing #8798 (removing the discarded
historyMetaItemround trip indiffMetaItem). Filed rather than fixed — thecatchbelow predates that card and deciding what a diff should answer during a history outage is a contract question, not a cleanup. Not assigned; recording, not starting.What happens
ObjectStackProtocolImplementation.diffMetaItem(packages/metadata-protocol/src/protocol.ts) reads the history rows inside atrywhosecatchis empty apart from a comment:histRowsthen stays empty, so the function returns a well-formed, successful response withadded: [],removed: [],changed: []andfromVersion/toVersionofnullor the caller's requested numbers.GET /api/v1/meta/:type/:name/diffanswers 200.Why it is worth a card
There is no field, status or header that distinguishes these two answers:
An operator comparing two versions before a rollback sees "no differences" in both cases. That is the failure shape Route & surface ownership §3 (Absence must be loud) and §4 (machine-readable surfaces must not lie) are written against: a verifier that silently degrades is worse than none, because it reports success. It is also the harder half of the AI-authoring axis — a client, SDK or agent reading this endpoint cannot see the degradation at all, so it will act on "unchanged" with full confidence.
Measured on
origin/mainwith a stub engine whosefindonsys_metadata_historythrows:diffMetaItem({ type: 'view', name: 'grid', fromVersion: 1, toVersion: 2 })resolves to{ added: [], removed: [], changed: [] }. No log line is emitted either, so the outage leaves no trace on any surface.Scope note relative to #8798
Before #8798 this swallow was reached inconsistently: the discarded
historyMetaItemcall above it was unguarded, so the same outage threw for a type passingisOverlayAllowed/isRuntimeCreateAllowedand was swallowed for the five types taking that gate's early return (field,job,api,capability,agent). #8798 removes the discarded call, so every type now reaches thecatchuniformly. That change makes the behaviour consistent and is pinned there; it deliberately does not decide whether swallowing is correct. This card is that decision. #8798 remains open on its own PR and is not addressed here.Options, for whoever picks this up
historyUnavailable: true) so clients and the UI can tell the two cases apart. Additive to the response contract; needs a spec change and a renderer that reads it.No recommendation attached — this is exactly the kind of response-contract question that should be ruled on rather than guessed.
Not a duplicate
Searched open issues before filing by keyword (
diffMetaItem,sys_metadata_history, outage, swallowed, silent, empty diff, history unavailable) and by file path. The only match is #8798 itself, which concerns the discarded round trip rather than thecatch.Generated by Claude Code