A legacy (no-digest) dict over a non-blank assistant row adopted the whole decoded DB row: tool_calls / reasoning* / codex_* were overwritten with the stored JSON (which still holds the escaped lone surrogate the sanitizer just fixed, re-injecting it into the provider payload) and live-only fields were popped. Resumed dicts (_rows_to_conversation stamps _row_id without a digest) and compaction clones hit this path. Adopt content only, as before this stack, via a content-only canonical handled like the metadata-only one. _insert_message_rows dropped a clone's parent digest but only the flush path restamped it, so clones made by archive_and_compact / replace / rotation handoff / import reached the legacy path and the first live edit after a clone was not persisted. Stamp the stored-row digest inside _insert_message_rows (one batched SELECT, cold paths only; the flush path statement count is unchanged) and drop the duplicate call in append_messages_batch. Define the _db_row_snapshot / _canonical_row keys once in agent/message_metadata.py and import them everywhere instead of repeating the literals.
50 lines
1.8 KiB
Python
50 lines
1.8 KiB
Python
"""Internal metadata attached to durable conversation messages."""
|
|
|
|
from __future__ import annotations
|
|
|
|
from time import time as wall_time
|
|
from typing import Any, MutableMapping, Optional, TypeVar
|
|
|
|
|
|
# These fields describe Hermes' durable record and timeline display, not
|
|
# provider-visible message content. The request builder strips them from every
|
|
# outgoing copy and the token estimator ignores them: one set, so an estimate
|
|
# never prices bytes the provider never receives (an edit's inline_diff in
|
|
# display_metadata is ~9KB and would trigger premature compaction).
|
|
# Transcript-repair bookkeeping riding on batch rows / live dicts (agent/transcript_repair.py): the
|
|
# stored-row CAS digest and the durable row adopted onto the live dict. Never transcript payload.
|
|
DB_ROW_SNAPSHOT = "_db_row_snapshot"
|
|
CANONICAL_ROW = "_canonical_row"
|
|
REPAIR_BOOKKEEPING_FIELDS = frozenset({DB_ROW_SNAPSHOT, CANONICAL_ROW})
|
|
PERSISTENCE_ONLY_MESSAGE_FIELDS = frozenset(
|
|
{"timestamp", "display_kind", "display_metadata", "_row_id"}
|
|
) | REPAIR_BOOKKEEPING_FIELDS
|
|
|
|
_Message = TypeVar("_Message", bound=MutableMapping[str, Any])
|
|
|
|
|
|
def stamp_message_timestamp(
|
|
message: _Message,
|
|
*,
|
|
timestamp: Optional[float] = None,
|
|
) -> _Message:
|
|
"""Attach a creation timestamp without replacing source-provided time.
|
|
|
|
Gateway adapters can supply the platform event time; all other callers use
|
|
the local wall clock. Returns the same mapping for use at append sites.
|
|
"""
|
|
if message.get("timestamp") is None:
|
|
message["timestamp"] = wall_time() if timestamp is None else timestamp
|
|
return message
|
|
|
|
|
|
def append_message(
|
|
messages: list[Any],
|
|
message: _Message,
|
|
*,
|
|
timestamp: Optional[float] = None,
|
|
) -> _Message:
|
|
"""Stamp and append one live transcript message."""
|
|
messages.append(stamp_message_timestamp(message, timestamp=timestamp))
|
|
return message
|