refactor: keep anthropic_content_blocks in-memory only (no state.db column)

Drop the hermes_state.py column + persistence plumbing from the salvaged
interleaved-thinking fix. The ordered-block channel covers the failure
window in-memory (turn replayed within the live conversation loop). A
session reloaded from disk after a crash falls back to reconstruction;
if that replay 400s, the thinking-signature recovery (#43667) strips
reasoning_details and retries — one degraded call in a rare resume path
instead of a schema column. Replaces the DB-roundtrip test with a
fallback-shape test.
This commit is contained in:
teknium1 2026-06-10 19:59:57 -07:00 committed by Teknium
parent 7a1eed8268
commit efcbbde48c
3 changed files with 26 additions and 70 deletions

View file

@ -488,7 +488,6 @@ CREATE TABLE IF NOT EXISTS messages (
reasoning TEXT,
reasoning_content TEXT,
reasoning_details TEXT,
anthropic_content_blocks TEXT,
codex_reasoning_items TEXT,
codex_message_items TEXT,
platform_message_id TEXT,
@ -2241,7 +2240,6 @@ class SessionDB:
reasoning: str = None,
reasoning_content: str = None,
reasoning_details: Any = None,
anthropic_content_blocks: Any = None,
codex_reasoning_items: Any = None,
codex_message_items: Any = None,
platform_message_id: str = None,
@ -2264,10 +2262,6 @@ class SessionDB:
json.dumps(reasoning_details)
if reasoning_details else None
)
anthropic_content_blocks_json = (
json.dumps(anthropic_content_blocks)
if anthropic_content_blocks else None
)
codex_items_json = (
json.dumps(codex_reasoning_items)
if codex_reasoning_items else None
@ -2290,10 +2284,9 @@ class SessionDB:
cursor = conn.execute(
"""INSERT INTO messages (session_id, role, content, tool_call_id,
tool_calls, tool_name, timestamp, token_count, finish_reason,
reasoning, reasoning_content, reasoning_details, anthropic_content_blocks,
codex_reasoning_items,
reasoning, reasoning_content, reasoning_details, codex_reasoning_items,
codex_message_items, platform_message_id, observed)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)""",
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)""",
(
session_id,
role,
@ -2307,7 +2300,6 @@ class SessionDB:
reasoning,
reasoning_content,
reasoning_details_json,
anthropic_content_blocks_json,
codex_items_json,
codex_message_items_json,
platform_message_id,
@ -2356,9 +2348,6 @@ class SessionDB:
role = msg.get("role", "unknown")
tool_calls = msg.get("tool_calls")
reasoning_details = msg.get("reasoning_details") if role == "assistant" else None
anthropic_content_blocks = (
msg.get("anthropic_content_blocks") if role == "assistant" else None
)
codex_reasoning_items = (
msg.get("codex_reasoning_items") if role == "assistant" else None
)
@ -2369,9 +2358,6 @@ class SessionDB:
reasoning_details_json = (
json.dumps(reasoning_details) if reasoning_details else None
)
anthropic_content_blocks_json = (
json.dumps(anthropic_content_blocks) if anthropic_content_blocks else None
)
codex_items_json = (
json.dumps(codex_reasoning_items) if codex_reasoning_items else None
)
@ -2388,10 +2374,9 @@ class SessionDB:
conn.execute(
"""INSERT INTO messages (session_id, role, content, tool_call_id,
tool_calls, tool_name, timestamp, token_count, finish_reason,
reasoning, reasoning_content, reasoning_details, anthropic_content_blocks,
codex_reasoning_items,
reasoning, reasoning_content, reasoning_details, codex_reasoning_items,
codex_message_items, platform_message_id, observed)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)""",
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)""",
(
session_id,
role,
@ -2405,7 +2390,6 @@ class SessionDB:
msg.get("reasoning") if role == "assistant" else None,
msg.get("reasoning_content") if role == "assistant" else None,
reasoning_details_json,
anthropic_content_blocks_json,
codex_items_json,
codex_message_items_json,
platform_msg_id,
@ -2748,7 +2732,6 @@ class SessionDB:
rows = self._conn.execute(
"SELECT role, content, tool_call_id, tool_calls, tool_name, "
"finish_reason, reasoning, reasoning_content, reasoning_details, "
"anthropic_content_blocks, "
"codex_reasoning_items, codex_message_items, platform_message_id, observed "
f"FROM messages WHERE session_id IN ({placeholders})"
f"{active_clause} ORDER BY id",
@ -2796,12 +2779,6 @@ class SessionDB:
except (json.JSONDecodeError, TypeError):
logger.warning("Failed to deserialize reasoning_details, falling back to None")
msg["reasoning_details"] = None
if row["anthropic_content_blocks"]:
try:
msg["anthropic_content_blocks"] = json.loads(row["anthropic_content_blocks"])
except (json.JSONDecodeError, TypeError):
logger.warning("Failed to deserialize anthropic_content_blocks, falling back to None")
msg["anthropic_content_blocks"] = None
if row["codex_reasoning_items"]:
try:
msg["codex_reasoning_items"] = json.loads(row["codex_reasoning_items"])

View file

@ -1597,7 +1597,6 @@ class AIAgent:
reasoning=msg.get("reasoning") if role == "assistant" else None,
reasoning_content=msg.get("reasoning_content") if role == "assistant" else None,
reasoning_details=msg.get("reasoning_details") if role == "assistant" else None,
anthropic_content_blocks=msg.get("anthropic_content_blocks") if role == "assistant" else None,
codex_reasoning_items=msg.get("codex_reasoning_items") if role == "assistant" else None,
codex_message_items=msg.get("codex_message_items") if role == "assistant" else None,
)

View file

@ -176,59 +176,39 @@ class TestInterleavedThinkingBlockOrder:
"in the latest assistant message cannot be modified'."
)
def test_interleaved_order_survives_db_roundtrip(self, tmp_path):
"""The ordered-block channel must survive SQLite persistence + reload.
def test_replay_falls_back_gracefully_without_ordered_blocks(self):
"""Without the ordered-block channel, conversion must not crash.
This is the exact path that fails after a gateway crash: the session
is reloaded from state.db via get_messages_as_conversation, then
replayed. If the verbatim block list is dropped or not deserialized,
the reconstruction reorders signed thinking blocks -> HTTP 400.
The channel is intentionally NOT persisted to state.db (in-memory
only): a session reloaded from disk after a crash loses the field
and falls back to reconstruction. That replay may take one HTTP 400,
which the thinking-signature recovery (#43667) absorbs by stripping
reasoning_details and retrying. This test pins the fallback shape:
conversion still produces a valid assistant message from the
parallel reasoning_details + tool_calls fields.
"""
import hermes_state
response = _interleaved_response()
original_order = _original_block_order(response)
transport = get_transport("anthropic_messages")
normalized = transport.normalize_response(response)
assistant_msg = _stored_assistant_message(normalized)
# Simulate a disk reload: the in-memory-only channel is gone.
assistant_msg.pop("anthropic_content_blocks", None)
db = hermes_state.SessionDB(tmp_path / "state.db")
sid = "sess_roundtrip"
db.create_session(sid, source="test")
db.append_message(
session_id=sid,
role="assistant",
content=assistant_msg["content"],
tool_calls=assistant_msg["tool_calls"],
reasoning_details=assistant_msg.get("reasoning_details"),
anthropic_content_blocks=assistant_msg.get("anthropic_content_blocks"),
)
db.append_message(session_id=sid, role="tool", tool_call_id="toolu_1", content="a ok")
db.append_message(session_id=sid, role="tool", tool_call_id="toolu_2", content="b ok")
# Reload via the conversation-restore path used on resume / crash recovery.
loaded = db.get_messages_as_conversation(sid)
reloaded_assistant = [m for m in loaded if m.get("role") == "assistant"]
assert reloaded_assistant, "no assistant message after DB reload"
# The ordered-block channel must come back as a deserialized list.
blocks = reloaded_assistant[0].get("anthropic_content_blocks")
assert isinstance(blocks, list) and len(blocks) == 4, (
"anthropic_content_blocks was not persisted/deserialized correctly"
)
messages = [
assistant_msg,
{"role": "tool", "tool_call_id": "toolu_1", "content": "a ok"},
{"role": "tool", "tool_call_id": "toolu_2", "content": "b ok"},
]
_system, anthropic_messages = convert_messages_to_anthropic(
loaded, base_url=None, model="claude-opus-4-8",
messages, base_url=None, model="claude-opus-4-8",
)
assistant_out = [m for m in anthropic_messages if m.get("role") == "assistant"]
assert assistant_out, "no assistant message in converted output"
replayed_order = _replayed_block_order(assistant_out[-1]["content"])
assert replayed_order == original_order, (
"Interleaved block order was lost across the SQLite round-trip.\n"
f" original: {original_order}\n"
f" replayed: {replayed_order}"
)
content = assistant_out[-1]["content"]
assert isinstance(content, list) and content, "fallback produced empty content"
# Reconstruction keeps both tool_use blocks (answered by results).
tool_ids = [b.get("id") for b in content if isinstance(b, dict) and b.get("type") == "tool_use"]
assert set(tool_ids) == {"toolu_1", "toolu_2"}
class TestInterleavedReplayCredentialRedaction: