Files
hermes-webui/tests/test_checkpoint_skips_redundant_sidecar_rewrite.py
T
ruizanthonyandAnthony Ruiz 0e5a0fadae perf(streaming): skip redundant sidecar rewrites during a turn (#7172)
The periodic checkpoint thread fires whenever a tool call completes and
unconditionally rewrites the whole session sidecar. A completed tool call
is not proof that anything the checkpoint persists actually changed:
`run_conversation()` mutates an internal copy of the transcript, and the
turn bookkeeping fields (`active_stream_id`, `pending_user_message`,
`pending_started_at`) are written once before the run starts. So the
recurring rewrite normally re-persists byte-identical state.

Each rewrite serializes the entire session while holding the GIL, so its
cost scales with transcript size. Because the HTTP server and the agent
workers share one interpreter, that cost is added latency for every
concurrent request — not just for the session being written.

Measured on this deployment (1,622 sidecars, 6.65 GB, median 2.31 MB,
28.8% above 5 MB, max 35.9 MB):

  sidecar    write    per 10-min turn (40 wakeups)
  1.8 MB    ~100 ms    4.1 s  ->  0.2 s
  24.7 MB   ~1.4 s    41.6 s  ->  2.1 s

A checkpoint now writes only when the state it persists actually moved,
compared through a cheap fingerprint that allocates nothing. The
fingerprint covers every field the checkpoint can legitimately advance
mid-run: session identity (compression rotates the SID), turn
bookkeeping, and the length of the persisted message projections.

Fail-closed on both sides: an unreadable fingerprint returns None and
forces the write, and a 5-minute idle refresh keeps `updated_at` from
going stale on a long quiet turn (the sidebar sorts on it). Crash
recovery is unchanged — any real mutation is still persisted within one
15 s interval.

Co-authored-by: Anthony Ruiz <ruizanthony@users.noreply.github.com>
2026-08-20 04:47:38 -07:00

139 lines
5.3 KiB
Python

"""Regression coverage: the periodic streaming checkpoint must not rewrite
byte-identical sidecars.
The checkpoint thread (#765) fires whenever a tool call completes. A completed
tool call is *not* proof that anything the checkpoint persists has changed:
``run_conversation()`` mutates an internal copy of the transcript, and the turn
bookkeeping fields are written once before the run starts.
Each rewrite re-serializes the whole session while holding the GIL. Because the
HTTP server and the agent workers share one interpreter, that cost is added
latency for every concurrent request — ~100 ms for a 1.8 MB sidecar and ~300 ms
for a 36 MB one, repeated every 15 s for the whole turn.
These tests pin the behavior at the decision level (the fingerprint) so they do
not depend on wall-clock timing of the background thread.
"""
from __future__ import annotations
def _session(tmp_path, **overrides):
from api.models import Session
s = Session(session_id="ckpt_redundant_1")
s.messages = [{"role": "assistant", "content": "hello"}]
s.context_messages = []
s.pending_user_message = "do the thing"
s.pending_started_at = 1787000000.0
s.active_stream_id = "streamabc"
for key, value in overrides.items():
setattr(s, key, value)
return s
def test_fingerprint_is_stable_when_nothing_changes(tmp_path):
"""Two reads of an untouched session produce the same fingerprint.
This is the redundant case: the checkpoint would rewrite identical bytes.
"""
from api.streaming import _streaming_checkpoint_fingerprint
s = _session(tmp_path)
assert _streaming_checkpoint_fingerprint(s) == _streaming_checkpoint_fingerprint(s)
def test_fingerprint_tracks_every_field_the_checkpoint_persists(tmp_path):
"""Any state a mid-run checkpoint can legitimately advance must be detected.
Missing one of these would make the skip lose real data, so each field is
asserted individually rather than as a group.
"""
from api.streaming import _streaming_checkpoint_fingerprint
base = _session(tmp_path)
reference = _streaming_checkpoint_fingerprint(base)
mutations = {
# compression rotates the session id mid-turn
"session_id": "rotated_sid",
"parent_session_id": "parent_sid",
"profile": "other-profile",
# turn bookkeeping cleared at the end of the turn
"active_stream_id": None,
"pending_user_message": None,
"pending_started_at": None,
"pending_user_source": "telegram",
# transcript growth (error rows, continuation tail, compression)
"messages": [{"role": "assistant", "content": "hello"}, {"role": "user", "content": "more"}],
"context_messages": [{"role": "user", "content": "ctx"}],
"compression_anchor_visible_idx": 3,
"pre_compression_snapshot": {"messages": []},
"pending_attachments": [{"name": "f.png"}],
}
for field, value in mutations.items():
mutated = _session(tmp_path, **{field: value})
assert _streaming_checkpoint_fingerprint(mutated) != reference, (
f"a change to {field!r} must force a checkpoint write"
)
def test_fingerprint_fails_closed_on_unreadable_session():
"""An unreadable session yields None so the caller keeps writing.
Fail-closed: never skip a checkpoint on uncertainty.
"""
from api.streaming import _streaming_checkpoint_fingerprint
class Hostile:
def __getattr__(self, name):
raise RuntimeError("boom")
assert _streaming_checkpoint_fingerprint(Hostile()) is None
def test_checkpoint_loop_skips_redundant_writes_but_persists_changes(monkeypatch, tmp_path):
"""End-to-end on the real loop body: identical state writes once.
Drives the actual decision sequence used by the checkpoint thread: an
unchanged session must be persisted once, not on every tool completion,
while a real mutation must be persisted promptly.
"""
import time
from api.streaming import (
_streaming_checkpoint_fingerprint,
_CHECKPOINT_IDLE_REFRESH_SECONDS,
)
s = _session(tmp_path)
writes = []
# Mirror of the loop body in _periodic_checkpoint(), kept in sync with it.
last_fingerprint = None
last_write_at = 0.0
now = time.time()
def tick(session, at):
nonlocal last_fingerprint, last_write_at
fingerprint = _streaming_checkpoint_fingerprint(session)
stale = (at - last_write_at) >= _CHECKPOINT_IDLE_REFRESH_SECONDS
if fingerprint is None or fingerprint != last_fingerprint or stale:
writes.append(at)
last_fingerprint = fingerprint
last_write_at = at
# 10 tool completions, nothing else changed -> exactly one write.
for i in range(10):
tick(s, now + i)
assert len(writes) == 1, f"redundant rewrites: {len(writes)} writes for an unchanged session"
# A real transcript change must be persisted on the next tick.
s.messages = list(s.messages) + [{"role": "assistant", "content": "new answer"}]
tick(s, now + 10)
assert len(writes) == 2, "a real mutation must force a checkpoint write"
# updated_at must not go stale forever on a very long quiet turn.
tick(s, now + 10 + _CHECKPOINT_IDLE_REFRESH_SECONDS)
assert len(writes) == 3, "a periodic refresh must still happen on long turns"