fix(tui_gateway): persist full message history when branching a session - #42273
Open
friendshipisover wants to merge 1 commit into
Open
fix(tui_gateway): persist full message history when branching a session#42273friendshipisover wants to merge 1 commit into
friendshipisover wants to merge 1 commit into
Conversation
## What does this PR do? When you branch (fork) a TUI conversation, the gateway copies the current history into a brand-new session row in the SQLite store. The problem is *how* it copied: it walked the history and called `append_message` with only `role` and `content`. Everything else that ties an agent turn together — `tool_calls`, `tool_call_id`, `tool_name` and the reasoning fields — was silently dropped on the floor. Here's why that bites you only later. Right after branching, the new session is fine, because `_init_session` is handed the full in-memory history list, so the live branch still has its tool linkage. The DB copy is the *persisted* one, though, and the DB is the source of truth on resume. So the moment you close the app (or it restarts) and `/resume` the branch, the transcript is rebuilt from those incomplete rows by `get_messages_as_conversation`. Now the assistant's tool call has no matching tool result, and the tool result has no `tool_call_id` pointing back at it. The next prompt ships that broken sequence to the provider, and Anthropic/OpenAI reject it with a 400 — or quietly drop context. In practice that means any branched agent session containing a tool call (i.e. almost all of them) is unusable after a restart. The fix is to reuse `replace_messages`, the same atomic, field-complete path that `/undo`, `/retry` and `/compress` already use. It serializes the whole message (tool linkage plus reasoning) and commits it in one transaction. Because the branch session is freshly created one line above, its internal `DELETE` is just a no-op, so we get the correct behavior with less code. ## Related Issue N/A ## Type of Change - [x] 🐛 Bug fix (non-breaking change that fixes an issue) ## Changes Made - `tui_gateway/server.py`: in the `session.branch` handler, replaced the per-message `append_message` loop (which forwarded only `role` and `content`) with a single `db.replace_messages(new_key, history)` call so the branched session persists `tool_calls`, `tool_call_id`, `tool_name` and reasoning fields. Added a comment explaining why. - `tests/test_tui_gateway_server.py`: added `test_session_branch_persists_tool_calls_for_resume`, which branches a session containing a tool-call round-trip and asserts the persisted transcript still round-trips the tool linkage through `get_messages_as_conversation`. ## How to Test 1. Run the new regression test: `scripts/run_tests.sh tests/test_tui_gateway_server.py -- -k test_session_branch_persists_tool_calls_for_resume` 2. It passes with the fix. To see the bug, revert the `server.py` change and rerun — the test fails because the persisted assistant message has no `tool_calls` and the tool result has no `tool_call_id`/`tool_name`. 3. Manual: in the TUI, run a prompt that triggers a tool call, branch the session, restart, then `/resume` the branch and send a follow-up — it no longer errors out on a malformed message sequence. ## Checklist ### Code - [x] I've read the Contributing Guide - [x] My commit messages follow Conventional Commits (`fix(scope):`, `feat(scope):`, etc.) - [x] I searched for existing PRs to make sure this isn't a duplicate - [x] My PR contains **only** changes related to this fix/feature (no unrelated commits) - [x] I've run `pytest tests/ -q` and all tests pass - [x] I've added tests for my changes (required for bug fixes, strongly encouraged for features) - [x] I've tested on my platform: macOS 15 (Darwin 25.5.0) ### Documentation & Housekeeping - [x] I've updated relevant documentation (README, `docs/`, docstrings) — or N/A - [x] I've updated `cli-config.yaml.example` if I added/changed config keys — or N/A - [x] I've updated `CONTRIBUTING.md` or `AGENTS.md` if I changed architecture or workflows — or N/A - [x] I've considered cross-platform impact (Windows, macOS) — or N/A - [x] I've updated tool descriptions/schemas if I changed tool behavior — or N/A
Contributor
|
✅ Verified — session branch preserves tool call linkage Reviewed the diff for
The fix is correct and the regression test covers the exact failure scenario (branch → close → resume → provider rejects missing tool result). No issues found. |
Contributor
|
Thanks for identifying a real transcript-durability issue. Current main still has the legacy Problems
Suggested changes
Automated hermes-sweeper review. |
13 tasks
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 does this PR do?
When you branch (fork) a TUI conversation, the gateway copies the
current history into a brand-new session row in the SQLite store. The
problem is how it copied: it walked the history and called
append_messagewith onlyroleandcontent. Everything else thatties an agent turn together —
tool_calls,tool_call_id,tool_nameand the reasoning fields — was silently dropped on the floor.
Here's why that bites you only later. Right after branching, the new
session is fine, because
_init_sessionis handed the full in-memoryhistory list, so the live branch still has its tool linkage. The DB
copy is the persisted one, though, and the DB is the source of truth
on resume. So the moment you close the app (or it restarts) and
/resumethe branch, the transcript is rebuilt from those incomplete rows by
get_messages_as_conversation. Now the assistant's tool call has nomatching tool result, and the tool result has no
tool_call_idpointingback at it. The next prompt ships that broken sequence to the provider,
and Anthropic/OpenAI reject it with a 400 — or quietly drop context. In
practice that means any branched agent session containing a tool call
(i.e. almost all of them) is unusable after a restart.
The fix is to reuse
replace_messages, the same atomic, field-completepath that
/undo,/retryand/compressalready use. It serializesthe whole message (tool linkage plus reasoning) and commits it in one
transaction. Because the branch session is freshly created one line
above, its internal
DELETEis just a no-op, so we get the correctbehavior with less code.
Related Issue
N/A
Type of Change
Changes Made
tui_gateway/server.py: in thesession.branchhandler, replaced theper-message
append_messageloop (which forwarded onlyroleandcontent) with a singledb.replace_messages(new_key, history)callso the branched session persists
tool_calls,tool_call_id,tool_nameand reasoning fields. Added a comment explaining why.tests/test_tui_gateway_server.py: addedtest_session_branch_persists_tool_calls_for_resume, which branches asession containing a tool-call round-trip and asserts the persisted
transcript still round-trips the tool linkage through
get_messages_as_conversation.How to Test
scripts/run_tests.sh tests/test_tui_gateway_server.py -- -k test_session_branch_persists_tool_calls_for_resumeserver.pychangeand rerun — the test fails because the persisted assistant message has
no
tool_callsand the tool result has notool_call_id/tool_name.session, restart, then
/resumethe branch and send a follow-up — itno longer errors out on a malformed message sequence.
Checklist
Code
fix(scope):,feat(scope):, etc.)pytest tests/ -qand all tests passDocumentation & Housekeeping
docs/, docstrings) — or N/Acli-config.yaml.exampleif I added/changed config keys — or N/ACONTRIBUTING.mdorAGENTS.mdif I changed architecture or workflows — or N/A