Reported from a task chat: a screenful of column names from `project.chat_sessions` and
nothing about what broke.
That message is, by construction, the useless half. Drizzle wraps a query failure in an
error whose message is `Failed query: <the whole statement> params: …` and puts the real
PostgresError — `column "x" does not exist`, `permission denied`, `connection terminated` —
in `cause`. `rethrowAsApiError` read `error.message` alone, so the reason was dropped before
it ever reached the operator. `startup-factory` already carried a private chain walker
because field reports of exactly this shape were undiagnosable; the dashboard never got one.
The walker is now shared (`describeErrorChain` for logs, `summarizeErrorForOperator` for
operator surfaces). The inversion is keyed narrowly on the `Failed query:` wrapper, never on
guessing which message reads better: an application-authored message is deliberate prose and
still leads, so the API boundary contract and its 29 tests are unchanged. Only the
machine-generated frame is demoted to truncated context behind its cause.
This does not fix the underlying query failure — it makes it reportable. The next occurrence
will name the column or condition that failed instead of the statement that contained it.
pnpm lint 0 errors, test:gate green, core + dashboard typecheck clean, 7 new tests.