mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-11-06 05:17:15 +00:00
perf(state): a display read hashes only rows a removed copy could match
RT-39 live pass: a warm session.resume of a 30k-row session ran ~12% slower than main (real tui_gateway over a WS, in-process uvicorn: 1.837 s vs 1.634 s median, 8 warm runs each, alternating). The composed tree matched this branch (1.822 s), so #140971 owns the regression. The cause is the warm path's display read (get_messages_as_conversation with include_compacted), which went from 0.30 s to 0.47 s. Since7e31a90112, _dedupe_display_generations(drop_removed=True) computed a sha256 of repr(display key) for EVERY live row, to compare against the removed rows' stored identities. That is 30k repr+sha256 calls per resume, plus the allocation churn behind them. The second lineage query and the sort were noise. Every display key carries the row's timestamp, so a live row can share a removed row's identity only at one of the removed rows' timestamps. Now: - no removed row in the lineage: no hashing at all; - otherwise only live rows at a removed row's timestamp are hashed. The message_uid / absorbed-uid copies are still compared for every row. Round 2's behavior is unchanged: removed rows are still read for identity only (no bodies), archived-before-live ordering holds, and a delete during resume still answers not_found. The guard (tests/hermes_state/test_display_read_identity_work.py) counts _display_identity calls instead of timing anything. A 400-row session with no removed row: 0 hashes (it was 400 on0bf1ed99ce). With a two-row rewind: at most 2. Live-pass finding on #140971 (package 10) Refs #134889 (RT-39) Refs #140719
This commit is contained in:
1 parent
0bf1ed99ce
commit
c6961c62af
2 files changed
+76
-1
No files matched your search
@@ -1141,6 +1141,14 @@ class SessionMessagesMixin(SessionDetachedResultsMixin, SessionTodoResultMixin):
|
||||
first_id: dict[tuple[Any, ...], int] = {}
|
||||
removed: dict[Any, bool] = {} # copy identity -> its newest copy is removed (rows come in id order)
|
||||
identities: dict[tuple[Any, ...], list[Any]] = {}
|
||||
removed_ts: set[Any] = set()
|
||||
if drop_removed:
|
||||
rows = rows if isinstance(rows, list) else list(rows)
|
||||
# Every display key carries the row's timestamp third, so a live row can share a removed row's identity
|
||||
# only at one of the removed rows' timestamps; hash just those (a sha256 per live row cost a warm
|
||||
# 30k-row resume ~0.17 s, RT-39). No removed row at all: nothing can hide a copy.
|
||||
removed_ts = {row["timestamp"] for row in rows if not row["active"] and not row["compacted"]}
|
||||
drop_removed = bool(removed_ts)
|
||||
for row in rows:
|
||||
copies, key = None, None
|
||||
if drop_removed:
|
||||
@@ -1148,7 +1156,9 @@ class SessionMessagesMixin(SessionDetachedResultsMixin, SessionTodoResultMixin):
|
||||
gone = not row["active"] and not row["compacted"]
|
||||
stored = row["display_identity"] if gone else None
|
||||
key = None if stored else self._display_dedupe_key(row)
|
||||
copies = [stored or self._display_identity(key), *(
|
||||
hashed = stored or (self._display_identity(key) if key is not None and (
|
||||
gone or row["timestamp"] in removed_ts) else None)
|
||||
copies = [*((hashed,) if hashed else ()), *(
|
||||
("uid", uid) for uid in (row["message_uid"], *_uid_list(row["absorbed_message_uids"])) if uid)]
|
||||
removed.update(dict.fromkeys(copies, gone))
|
||||
if gone:
|
||||
|
||||
@@ -0,0 +1,65 @@
|
||||
"""A display read hashes a display identity only where a removed copy could match it (RT-39).
|
||||
|
||||
``drop_removed`` hides a message whose newest copy was removed. Hashing every live row's display key to
|
||||
compare against the removed rows cost a warm 30k-row resume ~0.17 s (+12% end to end), though a live row
|
||||
can only share a removed row's key at one of the removed rows' timestamps. The guard counts hashes, not
|
||||
seconds. Real SessionDB, real rewind.
|
||||
"""
|
||||
from hermes_state import SessionDB
|
||||
|
||||
LIVE_TURNS = 200
|
||||
|
||||
|
||||
def _session(db, *, rewind):
|
||||
db.create_session('s', source='test')
|
||||
msgs = []
|
||||
for i in range(LIVE_TURNS):
|
||||
msgs += [{'role': 'user', 'content': f'U{i}', 'timestamp': 1_000_000 + 2 * i},
|
||||
{'role': 'assistant', 'content': f'A{i}', 'timestamp': 1_000_001 + 2 * i}]
|
||||
db.append_messages_batch('s', msgs)
|
||||
if rewind:
|
||||
db.append_messages_batch('s', [{'role': 'user', 'content': 'GONE', 'timestamp': 2_000_000},
|
||||
{'role': 'assistant', 'content': 'NO', 'timestamp': 2_000_001}])
|
||||
db.rewind_to_message('s', next(m['id'] for m in db.get_messages('s') if m['content'] == 'GONE'))
|
||||
|
||||
|
||||
def _hashes(db, monkeypatch, read):
|
||||
calls = []
|
||||
real = SessionDB._display_identity
|
||||
|
||||
def spy(key):
|
||||
calls.append(key)
|
||||
return real(key)
|
||||
monkeypatch.setattr(SessionDB, '_display_identity', staticmethod(spy))
|
||||
shown = read()
|
||||
monkeypatch.undo()
|
||||
return shown, len(calls)
|
||||
|
||||
|
||||
def _reads(db):
|
||||
return (lambda: db.get_messages_as_conversation('s', include_ancestors=True, include_compacted=True),
|
||||
lambda: db.get_resume_conversations('s')[1],
|
||||
lambda: db.get_messages_as_conversation('s', include_compacted=True))
|
||||
|
||||
|
||||
def test_a_display_read_with_no_removed_row_hashes_nothing(tmp_path, monkeypatch):
|
||||
with SessionDB(db_path=tmp_path / 'state.db') as db:
|
||||
_session(db, rewind=False)
|
||||
for read in _reads(db):
|
||||
shown, hashes = _hashes(db, monkeypatch, read)
|
||||
assert len(shown) == 2 * LIVE_TURNS
|
||||
assert hashes == 0
|
||||
|
||||
|
||||
def test_a_display_read_hashes_only_rows_at_a_removed_rows_timestamp(tmp_path, monkeypatch):
|
||||
with SessionDB(db_path=tmp_path / 'state.db') as db:
|
||||
_session(db, rewind=True)
|
||||
removed = db._read_one('SELECT COUNT(*) FROM messages WHERE active = 0 AND compacted = 0')[0]
|
||||
assert removed == 2
|
||||
for read in _reads(db):
|
||||
shown, hashes = _hashes(db, monkeypatch, read)
|
||||
assert [m['content'] for m in shown][-2:] == [f'U{LIVE_TURNS - 1}', f'A{LIVE_TURNS - 1}']
|
||||
assert 'GONE' not in [m['content'] for m in shown]
|
||||
# At most the removed rows themselves (a legacy row without a stored identity); no live row shares
|
||||
# their timestamps here.
|
||||
assert hashes <= removed, hashes
|
||||
Reference in new issue
Block a user