Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
41 changes: 20 additions & 21 deletions hermes_state.py
Original file line number Diff line number Diff line change
Expand Up @@ -3092,6 +3092,20 @@ def _decode_content(cls, content: Any) -> Any:
return content
return content

@classmethod
def _encode_json_column(cls, value: Any) -> Optional[str]:
"""Serialize a JSON column value for storage.

Accepts either a live structure (list/dict) or the column's own
already-serialized TEXT from ``get_messages()`` — the latter must
not be double-encoded on ``replace_messages()`` round-trips (#57240).
"""
if not value:
return None
if isinstance(value, str):
return value
return json.dumps(value)

def append_message(
self,
session_id: str,
Expand Down Expand Up @@ -3124,18 +3138,9 @@ def append_message(
message by its platform-side identifier.
"""
# Serialize structured fields to JSON before entering the write txn
reasoning_details_json = (
json.dumps(reasoning_details)
if reasoning_details else None
)
codex_items_json = (
json.dumps(codex_reasoning_items)
if codex_reasoning_items else None
)
codex_message_items_json = (
json.dumps(codex_message_items)
if codex_message_items else None
)
reasoning_details_json = self._encode_json_column(reasoning_details)
codex_items_json = self._encode_json_column(codex_reasoning_items)
codex_message_items_json = self._encode_json_column(codex_message_items)
tool_calls_json = json.dumps(tool_calls) if tool_calls else None
# Multimodal content (list of parts) must be JSON-encoded: sqlite3
# cannot bind list/dict parameters directly.
Expand Down Expand Up @@ -3232,15 +3237,9 @@ def _insert_message_rows(self, conn, session_id: str, messages: List[Dict[str, A
codex_message_items = (
msg.get("codex_message_items") if role == "assistant" else None
)
reasoning_details_json = (
json.dumps(reasoning_details) if reasoning_details else None
)
codex_items_json = (
json.dumps(codex_reasoning_items) if codex_reasoning_items else None
)
codex_message_items_json = (
json.dumps(codex_message_items) if codex_message_items else None
)
reasoning_details_json = self._encode_json_column(reasoning_details)
codex_items_json = self._encode_json_column(codex_reasoning_items)
codex_message_items_json = self._encode_json_column(codex_message_items)
tool_calls_json = json.dumps(tool_calls) if tool_calls else None
# Accept either `platform_message_id` (new explicit name) or
# `message_id` (yuanbao's existing convention on message dicts).
Expand Down
28 changes: 28 additions & 0 deletions tests/test_hermes_state.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
"""Tests for hermes_state.py — SessionDB SQLite CRUD, FTS5 search, export."""

import json
import sqlite3
import time
import pytest
Expand Down Expand Up @@ -847,6 +848,33 @@ def test_replace_messages_handles_multimodal_content(self, db):
assert msgs[0]["content"] == content
assert msgs[1]["content"] == "I see a screenshot."

def test_get_messages_roundtrip_preserves_reasoning_columns(self, db):
"""get_messages → replace_messages must not double-encode reasoning JSON (#57240)."""
reasoning = [{"type": "thinking", "thinking": "step one"}]
codex_items = [{"type": "reasoning", "encrypted_content": "enc_abc"}]
db.create_session(session_id="parent", source="api_server")
db.append_message(
"parent",
role="assistant",
content="answer",
reasoning_details=reasoning,
codex_reasoning_items=codex_items,
)

# get_messages leaves reasoning columns as stored TEXT — that is the
# fork/branch copy path (POST /api/sessions/{id}/fork).
loaded = db.get_messages("parent")
assert json.loads(loaded[0]["reasoning_details"]) == reasoning
assert json.loads(loaded[0]["codex_reasoning_items"]) == codex_items

fork_id = "parent-fork"
db.create_session(session_id=fork_id, source="api_server", parent_session_id="parent")
db.replace_messages(fork_id, db.get_messages("parent"))

fork_conv = db.get_messages_as_conversation(fork_id)
assert fork_conv[0]["reasoning_details"] == reasoning
assert fork_conv[0]["codex_reasoning_items"] == codex_items

def test_get_messages_as_conversation(self, db):
db.create_session(session_id="s1", source="cli")
db.append_message("s1", role="user", content="Hello")
Expand Down
Loading