Files
hermes-agent/hermes_state_coverage.py
T
Teknium 3745ea63bb fix(state): a dedupe claim needs the occurrence, its version and its replay bytes
Review round 5 (unsupportedpastels) on #144938, three findings:

- Compaction claims no longer accept matching text: the identity+timestamp and
  bare-identity claims are gone. Only a dict naming the row (_row_id, else
  message_uid) may stand for a tail row, so an identical message another
  surface appended after the watermark keeps its model-visible row (as main).
- A rewritten-carry (containment) claim needs the stored-row digest to equal
  the row's current version; a _row_id without a digest no longer proves it,
  so a row shortened in place after a row-addressed load keeps its new text.
  (A loader-stamped version witness would add an underscore key to
  row-addressed projections, which test_message_reactions pins to _row_id and
  _db_persisted; such carries are cloned as on main instead.)
- Both claim passes and the flush re-insert guard now compare what the next
  request replays: the guard compares _REPLAY_COLUMNS (owned columns +
  api_content + codex_reasoning_items) instead of the CAS ownership columns,
  and claims check api_content and the reasoning carriers through the
  writer's serializer. A newer sidecar or encrypted reasoning item is stored
  / kept instead of being dropped as "the same message".
2026-10-27 11:49:40 -07:00

484 lines
27 KiB
Python

"""Held-row coverage resolution for in-place compaction: which durable rows the compressor actually held.
Names carried-forward dicts, id-less held dicts and the rows an alternation repair merged, dropped or
folded, so ``archive_and_compact`` archives exactly those (or falls back to the watermark when a durable
held row cannot be named). Mixin bound via the MRO next to SessionMessagesMixin, which owns the writes.
"""
from __future__ import annotations
from collections import Counter
from typing import Any, Dict, List, Optional, Set, Tuple
from agent.message_metadata import DB_ROW_SNAPSHOT, MESSAGE_UID, message_uid_or_none
from hermes_cli.timefmt import coerce_epoch
from hermes_state_common import _id_chunks, _placeholders
from hermes_state_messages import _parse_tool_calls
def _text_of(content: Any) -> str:
"""The text a message carries: a string, or its text parts joined; image parts carry none."""
if isinstance(content, str):
return content
if isinstance(content, list):
return "\n".join(
str(part.get("text") or "") for part in content
if isinstance(part, dict) and part.get("type") in {"text", "input_text", "output_text"})
return ""
def _holds_as_block(inner: str, outer: str) -> bool:
"""True when *inner* sits in *outer* as whole lines: the compressor adds only whole blocks around a carried
row's text (header, markers, summary), so a hit that starts or ends mid-line is the row losing text instead.
Blanks beside the hit count as line edges: the load lens strips the stored text, never the copy inside."""
start = outer.find(inner)
while start != -1:
end = start + len(inner)
if not outer[:start].rpartition("\n")[2].strip() and not outer[end:].partition("\n")[0].strip():
return True
start = outer.find(inner, start + 1)
return False
def _images_replaced(stored: Any, held: Any) -> bool:
"""True when *held* is *stored* with at least one image part swapped for a text part, all else equal."""
from agent.context_compressor import _is_image_part
if not (isinstance(stored, list) and isinstance(held, list)) or len(stored) != len(held):
return False
swapped = False
for was, now in zip(stored, held):
if was == now:
continue
if not (_is_image_part(was) and isinstance(now, dict) and now.get("type") == "text"):
return False
swapped = True
return swapped
class SessionCoverageMixin:
"""Coverage helpers for SessionDB; relies on SessionMessagesMixin's codec/identity helpers."""
def _carried_tail_ids(
self, conn, tail_ids: list[int], compacted_messages: list[dict[str, Any]],
) -> tuple[dict[int, int], set[int]]:
"""Rows of *tail_ids* that *compacted_messages* already carry -> ``({row id: carrying dict index}, weak)``.
A row above the watermark is normally a concurrent append the summary never saw, so the commit
clones it after the compacted set. A row the compressor ALSO held (flushed after the watermark
was read, or left unnamed so coverage fell back to the watermark) is already in the compacted
set as a carried copy; cloning it too stores the message twice, with a fresh id and timestamp
each (#137197). A claim must prove the occurrence AND the version, so the next request sees exactly
what it would have seen with the clone minus the second copy: only a dict that names the row
(``_row_id``, else ``message_uid``) may claim it, never matching text, which another surface can
append independently. The first pass claims a named dict holding the row's stored content and its
replayed sidecars (``_replays_as``). The second lets a still-unclaimed named dict with other content
claim the row only when the compressor's own rewrite of the row's CURRENT version explains the
difference (see ``_rewrite_explains``), exactly one tail row carries that uid and no active row
outside *tail_ids* does (there the dict's original is that row, so the tail row is another version).
Anything else is cloned: a duplicate, never a stale or missing model input. Each dict claims at most
one row and each row is claimed once.
``weak`` holds the rows claimed by a rewritten carry: it is not the stored text, so the caller archives
those as compacted history, never rewinds them out of display.
"""
if not tail_ids or not compacted_messages:
return {}, set()
by_row, by_uid = self._index_held_dicts(compacted_messages)
rows = conn.execute(
# Every column: the claims compare replayed sidecars and the row's version digest (DB_ROW_SNAPSHOT).
f"SELECT * FROM messages WHERE id IN ({_placeholders(tail_ids)}) ORDER BY id", tail_ids,
).fetchall()
carried: dict[int, int] = {}
for row in rows:
index = self._claim_stored_copy(row, compacted_messages, by_row, by_uid, set(carried.values()))
if index is not None:
carried[int(row["id"])] = index
named_uids = {*by_uid, *(named[2] for named in by_row.values() if named[2] is not None)}
held_elsewhere = self._uids_active_outside(conn, rows, tail_ids, named_uids)
tail_uids = Counter(row[MESSAGE_UID] for row in rows if row[MESSAGE_UID])
weak: set[int] = set()
for row in rows:
row_id, row_uid = int(row["id"]), row[MESSAGE_UID] or None
if row_id in carried or row_uid is None or tail_uids[row_uid] != 1 or row_uid in held_elsewhere:
continue
index = self._rewritten_carry(row, row_uid, compacted_messages, by_row, by_uid, set(carried.values()))
if index is not None:
carried[row_id] = index
weak.add(row_id)
return carried, weak
def _index_held_dicts(self, compacted_messages: list[dict[str, Any]]) -> tuple[dict, dict]:
"""Held dicts by the name they carry: ``_row_id`` -> (index, identity, uid); uid -> [(index, identity)].
A dict naming neither is never a claim: equal text is not the same occurrence."""
by_row: dict[int, tuple[int, tuple[Any, ...], Optional[str]]] = {}
by_uid: dict[str, list[tuple[int, tuple[Any, ...]]]] = {}
for index, message in enumerate(compacted_messages):
if not isinstance(message, dict):
continue
identity = self._row_identity(
message.get("role", "unknown"), message.get("content"), message.get("tool_call_id"),
_parse_tool_calls(message.get("tool_calls")))
row_id = message.get("_row_id")
uid = message_uid_or_none(message)
if isinstance(row_id, int) and not isinstance(row_id, bool) and row_id > 0:
# Authoritative only while the stored identity still matches (a stale stamp names another row).
by_row.setdefault(row_id, (index, identity, uid))
elif uid is not None:
by_uid.setdefault(uid, []).append((index, identity))
return by_row, by_uid
def _claim_stored_copy(self, row, compacted_messages, by_row, by_uid, claimed) -> Optional[int]:
"""First pass: the unclaimed dict naming *row* (``_row_id``, else uid) that replays exactly as it does."""
identity = self._row_identity(
row["role"], self._decode_content(row["content"]), row["tool_call_id"], _parse_tool_calls(row["tool_calls"]))
named = by_row.get(int(row["id"]))
candidates = [named[0]] if named is not None and named[1] == identity else []
row_uid = row[MESSAGE_UID] or None
candidates += [index for index, held in by_uid.get(row_uid, ()) if held == identity] if row_uid else []
return next((index for index in candidates
if index not in claimed and self._replays_as(row, compacted_messages[index], True)), None)
def _rewritten_carry(self, row, row_uid, compacted_messages, by_row, by_uid, claimed) -> Optional[int]:
"""Second pass: the one unclaimed dict naming *row* (``_row_id`` with the row's uid, else its uid alone)
whose different content the compressor's rewrite of that row explains; None means clone."""
named = by_row.get(int(row["id"]))
if named is not None and named[2] == row_uid:
candidates = [named[0]]
else:
candidates = [index for index, _identity in by_uid.get(row_uid, ())]
candidates = [index for index in candidates if index not in claimed]
if len(candidates) != 1:
return None
if self._rewrite_explains(row, compacted_messages[candidates[0]]):
return candidates[0]
return None
def _replays_as(self, row, held: dict[str, Any], same_content: bool) -> bool:
"""Whether *held* replays the sidecars the next request would read from *row* (``api_content`` and the
reasoning carriers in ``REPLAY_SIDECARS``), compared through the writer's own serializer.
A sidecar the copy carries must equal the row's: a different one is an older version (a later
``set_message_api_content``, a re-stamped reasoning item). A copy WITHOUT the row's ``api_content``
stands for it only when its own content is those bytes (the flush stores sanitize-changed text as the
sidecar), or when the compressor rewrote the content, which drops the stale sidecar by design."""
from agent.transcript_repair import REPLAY_SIDECARS
serialized = self._serialized_message_row(row["session_id"], held, float(row["timestamp"] or 0))
content = self._decode_content(row["content"])
row_api, held_api = (None if api == text else api for api, text in (
(row["api_content"], content), (serialized["api_content"], held.get("content"))))
if any(serialized[column] is not None and serialized[column] != row[column] for column in REPLAY_SIDECARS):
return False
if held_api is not None or row_api is None:
return held_api == row_api
return not same_content or held.get("content") == row_api
@staticmethod
def _copy_of_current_row(row, held: dict[str, Any]) -> bool:
"""Whether *held* was copied from *row*'s current version, so extra text in it is the compressor's.
Containment alone cannot tell "the compressor added text" from "the row lost text after the copy"
(an in-place edit that shortens it). Only the stored-row digest a flushed dict carries settles it: a
mismatch means the row was rewritten after the copy. A copy without one (a bare uid copy, or a
row-addressed load, which omits the digest by design) may predate any number of rewrites, so the row
is cloned: a duplicate, never an older instruction in place of the current one."""
# Runtime import avoids the transcript_repair -> hermes_state_messages module cycle.
from agent.transcript_repair import transcript_row_snapshot
snapshot = held.get(DB_ROW_SNAPSHOT)
return isinstance(snapshot, str) and snapshot == transcript_row_snapshot(row)
def _rewrite_explains(self, row, held: dict[str, Any]) -> bool:
"""True when *held* differs from stored *row* only the way the compressor rewrites a carry it keeps:
image parts replaced by text placeholders, or, for a copy of the row's current version, the row's
text kept verbatim as whole lines inside a longer text (a summary merged in, prior-context header and
markers around it); and its sidecars still replay the row's. The summary flag alone is not enough: a
flagged copy taken before an in-place rewrite of the row would put the older text back."""
role = row["role"]
if (held.get("role"), held.get("tool_call_id") or None) != (role, row["tool_call_id"] or None):
return False
if (_parse_tool_calls(held.get("tool_calls")) or None) != (_parse_tool_calls(row["tool_calls"]) or None):
return False
stored = self._loaded_view_content(role, self._decode_content(row["content"]))
content = self._loaded_view_content(role, held.get("content"))
if not self._replays_as(row, held, stored == content):
return False
if _images_replaced(stored, content):
return True
if not self._copy_of_current_row(row, held):
return False
stored_text = _text_of(stored)
return bool(stored_text.strip()) and _holds_as_block(stored_text, _text_of(content))
@staticmethod
def _uids_active_outside(conn, rows, tail_ids: list[int], named_uids) -> set[str]:
"""uids of tail *rows* a held dict names that an ACTIVE row outside *tail_ids* (a covered row, below the
watermark) also carries: there the dict's own original is that row, so the tail row is another version."""
wanted = sorted({row[MESSAGE_UID] for row in rows if row[MESSAGE_UID] in named_uids})
if not wanted:
return set()
tail, session_id = set(tail_ids), rows[0]["session_id"]
return {
found[MESSAGE_UID]
for chunk in _id_chunks(wanted)
for found in conn.execute(
f"SELECT id, {MESSAGE_UID} FROM messages WHERE session_id = ? AND active = 1 "
f"AND {MESSAGE_UID} IN ({_placeholders(chunk)})", (session_id, *chunk)).fetchall()
if int(found["id"]) not in tail
}
@staticmethod
def _carried_tail_slots(carried: dict[int, int], compacted_count: int, tail_count: int) -> int:
"""How many of the carried tail's *tail_count* dicts name a row in *carried*: those originals sit
above the watermark, so the positional rewind below it must take that many fewer rows."""
if tail_count <= 0:
return 0
return sum(1 for index in carried.values() if index >= compacted_count - int(tail_count))
def _resolve_carried_row_ids(
self, conn, session_id: str, carried_messages: list[dict[str, Any]],
) -> list[int]:
"""Resolve byte-identical carried-forward live dicts to their ACTIVE durable originals.
_row_id is authoritative when the message carries it and the stored identity still matches.
Resume surfaces that intentionally omit row ids fall back to a UNIQUE
(role/content/tool identity, timestamp) match. Ambiguous or timestamp-less fallbacks are left
as compacted history rather than risking a false rewind classification.
"""
if not carried_messages:
return []
carried: list[tuple[tuple[Any, ...], Any, Any]] = []
for message in carried_messages:
if not isinstance(message, dict):
continue
identity = self._row_identity(
message.get("role", "unknown"), message.get("content"), message.get("tool_call_id"),
_parse_tool_calls(message.get("tool_calls")))
row_id = message.get("_row_id")
if not (isinstance(row_id, int) and not isinstance(row_id, bool) and row_id > 0):
row_id = None
carried.append((identity, row_id, message.get("timestamp")))
def _index(ids: Optional[list[int]]):
by_id: dict[int, tuple[Any, ...]] = {}
by_key: dict[tuple[Any, ...], list[int]] = {}
narrow = f" AND id IN ({_placeholders(ids)})" if ids else ""
for row in conn.execute(
"SELECT id, role, content, tool_call_id, tool_calls, timestamp FROM messages "
f"WHERE session_id = ? AND active = 1{narrow} ORDER BY id",
(session_id, *(ids or ())),
).fetchall():
rid = int(row["id"])
by_id[rid] = self._row_identity(
row["role"], self._decode_content(row["content"]), row["tool_call_id"],
_parse_tool_calls(row["tool_calls"]))
ts = coerce_epoch(row["timestamp"], field="message timestamp")
if ts is not None:
by_key.setdefault((*by_id[rid], ts), []).append(rid)
return by_id, by_key
# The common micro pass carries dicts that all hold a matching _row_id, so the identity
# check only needs those rows; a full active-row scan is reserved for the fallbacks.
row_ids = [row_id for _, row_id, _ in carried if row_id is not None]
by_id, by_key = _index(row_ids if len(row_ids) == len(carried) else None)
if len(row_ids) == len(carried) and any(by_id.get(rid) != ident for ident, rid, _ in carried):
by_id, by_key = _index(None)
resolved: list[int] = []
for identity, row_id, raw_timestamp in carried:
if row_id is not None and by_id.get(row_id) == identity:
resolved.append(row_id)
continue
timestamp = coerce_epoch(raw_timestamp, field="message timestamp")
if timestamp is None:
continue
matches = by_key.get((*identity, timestamp), [])
if len(matches) == 1:
resolved.append(matches[0])
return list(dict.fromkeys(resolved))
def _matching_active_ids(self, conn, session_id: str, message: dict[str, Any]) -> list[int]:
"""Active row ids whose stored role and content equal *message*. Empty when it was never persisted."""
content = message.get("content")
if not isinstance(content, str):
return []
stored = self._encode_content(self._loaded_view_content(message.get("role", "unknown"), content))
return [int(row["id"]) for row in conn.execute(
"SELECT id FROM messages WHERE session_id = ? AND active = 1 AND role = ? AND content = ?",
(session_id, message.get("role"), stored)).fetchall()]
def _matching_retired_ids(
self, conn, session_id: str, retired: dict[str, Any], watermark: Optional[int] = None,
) -> list[int]:
"""Active rows equal to a row the alternation repair retired without an id.
Matched on role, loaded-view content (an empty assistant stores ``None`` or ``""``), tool_call_id
and the ids of its tool calls. Only rows at or below *watermark*: the repair ran on a load taken
before the snapshot, so a later equal row is another surface's append.
"""
role = str(retired.get("role") or "")
bound = watermark is not None
rows = conn.execute(
"SELECT id, content, tool_call_id, tool_calls FROM messages WHERE session_id = ? AND active = 1 "
f"AND role = ?{' AND id <= ?' if bound else ''} ORDER BY id",
(session_id, role, *((int(watermark),) if bound else ()))).fetchall()
want_calls = list(retired.get("tool_call_ids") or ())
matches = []
for row in rows:
content = self._loaded_view_content(role, self._decode_content(row["content"]))
if (content or None) != (retired.get("content") or None):
continue
if (row["tool_call_id"] or None) != (retired.get("tool_call_id") or None):
continue
calls = _parse_tool_calls(row["tool_calls"]) or []
ids = [call.get("id") for call in calls if isinstance(call, dict)] if isinstance(calls, list) else []
if ids != want_calls:
continue
matches.append(int(row["id"]))
return matches
def _merged_user_run(
self, conn, session_id: str, message: dict[str, Any], watermark: Optional[int] = None,
) -> Optional[list[int]]:
"""Active rows an alternation repair merged into *message*: ``[]`` for none, None when ambiguous.
A reload without row ids turns a durable ``user;user`` pair (a prompt that never got its reply)
into one dict equal to neither row. Read as an unpersisted turn, its rows would be re-sequenced
after the compacted set like concurrent appends, behind the turn that is running.
Only a dict the repair stamped: a prompt that was never persisted can carry the same text as
rows another surface appended, and those were never held.
Only rows at or below *watermark*: the repair ran on a load taken before the snapshot, so a
run appended later that joins to the same text is another surface's, not the merged one.
"""
from agent.conversation_compression_archive import MERGED_DURABLE_ROWS, OWN_ROW, RETIRED_DURABLE_ROWS
content, width = message.get("content"), message.get(MERGED_DURABLE_ROWS)
if message.get("role") != "user" or not isinstance(content, str) or type(width) is not int or width < 2:
return []
from gateway.message_timestamps import strip_leading_message_timestamps
content, _ = strip_leading_message_timestamps(content)
bound = watermark is not None
rows = [
(int(row["id"]), row["role"], self._loaded_view_content(row["role"], self._decode_content(row["content"])))
for row in conn.execute(
f"SELECT id, role, content FROM messages WHERE session_id = ? AND active = 1"
f"{' AND id <= ?' if bound else ''} ORDER BY id",
(session_id, *((int(watermark),) if bound else ()))).fetchall()]
# Dropping an orphan can make user rows adjacent only in the repaired view.
# Skip only uniquely proved retired originals, never arbitrary intervening rows.
retired_ids = set()
for retired in message.get(RETIRED_DURABLE_ROWS) or ():
if retired.get(OWN_ROW):
continue
matches = self._matching_retired_ids(conn, session_id, retired, watermark)
if len(matches) != 1:
return None
retired_ids.update(matches)
rows = [row for row in rows if row[0] not in retired_ids]
runs: list[list[int]] = []
for start in range(len(rows)):
merged = ""
for end in range(start, len(rows)):
_row_id, role, part = rows[end]
if role != "user" or not isinstance(part, str):
break
merged = f"{merged}\n\n{part}" if merged and part else (merged or part)
if not content.startswith(merged):
break
if end - start + 1 == width:
if merged == content:
runs.append([rows[i][0] for i in range(start, end + 1)])
break
if len(runs) > 1:
return None
return runs[0] if runs else []
def _proved_coverage(
self, conn, session_id: str, covered_ids: Optional[list[int]],
unresolved_held: Optional[list[dict[str, Any]]], watermark: Optional[int] = None,
) -> Optional[tuple[list[int], set[int]]]:
"""``(ids safe to archive as summarized, ids merged into another held dict)``, or None when
a durable held row cannot be named.
An unresolved dict that still carries the persist marker was loaded from the DB.
Failing to name it means the watermark path, which archives the rows the compressor
saw, including ones whose ids were stripped. A marker-less miss is an unpersisted
turn: it names nothing, and it is not a reason to abandon the ids we do have.
Several active rows with the same content are ambiguous, so that also abandons,
and so does a merged dict whose run cannot be named.
The second set holds only merges the caller could not count: a dict that lists its
own ``_absorbed_row_ids`` is already counted in ``tail_count``.
"""
if covered_ids is None:
return None
from agent.context_compressor import _DB_PERSISTED_MARKER
from agent.conversation_compression_archive import (
ABSORBED_ROW_IDS, MERGED_DURABLE_ROWS, OWN_ROW, RETIRED_DURABLE_ROWS, RETIRED_ROW)
proved = [int(row_id) for row_id in covered_ids if isinstance(row_id, int) and row_id > 0]
merged_away: set[int] = set()
for message in unresolved_held or ():
if not isinstance(message, dict):
continue
if message.get(RETIRED_ROW):
# A row the repair dropped behind another held dict: that dict stands for it in the tail.
# The dict's own row, recorded before a fold rewrote its text, is its own tail slot.
matches = self._matching_retired_ids(conn, session_id, message, watermark)
if len(matches) != 1:
return None
proved.extend(matches)
if not message.get(OWN_ROW):
merged_away.update(matches)
continue
# A folded assistant's own row is named by its OWN_ROW record above. Its combined text never
# existed in storage, so matching it by content could only claim another surface's row.
if any(isinstance(row, dict) and row.get(OWN_ROW) for row in message.get(RETIRED_DURABLE_ROWS) or ()):
continue
# The stamp is provenance and text is not: a surface may have re-rendered the dict since the
# repair (gateway timestamps), and a later row can equal the merged text. So the run is
# resolved first, and a stamped dict that names none is still durable, never an unpersisted turn.
run = self._merged_user_run(conn, session_id, message, watermark)
if message.get(MERGED_DURABLE_ROWS) and not run:
return None
matches = [] if run else self._matching_active_ids(conn, session_id, message)
if len(matches) > 1 or (
message.get(_DB_PERSISTED_MARKER) and len(matches) != 1 and not run):
return None
proved.extend(matches or run)
merged_away.update(set(run[1:]) - set(message.get(ABSORBED_ROW_IDS) or ()))
return list(dict.fromkeys(proved)), merged_away
@staticmethod
def _uncounted_merged_rows(tail: list[dict[str, Any]]) -> int:
"""Durable rows behind the carried *tail* beyond one per dict, for the positional rewind.
The watermark path cannot name a merged dict's run, so it widens by the stamp, less the
rows the dict lists in ``_absorbed_row_ids``: those are already counted in ``tail_count``.
Rows the repair dropped behind a dict, or folded into an assistant turn, on a reload
without ids are in neither, so their count is added. A dict that holds a ``_row_id`` was
written back as a single row by an earlier compaction.
"""
from agent.conversation_compression_archive import ABSORBED_ROW_IDS, MERGED_DURABLE_ROWS, UNNAMED_DURABLE_ROWS
def behind(message: dict[str, Any]) -> int:
merged, unnamed = message.get(MERGED_DURABLE_ROWS), message.get(UNNAMED_DURABLE_ROWS)
unlisted = merged - 1 - len(message.get(ABSORBED_ROW_IDS) or ()) if type(merged) is int else 0
return max(0, unlisted) + (unnamed if type(unnamed) is int else 0)
return sum(
behind(message) for message in tail
if isinstance(message, dict) and not isinstance(message.get("_row_id"), int))
@staticmethod
def _tail_originals(covered_active: list[int], tail_count: int, merged_away: set[int]) -> list[int]:
"""Newest rows behind *tail_count* carried dicts; a dict that merged rows stands for each of them."""
width = int(tail_count)
while True:
window = covered_active[-width:]
need = int(tail_count) + sum(1 for row_id in window if row_id in merged_away)
if need <= width or width >= len(covered_active):
return window
width = need