Stage 368: PR #2377 — Add run-journal replay timeline parity checks by @franksong2702

This commit is contained in:
Hermes Agent 2026-05-16 17:19:07 +00:00
commit bcf44e586a
2 changed files with 63 additions and 2 deletions

View file

@ -78,7 +78,10 @@ while WebUI still has multiple overlapping state stores.
assistant just acted. assistant just acted.
5. **Replay is idempotent.** Replaying a run from a cursor must not duplicate 5. **Replay is idempotent.** Replaying a run from a cursor must not duplicate
transcript rows, thinking content, interim assistant text, tool cards, or transcript rows, thinking content, interim assistant text, tool cards, or
compression cards. compression cards. Replayed long-task events should enter the same
browser-facing timeline renderer as live SSE events so recovery does not
downgrade a structured Thinking / progress / tool / compression turn into a
separate flattened presentation.
6. **Compression is not current intent.** Automatic compression summaries and 6. **Compression is not current intent.** Automatic compression summaries and
reference cards are recovery/handoff material. They must not be treated as a reference cards are recovery/handoff material. They must not be treated as a
new user request, active-turn content, or the default visible explanation for new user request, active-turn content, or the default visible explanation for
@ -102,6 +105,8 @@ context reconstruction, or session metadata:
- What happens after browser refresh, session switch, SSE reconnect, and WebUI - What happens after browser refresh, session switch, SSE reconnect, and WebUI
restart? restart?
- Does replay rebuild the same scene without duplicates? - Does replay rebuild the same scene without duplicates?
- Does replay use the same timeline-rendering path as live SSE for thinking,
interim assistant text, tool cards, compression cards, and terminal states?
- Can this change move a session in the sidebar without meaningful user or - Can this change move a session in the sidebar without meaningful user or
assistant activity? assistant activity?
- Can automatic compression or recovery text become visible active-turn content? - Can automatic compression or recovery text become visible active-turn content?
@ -147,4 +152,3 @@ The two documents should be read together:
4. If #1925 introduces a new adapter-backed runtime layer, update this RFC or 4. If #1925 introduces a new adapter-backed runtime layer, update this RFC or
replace it with the accepted implementation contract so these invariants do replace it with the accepted implementation contract so these invariants do
not live only in historical discussion. not live only in historical discussion.

View file

@ -35,3 +35,60 @@ def test_frontend_replay_cursor_uses_eventsource_last_event_id():
assert "source.addEventListener(_runJournalEventName,_rememberRunJournalCursor)" in MESSAGES_SRC assert "source.addEventListener(_runJournalEventName,_rememberRunJournalCursor)" in MESSAGES_SRC
assert "after_seq=${encodeURIComponent(String(_runJournalReplayAfterSeq()))}" in MESSAGES_SRC assert "after_seq=${encodeURIComponent(String(_runJournalReplayAfterSeq()))}" in MESSAGES_SRC
assert "after_seq=0" not in MESSAGES_SRC assert "after_seq=0" not in MESSAGES_SRC
def test_replayed_long_task_events_enter_the_same_live_timeline_handlers():
"""Run-journal replay must not grow a parallel long-task renderer.
The run-state consistency contract depends on replayed journal events
flowing through the same EventSource handlers as live streams. Otherwise a
live long task can render as Thinking -> progress text -> tool cards, while
the same journaled event sequence replays as a flattened or reordered scene.
"""
wire_pos = MESSAGES_SRC.index("function _wireSSE(source)")
wire_block = MESSAGES_SRC[wire_pos : MESSAGES_SRC.index("async function _restoreSettledSession", wire_pos)]
replay_events = [
"reasoning",
"interim_assistant",
"tool",
"tool_complete",
"compressing",
"compressed",
"metering",
"done",
"apperror",
]
for event_name in replay_events:
assert f"source.addEventListener('{event_name}'" in wire_block, (
f"{event_name} must be handled by the shared live/replay SSE pipeline"
)
assert "updateThinking(" in wire_block, "reasoning replay should use the live Thinking card path"
assert "appendLiveToolCard(tc)" in wire_block, "tool replay should use live tool-card rendering"
assert "setCompressionUi({" in wire_block, "compression replay should use the compression card path"
assert "_runJournalReplayParams()" in MESSAGES_SRC, "replay attachments should enter _wireSSE via EventSource"
def test_run_journal_cursor_tracks_every_long_task_timeline_event():
"""Every user-visible long-task event needs cursor tracking for parity replay."""
cursor_loop_pos = MESSAGES_SRC.index("for(const _runJournalEventName of [")
cursor_loop = MESSAGES_SRC[cursor_loop_pos : MESSAGES_SRC.index("]", cursor_loop_pos)]
timeline_events = [
"token",
"interim_assistant",
"reasoning",
"tool",
"tool_complete",
"compressing",
"compressed",
"metering",
"done",
"apperror",
"cancel",
]
for event_name in timeline_events:
assert f"'{event_name}'" in cursor_loop, (
f"{event_name} must advance the replay cursor to avoid duplicate timeline replay"
)