Release v0.51.740 — runtime-journal anchor-scene parity test (#5183, @franksong2702)
This commit is contained in:
2 files changed
+182
No files matched your search
@@ -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.
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
Reference in new issue
Block a user