From efcbbde48c38acbf3489ec1f7fc91ce1a30822f4 Mon Sep 17 00:00:00 2001 From: teknium1 <127238744+teknium1@users.noreply.github.com> Date: Wed, 10 Jun 2026 19:59:57 -0700 Subject: [PATCH] refactor: keep anthropic_content_blocks in-memory only (no state.db column) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- hermes_state.py | 31 ++------- run_agent.py | 1 - .../test_anthropic_thinking_block_order.py | 64 +++++++------------ 3 files changed, 26 insertions(+), 70 deletions(-) diff --git a/hermes_state.py b/hermes_state.py index 7b8a2bfcfb4..bda6eeacd62 100644 --- a/hermes_state.py +++ b/hermes_state.py @@ -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"]) diff --git a/run_agent.py b/run_agent.py index eda39064c55..e81bf3b93e7 100644 --- a/run_agent.py +++ b/run_agent.py @@ -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, ) diff --git a/tests/agent/test_anthropic_thinking_block_order.py b/tests/agent/test_anthropic_thinking_block_order.py index 3d090983f00..5455b3339a7 100644 --- a/tests/agent/test_anthropic_thinking_block_order.py +++ b/tests/agent/test_anthropic_thinking_block_order.py @@ -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: