fix(state,cli,tui-gateway): keep reasoning fields intact across forks and branches - #57248
fix(state,cli,tui-gateway): keep reasoning fields intact across forks and branches#57248cryptoyasenka wants to merge 1 commit into
Conversation
|
Thanks for isolating the raw-row API fork failure. The write-path guard matches the current API flow: Problems
Suggested changes
Automated hermes-sweeper review. |
0d4d653 to
0382edf
Compare
|
Rebased onto current main and re-verified against it; the rebase carried both commits over with zero content drift (range-diff clean). The forwarding suggested above is in the PR as the second commit ( The regressions cannot pass against the unfixed writers: with the branch-writer forwarding reverted to Against today's Ready for another look. |
71f5a0a to
2862e1f
Compare
… and branches
get_messages() only deserializes content and tool_calls; the structured
reasoning columns (reasoning_details, codex_reasoning_items,
codex_message_items) come back as the raw TEXT they were stored as.
Feeding those rows straight back into a write, which is exactly what
the POST /api/sessions/{id}/fork handler does by piping get_messages()
into replace_messages(), hit an unguarded json.dumps() and stored the
already-serialized string encoded a second time. On replay of the fork,
json.loads() then yields the inner string instead of a list, and every
consumer's isinstance(..., list) gate silently drops it: preserved
Anthropic thinking blocks, Codex encrypted-reasoning/message-item
replay, and OpenRouter multi-turn reasoning context are all lost after
a fork, with one more encoding layer added per fork.
The /branch copy loop had the same defect from the other side: it
forwarded reasoning but none of the structured columns, and both TUI
branch writers persisted role/content alone, dropping reasoning and
reasoning_content along with them.
Route the six dumps sites in append_message and _insert_message_rows
through a shared guard that keeps already-serialized strings as-is;
structured values from the live runtime are dumped exactly as before.
Forward the reasoning fields in all three branch writers, matching the
set gateway/slash_commands.py already forwards on its own /branch path.
2862e1f to
9451c52
Compare
|
Merged via PR #82109 with your commit cherry-picked intact (authorship preserved) — rebased onto current main with one additive test-file conflict resolved. Thank you @cryptoyasenka for the thorough fix: the branch-writer coverage across all three surfaces is what made this the salvage base over the competing fix. Closing this original since the salvage landed. |
What does this PR do?
Keeps a session's reasoning fields intact when the session is copied. Both
copy paths lost them, two different ways.
Fork, double-encoding.
get_messages()returnsreasoning_details/codex_reasoning_items/codex_message_itemsas the raw TEXT stored inthose columns (only
contentandtool_callsare hydrated), and bothwriters re-ran an unguarded
json.dumps()on whatever they received. Thefork endpoint pipes
get_messages()straight intoreplace_messages()(
_handle_fork_sessioningateway/platforms/api_server.py, lines3498-3499), so every forked session stored its reasoning fields wrapped in
one extra JSON-string layer. On resume,
get_messages_as_conversation()decoded the column back to the innerstring, and every consumer's
isinstance(..., list)gate silently droppedit: preserved Anthropic thinking blocks, Codex encrypted-reasoning and
message-item replay, and OpenRouter multi-turn reasoning context were all
lost after a fork. One more encoding layer accumulates per fork, and the
double-encoded string still went out to providers in a shape none of them
ever produced.
Branch, dropped columns. The branch writers never agreed on what to
copy.
gateway/slash_commands.pyforwarded the full field set, but thehermes_cli/branchloop forwardedreasoningand none of the structuredcolumns, and both TUI branch writers persisted role/content/timestamp alone,
so the same
/branch, typed in three places, produced three different rows.A TUI branch is a draft until its first submit, so the seed write is the only
write that ever persists the copied transcript: whatever it drops is gone.
Same end state as the fork bug, reached by omission instead of encoding.
The
hermes_state.pywriters now route the three structured fields througha shared guard that keeps already-serialized TEXT as-is and dumps live
structures exactly as before. The CLI and TUI branch writers now forward the
reasoning fields the gateway path already forwarded. The read path is
untouched, so
get_messages, the GET messages endpoint, session export andsession search all keep their current shapes.
Related Issue
Fixes #57240
Related PRs, not duplicates. Three open PRs overlap this area. #57454
("preserve reasoning fields when forking sessions", 2026-07-03) is an
independent fix for the same issue, scoped to the fork path; if it lands
first, only the branch half of this PR is still needed. #24769 ("preserve
tool and reasoning metadata during branch", 2026-05-13) and #42273 ("persist
full message history when branching", 2026-06-08) both predate this PR, sit
on older bases, and address the branch half alone. This PR is the only one
that covers the fork and branch paths together, at the writer level.
Type of Change
Changes Made
hermes_state.pySessionDB._reasoning_json_text()helper: falsy →NULL,str(the column's own TEXT coming back through
get_messages) → storedas-is, anything else →
json.dumpsas beforeappend_messageand_insert_message_rowsuse it at all six dumpssites (3 fields × 2 writers)
hermes_cli/cli_commands_mixin.py/branchcopy loop forwardsreasoning_details,codex_reasoning_itemsandcodex_message_itemsnext to thereasoningit already carriedtui_gateway/server.py,tui_gateway/methods_session.py_persist_branch_seed()and thesession.branchRPC forward thereasoning fields instead of role/content/timestamp alone
tests/hermes_state/test_reasoning_roundtrip.py(new)get_messages→replace_messages, thefork handler's exact copy step), fork-of-fork stability, an
append_messageround-trip with a stored row's TEXT, and adirect-write control pinning that live-runtime serialization is
unchanged
tests/cli/test_branch_command.py,tests/test_tui_gateway_server.pyturn carries every reasoning field, reload the branched session, and
assert each field came back
The branch writers and their regressions were added in response to
review on this PR; the round-trip guard alone left the branch
paths dropping the same fields before they ever reached it.
How to Test
main, the three suites this PR touches:python -m pytest tests/cli/test_branch_command.py tests/hermes_state/test_reasoning_roundtrip.py tests/test_tui_gateway_server.py -qgives 535 passed.
main, with the new tests in place and theproduction changes reverted: 8 failed, 527 passed. The 8 are exactly the
8 new tests (the 5 round-trip variants, the CLI branch regression, and
the two TUI branch regressions). The 6th test in the new file,
TestDirectWrite::test_reasoning_fields_hydrate_as_structures, passes inboth runs, pinning that live writes behave identically before and after.
regressions failing on the reloaded row with a
KeyErroron the firstdropped field (
'reasoning_details'in the CLI test, whose writeralready carried
reasoning;'reasoning'in both TUI tests), so theycannot pass against the unfixed writers.
Before:
<class 'str'>. After:<class 'list'>.Verified on Windows 11, Python 3.13.
Green ubuntu run for this head: https://github.com/cryptoyasenka/hermes-agent/actions/runs/31055688426
Checklist
fix(scope):,feat(scope):, etc.)tests/cli/test_branch_command.py,tests/hermes_state/test_reasoning_roundtrip.py,tests/test_tui_gateway_server.py): 535 passed. Fullpytest tests/ -qin my environment has pre-existing collection errors from optional deps that aren't installed, unrelated to this change; the full matrix is covered by the ubuntu run linked aboveDocumentation & Housekeeping
docs/, docstrings): the helper's docstring documents the round-trip contractcli-config.yaml.exampleif I added/changed config keys: N/A, no config keysCONTRIBUTING.mdorAGENTS.mdif I changed architecture or workflows: N/Atmp_pathScreenshots / Logs
On
main, with the new tests in place and the production changes reverted:With the branch-writer forwarding reverted, the new branch regressions: