Skip to content

[finding] diffMetaItem answers 200 with an empty diff when sys_metadata_history is unreadable — an outage is indistinguishable from "nothing changed" #8833

Description

@hotlong

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

Metadata

Metadata

Assignees

Type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions