mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-07-28 18:19:28 +00:00
fix(tui_gateway): candidate-inclusive display on child-watch resume + E2E
Complete the #65919 warm/live-payload fix across its sibling path and add real-SessionDB cross-builder coverage. - Child-watch (lazy) resume: the delegated-subagent watch window served _history_to_messages(repaired_history) for its user-visible messages, which collapses out persisted verification candidates just like the warm-payload path did. Build the visible messages from the verbatim child-only display projection (repair_alternation=False) while the repaired history still feeds live replay; fall back to the repaired history if the display read fails. - E2E cross-builder consistency (real SessionDB, not mocks): a persisted verification candidate is collapsed out of the model projection but kept in the display projection, and _live_visible_history now equals the eager session.resume display projection (candidate present). Adds the combined candidate + fully-flushed-second-turn case and a lazy child-watch handler test that asserts the candidate survives in resp["result"]["messages"].
This commit is contained in:
parent
850b8da332
commit
77855ce1f8
2 changed files with 128 additions and 1 deletions
|
|
@ -1582,6 +1582,120 @@ def test_reconcile_display_with_live_trusts_db_when_tail_absent():
|
|||
assert server._reconcile_display_with_live(db_display, []) == db_display
|
||||
|
||||
|
||||
def test_live_visible_history_matches_eager_resume_with_real_db(tmp_path):
|
||||
"""E2E cross-builder consistency against a real SessionDB.
|
||||
|
||||
A persisted verification candidate (finish_reason=verification_required)
|
||||
is collapsed out of the model history by repair_message_sequence but kept
|
||||
in the display lineage (#65919). The warm/live projection
|
||||
(_live_visible_history) must equal the eager session.resume display
|
||||
projection — both keeping the candidate — so switching to a live session
|
||||
shows the same substantive answer a cold resume would.
|
||||
"""
|
||||
from hermes_state import SessionDB
|
||||
|
||||
db = SessionDB(db_path=tmp_path / "state.db")
|
||||
db.create_session("s1", source="tui")
|
||||
db.append_message("s1", role="user", content="do the thing")
|
||||
db.append_message(
|
||||
"s1", role="assistant", content="long substantive answer",
|
||||
finish_reason="verification_required",
|
||||
)
|
||||
db.append_message(
|
||||
"s1", role="assistant", content="terse verified reply", finish_reason="stop",
|
||||
)
|
||||
|
||||
model_history, display_history = db.get_resume_conversations("s1")
|
||||
|
||||
# The divergence #65919 introduced: candidate absent from the model
|
||||
# projection, present in the display projection.
|
||||
assert not any("long substantive" in (m.get("content") or "") for m in model_history)
|
||||
assert any("long substantive" in (m.get("content") or "") for m in display_history)
|
||||
|
||||
# Eager session.resume serves the display projection.
|
||||
eager_messages = server._history_to_messages(display_history)
|
||||
# Warm/live reuse: in-memory history is the collapsed model projection.
|
||||
live_history = server._live_visible_history({"session_key": "s1"}, db, list(model_history))
|
||||
# They must agree — the candidate survives the warm switch.
|
||||
assert server._history_to_messages(live_history) == eager_messages
|
||||
assert any(m.get("text") == "long substantive answer" for m in eager_messages)
|
||||
|
||||
|
||||
def test_live_visible_history_keeps_candidate_and_new_flushed_turn_real_db(tmp_path):
|
||||
"""Real-DB variant of the combined case: a candidate from turn 1 AND a
|
||||
fully-flushed turn 2 both appear once."""
|
||||
from hermes_state import SessionDB
|
||||
|
||||
db = SessionDB(db_path=tmp_path / "state.db")
|
||||
db.create_session("s1", source="tui")
|
||||
db.append_message("s1", role="user", content="turn 1")
|
||||
db.append_message(
|
||||
"s1", role="assistant", content="candidate answer",
|
||||
finish_reason="verification_required",
|
||||
)
|
||||
db.append_message("s1", role="assistant", content="verified reply", finish_reason="stop")
|
||||
db.append_message("s1", role="user", content="turn 2")
|
||||
db.append_message("s1", role="assistant", content="turn 2 reply", finish_reason="stop")
|
||||
|
||||
model_history, display_history = db.get_resume_conversations("s1")
|
||||
live_history = server._live_visible_history({"session_key": "s1"}, db, list(model_history))
|
||||
texts = [m.get("text") for m in server._history_to_messages(live_history)]
|
||||
|
||||
assert texts == [
|
||||
"turn 1",
|
||||
"candidate answer",
|
||||
"verified reply",
|
||||
"turn 2",
|
||||
"turn 2 reply",
|
||||
]
|
||||
|
||||
|
||||
def test_lazy_child_watch_resume_serves_candidate_inclusive_display(monkeypatch, tmp_path):
|
||||
"""The delegated-child watch-window cold resume (lazy=True) must serve the
|
||||
verbatim display projection so a persisted verification candidate is not
|
||||
collapsed out of the watch window (#65919 sibling of the warm-payload fix).
|
||||
"""
|
||||
from hermes_state import SessionDB
|
||||
|
||||
db = SessionDB(db_path=tmp_path / "state.db")
|
||||
db.create_session("child1", source="tui")
|
||||
db.append_message("child1", role="user", content="child prompt")
|
||||
db.append_message(
|
||||
"child1", role="assistant", content="child substantive answer",
|
||||
finish_reason="verification_required",
|
||||
)
|
||||
db.append_message(
|
||||
"child1", role="assistant", content="child terse reply", finish_reason="stop",
|
||||
)
|
||||
|
||||
lease = types.SimpleNamespace(session_id="child1", release=lambda: None)
|
||||
|
||||
monkeypatch.setattr(server, "_get_db", lambda: db)
|
||||
monkeypatch.setattr(server, "_enable_gateway_prompts", lambda: None)
|
||||
monkeypatch.setattr(
|
||||
server, "_claim_active_session_slot", lambda *a, **k: (lease, None)
|
||||
)
|
||||
monkeypatch.setattr(
|
||||
server, "_deferred_session_record", lambda *a, **k: {"created_at": 123.0}
|
||||
)
|
||||
monkeypatch.setattr(server, "_claim_or_reuse_live", lambda *a, **k: None)
|
||||
monkeypatch.setattr(server, "_child_run_active", lambda *a, **k: False)
|
||||
monkeypatch.setattr(server, "_schedule_session_cap_enforcement", lambda *a, **k: None)
|
||||
|
||||
resp = server.handle_request(
|
||||
{
|
||||
"id": "1",
|
||||
"method": "session.resume",
|
||||
"params": {"session_id": "child1", "lazy": True},
|
||||
}
|
||||
)
|
||||
|
||||
assert "error" not in resp, resp
|
||||
texts = [m.get("text") for m in resp["result"]["messages"]]
|
||||
assert "child substantive answer" in texts
|
||||
assert texts == ["child prompt", "child substantive answer", "child terse reply"]
|
||||
|
||||
|
||||
def test_session_resume_follows_compression_tip(monkeypatch, tmp_path):
|
||||
"""Resuming a rotated-out parent id must load the continuation's messages.
|
||||
|
||||
|
|
|
|||
|
|
@ -6289,7 +6289,20 @@ def _(rid, params: dict) -> dict:
|
|||
# A delegated child mid-run emits no session events of its own — report
|
||||
# its liveness from the relay registry so the window shows a busy turn.
|
||||
child_running = _child_run_active(target)
|
||||
messages = _history_to_messages(history)
|
||||
# User-visible messages use the VERBATIM display projection (child-only,
|
||||
# no ancestors — matching the repaired read above), so model-invisible
|
||||
# rows persisted by #65919 (verification candidates collapsed by
|
||||
# repair_message_sequence) survive in the watch window just as they do
|
||||
# on the eager resume + REST paths. The repaired ``history`` above still
|
||||
# feeds live replay. Fall back to it if the display read fails.
|
||||
try:
|
||||
display_history = db.get_messages_as_conversation(
|
||||
target, repair_alternation=False
|
||||
)
|
||||
except Exception:
|
||||
logger.debug("child-watch display projection read failed", exc_info=True)
|
||||
display_history = history
|
||||
messages = _history_to_messages(display_history)
|
||||
return _ok(
|
||||
rid,
|
||||
{
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue