fix(hooks): pass the tool exit code to Runtime API tool_call_after - #6656
Merged
3 commits merged intoSep 28, 2026
Merged
3 commits merged into
3 commits merged into
Conversation
Codex Review SummaryThis comment shows the latest Codex review activity on this pull request.
ℹ️ About Codex in GitHubYour team has set up Codex to review pull requests in this repo. Reviews are triggered when you
Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings. |
fire_runtime_tool_completion_hooks hard-coded the exit code to None, so DEEPSEEK_TOOL_EXIT_CODE was never set and exit_code conditions never matched on Runtime API threads, while the TUI read it from the tool's metadata. Move reported_tool_exit_code from tui::tool_routing into hooks::executor and have both surfaces use it; its unit test moves with it. Also correct the hooks module doc, which still said only the TUI fires hooks (docs/HOOKS.md already lists Runtime API threads). Refs #6582 Tests (focused, codewhale-tui --lib): 22 passed, 0 failed (runtime_tool_completion_fires_after_and_error_hooks, runtime_shell_completion_delivers_exit_code_to_after_hook, reported_tool_exit_code_reads_only_real_metadata_codes, tool_routing::). Negative control: with the exit code discarded, the new test fails ("call-shell unset" vs "call-shell 0"). Gates: cargo fmt --check clean; clippy -p codewhale-tui --all-targets --all-features with CI flags clean; sync-changelog --check, provider registry, command boundaries, migration manifest, reqwest builders, dead-code and blocking-calls budgets, locale parity, product vocabulary, contributor credit all pass. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
bash reports a nonzero exit, timeout, or kill as Err(ToolError::ExecutionFailed), and that error carried no metadata; the exit_code/status metadata was built only for the success path, and the hook helper read metadata only from Ok results. So every failing command reached tool_call_after and on_error with no DEEPSEEK_TOOL_EXIT_CODE, in the TUI and on Runtime API threads. - ToolError::ExecutionFailed gains `metadata: Option<Value>`, with execution_failed_with_metadata() and metadata(). execution_failed() and the error's display text are unchanged, so the model still sees a failed call with the same message. - bash's failure path attaches the same metadata a success carries. - HookContext::with_tool_outcome(&result) sets text, success, exit code and status; the TUI and Runtime API completion hooks both use it and drop their own copies of that derivation. - New DEEPSEEK_TOOL_STATUS: completed / failed / timed_out / killed / running, only from the shell statuses tools record. - docs/HOOKS.md (+ zh_hans), CHANGELOG, synced crates/tui/CHANGELOG.md, regenerated web/lib/changelog.generated.ts. Refs #6582 Tests (focused, codewhale-tui --lib): 52 passed, 0 failed for the hook, shell and tool_routing filters, including the new bash_completion_hooks_get_exit_code_and_status_for_failures (TUI) and runtime_shell_completion_delivers_exit_code_and_status_to_hooks (Runtime API), each covering exit 0, exit 1, exit 127 and a timeout; hooks::/tool_routing::/error_taxonomy/protocol_parity run: 224 passed, 0 failed; codewhale-tools: 29 passed, 0 failed. Negative control: without the error metadata those 2 tests plus the 2 updated shell tests fail (4 failed; hooks saw "call-exit-1 unset unset"). Gates: cargo fmt --check clean; clippy -p codewhale-tools -p codewhale-tui --all-targets --all-features with CI flags clean; sync-changelog --check, check-versions --range-audit-advisory, contributor credit (v0.10.0 and default), provider registry, command boundaries, migration manifest, reqwest builders, dead-code and blocking-calls budgets, locale parity, product vocabulary, README translations/locales, web derive scripts, derive-install --check, vitest lib/public-copy.test.ts (6 passed). Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Hmbown
force-pushed
the
fix/runtime-hook-exit-code
branch
from
September 27, 2026 02:55
87ae150 to
e8e0d4e
Compare
…-code # Conflicts: # CHANGELOG.md # crates/tui/CHANGELOG.md
Hmbown
pushed a commit
that referenced
this pull request
Sep 27, 2026
# Conflicts: # CHANGELOG.md # crates/tui/CHANGELOG.md # crates/tui/src/runtime_threads/tests.rs
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.
What
Hooks now get a shell command's real exit code, and how it ended, on both the TUI and Runtime API threads. That holds for failing commands as well as passing ones.
There were two causes:
fire_runtime_tool_completion_hooks(crates/tui/src/runtime_threads.rs) called.with_tool_result(&text, success, None), soDEEPSEEK_TOOL_EXIT_CODEwas never set on Runtime API threads.bashcommand lost its exit code on both surfaces.finish_contract_bash_result(crates/tui/src/tools/shell.rs) reports a nonzero exit, a timeout, or a kill asErr(ToolError::ExecutionFailed { message }). Theexit_code/statusmetadata it builds went only on the success path, and the hook helper read metadata only fromOkresults. So every failing command reachedtool_call_afterandon_errorwith no exit code.Changes
ToolError::ExecutionFailedgainsmetadata: Option<Value>(crates/tools/src/lib.rs), along withToolError::execution_failed_with_metadataandToolError::metadata().execution_failed(..)is unchanged (None). Patterns that named{ message }now say{ message, .. }. The error's display text and the model-facing contract ("nonzero must be a failed tool call") are unchanged.bash's failure path attaches the same metadata a success carries (exit_code,status,duration_ms, …).HookContext::with_tool_outcome(&result)sets the text, success flag, exit code, and status from a settled call. The TUI (tui/tool_routing.rs) and Runtime API (runtime_threads.rs) completion hooks both use it now, so each drops its own copy of the text/success/exit-code derivation.reported_tool_exit_codereads metadata from anOkresult or anErr, and is private tohooks::executor.DEEPSEEK_TOOL_STATUS:completed,failed,timed_out,killed, orrunning. It is set only when a shell tool recorded one of those statuses, and never passes through an arbitrary metadata string. A timed-out command has no exit code, and this is how a hook can tell it apart.docs/HOOKS.md,docs/zh_hans/HOOKS.md, and the CHANGELOG are updated.crates/tui/CHANGELOG.mdis synced.hooksmodule doc no longer says only the TUI fires hooks.docs/HOOKS.mdalready listed Runtime API threads.Tests
Both paths run the real
bashtool for exit 0, exit 1, exit 127 (a missing command), and a timeout. They assert whattool_call_afterandon_errorreceive:DEEPSEEK_TOOL_EXIT_CODEDEEPSEEK_TOOL_STATUSDEEPSEEK_TOOL_SUCCESSon_error0completedtrue1failedfalse127failedfalsetimed_outfalsetui::tool_routing::tests::bash_completion_hooks_get_exit_code_and_status_for_failures(throughhandle_tool_call_complete).runtime_threads::tests::runtime_shell_completion_delivers_exit_code_and_status_to_hooks(throughfire_runtime_tool_completion_hooks).reported_tool_exit_code_reads_only_real_metadata_codesnow also covers an error carrying code 127, a timeout with a status but no code, and an unknown status string that is not passed through.contract_bash_nonzero_is_an_error_with_status_after_outputandlowercase_bash_timeout_uses_seconds_and_failsnow assert that the error carriesexit_code/status.Results:
codewhale-tui --librun for the hook, shell, and tool_routing filters: 52 passed, 0 failed. Thehooks::,tool_routing::,error_taxonomy, andprotocol_parityrun: 224 passed, 0 failed.codewhale-tools: 29 passed, 0 failed.call-exit-1 unset unset false.cargo fmt --checkis clean. Clippy (-p codewhale-tools -p codewhale-tui --all-targets --all-features, CI flags) is clean. These all pass:sync-changelog --check,check-versions --range-audit-advisory, contributor credit, the cheap Lint scripts, the web derive scripts, and vitestlib/public-copy.test.ts(6 passed). The persistence-backlog and runtime-contract measurement budgets were not run locally; CI runs them.Refs #6582. The structured receipt feature requested there stays open.
🤖 Generated with Claude Code