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 20:45:16 -07:00
committed by Teknium
parent 7a1eed8268
commit efcbbde48c
3 changed files with 26 additions and 70 deletions
+4 -27
View File
@@ -488,7 +488,6 @@ CREATE TABLE IF NOT EXISTS messages (
reasoning TEXT, reasoning TEXT,
reasoning_content TEXT, reasoning_content TEXT,
reasoning_details TEXT, reasoning_details TEXT,
anthropic_content_blocks TEXT,
codex_reasoning_items TEXT, codex_reasoning_items TEXT,
codex_message_items TEXT, codex_message_items TEXT,
platform_message_id TEXT, platform_message_id TEXT,
@@ -2241,7 +2240,6 @@ class SessionDB:
reasoning: str = None, reasoning: str = None,
reasoning_content: str = None, reasoning_content: str = None,
reasoning_details: Any = None, reasoning_details: Any = None,
anthropic_content_blocks: Any = None,
codex_reasoning_items: Any = None, codex_reasoning_items: Any = None,
codex_message_items: Any = None, codex_message_items: Any = None,
platform_message_id: str = None, platform_message_id: str = None,
@@ -2264,10 +2262,6 @@ class SessionDB:
json.dumps(reasoning_details) json.dumps(reasoning_details)
if reasoning_details else None if reasoning_details else None
) )
anthropic_content_blocks_json = (
json.dumps(anthropic_content_blocks)
if anthropic_content_blocks else None
)
codex_items_json = ( codex_items_json = (
json.dumps(codex_reasoning_items) json.dumps(codex_reasoning_items)
if codex_reasoning_items else None if codex_reasoning_items else None
@@ -2290,10 +2284,9 @@ class SessionDB:
cursor = conn.execute( cursor = conn.execute(
"""INSERT INTO messages (session_id, role, content, tool_call_id, """INSERT INTO messages (session_id, role, content, tool_call_id,
tool_calls, tool_name, timestamp, token_count, finish_reason, tool_calls, tool_name, timestamp, token_count, finish_reason,
reasoning, reasoning_content, reasoning_details, anthropic_content_blocks, reasoning, reasoning_content, reasoning_details, codex_reasoning_items,
codex_reasoning_items,
codex_message_items, platform_message_id, observed) codex_message_items, platform_message_id, observed)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)""", VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)""",
( (
session_id, session_id,
role, role,
@@ -2307,7 +2300,6 @@ class SessionDB:
reasoning, reasoning,
reasoning_content, reasoning_content,
reasoning_details_json, reasoning_details_json,
anthropic_content_blocks_json,
codex_items_json, codex_items_json,
codex_message_items_json, codex_message_items_json,
platform_message_id, platform_message_id,
@@ -2356,9 +2348,6 @@ class SessionDB:
role = msg.get("role", "unknown") role = msg.get("role", "unknown")
tool_calls = msg.get("tool_calls") tool_calls = msg.get("tool_calls")
reasoning_details = msg.get("reasoning_details") 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 = ( codex_reasoning_items = (
msg.get("codex_reasoning_items") if role == "assistant" else None msg.get("codex_reasoning_items") if role == "assistant" else None
) )
@@ -2369,9 +2358,6 @@ class SessionDB:
reasoning_details_json = ( reasoning_details_json = (
json.dumps(reasoning_details) if reasoning_details else None 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 = ( codex_items_json = (
json.dumps(codex_reasoning_items) if codex_reasoning_items else None json.dumps(codex_reasoning_items) if codex_reasoning_items else None
) )
@@ -2388,10 +2374,9 @@ class SessionDB:
conn.execute( conn.execute(
"""INSERT INTO messages (session_id, role, content, tool_call_id, """INSERT INTO messages (session_id, role, content, tool_call_id,
tool_calls, tool_name, timestamp, token_count, finish_reason, tool_calls, tool_name, timestamp, token_count, finish_reason,
reasoning, reasoning_content, reasoning_details, anthropic_content_blocks, reasoning, reasoning_content, reasoning_details, codex_reasoning_items,
codex_reasoning_items,
codex_message_items, platform_message_id, observed) codex_message_items, platform_message_id, observed)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)""", VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)""",
( (
session_id, session_id,
role, role,
@@ -2405,7 +2390,6 @@ class SessionDB:
msg.get("reasoning") if role == "assistant" else None, msg.get("reasoning") if role == "assistant" else None,
msg.get("reasoning_content") if role == "assistant" else None, msg.get("reasoning_content") if role == "assistant" else None,
reasoning_details_json, reasoning_details_json,
anthropic_content_blocks_json,
codex_items_json, codex_items_json,
codex_message_items_json, codex_message_items_json,
platform_msg_id, platform_msg_id,
@@ -2748,7 +2732,6 @@ class SessionDB:
rows = self._conn.execute( rows = self._conn.execute(
"SELECT role, content, tool_call_id, tool_calls, tool_name, " "SELECT role, content, tool_call_id, tool_calls, tool_name, "
"finish_reason, reasoning, reasoning_content, reasoning_details, " "finish_reason, reasoning, reasoning_content, reasoning_details, "
"anthropic_content_blocks, "
"codex_reasoning_items, codex_message_items, platform_message_id, observed " "codex_reasoning_items, codex_message_items, platform_message_id, observed "
f"FROM messages WHERE session_id IN ({placeholders})" f"FROM messages WHERE session_id IN ({placeholders})"
f"{active_clause} ORDER BY id", f"{active_clause} ORDER BY id",
@@ -2796,12 +2779,6 @@ class SessionDB:
except (json.JSONDecodeError, TypeError): except (json.JSONDecodeError, TypeError):
logger.warning("Failed to deserialize reasoning_details, falling back to None") logger.warning("Failed to deserialize reasoning_details, falling back to None")
msg["reasoning_details"] = 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"]: if row["codex_reasoning_items"]:
try: try:
msg["codex_reasoning_items"] = json.loads(row["codex_reasoning_items"]) msg["codex_reasoning_items"] = json.loads(row["codex_reasoning_items"])
-1
View File
@@ -1597,7 +1597,6 @@ class AIAgent:
reasoning=msg.get("reasoning") if role == "assistant" else None, reasoning=msg.get("reasoning") if role == "assistant" else None,
reasoning_content=msg.get("reasoning_content") 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, 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_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, codex_message_items=msg.get("codex_message_items") if role == "assistant" else None,
) )
@@ -176,59 +176,39 @@ class TestInterleavedThinkingBlockOrder:
"in the latest assistant message cannot be modified'." "in the latest assistant message cannot be modified'."
) )
def test_interleaved_order_survives_db_roundtrip(self, tmp_path): def test_replay_falls_back_gracefully_without_ordered_blocks(self):
"""The ordered-block channel must survive SQLite persistence + reload. """Without the ordered-block channel, conversion must not crash.
This is the exact path that fails after a gateway crash: the session The channel is intentionally NOT persisted to state.db (in-memory
is reloaded from state.db via get_messages_as_conversation, then only): a session reloaded from disk after a crash loses the field
replayed. If the verbatim block list is dropped or not deserialized, and falls back to reconstruction. That replay may take one HTTP 400,
the reconstruction reorders signed thinking blocks -> 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() response = _interleaved_response()
original_order = _original_block_order(response)
transport = get_transport("anthropic_messages") transport = get_transport("anthropic_messages")
normalized = transport.normalize_response(response) normalized = transport.normalize_response(response)
assistant_msg = _stored_assistant_message(normalized) 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") messages = [
sid = "sess_roundtrip" assistant_msg,
db.create_session(sid, source="test") {"role": "tool", "tool_call_id": "toolu_1", "content": "a ok"},
db.append_message( {"role": "tool", "tool_call_id": "toolu_2", "content": "b ok"},
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"
)
_system, anthropic_messages = convert_messages_to_anthropic( _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"] assistant_out = [m for m in anthropic_messages if m.get("role") == "assistant"]
assert assistant_out, "no assistant message in converted output" assert assistant_out, "no assistant message in converted output"
replayed_order = _replayed_block_order(assistant_out[-1]["content"]) content = assistant_out[-1]["content"]
assert isinstance(content, list) and content, "fallback produced empty content"
assert replayed_order == original_order, ( # Reconstruction keeps both tool_use blocks (answered by results).
"Interleaved block order was lost across the SQLite round-trip.\n" tool_ids = [b.get("id") for b in content if isinstance(b, dict) and b.get("type") == "tool_use"]
f" original: {original_order}\n" assert set(tool_ids) == {"toolu_1", "toolu_2"}
f" replayed: {replayed_order}"
)
class TestInterleavedReplayCredentialRedaction: class TestInterleavedReplayCredentialRedaction: