Skip to content

feat(*): let a failed turn's row open to its whole error - #978

Merged
LivXue merged 11 commits into
mainfrom
feat/full_failure_text
Oct 17, 2026
Merged

LivXue merged 11 commits into
mainfrom
feat/full_failure_text

Conversation

@LivXue

@LivXue LivXue commented Oct 16, 2026 •

Copy link
Copy Markdown
Member

Summary

A failed turn's Web row can expand to the provider's full diagnostic text, up to 8,000 characters, while the existing bounded reason stays unchanged. The optional full_detail field travels through the runner, lane, RPC event and session marker, so live display and replay expose the same account. Copy includes the expanded account. Quiet runtime notices use "Show details" instead of the error label.

Failure details and call-record bodies remove known credential shapes before the final cap. An unterminated private-key block is removed through the end of the input, including when an earlier scan or call record cut away its closing armour. Known cut boundaries also remove incomplete JWTs, vendor tokens, URL passwords and labelled credential tails; each of those patterns is searched only from behind the run of its own value's characters that ends the input and the label in front of it, so the scan stays linear whatever the text holds and still reads a label gap of any width. The authorization and JWT patterns avoid repeated backtracking, and the turn path and outer runner reuse the computed detail. Attachment and lane reads use the base exception's instance storage, so a foreign read-only property or throwing getter cannot replace the scrubbed value or swallow the terminal failure event.

The optional fields are mirrored in the RPC schema and both generated clients. The design document separates this delivery from the subsequent Turn Continuation feature; Retry still starts an ordinary turn.

Type

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

Verification

The automated suites and static/build checks below were re-run after synchronizing with main, using the existing local dependencies. The independent behavioral and timing probes were measured before that synchronization. The last three commits change only raven/security/redact.py, two test files and the design document; on that final tree the Python suite, the ui-web tests, the type, generation and lint checks, ty, the import contracts, the range gates and pre-commit were run again, and the ui-web build checks were not, since none of their inputs changed.

  • uv run --no-sync pytest -q tests/test_agent_loop_failure_text.py tests/test_llm_call_record.py tests/test_agent_loop_broken_turn.py tests/test_agent_loop_llm_error_patience.py tests/test_agent_loop_run_emit.py tests/test_spine_scheduler_lane.py tests/test_spine_events.py tests/test_rpc_spine.py tests/test_rpc_schema_match.py tests/test_kernel_budget.py tests/test_acp_redact.py tests/test_security_untrusted_context.py -x --tb=short --show-capture=no: 904 passed.

  • From ui-web: npm test -- --no-file-parallelism src/features/transcript/TranscriptPage.test.tsx src/state/session/pipeline.events.test.ts scripts/gates/i18n-keys.test.mjs: 225 passed.

  • From ui-web: npm run type-check, npm run gen:check, node ../ui-tui/scripts/gen-i18n.mjs --check, node ../ui-tui/scripts/gen-rpc-types.mjs --check, and node node_modules/eslint/bin/eslint.js src/features/transcript/TranscriptPage.tsx src/features/transcript/TranscriptPage.test.tsx: passed.

  • uv run --no-sync lint-imports: 10 contracts kept. uv run --no-sync ty check raven evolver agents plugins-dist scripts: passed. Ruff check and format check passed on the 20 Python files this PR changes.

  • From ui-web: npm run build, uv run --no-sync python build.py, node scripts/check-page.mjs, node scripts/check-css.mjs, and node scripts/check-class-namespace.mjs: passed, including both boot snapshots.

  • UV_NO_SYNC=1 make check-large-files check-source-language COMMIT_RANGE=origin/main..HEAD: passed. PYTHONPATH=. uv run --no-sync python scripts/check_commit_messages.py origin/main..HEAD, commitlint over the same range and git diff --check origin/main...HEAD: passed.

  • uv run --no-sync pre-commit run --from-ref origin/main --to-ref HEAD: all applicable hooks passed except the whole-TUI Prettier hook, which reports newInstancePicker.tsx and subagentsHub.tsx. Both files and the formatter config are unchanged from main; an isolated main-file control with the same locked Prettier version reproduces both failures. node ui-tui/node_modules/prettier/bin/prettier.cjs --check ui-tui/src/i18n/messages.generated.ts: passed.

  • Regression coverage includes truncated private keys, incomplete JWT and credential tails, terminal-event delivery with foreign exception attributes, scan reuse and CPU cost, the cut-tail scan's cost on label-shaped runs and long gaps, its agreement with a whole-text search, a label gap of any width, the JWT boundary and private-key END rules, and quiet-notice expansion. The five independent boundary probes passed before the base synchronization; the committed regression suite above passed again afterwards.

  • On the same 32,267-character synthetic crash input, the queued event-loop callback delay fell from 15.351 seconds to 0.00951 seconds. The regression checks use thread CPU time with a 0.5-second ceiling.

  • Cut-tail scan: a failed turn whose crash text holds 31,000 spaces before its last two words took 32.6 seconds while the region came from a regex search for the last whitespace, and 0.016 seconds now; 64,000 characters of pwd= ending in a comma took 10.8 seconds through the whole-text tail search and 0.003 seconds now, and a 300,000-character call-record body of either shape takes under 0.3 seconds. These and the figures below were measured with the machine's load average between 36 and 132. Across 3,728 timed texts, every label or prefix unit repeated to 64,000 characters behind a cut and to 300,000 past the scan cap, the slowest measured alone took about 0.2 seconds against about 0.1 seconds on the base revision, and a text past the cap takes the same time at twice the size. Searching the whole text again turns six timing cases red; a smaller name room, a skipped gap or scheme step, or any value class narrowed below its pattern's turns the property or gap test red, as does removing the JWT boundary or restoring the END-only private-key rule. On 12,008 generated texts with gaps up to 5,000 characters, the output equals the whole-text version's.

  • Chromium with synthetic local data: full text retained and HTML escaped, expanded tooltip removed, internal scrolling and Retry preserved, neutral notice label displayed, no page errors. No live vendor call or native Windows rerun is claimed for these fixes.

  • Relevant tests pass locally

  • Relevant lint / type checks pass locally

  • User-facing docs or screenshots are updated when needed

Risk

  • Security impact considered: the long text is scrubbed of recognized credential shapes before persistence and publication. It remains diagnostic text and may contain hostnames or paths. Model history and chat-channel replies keep their existing bounded content. A session export can include the long text. raven.security.redact is also the scrubber for every ACP surface (tool titles, result previews, permission prompts, replay, error messages) and for the outbound client's permission log, so two of its rules reach those surfaces too: a private-key header without END hides the rest of that text, and a JWT glued to a preceding word character, such as /verify-eyJ..., is left unredacted. Tests pin both.
  • Backward compatibility considered: the new fields are optional, older markers fall back to their bounded reason, and the TUI can ignore the extra field. Incomplete private-key blocks are conservatively removed to the end, which can also omit trailing diagnostic text. Custom runners attach long details in the exception's instance dictionary; descriptors are ignored.
  • Rollback path is clear for risky changes: revert the squash commit. Older code ignores the optional marker field.

Related Issues

N/A

@LivXue
LivXue requested review from 0xKT and gloryfromca October 16, 2026 03:17

@gloryfromca gloryfromca left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

No blockers; suggestions only, and they are marked inline.

I reviewed the full github/main...HEAD diff and found one narrow, nonblocking UI-label regression. The runtime-to-RPC-to-Web path otherwise preserves the bounded text, carries the scrubbed/capped long form consistently live and on replay, and keeps the kernel boundary intact.

Coverage included AGENTS.md/CLAUDE.md and the Runtime/Web CONTEXT terms, the changed code and tests, relevant callers and history, backward compatibility for older markers, test-strength changes, generated contracts, and architecture constraints. I found no weakened tests.

Verification:

  • Python 3.12 with the dev extra: 808 focused runtime, schema, and kernel tests passed.
  • Web UI: 217 focused tests passed; RPC generation check and TypeScript type-check passed.
  • Import-linter: 10 contracts kept, 0 broken.
  • Source-language, large-file, commit-message, and git diff --check gates passed (the three repository scripts were invoked directly because make is unavailable).
  • The first default Python run produced 808 setup errors because the fresh default environment lacked raven_everos; the documented all-extras sync then could not build datrie because this host has no GCC. Syncing the repository's dev extra under Python 3.12 supplied the required test plugins and produced the passing run above.

Comment thread ui-web/src/features/transcript/TranscriptPage.tsx
@LivXue

LivXue commented Oct 16, 2026

Copy link
Copy Markdown
Member Author

Windows compatibility review of head bd9048019937491759e88fb974963540168aa115 against base 400b30dfa369a094e80949c6860a7d215c835e07.

No actionable Windows regression found in the inspected 35-file change. Code inspection covered exception/body extraction, scrubbing and caps, runner/lane/RPC propagation, UTF-8 session persistence, both generated clients, web live/replay rendering, tests and docs. The continuation design remains documentation; this PR implements full failure text.

Native Windows verification (Python 3.13.12, isolated source/environment/home):

  • 525 passed in the focused runner, lane, event and RPC contract selection below. The earlier broad run also completed all 81 failure-text, call-record and broken-turn cases.
  • 1 native probe passed: an actual held-open-file deletion raised WinError 32; its long Windows path, containing Unicode, a space and literal %20, survived the real failed-turn JSONL marker unchanged while reason stayed bounded.
  • Web: npm test -- --no-file-parallelism src/features/transcript/TranscriptPage.test.tsx src/state/session/pipeline.events.test.ts: 217 passed; npm run gen:check and npm run type-check: passed. TUI: npm run lint:rpc and npm run lint:i18n: passed.

Broader Python selections stopped on three existing tests: the two image-window tests below counted 5 provider calls instead of 4 after Raven's permission judge intercepted their fake tool; the tool-event test returned an approval error because its fake provider lacks chat_with_retry. Each identical failure was reproduced at the exact base SHA, with the same dependency lockfile. These limit the broad-suite result; they are not reported as regressions from this PR.

Exact focused commands and baseline controls

All pytest invocations appended -n 0 -x --tb=short --basetemp "$check/pytest" --junitxml "$check/junit.xml". $check was the corresponding suite directory under C:/Users/livxu/AppData/Local/Temp/raven-windows-review/cc3b425a13254574afc5fed8067d31f7/checks/.

Focused head command (978-changed-tail, 525 passed):

uv run pytest `
  tests/test_agent_loop_llm_error_patience.py::test_a_call_the_ladder_gave_up_on_keeps_the_body_its_record_kept `
  tests/test_agent_loop_run_emit.py::test_run_turn_hands_the_lane_the_long_account_of_a_crash `
  tests/test_rpc_spine.py::test_outlet_emit_error_includes_the_long_account_when_present `
  tests/test_rpc_spine.py::test_a_failed_turn_sends_the_runners_long_account `
  tests/test_spine_scheduler_lane.py::test_a_runners_long_account_rides_the_failure `
  tests/test_spine_scheduler_lane.py::test_an_answerless_turn_carries_its_long_account `
  tests/test_spine_scheduler_lane.py::test_a_crash_with_no_long_account_carries_none `
  tests/test_spine_scheduler_lane.py::test_only_a_non_empty_string_counts_as_a_long_account `
  tests/test_spine_events.py tests/test_rpc_schema_match.py `
  -n 0 -x --tb=short --basetemp "$check/pytest" --junitxml "$check/junit.xml"

Native probe (978-native-path, 1 passed): uv run pytest -c pyproject.toml C:/Users/livxu/AppData/Local/Temp/raven-windows-review/cc3b425a13254574afc5fed8067d31f7/test_windows_failure_path.py, with the pytest flags above.

Base controls, each run separately with the same flags (each failed as at head):

uv run pytest tests/test_agent_loop_llm_error_patience.py::test_a_size_refusal_closes_the_window_for_the_rest_of_the_turn
uv run pytest tests/test_agent_loop_llm_error_patience.py::test_the_configured_budget_collapses_the_window_and_zero_leaves_it_open
uv run pytest tests/test_agent_loop_run_emit.py::test_run_tool_call_emits_tool_events_and_notice

The control suite directories were 978-failure-base, 978-budget-base and 978-tool-base, respectively. Environment setup: uv sync --frozen --python 3.13 --all-packages --extra dev; UI dependencies: npm ci --ignore-scripts --no-audit --no-fund.

Coverage limits: no live vendor call, browser end-to-end session or full-suite pass claimed. Git fetch/local archive problems required full-SHA GitHub source archives; all 35 changed head-file blobs were verified against API metadata.

@LivXue
LivXue force-pushed the feat/full_failure_text branch from bd90480 to 3df58d3 Compare October 16, 2026 07:47

@gloryfromca gloryfromca left a comment

Copy link
Copy Markdown
Member

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.

I reviewed the revision delta and range-diff from bd9048019937, including the new failure-detail cache, descriptor-safe transport, truncated-secret handling, call-record integration, neutral notice label, and their callers/tests. The prior UI-label follow-up is fixed for live and replayed notices. I found no new correctness, compatibility, security, test-strength, or architecture issue.

Coverage included the repository rules and Runtime/Web context, the changed implementation and callers, history/rebase separation, older-record compatibility, test assertions, generated localization, and the kernel/import boundaries. Tests were strengthened rather than weakened.

Verification:

  • 175 focused Python security/failure-path tests passed.
  • 497 kernel, RPC-schema, and architecture tests passed.
  • 187 transcript tests passed.
  • Web TypeScript type-check and RPC generation check passed.
  • TUI i18n generation check passed after installing its locked dependencies.
  • Import-linter kept all 10 contracts.
  • Source-language, large-file, commit-message, and git diff --check gates passed against the current PR base.

@gloryfromca gloryfromca left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

No blockers; suggestions only, and they are marked inline.

I reviewed the delta from 3df58d385b80, its callers, security trade-offs, tests, history, compatibility, repository rules/context, and architecture boundaries. The CPU-cost optimization is effective and the added tests strengthen coverage, but its fixed 64-character pre-run window creates one narrow redaction gap when a known-cut credential label and short value have at least 64 whitespace characters between them. This is a low-reachability follow-up, not a merge blocker under the fourth-round bar.

Verification:

  • 179 focused security, failure-path, call-record, persistence, and lane tests passed.
  • Import-linter kept all 10 contracts.
  • Source-language, large-file, commit-message, and git diff --check gates passed for the revision delta.
  • A direct public-API reproduction confirmed the named follow-up.

Comment thread raven/security/redact.py Outdated
LivXue and others added 11 commits October 16, 2026 11:45
A failed turn showed one line, cut at 140 characters of a text the
runtime had already bounded to 200, so a provider's own account (which
pool refused, what to do about it) never reached the page.

The runner now builds a long account beside the bounded text: the
exception's message, then the body its HTTP client kept on it (litellm
reduces an OpenAI-style error body to one line and keeps the rest on
exc.body), scrubbed with the call record's body scrubber and then
raven.security.redact, read within a bounded window and capped at the
call record's 8,000-character body ceiling. It is set on the exception
the runner lets out, since the kernel imports nothing to scrub with,
and only when it says more than the bounded text. The lane copies it
onto TurnFailed.full_detail, and the error event and the turn's session
marker carry it as an optional full_detail field.

The page's failed row gains a toggle that opens the account in a
wrapped, scrolling block, live and when a session is replayed. Every
200-character text is unchanged.
The full-failure-text plan named its worktree and the main checkout by
their absolute paths on one machine. A reader elsewhere cannot follow
them, and they carried a local directory name into the public tree.
The plan now names the two once, as $WT and $MAIN, and its commands
use those.
…ries

The comment above the lane's crash arm said the error event carries only
the exception's class and message. The event can now carry the long
account a runner attached as well, so the comment says that, and keeps
the point it was making: the traceback never leaves the process, so this
log line is its only record.
The long account runs two scrubbers. Every credential test used a
vendor-shaped key, which raven.security.redact catches alone, so dropping
the call record's body scrubber failed only the scan-window test, whose
spy replaces that scrubber. A provider body that echoes a request's
Authorization header as JSON, with a token of no vendor shape, is caught
by the body scrubber alone; the new test holds that, and fails on its own
when the body scrubber is dropped.
Two single-construct mutants of the full failure text left every test
green: a cap that cut an account of exactly MAX_BODY_CHARS characters,
and an HTTP error whose parsed body is {} or [] read as that body. Each
now has a test that fails on its own mutant: an account at the ceiling
is kept whole, and an empty parsed body is no body, so the call record
falls back to the exception's message.
…e and copy

Three mutants of the page's half left the suite green. The test that
compares a live row with a replayed one drew the live row with
mount.note, past noteRow, so dropping noteRow's full option went unseen;
it now draws through noteRow, the call the session's error stage makes.
An opened row keeping its tooltip, and Copy carrying the short line
instead of the whole account, are now pinned in the toggle test and in a
new copy test.
ui-web/CONTEXT.md retires "page" for the whole document, and the
turn-continuation spec, the full-failure-text plan and one test docstring
used it for the web client. They now say "web client". The spec also
qualifies two words it took from neighbouring entries, "an older Raven
gateway" and wsCurrentTurn() for the workspace's turn count, and a stale
row now sends a go-on message rather than injecting one.
Scrub incomplete private-key blocks before they reach failure records, avoid
backtracking on authorization whitespace and repeated JWT prefixes, and
reuse each exception's scrubbed account. Read and write the base exception
storage so foreign descriptors cannot publish raw data or suppress failure
notification. Give quiet notices a neutral expansion label and refresh the
specification and plan.

Co-authored-by: Codex <noreply@openai.com>
The tail patterns that remove a credential at a known cut end at the end
of the input, and re.sub retries them at every position. A
credential-shaped run that ended before other text therefore made each
retry walk the rest of the run again, so the cost grew with the square of
the run's length: 32,000 characters of "--token=" followed by a space cost
0.86 seconds of CPU on the failure path, past the 0.5-second pin, and a
262,000-character provider body cost 9 to 36 seconds on the event loop
inside the call record.

A tail match can only lie in the text's last run of non-whitespace
characters and the label in front of it, so the patterns now read that
region. A long final run is still read whole, which keeps a truncated
token hidden. Over 48 inputs the output is identical to scanning the
whole text. Both shapes join the timing test.
raven.security.redact scrubs every ACP surface as well as failure
records, so two choices this branch made reach tool titles, result
previews, permission prompts, replay and error messages too. A JWT is
matched only where a token can start, which keeps a body of "eyJ" runs
linear and leaves a token glued to a word, such as "/verify-eyJ...",
unredacted. A private-key header with no END replaces everything after
it, so a preview that only quotes the header loses the lines below it.
Pin both with the cases each still catches, and name what the JWT
boundary gives up in the design document.
The tail patterns' region was found with a regex search for the last
whitespace, which retries every position: 31,000 spaces before the
last two words of a failure cost 32 seconds on the event loop. The
region also began only 64 characters before the final run, so a label
with a wider gap before a short cut value was missed, and a text with
no whitespace in its last stretch kept the whole-text cost: 64,000
characters of "pwd=" ending in a comma took 10.8 seconds, and the scan
cap turns the same pass on for any text over 256 KiB on every ACP
surface.

A tail pattern's value runs to the end of the text and is made of one
class of characters, so its match lies in the run of that class that
ends the text, behind a label whose gap, scheme word and name come
before it. Each search now starts behind those, found by matching the
reversed text step by step, which reads a gap of any width once. A
property test checks the result against the whole-text search on
3,000 generated texts and one long value per pattern, and 12,008 other
texts with gaps up to 5,000 characters give the same output as the
whole-text version. The reported shapes, a label gap and three label
runs join the timing tests.
@LivXue
LivXue force-pushed the feat/full_failure_text branch from e307bc8 to e72634a Compare October 16, 2026 11:50

@gloryfromca gloryfromca left a comment

Copy link
Copy Markdown
Member

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.

The latest redaction change fixes the previously reported unbounded-gap miss while retaining the bounded failure-path cost. I found no new actionable defect.

Covered the refreshed github/main...HEAD diff and the latest commit delta; applicable AGENTS/CLAUDE/CONTEXT rules; redaction callers and commit history; backward compatibility against the former whole-text behavior; test changes for weakening or overfitting; and the existing agent/lane/RPC/UI architecture boundaries. The propagation and rendering portions are unchanged from the earlier rounds.

Verification: 190 focused Python tests passed; 50,000 independently generated cut-tail inputs matched the whole-text reference; the original 64-space case and a 10,000-space case redact correctly; long-run stress took 0.166 s CPU; all 10 import contracts passed; source-language and large-file policy checks passed.

@LivXue

LivXue commented Oct 16, 2026

Copy link
Copy Markdown
Member Author

Windows compatibility follow-up at e72634accfba7546139223776c7ee98a2f6a77ac, base 18f67b558d599e986f47e2f57c547cf57c050230.

No new actionable Windows issue found in the inspected 37-file scope. Range-diff confirms the seven previously reviewed commits replay unchanged. Inspected the added bounded redaction/cache and exception-storage changes, their callers/tests, the quiet-notice label, documentation and the upstream stylesheet overlap.

Native Windows / Python 3.13.12 verification:

  • 786 passed in the Python selection below, including cut-tail CPU bounds, foreign exception properties, persistence, lane delivery and RPC schemas.
  • 1 native probe passed: a real WinError 32 with a Unicode/space/literal-%20 Windows path retained its full text in the persisted marker, with the short reason still bounded.
  • In ui-web: npm test -- --no-file-parallelism src/features/transcript/TranscriptPage.test.tsx src/state/session/pipeline.events.test.ts scripts/gates/i18n-keys.test.mjs: 225 passed. npm run gen:check and npm run type-check: passed.
Python commands

$scratch = C:/Users/livxu/AppData/Local/Temp/raven-windows-review/666702105adf49d5b459a82710c0bf59. Both commands appended -n 0 -x --tb=short --basetemp "$scratch/checks/SUITE/pytest" --junitxml "$scratch/checks/SUITE/junit.xml".

Suite 978-failure-path:

uv run pytest tests/test_agent_loop_failure_text.py tests/test_llm_call_record.py tests/test_agent_loop_broken_turn.py tests/test_spine_scheduler_lane.py tests/test_spine_events.py tests/test_rpc_spine.py tests/test_rpc_schema_match.py tests/test_acp_redact.py tests/test_agent_loop_run_emit.py::test_run_turn_hands_the_lane_the_long_account_of_a_crash tests/test_agent_loop_llm_error_patience.py::test_a_call_the_ladder_gave_up_on_keeps_the_body_its_record_kept

Suite 978-native-path: uv run pytest -c pyproject.toml "$scratch/test_windows_failure_path.py".

Setup: uv sync --frozen --python 3.13 --all-packages --extra dev; Web dependencies: npm ci --ignore-scripts --no-audit --no-fund.

Limits: no live provider, browser end-to-end or full-suite pass claimed; only the named cases from the broader run-emit and error-patience files were rerun. Tests used isolated exact-SHA source/home/environment, with every changed blob verified.

@LivXue
LivXue merged commit ccf1e2d into main Oct 17, 2026
28 checks passed
@LivXue
LivXue deleted the feat/full_failure_text branch October 17, 2026 06:28
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.

3 participants