Conversation
Forking a compacted conversation and sending the next message could fail on that first message with `400 No tool output found for tool call …`, and retrying the fork answered `cannot identify an exact saved-history boundary`. Both start in a turn whose tool call never produced a result — a failed tool, or a process that died mid-call. That call is persisted as a `tool_call` item carrying the call and its failure text, with no `tool_result_for` item, so a history rebuilt from the records emitted the call alone: the response that followed was glued into the same assistant message (no result had flushed it), and the repair answering the call put its answer next to a message that now held two responses — a shape the Responses API reads as the earlier call never being answered. The messages that repair wrote also stopped the fork's document from matching its records, so `/undo`, retry and fork-again refused. A rebuilt turn now answers that call where the live transcript held its answer: the item's own failure text for a failure, and the repository's interrupted-call notice for a call the process never finished — the same spelling the session repair writes. An answer is paired only with a call the rebuild actually emits (a snapshot with no tool name is skipped, Hmbown#5823), and a call the turn already answers elsewhere is left alone. A call still running is deliberately untouched: the engine is waiting for its result. The recovery projection also reads the compaction checkpoint the engine installs into a synced history as the engine's own message rather than a prompt no turn record holds. A fork of a compacted source inherits the summary in its system prompt, so the engine wrote that checkpoint into the fork's own document on its first load, and the prompt walk then refused the cut. Tests: cargo test -p codewhale-tui --lib runtime_threads — 240 passed, 1 failed (host_goal_loop_kickoff_arms_one_continuation_and_parks_at_engine_cap, a load-sensitive goal-continuation test that fails the same way on the unmodified checkout and passes alone). All three new tests fail without the fix: a_rebuilt_turn_answers_a_tool_call_whose_outcome_the_store_lost (the fork's document came out as assistant[thinking, tool_use, thinking, tool_use] plus a repair receipt), a_compaction_checkpoint_is_not_a_kept_prompt, and a_lost_call_is_answered_only_where_its_own_outcome_is_known. compaction — 154 passed; session_manager — 107 passed; codewhale-runtime tool_history_repair — 8 passed; rustfmt clean; clippy clean for the touched crates under the CI allow list.
Hmbown
pushed a commit
that referenced
this pull request
Sep 27, 2026
Contributor PR by @gaord (Ben Gao); their commit 9ddf26b is kept as-is with its original authorship. crates/tui/src/compaction.rs: the already-merged compaction handoff work replaced the Codex-derived SUMMARY_HEADER ("Another language model started...") with the "Codewhale handoff note" header plus HANDOFF_SECTIONS; #6664 only made the old SUMMARY_HEADER pub(crate) so the runtime_threads recovery-projection tests can build a checkpoint message. Kept the new handoff header text and sections, and made that SUMMARY_HEADER pub(crate) with #6664's reason. #6664's test builds the checkpoint from SUMMARY_HEADER, which still starts with COMPACTION_SUMMARY_MARKER, so it remains a structural (wire) checkpoint. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This branch has not been deployed
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.
Summary
Continuing a forked conversation could fail on the first message with
400 No tool output found for tool call …, and retrying the fork answeredcannot identify an exact saved-history boundary. Both come from the sameplace, found while using the fork capability from #6580 on a compacted
conversation.
A turn whose tool call never produced a result — a failed tool, or a process
that died mid-call — is persisted as one
tool_callitem that carries the calland its failure text, with no
tool_result_foritem. A history rebuilt fromthe turn records (what a fork of a compacted source does) therefore emitted the
call alone, and two things followed:
(
thinking,call,thinking,call), because the missing result is whatfailed to flush the first response — the live transcript held those as two
separate messages; and
merged message, which the Responses API reads as the first call never being
answered, so every turn the fork ran failed the same way.
The recovery projection had a second, independent problem: a fork inherits the
source's compaction summary as its system prompt, so on its first load the
engine installs that checkpoint message into the fork's own document. No turn
record reproduces it, so the document read as drifted from the records it was
rebuilt from, and the next cut —
/undo, retry, fork-again on the fork itself— refused.
What changed
reconstruct_messages_from_turns_withanswers a call whose item records nooutcome, where the live transcript held the answer: the item's own failure
text for a failure, and the repository's interrupted-call notice for a call
the process never finished (
CRASH_REPAIR_CONTENT, now the one spelling thesession repair and a rebuild share). An answer is paired only with a call the
rebuild actually emits — a snapshot with no tool name is skipped (serve --http: threads with tool-call history fail with 400 missing field name after a runtime restart #5823), so
the identity rule is checked here too — and a call the turn already answers
with its own
tool_result_foritem is left alone. A call still running isdeliberately untouched: the engine is waiting for its result.
engine's own message rather than as a prompt no record holds, so a document
carrying one is still cut at the turn the caller names.
SUMMARY_HEADERbecomes crate-visible so the projection's recognition ruleand its tests share one literal.
No new layer; the existing
tool_history_repairmodule keeps ownership of theinterrupted-call notice.
Testing
Run against the commit this branch was rebased from (
d49fb9d99); the branchis based on
2d9613bac, whose changes to these files are in unrelated regions(sub-agent display names, custom-thread route tests), so CI covers the rebased
tree.
cargo fmt --all -- --check— clean.cargo clippy -p codewhale-tui -p codewhale-runtime --all-targets -- -D warnings -A clippy::uninlined_format_args -A clippy::too_many_arguments -A clippy::unnecessary_map_or -A clippy::collapsible_if -A clippy::assertions_on_constants— clean.cargo test -p codewhale-tui --lib runtime_threads—240 passed; 1 failed; 2 ignored. The failure ishost_goal_loop_kickoff_arms_one_continuation_and_parks_at_engine_cap, a load-sensitive goal-continuation test: it fails the same way on the unmodified checkout under the same parallel load and passes when run alone. Not from this change.cargo test -p codewhale-tui --lib compaction—154 passed; 0 failed.cargo test -p codewhale-tui --lib session_manager—107 passed; 0 failed; 1 ignored.cargo test -p codewhale-runtime tool_history_repair—8 passed; 0 failed.All three new tests were shown failing with the two behaviours disabled, and
passing with them:
a_rebuilt_turn_answers_a_tool_call_whose_outcome_the_store_lost— a turnwhose first response lost its call is rebuilt as two responses, each with its
own call and its own answer. Without the fix the fork's document came out as
("assistant", ["thinking", "tool_use", "thinking", "tool_use"])plus arepair notice, and the calls were left unanswered. The second half retries the
fork itself, over a document carrying the engine's checkpoint.
a_compaction_checkpoint_is_not_a_kept_prompt— without the fix the promptwalk returns
None(the refusal) on a transcript that carries a checkpoint;with it, the boundary is found.
a_lost_call_is_answered_only_where_its_own_outcome_is_known— the failuretext, the interrupted-call notice and every status this must leave alone.
Checklist
→ no new layer; the existing repair module keeps the notice's ownership.
CHANGELOG.mdandcrates/tui/CHANGELOG.mdare deliberately untouched(CONTRIBUTING: entries are written on
mainat merge time).What this does not address
Documents an older build already wrote keep the merged assistant message, so
cutting one forward (undo/retry/fork on that thread) hands the shape back. A
fork built from the turn records — the path a compacted source takes — is clean
with this change, so branching again from the conversation produces a healthy
history. Healing an existing document would mean splitting a merged assistant
message (and its results) in the history-repair layer, which is a wider change
than this fix; left out deliberately.