Skip to content

fix(session): repair unanswered tool_use so a recovered session stays usable - #19

Merged
ForkedInTime merged 1 commit into
mainfrom
fix/phase4-history-validity
Aug 4, 2026
Merged

fix(session): repair unanswered tool_use so a recovered session stays usable#19
ForkedInTime merged 1 commit into
mainfrom
fix/phase4-history-validity

Conversation

@ForkedInTime

Copy link
Copy Markdown
Owner

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'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:

  • Stubs are spliced into the existing following user turn where there is one, rather than inserting a second user message and leaving 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, tool error. The ? at the call site is vestigial; nothing inside can return Err.
  • Cancellation is safe. Messages persist 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 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.

… 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>
@ForkedInTime
ForkedInTime merged commit f4957c0 into main Aug 4, 2026
5 checks passed
@ForkedInTime
ForkedInTime deleted the fix/phase4-history-validity branch August 4, 2026 00:23
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.

1 participant