Skip to content

fix(tui_rpc): surface the cause of an internal_error, not just the code - #296

Open
arelchan wants to merge 2 commits into
mainfrom
fix/tui_surface_internal_error_detail
Open

fix(tui_rpc): surface the cause of an internal_error, not just the code#296
arelchan wants to merge 2 commits into
mainfrom
fix/tui_surface_internal_error_detail

Conversation

@arelchan

Copy link
Copy Markdown
Contributor

Summary

A -32603 reached the user as exactly this, and nothing else:

error: [rpc -32603] internal_error

No cause, no file, no log path. The information existed the whole time and was discarded
three separate times:

  1. The dispatcher dropped detail. raven/tui_rpc/dispatcher.py used
    if exc.data is not None: ... elif exc.detail: ..., so a raiser that set both lost the
    detail. _build_tui_agent_loop always sets both.
  2. turn.send emitted only {code, message} for a latched init crash, although the
    ErrorEvent schema already has a detail field and chatStream.onError already renders
    it.
  3. The client formatted [rpc <code>] <message>, where message is a fixed code name
    (internal_error), and every call site prints err.message.

So an AgentLoop that cannot start produced a dead end. A real instance: a config file
containing a key the running branch does not know about fails validation, _build_tui_agent_loop
raises InternalError with the full pydantic message and log_path, and the TUI showed the
line above.

The fix keeps message protocol-faithful and puts the cause where callers already look:

  • error_data(exc) in raven/tui_rpc/errors.py folds detail into data, and both the
    dispatcher and the turn error events use it, so the two paths carry the same context.
  • turn.send passes the init-crash detail plus its log_path into the emitted event.
  • formatRpcError in ui-tui/src/rpc/errors.ts builds the message from detail,
    exception_message or reason, and appends the log path. A multi-line cause (a config
    error listing each offending field) keeps its line breaks below the summary line.

Same failure after the change, rendered by feeding a real dispatcher frame through the client
path:

error: [rpc -32603] internal_error:
Config at /Users/admin/.raven/config.json fails schema validation:
1 validation error for Config
subagents
  Extra inputs are not permitted
(details in ~/.raven/logs/tui.log)

Type

  • Fix
  • Feature
  • Docs
  • CI / tooling
  • Refactor
  • Other

Verification

uv run pytest tests/ -k "tui_rpc or dispatcher or tui_bootstrap" -> 523 passed. New cases:
the dispatcher keeps detail next to reason and log_path in one frame; turn.send emits
the build-error cause with its log path; and it omits detail entirely when the error has no
cause to report.

npx vitest run in ui-tui/ -> 86 files, 988 tests passed, including six new
rpcErrorFromFrame cases: a bare frame keeps the old one-line form; data context is
surfaced with the log path; exception_message and reason are read when detail is absent;
a multi-line cause keeps its shape; non-object or blank data cannot corrupt the message; and
typed subclass selection plus raw data access are unchanged.

npm run type-check, npx eslint, npx prettier --check, ruff check, ruff format --check
all clean.

End to end, the exact production shape: an InternalError raised the way
_build_tui_agent_loop raises it was dispatched to a JSON-RPC frame, and that frame was fed
through rpcErrorFromFrame with tsx, producing the output shown above.

  • Relevant tests pass locally
  • Relevant lint / type checks pass locally
  • User-facing docs or screenshots are updated when needed

Risk

  • Security impact considered
  • Backward compatibility considered
  • Rollback path is clear for risky changes

Notes: error.code and error.message are unchanged on the wire, so nothing that branches on
them is affected; the added data.detail and payload.detail are both already in the schema.
err.message gains a suffix, which the one message-matching call site
(SESSION_BUSY_RE in useSubmission.ts) is unaffected by, since it matches text that still
appears. Detail text originates from server-side exceptions and is already written to
~/.raven/logs/tui.log, so this exposes nothing new to the client; the client accepts it only
when it is a string and ignores any other shape. Rollback is a revert of this commit.

Related Issues

N/A

@gloryfromca gloryfromca left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No blockers; this can merge as far as I am concerned. One non-blocking note inline about the log_path suffix not reaching the user in the multi-line case.

What I checked

  • The diff itself, plus the callers on both sides: _build_tui_agent_loop (raven/cli/tui_commands.py:483-525) is the only producer of these InternalErrors, and it always passes data, which is exactly the case the old dispatcher dropped detail for. The consumer of the new event field is onError in ui-tui/src/app/chatStream.ts:210-224, which is live (wired through createChatStream in useMainApp.ts), so the server half is not dead code.
  • Schema: ErrorEventPayload.detail already exists in raven/tui_rpc/models.py:212-216 and in the checked-in ui-tui/src/rpc/generated.ts, so nothing needed regenerating and the strict payload model still accepts the emitted dict.
  • Backward compatibility: both new fields are additive. An older client ignores the extra detail key; a newer client against an older server just formats the bare code name, which is what the first test in rpc.test.ts pins.
  • error_data semantics: setdefault means a raiser that sets both data["detail"] and exc.detail keeps the data one, and data or None now omits error.data when a raiser passes an empty dict. Neither has a caller in the tree, and no consumer requires data to be present, so I am not raising them.
  • Project rules: commit header follows Conventional Commits with a real scope, the whole message is ASCII (git log -1 --format=%B | grep -nP "[^\x00-\x7F]" prints nothing), no new test files were created where an existing one exists (AGENTS.md 5.4), no assets.
  • Tests were not weakened. Both new Python tests assert real behaviour, and the negative one (..._omits_detail_when_the_build_error_has_no_cause) is a genuine assertion, not a skip. The TS ignores non-object and blank data case loops over six hostile shapes rather than asserting one.

What I ran

uv run pytest tests -k tui -q
611 passed, 7 skipped, 5335 deselected in 40.42s

The 7 skips are the pre-existing optional-channel import guards (dingtalk_stream, lark_oapi, nio, botpy, slack_sdk, telegram, wecom_aibot_sdk) and are unrelated to this change.

cd ui-tui && npx vitest run
Test Files  86 passed (86)
     Tests  988 passed (988)

npx tsc --noEmit      # clean
uv run ruff check raven/tui_rpc tests/test_tui_rpc_system.py tests/test_tui_rpc_turn_send.py
All checks passed!

(The suite needs packages/hermes-ink built first -- npx esbuild src/entry-exports.ts --bundle --platform=node --format=esm --packages=external --outdir=dist -- otherwise 41 files fail on a missing ./dist/entry-exports.js. That is an environment prerequisite, not this branch.)

Nit, take it or leave it

The doc comment at ui-tui/src/rpc/errors.ts:21 writes the example as (see ~/.raven/logs/tui.log) but the code emits (details in ...).

Comment thread raven/tui_rpc/methods/turn.py Outdated
return None
log_path = data.get("log_path")
if isinstance(log_path, str) and log_path.strip():
return f"{detail.strip()} (details in {log_path.strip()})"

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Non-blocking: the log_path suffix does not survive to the user in the case that motivated it.

The renderer on the other end keeps only the first line of detail, capped at 200 chars (ui-tui/src/app/chatStream.ts:221, pre-existing):

const extra = detail ? `: ${detail.split('\n')[0].slice(0, 200)}` : ''

ValidationError is one of the caught init-crash types and its str() is always multi-line -- I checked rather than assumed:

$ uv run python -c "...M(a='x', b='y')..."
"2 validation errors for M\na\n  Input should be a valid integer ..."
LINES 7

So for the exact scenario the commit message cites -- a config the running branch cannot parse -- the appended (details in ~/.raven/logs/tui.log) lands on line 7 and the user sees only error: internal_error (code=-32603): 2 validation errors for RavenConfig. No log path, which is the one thing this docstring says the path is riding along to provide.

Single-line causes (FileNotFoundError, OSError, MissingCredentialsError) are fine, so this is a partial gap rather than a regression -- main showed nothing at all. Cheapest fix is to put the pointer where the truncation cannot reach it, e.g. f"{log_path} <- {first_line_of_detail}", or prepend it:

return f"(details in {log_path.strip()}) {detail.strip()}"

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in the pushed commit -- verified the failure first rather than taking the shape of it on faith.

ValidationError lines: 7
what the user saw:     "2 validation errors for M"
log path visible:      False

So the pointer was lost for precisely the scenario the commit message cites, exactly as you described. Prepended it, which is the cheaper of the two options you offered and keeps single-line causes reading the same.

Also replaced the assertion, since that is the part that let this through: the old test compared the whole detail string, which a trailing pointer satisfies just as well as a leading one. The new test builds a multi-line cause and asserts against detail.split("\n")[0][:200] -- the renderer's own slice -- so the next person to move this pointer gets told.

Rebased on current main (it was 25 behind) and re-ran: 52 passed across the turn-send and session-init files, ruff clean. Needs another approval since the push landed after yours.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Confirmed at e8417b22. Prepending is the right resolution, and the added assertion is the part that matters -- it slices detail the way the renderer does, so a future trailing pointer fails the test instead of passing it.

uv run pytest tests -k tui -q
617 passed, 7 skipped, 5827 deselected

cd ui-tui && npx vitest run
Test Files  86 passed (86)
     Tests  990 passed (990)

gloryfromca
gloryfromca previously approved these changes Aug 15, 2026
arelchan and others added 2 commits August 17, 2026 21:08
A -32603 reached the user as the bare line `error: [rpc -32603]
internal_error`, with nothing about what failed or where to look. The cause
was collected and then thrown away three times over:

- the dispatcher dropped `RpcError.detail` whenever the raiser also passed
  `data`, which `_build_tui_agent_loop` always does;
- `turn.send` emitted a latched init crash as `{code, message}` only, even
  though the ErrorEvent schema already has a `detail` field the front end
  renders;
- the client formatted `[rpc <code>] <message>`, where `message` is a fixed
  code name, and every call site prints `err.message`.

So an AgentLoop that cannot start (a config the running branch cannot parse,
for instance) produced an error that named neither the file nor the log.

Fold `detail` into `error.data` in a shared `error_data` helper used by both
the dispatcher and the turn events, pass the init-crash detail plus its
`log_path` into the emitted event, and build the client-side message from
`detail` / `exception_message` / `reason` with the log path appended. A
multi-line cause (a config error listing each offending field) keeps its
line breaks below the summary line.

Co-authored-by: Claude (claude-opus-5) <noreply@anthropic.com>
Review caught that the pointer did not survive to the user in the case
that motivated it. The transcript renders the first line of `detail` and
nothing more (ui-tui/src/app/chatStream.ts), and the crash this detail
exists for -- a config the running build cannot parse -- raises a
ValidationError whose str() is multi-line: 7 lines for two bad fields.
Appended, `(details in ~/.raven/logs/tui.log)` landed on the last line, so
the user saw `2 validation errors for RavenConfig` and no path. Verified
before changing anything.

Prepend it instead. Single-line causes read the same either way, and the
multi-line ones now keep both halves in the rendered slice.

The existing test asserted the whole string, which a trailing pointer
passes just as well; the new one asserts what the renderer actually keeps.

Co-authored-by: Claude (claude-opus-5) <noreply@anthropic.com>
@arelchan
arelchan force-pushed the fix/tui_surface_internal_error_detail branch from b6b8ab4 to e8417b2 Compare August 17, 2026 13:08
@claude

claude Bot commented Aug 17, 2026

Copy link
Copy Markdown

Claude finished @arelchan's task in 3m 0s —— View job


Review

No blockers; this can merge.

  • Read prior review comments (gloryfromca) to avoid repeats
  • Fetch diff (git diff origin/main...HEAD) and read changed files
  • Check whether the second commit addresses gloryfromca's log_path-truncation nit
  • Review correctness: dispatcher, errors.py, turn.py, errors.ts
  • Review repo conventions (commit format, tests, CONTEXT-MAP)
  • Review test coverage/weakening
  • Post final findings

What's new since gloryfromca's review

The second commit (e8417b2, "put the log pointer where the renderer cannot cut it") directly resolves gloryfromca's non-blocking nit. I traced it end to end:

  • raven/tui_rpc/methods/turn.py:137-159 (_build_error_detail) now prepends (details in {log_path}) in front of detail instead of appending it. ui-tui/src/app/chatStream.ts:221 still does detail.split('\n')[0].slice(0, 200), i.e. only the first line survives — with the log path now first, it lands inside that surviving slice even when the cause is a multi-line ValidationError (the exact motivating case: a config the running build cannot parse). Confirmed the new test tests/test_tui_rpc_turn_send.py::test_the_log_path_survives_the_transcript_renderer_on_a_multiline_cause asserts against the renderer's own slicing logic, not just the raw string. This is a genuine fix, not a workaround.
  • The other new path, ui-tui/src/rpc/errors.ts (formatRpcError, used for generic dispatcher RpcErrors, not the per-turn ErrorEvent.detail), still appends log_path at the end. That's fine there — I checked every call site that renders err.message (useInputHandlers.ts:506,543, useSessionLifecycle.ts:233, entry.tsx:104) and none of them truncate, so there's no equivalent risk on that path.
  • error_data() (raven/tui_rpc/errors.py:64-75) is shared by both the dispatcher's error frame and _build_error_detail, so detail/reason/log_path stay consistent across the two emission points, matching the PR's stated goal.
  • Confirmed _build_tui_agent_loop (raven/cli/tui_commands.py:517-559) is still the only producer and always sets detail=str(e) plus log_path in data, so the fix actually reaches the scenario it's meant for.

The doc-comment nit gloryfromca flagged (ui-tui/src/rpc/errors.ts:21, "see" vs. "details in") is still present — not re-raising it since it was already called out as a non-blocking nit and nothing new here changes that.

Conventions

Both commit headers are Conventional Commits with a real scope, ASCII-only (git log -2 | grep -P "[^\x00-\x7F]" empty), and no new files were added (all 7 changed files are modifications to existing tests/sources — no tests/test_* naming violation, no assets).

Tests

New/changed tests genuinely assert behavior (not weakened): the dispatcher test asserts detail/reason/log_path all land in error.data together; the turn.py tests cover the cause being emitted, the multi-line-survives-truncation case, and the no-cause-omits-detail case; the TS tests cover the bare/data/multi-line/hostile-shape/typed-subclass cases. All look like real assertions, no snapshot-only or trivially-true checks.

Note: I could not execute uv run pytest / npx vitest myself in this sandbox (Bash network/tool execution requires approval I don't have here), so I verified correctness by static trace against callers and existing test expectations rather than by re-running the suite. gloryfromca's review already reports both suites green after commit 9c7e879; e8417b2's new tests are consistent with the implementation on inspection.

@gloryfromca gloryfromca left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No blockers; this can merge as far as I am concerned.

Re-reviewing only the delta since b6b8ab48, which is where I left off. The first commit is byte-identical after the rebase (diff <(git show b6b8ab4851b7 --format="") <(git show 9c7e8797 --format="") is empty), so the substance I already reviewed has not moved and I am not repeating it. The new commit e8417b22 is the one thing I asked for and nothing else: the log pointer moved to the front of detail, and the existing whole-string assertion was updated rather than relaxed, with a second test added that slices detail the way chatStream.ts does -- so the shape that failed before now fails the suite instead of passing it. That is the right place to put the assertion; the previous one passed a trailing pointer just as happily, which is exactly how the gap got in.

On the delta I checked: that the renderer this is calibrated against is still the same after the rebase (ui-tui/src/app/chatStream.ts:250, moved from :221 by main's changes, logic unchanged); that no other test or source pins the old appended order (grep -rn "details in" finds only the new Python string, its test, and the TS frame path, which puts the pointer on its own line and is unaffected); that the 200-char slice still leaves ~165 chars for the cause after the ~35-char pointer; and that the commit message is ASCII with a real scope.

uv run pytest tests -k tui -q
617 passed, 7 skipped, 5827 deselected in 25.57s

cd ui-tui && npx vitest run
Test Files  86 passed (86)
     Tests  990 passed (990)

uv run ruff check raven/tui_rpc tests/test_tui_rpc_turn_send.py
All checks passed!

Count went 611 -> 617: one is the new test here, the rest came in with the rebase onto main. The 7 skips are the same pre-existing optional-channel import guards.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants