diff --git a/hermes_state.py b/hermes_state.py index a808b684c7..b3e00b9ff6 100644 --- a/hermes_state.py +++ b/hermes_state.py @@ -1464,8 +1464,8 @@ class SessionDB: placeholders = ",".join("?" for _ in session_ids) rows = self._conn.execute( "SELECT role, content, tool_call_id, tool_calls, tool_name, " - "reasoning, reasoning_content, reasoning_details, codex_reasoning_items, " - "codex_message_items " + "finish_reason, reasoning, reasoning_content, reasoning_details, " + "codex_reasoning_items, codex_message_items " f"FROM messages WHERE session_id IN ({placeholders}) ORDER BY timestamp, id", tuple(session_ids), ).fetchall() @@ -1490,6 +1490,8 @@ class SessionDB: # that replay reasoning (OpenRouter, OpenAI, Nous) receive # coherent multi-turn reasoning context. if row["role"] == "assistant": + if row["finish_reason"]: + msg["finish_reason"] = row["finish_reason"] if row["reasoning"]: msg["reasoning"] = row["reasoning"] if row["reasoning_content"] is not None: diff --git a/tests/test_hermes_state.py b/tests/test_hermes_state.py index a2c48366de..806735f5df 100644 --- a/tests/test_hermes_state.py +++ b/tests/test_hermes_state.py @@ -399,6 +399,27 @@ class TestMessageStorage: assert msg["reasoning"] == "Thinking about what to say" assert msg["reasoning_details"] == details + def test_finish_reason_restored_by_get_messages_as_conversation(self, db): + """finish_reason on assistant messages must survive conversation replay. + + Without this, /branch copies and other transcript round-trips silently + drop the provider's stop signal. + """ + db.create_session(session_id="s1", source="cli") + db.append_message( + "s1", + role="assistant", + content="Done", + finish_reason="tool_calls", + ) + db.append_message("s1", role="user", content="next") + + conv = db.get_messages_as_conversation("s1") + assert conv[0]["role"] == "assistant" + assert conv[0]["finish_reason"] == "tool_calls" + # Non-assistant rows should not have a finish_reason key added. + assert "finish_reason" not in conv[1] + def test_reasoning_content_persisted_and_restored(self, db): """reasoning_content must survive session replay as its own field.""" db.create_session(session_id="s1", source="cli")