fix(session): repair unanswered tool_use so a recovered session stays usable - #19
Merged
Merged
Conversation
… usable Phase 4 (API & provider layer). One real defect, found in the interaction between two things that are each individually correct. ## A recovered session could be permanently un-sendable The API rejects an assistant turn containing a `tool_use` that no following user turn answers. Nothing in the codebase validated history before sending — there is no repair, sanitise, or validate step anywhere on the request path. History can legitimately reach that shape. PR #18 made `load_messages` tolerate a torn final line so a crash mid-append costs one turn instead of the whole conversation. But if the torn line was the *tool_result*, recovery drops it and leaves the preceding assistant `tool_use` unanswered. The session file is now intact and the session is unusable: the next request 400s, and so does every one after it, with no path back. So the Phase 3 fix could convert total loss into permanent breakage. Better, but not fixed. `repair_dangling_tool_uses` now runs after parsing: any `tool_use` without a matching `tool_result` gets a synthesised error result saying the turn was interrupted. That is honest — the tool genuinely produced no recorded result — and it is the only shape the API accepts short of discarding the assistant turn, which would lose more than it saves. Two details that matter: - Stubs are spliced into the *existing* following user turn when there is one, rather than inserting a second user message, which would leave two user turns in a row. - Only unanswered ids are stubbed, so a partially-answered parallel tool call keeps its real results. ## Checked and cleared, not changed - **The live tool loop cannot produce an unanswered `tool_use`.** Every path through `execute_tools` pushes a `ToolResult` first: hook-blocked, middleware -denied, unknown tool, and tool error all push before continuing. The `?` at the call site is vestigial — nothing inside the function can return `Err`. - **Cancellation is safe.** Messages are persisted only on turn completion (`AppEvent::Done`), so an abandoned turn never reaches disk. - Zero panics in production code across all five Phase 4 files. - SSE idle timeout (4.1) and cost estimate flagging (4.2) landed earlier. ## Still open No retry/backoff on 429 anywhere. 529 is handled (falls back to a secondary model once); rate limiting is not, and `retry-after` is ignored. Tracked as a cross-cutting item rather than smuggled into this change. QA (triple-checked): 611 tests, 0 failures. Clippy clean under the CI gate. Release 19.08 MB. Phase 1-3 suites re-run green. The repair verified by disabling it, which fails the end-to-end recovery test. Co-Authored-By: Arch Linux <noreply@archlinux.org>
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.
Phase 4 (API & provider layer). One real defect, found in the interaction between two things that are each individually correct.
A recovered session could be permanently un-sendable
The API rejects an assistant turn containing a
tool_usethat no following user turn answers. Nothing in the codebase validated history before sending — there is no repair, sanitise, or validate step anywhere on the request path.History can legitimately reach that shape. PR #18 made
load_messagestolerate a torn final line, so a crash mid-append costs one turn instead of the whole conversation. But if the torn line was thetool_result, recovery drops it and leaves the preceding assistanttool_useunanswered. The session file is now intact and the session is unusable: the next request 400s, and so does every one after it, with no path back.So the Phase 3 fix could convert total loss into permanent breakage. Better, but not fixed.
repair_dangling_tool_usesnow runs after parsing: anytool_usewithout a matchingtool_resultgets a synthesised error result saying the turn was interrupted. That's honest — the tool genuinely produced no recorded result — and it's the only shape the API accepts short of discarding the assistant turn, which loses more.Two details that matter:
Checked and cleared, not changed
tool_use. Every path throughexecute_toolspushes aToolResultfirst — hook-blocked, middleware-denied, unknown tool, tool error. The?at the call site is vestigial; nothing inside can returnErr.AppEvent::Done), so an abandoned turn never reaches disk.Still open
No retry/backoff on 429 anywhere. 529 is handled (falls back to a secondary model once); rate limiting is not, and
retry-afteris ignored. Tracked as a cross-cutting item rather than smuggled into this change.QA (triple-checked)
611 tests, 0 failures. Clippy clean under the exact CI command. Release 19.08 MB. Phase 1–3 suites re-run green.
The repair verified by disabling it, which fails the end-to-end recovery test. Worth noting: the unit tests still passed with it disabled, because they call the function directly — the same wiring-coverage gap found in Phase 1, which is why both layers exist here.