fix(agent): invalidate flush-scan cursor when finalizer pops db marker
The bounded flush-scan in _flush_messages_to_session_db_unlocked skips the identity-matched prefix of its previous snapshot, on the documented assumption that no code path pops _DB_PERSISTED_MARKER from a live dict in place. finalize_turn's pure-tool-call-tail fill is exactly that path: it pops the marker so the filled content gets re-persisted — but the cursor then skips the row anyway, so the delivered final response never reaches state.db and /resume replays content="" (the #43849/#44100 class resurfacing via the perf cursor). Invalidate the cursor at the pop site so the filled row is re-examined.
This commit is contained in:
parent
a266155cc4
commit
2aaeee2ee5
|
|
@ -339,6 +339,12 @@ def finalize_turn(
|
|||
# otherwise ``/resume`` reloads ``content=""`` and the bug
|
||||
# resurfaces cross-session.
|
||||
_tail.pop("_db_persisted", None)
|
||||
# The bounded flush-scan cursor (run_agent.py) skips the
|
||||
# identity-matched prefix of its previous snapshot on the
|
||||
# assumption that no live dict loses the marker in place —
|
||||
# this pop is the one place that does. Invalidate it so the
|
||||
# filled row is re-examined instead of skipped.
|
||||
agent._db_flush_scan_prefix = None
|
||||
|
||||
# The model has completed its request, so replace API-local
|
||||
# voice/model/skill guidance with the clean user input before writing the
|
||||
|
|
|
|||
|
|
@ -180,3 +180,46 @@ def test_final_response_fills_pure_tool_call_tail(monkeypatch):
|
|||
|
||||
|
||||
|
||||
|
||||
|
||||
def test_final_response_fill_invalidates_flush_scan_cursor():
|
||||
"""The fill's marker pop must invalidate the bounded flush-scan cursor.
|
||||
|
||||
The cursor (run_agent.py) skips the identity-matched prefix of its
|
||||
previous snapshot assuming no live dict loses ``_db_persisted`` in place
|
||||
— the fill is the one path that pops it. Without invalidation, the
|
||||
turn-end flush skips the filled row as 'already stamped' and the
|
||||
delivered answer never reaches state.db (the #43849 class resurfacing).
|
||||
"""
|
||||
agent = FakeAgent()
|
||||
agent._db_flush_scan_prefix = ["prior-snapshot"]
|
||||
messages = [
|
||||
{"role": "user", "content": "q"},
|
||||
{
|
||||
"role": "assistant",
|
||||
"content": "",
|
||||
"tool_calls": [
|
||||
{"id": "t1", "type": "function",
|
||||
"function": {"name": "f", "arguments": "{}"}}
|
||||
],
|
||||
"_db_persisted": True,
|
||||
},
|
||||
]
|
||||
|
||||
finalize_turn(
|
||||
agent,
|
||||
final_response="Here is your answer.",
|
||||
api_call_count=3,
|
||||
interrupted=False,
|
||||
failed=False,
|
||||
messages=messages,
|
||||
conversation_history=[],
|
||||
effective_task_id="t",
|
||||
turn_id="tid",
|
||||
user_message="q",
|
||||
original_user_message="q",
|
||||
_should_review_memory=False,
|
||||
_turn_exit_reason="text_response(final)",
|
||||
)
|
||||
|
||||
assert agent._db_flush_scan_prefix is None
|
||||
|
|
|
|||
|
|
@ -197,3 +197,41 @@ class TestIdentityFlush:
|
|||
assert new_assistant.get("_db_persisted") is True
|
||||
finally:
|
||||
db.close()
|
||||
|
||||
|
||||
class TestFlushCursorMarkerPop:
|
||||
def test_filled_row_repersists_after_marker_pop_and_cursor_invalidation(self):
|
||||
"""End-to-end: incremental flush stamps the tool-call tail; the
|
||||
finalizer fills its content and pops the marker; with the cursor
|
||||
invalidated (as the pop site now does), the turn-end flush must
|
||||
persist the filled answer — not skip the row as already-stamped."""
|
||||
from hermes_state import SessionDB
|
||||
|
||||
with tempfile.TemporaryDirectory() as tmpdir:
|
||||
db = SessionDB(db_path=Path(tmpdir) / "t.db")
|
||||
try:
|
||||
agent = _make_agent(db)
|
||||
tool_row = {
|
||||
"role": "assistant",
|
||||
"content": "",
|
||||
"tool_calls": [
|
||||
{"id": "t1", "type": "function",
|
||||
"function": {"name": "f", "arguments": "{}"}}
|
||||
],
|
||||
}
|
||||
messages = [tool_row]
|
||||
agent._flush_messages_to_session_db_unlocked(messages)
|
||||
assert messages[0].get("_db_persisted") is True
|
||||
assert agent._db_flush_scan_prefix is not None
|
||||
|
||||
# What finalize_turn's fill does at the pop site:
|
||||
messages[0]["content"] = "the final answer"
|
||||
messages[0].pop("_db_persisted", None)
|
||||
agent._db_flush_scan_prefix = None
|
||||
|
||||
messages.append({"role": "user", "content": "next question"})
|
||||
agent._flush_messages_to_session_db_unlocked(messages)
|
||||
|
||||
assert "the final answer" in _contents(db)
|
||||
finally:
|
||||
db.close()
|
||||
|
|
|
|||
Loading…
Reference in New Issue