fix(sessions): explain why a session failed on the channel card#619
Merged
Conversation
A failed session card in a channel showed a bare "✕ Failed after 8s". The cause was captured — it reaches the session pane through session_records — but nothing carried it to the channel: the session.completed payload had only an empty resultExcerpt (result_text is null on failure), and the session row had no failure column at all, so a cold load had nothing to render either (a channel's timeline query excludes thread events, so the completion event never reaches the card). Observed in prod: a codex run died on "You've hit your usage limit …try again at Jul 25th" and the channel said only that it failed. Persist failure_class + terminal_reason from the terminal execution_state frame onto the session row, carry them on session.completed and /sync's snapshot rows, and render them through the existing classifyFailure via a new failureLine helper (web + mobile). A steer clears them, server-side and in the fold, so a revived session stops explaining itself with the previous turn's failure. GET /api/sessions stays lean — these ride /sync's snapshot row only.
gbasin
enabled auto-merge
July 20, 2026 14:01
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
The gap
A failed session card in a channel showed a bare "✕ Failed after 8s" with no cause.
The cause was captured — it reaches the session pane via
session_records→FailureNotice. But nothing carried it to the channel:session.completedpayload had onlyresultExcerpt, which is derived fromresult_text— null on failure.Observed in prod: a codex run died on "You've hit your usage limit … try again at Jul 25th, 2026 3:25 AM" and the channel said only that it failed. The actionable part was one click away instead of on the card.
Change
sessions.failure_class,sessions.failure_reason.execution_stateframe; carry them onsession.completedand on/sync's snapshot rows. A steer clears them (the new turn owns the outcome).Session;mergeSessionEntitypreserves them so a snapshot can't erase what the fold explained;status_changed's new-turn branch clears them.failureLine()in centaur-client wraps the existingclassifyFailure, preferring the engine's own words over our class summary ("You've hit your usage limit…" beats "The agent hit an error and stopped"). Wired into web + mobileMessageRow.GET /api/sessionsstays lean — these ride/sync's snapshot row only, respecting the existing "stays lean" guard.Purely additive: with no reason reported,
failureLinereturns null and callers keep the existing bare "Failed". Falls back toresultTextfor failures logged before this.Testing
pnpm check— lint + typecheck + 2811 unit tests, all greenpnpm e2e— 120 passed (4.6m)failureLineunit testsNote
Depends on nothing, but pairs with #618 — that PR fixes the bug that produced the wrong reason on the retry; this one makes whatever reason we have visible.