Merge pull request #5192 — runtime-journal anchor-scene parity test (#5183, @franksong2702)

Release v0.51.740 — runtime-journal anchor-scene parity test (#5183, @franksong2702)
This commit is contained in:
nesquena-hermes authored and GitHub committed 2026-06-28 22:40:45 -07:00
commit f2ffeacc64
2 files changed
+182

No files matched your search

+2
View File
@@ -22,6 +22,8 @@
### Changed
- **Internal: added a runtime-journal ↔ settled-hydration parity regression test** for anchor activity scenes. The test drives a real `RunJournalWriter` through a full turn (token / reasoning / tool / tool_complete) and asserts that `_run_journal_live_snapshot(...).anchor_activity_scene` and the persisted-then-`_hydrate_anchor_activity_scenes(...)` settled scene expose identical visible semantics — locking in the invariant that a live-streaming turn and a reloaded settled turn render the same anchor activity. Test-only; no behavior change. Thanks @franksong2702. (#5183)
- **The new-conversation splash logo now breathes free instead of sitting in an app-icon tile.** On the empty-state hero, the caduceus mark dropped its glossy navy rounded-rect (which read like a misplaced macOS app-launcher tile — a metaphor mismatch, since nothing launches there) in favor of a bare, larger (~88px) mark over a soft radial "spirit" bloom, matching how Claude/ChatGPT/Gemini render their empty-state marks. The mark gradient and the bloom are tuned per theme: dark themes get the bright `#08EBF1 → #3889FD` mark with a luminous cyan bloom, light themes a deeper `#0AA6CC → #1E63D6` mark with a cooler, restrained bloom so it stays crisp on the pale background. A gentle entrance is gated behind `prefers-reduced-motion`. The titlebar icon and favicon family (which legitimately *are* app icons) keep the navy badge. Thanks @rodboev for the core call.
- **New Hermes WebUI brand mark — a cyan→blue caduceus on a navy badge.** The titlebar icon and the full favicon family (`favicon.svg`/`favicon-512.svg`/`favicon.ico` + 32/192/512 PNGs + `apple-touch-icon.png`) are regenerated from a single clean vector mark (gradient `#08EBF1 → #3889FD` on a radial-depth navy tile). (The empty-state hero mark later dropped the navy tile — see the splash-logo entry above.) Five per-skin `.app-titlebar-icon` force-fill overrides (graphite, codex, terracotta, github, geist-contrast) that assumed the old monochrome caduceus were removed so the new full-color badge renders consistently across skins. Verified in dark + light themes.
+180
View File
@@ -21,6 +21,68 @@ def _client_anchor_scene_message_ref(message):
return json.dumps(payload, ensure_ascii=False, separators=(",", ":"))
def _anchor_scene_visible_semantics(scene, *, include_terminal=False):
semantics = []
for row in scene.get("activity_rows") or []:
role = row.get("role")
if role == "terminal" and not include_terminal:
continue
if role in {"prose", "thinking"}:
semantics.append(
{
"role": role,
"kind": row.get("kind"),
"text": " ".join(str(row.get("text") or "").split()),
}
)
continue
if role == "tool":
tool = row.get("tool") if isinstance(row.get("tool"), dict) else {}
payload = row.get("payload") if isinstance(row.get("payload"), dict) else {}
args = tool.get("args") if "args" in tool else payload.get("args")
if args is None:
args = {}
semantics.append(
{
"role": role,
"kind": row.get("kind"),
"status": row.get("status"),
"tool_call_id": row.get("tool_call_id") or tool.get("id") or payload.get("tid"),
"name": tool.get("name") or payload.get("name"),
"args": args,
"done": tool.get("done"),
}
)
continue
if role == "terminal":
semantics.append(
{
"role": role,
"kind": row.get("kind"),
"source_event_type": row.get("source_event_type"),
"status": row.get("status"),
}
)
return semantics
def test_anchor_scene_visible_semantics_preserves_empty_tool_args():
scene = {
"activity_rows": [
{
"role": "tool",
"kind": "tool_completed",
"status": "completed",
"tool_call_id": "call-1",
"tool": {"id": "call-1", "name": "terminal", "args": {}},
"payload": {"tid": "call-1", "name": "terminal", "args": {"command": "stale"}},
}
]
}
assert _anchor_scene_visible_semantics(scene)[0]["args"] == {}
def test_anchor_scene_persistence_round_trip_outside_provider_messages(tmp_path, monkeypatch):
from api import models, routes
from api.models import Session
@@ -2029,6 +2091,124 @@ def test_anchor_scene_hydration_seals_unmatched_live_running_activity_rows():
assert [row.get("role") for row in activity_rows] == ["thinking", "prose", "tool"]
def test_runtime_journal_anchor_scene_matches_settled_hydrated_visible_semantics(tmp_path, monkeypatch):
"""Runtime journal replay and settled read hydration must preserve the same
visible anchor activity semantics for one turn.
Compared path:
_run_journal_live_snapshot(...).anchor_activity_scene
-> persisted anchor_activity_scenes record
-> _hydrate_anchor_activity_scenes(...)._anchor_activity_scene.
"""
from api import models, routes
from api.run_journal import RunJournalWriter
session_dir = tmp_path / "sessions"
session_dir.mkdir()
monkeypatch.setattr(models, "SESSION_DIR", session_dir)
monkeypatch.setattr(routes, "SESSION_DIR", session_dir)
session_id = "anchorparity1"
stream_id = "stream-parity-1"
process_before_tool = "first progress"
thinking = "thinking through plan"
process_after_tool = "checkpoint tail"
final_answer = "Final answer: keep the activity above this answer."
writer = RunJournalWriter(session_id, stream_id, session_dir=session_dir)
writer.append_sse_event("token", {"text": process_before_tool})
writer.append_sse_event("reasoning", {"text": thinking})
writer.append_sse_event(
"tool",
{"name": "terminal", "tid": "call-1", "args": {"command": "pytest"}},
)
writer.append_sse_event(
"tool_complete",
{"name": "terminal", "tid": "call-1", "preview": "ok"},
)
writer.append_sse_event("token", {"text": f" {process_after_tool}"})
runtime_snapshot = routes._run_journal_live_snapshot(stream_id)
assert runtime_snapshot is not None
runtime_scene = runtime_snapshot.get("anchor_activity_scene")
assert isinstance(runtime_scene, dict)
assert _anchor_scene_visible_semantics(runtime_scene) == [
{"role": "prose", "kind": "process_prose", "text": process_before_tool},
{"role": "thinking", "kind": "reasoning", "text": thinking},
{
"role": "tool",
"kind": "tool_completed",
"status": "completed",
"tool_call_id": "call-1",
"name": "terminal",
"args": {"command": "pytest"},
"done": True,
},
{"role": "prose", "kind": "process_prose", "text": process_after_tool},
]
persisted_scene = json.loads(json.dumps(runtime_scene))
persisted_scene["activity_rows"].append(
{
"row_id": "terminal-done",
"role": "terminal",
"kind": "terminal_status",
"source_event_type": "done",
"status": "completed",
"text": "Response complete",
}
)
messages = [
{"role": "user", "content": "question"},
{
"role": "assistant",
"content": process_before_tool,
"reasoning": thinking,
"tool_calls": [
{
"id": "call-1",
"name": "terminal",
"args": {"command": "pytest"},
"preview": "pytest",
"snippet": "ok",
}
],
},
{"role": "tool", "tool_call_id": "call-1", "content": "ok"},
{"role": "assistant", "content": process_after_tool},
{"role": "assistant", "content": final_answer},
]
message_ref = routes._assistant_anchor_scene_message_ref(messages[4])
records = {
message_ref: {
"version": "anchor_activity_scene_record_v1",
"message_index": 4,
"message_ref": message_ref,
"stream_id": stream_id,
"scene": persisted_scene,
}
}
hydrated = routes._hydrate_anchor_activity_scenes(messages, records)
settled_scene = hydrated[4]["_anchor_activity_scene"]
assert settled_scene["final_answer"] == final_answer
assert final_answer not in [
row.get("text") for row in settled_scene["activity_rows"] if row.get("role") != "terminal"
]
assert _anchor_scene_visible_semantics(settled_scene) == _anchor_scene_visible_semantics(runtime_scene)
assert _anchor_scene_visible_semantics(settled_scene, include_terminal=True)[-1] == {
"role": "terminal",
"kind": "terminal_status",
"source_event_type": "done",
"status": "completed",
}
assert not any(
row.get("role") in {"prose", "thinking", "tool"} and row.get("status") == "running"
for row in settled_scene["activity_rows"]
)
def test_runtime_journal_snapshot_includes_live_anchor_activity_scene(monkeypatch):
from api import routes