Found by the pre-merge correctness and hermes-specific review of this stack: - A fold after a row whose one response repeated a provider id (one shared uid) appended the absorbed turn's uid in the wrong slot. The shared uid now fills each of the row's own occurrences before the fold appends (per_occurrence_tool_call_uids). A single response keeps one shared uid for a repeated id: every result carries it, so no call looks unanswered. - A digest-less re-flush of a restored fold (replay heal, adopt after a digest mismatch) let the stored pre-fold map overwrite the longer live list, so the later occurrence's results paired with nothing. A live list that starts with every stored occurrence is kept (stored uid or list). - A dict that lost its uid map (a clone) rewrote tool_call_uids to NULL. The row-addressed rewrite now fills stored uids for the calls the dict still names; a call it dropped keeps none. - append_messages_batch(chunk_rows=...) reset the pairing index per chunk, so a result in the chunk after its call was stored without tool_call_uid (branch/seed copies). The whole batch is paired before it is chunked. - _merge_assistant_into keeps its dict guard on the absorbed turn's map (a plugin-built dict with a non-dict map no longer raises).
542 lines
31 KiB
Python
542 lines
31 KiB
Python
"""``messages.message_uid``: a durable per-message id that survives every host copy path.
|
|
|
|
The physical ``id`` is re-issued by every copy (in-place compaction generation, rotation child,
|
|
concurrent-tail clone, ``replace_messages``), and ``_row_id`` is opt-in on restore. A consumer that
|
|
keys on messages across restarts and boundaries (a context engine plugin) therefore had nothing
|
|
stable to key on and re-identified rows by content and timestamp. ``message_uid`` is minted once at
|
|
the row's first insert, stamped on the caller's dict, restored unconditionally, copied by every
|
|
clone, kept by every re-insert of the same dict and left alone by row-addressed rewrites.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import copy
|
|
import json
|
|
import re
|
|
|
|
import pytest
|
|
|
|
from hermes_state import SessionDB
|
|
|
|
UID_RE = re.compile(r"^[0-9a-f]{32}$")
|
|
|
|
|
|
@pytest.fixture()
|
|
def db(tmp_path):
|
|
handle = SessionDB(db_path=tmp_path / "state.db")
|
|
try:
|
|
yield handle
|
|
finally:
|
|
handle.close()
|
|
|
|
|
|
def _rows(db, sid, *, active_only=True, include_session=False):
|
|
clause = " AND active = 1" if active_only else ""
|
|
cols = "id, session_id, role, content, message_uid, active, compacted"
|
|
return [dict(r) for r in db._conn.execute(
|
|
f"SELECT {cols} FROM messages WHERE session_id = ?{clause} ORDER BY id", (sid,)).fetchall()]
|
|
|
|
|
|
def _seed(db, sid, n=4):
|
|
db.create_session(sid, "cli", model="m")
|
|
for i in range(n):
|
|
db.append_message(session_id=sid, role="user" if i % 2 == 0 else "assistant", content=f"msg {i}")
|
|
return _rows(db, sid)
|
|
|
|
|
|
class TestMintAndRestore:
|
|
def test_insert_mints_a_uid_and_stamps_the_callers_dict(self, db):
|
|
db.create_session("s", "cli")
|
|
msgs = [{"role": "user", "content": "q"}, {"role": "assistant", "content": "a"}]
|
|
db.append_messages_batch("s", msgs)
|
|
stored = _rows(db, "s")
|
|
assert [r["message_uid"] for r in stored] == [m["message_uid"] for m in msgs]
|
|
assert all(UID_RE.match(r["message_uid"]) for r in stored)
|
|
|
|
def test_uid_is_per_occurrence_not_derived_from_content_or_time(self, db):
|
|
db.create_session("s", "cli")
|
|
twins = [{"role": "user", "content": "ok", "timestamp": 1_700_000_000.5},
|
|
{"role": "user", "content": "ok", "timestamp": 1_700_000_000.5}]
|
|
db.append_messages_batch("s", twins)
|
|
assert twins[0]["message_uid"] != twins[1]["message_uid"]
|
|
|
|
def test_restore_carries_the_uid_without_include_row_ids(self, db):
|
|
stored = _seed(db, "s")
|
|
# The ACP / gateway / compression-adoption shape: no ``include_row_ids``.
|
|
restored = db.get_messages_as_conversation("s", repair_alternation=True)
|
|
assert "_row_id" not in restored[0]
|
|
assert [m["message_uid"] for m in restored] == [r["message_uid"] for r in stored]
|
|
# And every other projection (display resume, lineage) agrees.
|
|
model_history, display_history = db.get_resume_conversations("s")
|
|
assert [m["message_uid"] for m in model_history] == [r["message_uid"] for r in stored]
|
|
assert [m["message_uid"] for m in display_history] == [r["message_uid"] for r in stored]
|
|
|
|
def test_legacy_rows_are_backfilled_in_bounded_slices_across_opens(self, tmp_path, monkeypatch):
|
|
"""A single full-table UPDATE held the write lock for minutes on a large store; each open now
|
|
mints at most one chunk past its time budget, keeps earlier uids, and converges."""
|
|
import hermes_state_schema
|
|
|
|
monkeypatch.setattr(hermes_state_schema, "_MESSAGE_UID_BACKFILL_CHUNK", 2)
|
|
monkeypatch.setattr(hermes_state_schema, "_MESSAGE_UID_BACKFILL_BUDGET_S", 0.0)
|
|
path = tmp_path / "legacy.db"
|
|
first = SessionDB(db_path=path)
|
|
try:
|
|
_seed(first, "s", n=5)
|
|
# Rows written by a build that predates the column: NULL uid, no backfill marker.
|
|
first._conn.execute("UPDATE messages SET message_uid = NULL")
|
|
first._conn.execute("DELETE FROM state_meta WHERE key = 'message_uid_backfill'")
|
|
first._conn.commit()
|
|
finally:
|
|
first.close()
|
|
seen = []
|
|
for _ in range(3):
|
|
handle = SessionDB(db_path=path)
|
|
try:
|
|
seen.append([r["message_uid"] for r in _rows(handle, "s")])
|
|
done = handle.get_meta("message_uid_backfill")
|
|
finally:
|
|
handle.close()
|
|
assert [sum(u is not None for u in uids) for uids in seen] == [2, 4, 5]
|
|
assert seen[1][:2] == seen[0][:2] and seen[2][:4] == seen[1][:4]
|
|
assert all(UID_RE.match(u) for u in seen[2]) and len(set(seen[2])) == 5
|
|
assert done == "1"
|
|
|
|
def test_a_row_inserted_without_a_uid_by_an_older_writer_gets_one(self, db):
|
|
"""The store mints for a build that predates the column (its INSERT binds no uid)."""
|
|
db.create_session("s", "cli", model="test/model")
|
|
db._conn.execute(
|
|
"INSERT INTO messages (session_id, role, content, timestamp) VALUES ('s', 'user', 'old writer', 1.0)"
|
|
)
|
|
db._conn.commit()
|
|
uid = db._conn.execute("SELECT message_uid FROM messages WHERE content = 'old writer'").fetchone()[0]
|
|
assert uid and UID_RE.match(uid)
|
|
assert db.get_messages_as_conversation("s")[0]["message_uid"] == uid
|
|
|
|
|
|
class TestCopyPathsKeepTheUid:
|
|
def test_in_place_compaction_keeps_uids_on_the_new_generation_and_the_tail_clone(self, db):
|
|
stored = _seed(db, "s", n=4)
|
|
watermark = stored[-1]["id"]
|
|
# Concurrent appends after the compressor captured its watermark.
|
|
db.append_message(session_id="s", role="user", content="late user")
|
|
db.append_message(session_id="s", role="assistant", content="late assistant")
|
|
late = _rows(db, "s")[4:]
|
|
restored = db.get_messages_as_conversation("s")
|
|
# The compressor's output: a summary the ENGINE already identified plus marker-swept COPIES of the
|
|
# kept tail (an engine may pre-mint its own rows' uids; the host writes them as given).
|
|
compacted = [{"role": "user", "content": "[CONTEXT COMPACTION] summary", "message_uid": "e" * 32},
|
|
copy.copy(restored[2]), copy.copy(restored[3])]
|
|
db.archive_and_compact("s", compacted, watermark=watermark)
|
|
active = _rows(db, "s")
|
|
assert [r["content"] for r in active] == [
|
|
"[CONTEXT COMPACTION] summary", "msg 2", "msg 3", "late user", "late assistant"]
|
|
# New generation: the copies keep the archived originals' uids; the clone keeps the late rows' uids.
|
|
assert [r["message_uid"] for r in active[1:3]] == [stored[2]["message_uid"], stored[3]["message_uid"]]
|
|
assert [r["message_uid"] for r in active[3:]] == [r["message_uid"] for r in late]
|
|
assert active[0]["message_uid"] == compacted[0]["message_uid"] == "e" * 32
|
|
# The physical ids DID change (that is the whole point), the archived originals keep theirs too.
|
|
assert {r["id"] for r in active}.isdisjoint({r["id"] for r in stored} | {r["id"] for r in late})
|
|
archived = [r for r in _rows(db, "s", active_only=False) if not r["active"]]
|
|
assert [r["message_uid"] for r in archived] == [r["message_uid"] for r in stored + late]
|
|
|
|
def test_rotation_child_keeps_uids_on_handoff_copies_and_the_foreign_tail_clone(self, db):
|
|
stored = _seed(db, "parent", n=2)
|
|
watermark = stored[-1]["id"]
|
|
db.append_message(session_id="parent", role="user", content="foreign append")
|
|
foreign = _rows(db, "parent")[2]
|
|
restored = db.get_messages_as_conversation("parent")
|
|
handoff = [{"role": "user", "content": "[CONTEXT COMPACTION] summary", "message_uid": "e" * 32},
|
|
copy.copy(restored[1])]
|
|
db.publish_compression_child(
|
|
parent_session_id="parent", child_session_id="child", source="cli", messages=handoff,
|
|
require_compression_lease=False, watermark=watermark, watermark_ceiling=foreign["id"])
|
|
child = _rows(db, "child")
|
|
assert [r["content"] for r in child] == ["[CONTEXT COMPACTION] summary", "msg 1", "foreign append"]
|
|
assert child[1]["message_uid"] == stored[1]["message_uid"]
|
|
assert child[2]["message_uid"] == foreign["message_uid"]
|
|
assert child[0]["message_uid"] == "e" * 32
|
|
# A restore of the child (what the resumed agent sees) carries the same uids.
|
|
assert [m["message_uid"] for m in db.get_messages_as_conversation("child")] == [
|
|
r["message_uid"] for r in child]
|
|
|
|
def test_replace_messages_keeps_uids_on_the_kept_prefix_and_the_reissued_rows(self, db):
|
|
stored = _seed(db, "s", n=4)
|
|
restored = db.get_messages_as_conversation("s")
|
|
# ACP non-owning persist / gateway rewrite: DELETE every active row and re-INSERT the history.
|
|
db.replace_messages("s", restored + [{"role": "user", "content": "new"}], active_only=True)
|
|
reissued = _rows(db, "s")
|
|
assert [r["message_uid"] for r in reissued[:4]] == [r["message_uid"] for r in stored]
|
|
assert reissued[4]["message_uid"] and reissued[4]["message_uid"] not in {r["message_uid"] for r in stored}
|
|
assert {r["id"] for r in reissued[:4]}.isdisjoint({r["id"] for r in stored})
|
|
# Rewind-style replace: the matched live prefix keeps its rows AND stamps their uids on the dicts.
|
|
prefix = [{"role": m["role"], "content": m["content"]} for m in restored[:2]]
|
|
db.replace_messages("s", prefix + [{"role": "user", "content": "edited"}], archive_dropped=True)
|
|
assert [m["message_uid"] for m in prefix] == [r["message_uid"] for r in reissued[:2]]
|
|
assert [r["message_uid"] for r in _rows(db, "s")[:2]] == [r["message_uid"] for r in reissued[:2]]
|
|
|
|
def test_row_addressed_rewrite_keeps_the_uid(self, db):
|
|
db.create_session("s", "cli")
|
|
msg = {"role": "user", "content": "api variant of the prompt"}
|
|
db.append_messages_batch("s", [msg])
|
|
before = _rows(db, "s")[0]
|
|
# The persist override / sanitizer rewrite: same dict, same row id and digest, new content.
|
|
msg["content"] = "clean prompt"
|
|
db.append_messages_batch("s", [msg])
|
|
after = _rows(db, "s")
|
|
assert len(after) == 1 and after[0]["id"] == before["id"]
|
|
assert after[0]["content"] == "clean prompt"
|
|
assert after[0]["message_uid"] == before["message_uid"] == msg["message_uid"]
|
|
|
|
def test_rewrite_adopts_the_stored_uid_onto_a_dict_that_lacks_one(self, db):
|
|
db.create_session("s", "cli")
|
|
msg = {"role": "user", "content": "prompt"}
|
|
db.append_messages_batch("s", [msg])
|
|
stored_uid = msg.pop("message_uid")
|
|
msg["content"] = "prompt (edited)"
|
|
db.append_messages_batch("s", [msg])
|
|
assert msg["message_uid"] == stored_uid
|
|
assert [r["message_uid"] for r in _rows(db, "s")] == [stored_uid]
|
|
|
|
def test_export_import_round_trip_keeps_the_uid(self, db, tmp_path):
|
|
stored = _seed(db, "s", n=3)
|
|
payload = db.export_session("s")
|
|
assert [m["message_uid"] for m in payload["messages"]] == [r["message_uid"] for r in stored]
|
|
other = SessionDB(db_path=tmp_path / "other.db")
|
|
try:
|
|
assert other.import_sessions([payload])["ok"]
|
|
assert [r["message_uid"] for r in _rows(other, "s")] == [r["message_uid"] for r in stored]
|
|
finally:
|
|
other.close()
|
|
|
|
|
|
def _absorbed(db, sid):
|
|
return [r[0] for r in db._conn.execute(
|
|
"SELECT absorbed_message_uids FROM messages WHERE session_id = ? AND active = 1 ORDER BY id", (sid,))]
|
|
|
|
|
|
class TestPersistedMergeWitness:
|
|
"""``_absorbed_message_uids`` (which rows a consecutive-user merge folded into the survivor) rides on the
|
|
survivor's row, so a consumer that restarts after the merge still knows the composite's constituents."""
|
|
|
|
def test_flushed_survivor_persists_the_absorbed_uids_and_restore_returns_them(self, db):
|
|
from agent.agent_runtime_helpers import _merge_consecutive_users
|
|
|
|
db.create_session("s", "cli")
|
|
dangling = {"role": "user", "content": "first, never answered"}
|
|
db.append_messages_batch("s", [dangling])
|
|
prompt = {"role": "user", "content": "second"}
|
|
db.append_messages_batch("s", [prompt])
|
|
_merge_consecutive_users([dangling, prompt])
|
|
assert dangling["_absorbed_message_uids"] == [prompt["message_uid"]]
|
|
# The survivor re-flushes as a row-addressed rewrite of its own row (same id, same uid).
|
|
db.append_messages_batch("s", [dangling])
|
|
rows = _rows(db, "s")
|
|
assert [r["content"] for r in rows] == ["first, never answered\n\nsecond", "second"]
|
|
assert rows[0]["message_uid"] == dangling["message_uid"]
|
|
assert _absorbed(db, "s") == ['["%s"]' % prompt["message_uid"], None]
|
|
restored = db.get_messages_as_conversation("s")
|
|
assert restored[0]["_absorbed_message_uids"] == [prompt["message_uid"]]
|
|
assert "_absorbed_message_uids" not in restored[1]
|
|
|
|
def test_tool_call_uids_are_minted_on_the_assistant_and_paired_onto_the_results(self, db):
|
|
db.create_session("s", "cli")
|
|
calls = [{"id": "call_1", "type": "function", "function": {"name": "t", "arguments": "{}"}},
|
|
{"id": "call_2", "type": "function", "function": {"name": "t", "arguments": "{}"}}]
|
|
assistant = {"role": "assistant", "content": "", "tool_calls": calls}
|
|
first = {"role": "tool", "content": "r1", "tool_call_id": "call_1", "tool_name": "t"}
|
|
db.append_messages_batch("s", [assistant, first])
|
|
uids = assistant["_tool_call_uids"]
|
|
assert set(uids) == {"call_1", "call_2"} and len({*uids.values()}) == 2
|
|
assert all(UID_RE.match(u) for u in uids.values())
|
|
assert first["_tool_call_uid"] == uids["call_1"]
|
|
# The provider-facing entries are untouched: the stored tool_calls JSON carries no uid.
|
|
stored_calls = db._conn.execute(
|
|
"SELECT tool_calls FROM messages WHERE session_id = ? AND role = 'assistant'", ("s",)).fetchone()[0]
|
|
assert "uid" not in stored_calls and [c["id"] for c in calls] == ["call_1", "call_2"]
|
|
# A result that lands in a LATER batch with no live list to pair it: stored NULL, derived on restore
|
|
# from the assistant row that named it (rows are read in id order).
|
|
second = {"role": "tool", "content": "r2", "tool_call_id": "call_2", "tool_name": "t"}
|
|
db.append_messages_batch("s", [second])
|
|
assert "_tool_call_uid" not in second
|
|
restored = db.get_messages_as_conversation("s")
|
|
assert restored[0]["_tool_call_uids"] == uids
|
|
assert [m["_tool_call_uid"] for m in restored[1:]] == [uids["call_1"], uids["call_2"]]
|
|
|
|
def test_tool_call_uids_survive_compaction_copies_rotation_and_import(self, db, tmp_path):
|
|
db.create_session("s", "cli")
|
|
assistant = {"role": "assistant", "content": "",
|
|
"tool_calls": [{"id": "call_1", "type": "function", "function": {"name": "t", "arguments": "{}"}}]}
|
|
result = {"role": "tool", "content": "r", "tool_call_id": "call_1", "tool_name": "t"}
|
|
db.append_messages_batch("s", [{"role": "user", "content": "q"}, assistant, result,
|
|
{"role": "assistant", "content": "done"}])
|
|
uid = assistant["_tool_call_uids"]["call_1"]
|
|
restored = db.get_messages_as_conversation("s")
|
|
db.archive_and_compact("s", [{"role": "user", "content": "[CONTEXT COMPACTION] s"},
|
|
copy.copy(restored[1]), copy.copy(restored[2]), copy.copy(restored[3])])
|
|
after = db.get_messages_as_conversation("s")
|
|
assert after[1]["_tool_call_uids"] == {"call_1": uid} and after[2]["_tool_call_uid"] == uid
|
|
db.publish_compression_child(
|
|
parent_session_id="s", child_session_id="child", source="cli",
|
|
messages=[copy.copy(m) for m in after], require_compression_lease=False)
|
|
child = db.get_messages_as_conversation("child")
|
|
assert child[1]["_tool_call_uids"] == {"call_1": uid} and child[2]["_tool_call_uid"] == uid
|
|
other = SessionDB(db_path=tmp_path / "other.db")
|
|
try:
|
|
assert other.import_sessions([db.export_session("child")])["ok"]
|
|
imported = other.get_messages_as_conversation("child")
|
|
assert imported[1]["_tool_call_uids"] == {"call_1": uid} and imported[2]["_tool_call_uid"] == uid
|
|
finally:
|
|
other.close()
|
|
|
|
def test_a_reused_provider_id_pairs_with_the_nearest_call_only(self, db):
|
|
"""Provider ids repeat: on restore a result binds to the NEAREST preceding call; a legacy call row
|
|
without a map (an older writer's) leaves its result unpaired instead of inheriting an older uid, and
|
|
nothing pairs across a user turn."""
|
|
sid = "s"
|
|
db.create_session(sid, "cli", model="m")
|
|
call = [{"id": "call_x", "type": "function", "function": {"name": "t", "arguments": "{}"}}]
|
|
db.append_message(session_id=sid, role="user", content="q1")
|
|
db.append_message(session_id=sid, role="assistant", content="", tool_calls=call)
|
|
db.append_message(session_id=sid, role="tool", content="r1", tool_call_id="call_x", tool_name="t")
|
|
db.append_message(session_id=sid, role="user", content="q2")
|
|
# An older writer's rows: same provider id, no uid map on the call, no uid on the result.
|
|
db._conn.execute(
|
|
"INSERT INTO messages (session_id, role, content, tool_calls, timestamp) VALUES (?, 'assistant', '', ?, 5.0)",
|
|
(sid, json.dumps(call)))
|
|
db._conn.execute(
|
|
"INSERT INTO messages (session_id, role, content, tool_call_id, tool_name, timestamp) "
|
|
"VALUES (?, 'tool', 'r2', 'call_x', 't', 6.0)", (sid,))
|
|
db._conn.commit()
|
|
db.append_message(session_id=sid, role="user", content="q3")
|
|
db.append_message(session_id=sid, role="assistant", content="", tool_calls=call)
|
|
db.append_message(session_id=sid, role="tool", content="r3", tool_call_id="call_x", tool_name="t")
|
|
db.append_message(session_id=sid, role="user", content="q4")
|
|
db.append_message(session_id=sid, role="tool", content="stray", tool_call_id="call_x", tool_name="t")
|
|
|
|
restored = db.get_messages_as_conversation(sid, repair_alternation=False)
|
|
by_content = {m["content"]: m for m in restored}
|
|
uid1 = by_content["r1"]["_tool_call_uid"]
|
|
uid3 = by_content["r3"]["_tool_call_uid"]
|
|
assert UID_RE.match(uid1) and UID_RE.match(uid3) and uid1 != uid3
|
|
assert "_tool_call_uid" not in by_content["r2"]
|
|
assert "_tool_call_uid" not in by_content["stray"]
|
|
# The same rule at batch insert: a result after a user turn pairs with nothing.
|
|
assert db._conn.execute(
|
|
"SELECT tool_call_uid FROM messages WHERE content = 'stray'").fetchone()[0] is None
|
|
|
|
def test_a_composite_rewind_reports_the_replacement_rows_uid(self, db):
|
|
from agent.context_compressor import HISTORICAL_TASK_HEADING, SUMMARY_PREFIX, _SUMMARY_END_MARKER
|
|
|
|
sid = "s"
|
|
db.create_session(sid, "cli", model="m")
|
|
db.append_message(session_id=sid, role="user", content="older ask")
|
|
db.append_message(session_id=sid, role="assistant", content="done")
|
|
carrier = f"{SUMMARY_PREFIX}\n{HISTORICAL_TASK_HEADING}\nold task\n\n{_SUMMARY_END_MARKER}\n\nREAL ASK"
|
|
target_id = db.append_message(session_id=sid, role="user", content=carrier)
|
|
db.append_message(session_id=sid, role="assistant", content="failed")
|
|
|
|
result = db.rewind_to_message(sid, target_id, preserve_compaction_handoff=True,
|
|
expected_target_content="REAL ASK")
|
|
|
|
head = db.get_messages_as_conversation(sid, include_row_ids=True)[-1]
|
|
assert head["_row_id"] == result["replacement_message_id"]
|
|
assert result["replacement_message_uid"] == head["message_uid"]
|
|
assert UID_RE.match(head["message_uid"])
|
|
|
|
def test_a_live_undo_of_a_composite_turn_installs_the_replacement_rows_identity(self, db):
|
|
"""The CLI/TUI/gateway undo path (``rewind_user_turn``) installs the hidden scaffold as the live head:
|
|
it must carry the replacement row's uid, not wait for a restart to learn it."""
|
|
from agent.context_compressor import HISTORICAL_TASK_HEADING, SUMMARY_PREFIX, _SUMMARY_END_MARKER
|
|
|
|
sid = "s"
|
|
db.create_session(sid, "cli", model="m")
|
|
db.append_message(session_id=sid, role="user", content="q1")
|
|
db.append_message(session_id=sid, role="assistant", content="a1")
|
|
carrier = f"{SUMMARY_PREFIX}\n{HISTORICAL_TASK_HEADING}\nold task\n\n{_SUMMARY_END_MARKER}\n\nREAL ASK"
|
|
db.append_message(session_id=sid, role="user", content=carrier)
|
|
db.append_message(session_id=sid, role="assistant", content="failed")
|
|
warm = db.get_resume_conversations(sid)[0]
|
|
|
|
outcome = db.rewind_user_turn(sid, 1, warm_history=warm)
|
|
|
|
head = db.get_messages_as_conversation(sid, include_row_ids=True)[-1]
|
|
assert head["display_kind"] == "hidden" and SUMMARY_PREFIX in head["content"]
|
|
assert outcome.prefix[-1]["_row_id"] == head["_row_id"]
|
|
assert outcome.prefix[-1]["message_uid"] == head["message_uid"]
|
|
assert UID_RE.match(head["message_uid"])
|
|
|
|
def test_foreign_history_import_mints_uids_and_keeps_supplied_ones(self, db):
|
|
origin = {"tool": "other-agent", "path": "/x/y.jsonl"}
|
|
supplied = "f" * 32
|
|
result = db.import_foreign_history(
|
|
origin, [{"role": "user", "content": "hi"}, {"role": "assistant", "content": "hello", "message_uid": supplied}],
|
|
title="imported", cwd="/tmp", profile="default")
|
|
rows = _rows(db, result["session_id"])
|
|
assert [r["content"] for r in rows] == ["hi", "hello"]
|
|
assert UID_RE.match(rows[0]["message_uid"]) and rows[1]["message_uid"] == supplied
|
|
assert [m["message_uid"] for m in db.get_messages_as_conversation(result["session_id"])] == [
|
|
rows[0]["message_uid"], supplied]
|
|
|
|
def test_a_restore_time_merge_records_the_witness(self, db):
|
|
"""Two stored user rows in a row (a turn that never got an answer) are merged while restoring with
|
|
``repair_alternation=True``; the survivor names the absorbed row by uid, on every restore shape."""
|
|
db.create_session("s", "cli", model="m")
|
|
db.append_message(session_id="s", role="user", content="first ask")
|
|
db.append_message(session_id="s", role="user", content="second ask")
|
|
db.append_message(session_id="s", role="assistant", content="answer")
|
|
first, second = [r["message_uid"] for r in _rows(db, "s")][:2]
|
|
for kwargs in ({}, {"include_row_ids": True}):
|
|
restored = db.get_messages_as_conversation("s", repair_alternation=True, **kwargs)
|
|
assert [m["role"] for m in restored] == ["user", "assistant"]
|
|
assert restored[0]["content"] == "first ask\n\nsecond ask"
|
|
assert restored[0]["message_uid"] == first
|
|
assert restored[0]["_absorbed_message_uids"] == [second]
|
|
assert db.get_resume_conversations("s")[0][0]["_absorbed_message_uids"] == [second]
|
|
|
|
def test_a_row_rewrite_keeps_the_tool_call_uids(self, db):
|
|
db.create_session("s", "cli")
|
|
assistant = {"role": "assistant", "content": "partial",
|
|
"tool_calls": [{"id": "call_1", "type": "function", "function": {"name": "t", "arguments": "{}"}}]}
|
|
db.append_messages_batch("s", [assistant])
|
|
uids = assistant.pop("_tool_call_uids") # a restored/cloned dict that lost its map: the row keeps it
|
|
assistant["content"] = "partial, then filled by the sanitizer"
|
|
db.append_messages_batch("s", [assistant])
|
|
rows = _rows(db, "s")
|
|
assert len(rows) == 1 and rows[0]["content"] == "partial, then filled by the sanitizer"
|
|
assert db.get_messages_as_conversation("s")[0]["_tool_call_uids"] == uids
|
|
|
|
def test_compaction_copy_and_export_import_keep_the_witness(self, db, tmp_path):
|
|
db.create_session("s", "cli")
|
|
composite = {"role": "user", "content": "a\n\nb", "message_uid": "a" * 32,
|
|
"_absorbed_message_uids": ["b" * 32]}
|
|
db.append_messages_batch("s", [composite, {"role": "assistant", "content": "ok"}])
|
|
restored = db.get_messages_as_conversation("s")
|
|
db.archive_and_compact("s", [{"role": "user", "content": "[CONTEXT COMPACTION] s"},
|
|
copy.copy(restored[0]), copy.copy(restored[1])])
|
|
assert _absorbed(db, "s") == [None, '["%s"]' % ("b" * 32), None]
|
|
payload = db.export_session("s")
|
|
other = SessionDB(db_path=tmp_path / "other.db")
|
|
try:
|
|
assert other.import_sessions([payload])["ok"]
|
|
assert other.get_messages_as_conversation("s")[1]["_absorbed_message_uids"] == ["b" * 32]
|
|
finally:
|
|
other.close()
|
|
|
|
|
|
def test_a_branch_copy_writes_the_uids_its_live_history_keeps(tmp_path):
|
|
"""The branch child keeps using the copied live dicts; rows minted with other uids would restore a
|
|
different identity than the running agent saw. A dict without a uid gets one on both sides."""
|
|
from tui_gateway import server
|
|
|
|
db = SessionDB(db_path=tmp_path / "state.db")
|
|
try:
|
|
db.create_session("parent", source="desktop", model="m")
|
|
history = [{"role": "user", "content": "hello", "message_uid": "a" * 32}, {"role": "assistant", "content": "hi"}]
|
|
server._persist_branch(db, "child", "parent", "Branch", history, source="desktop", cwd=str(tmp_path),
|
|
profile_name="default", model="m")
|
|
assert [r["message_uid"] for r in _rows(db, "child")] == [m.get("message_uid") for m in history]
|
|
assert history[0]["message_uid"] == "a" * 32 and UID_RE.match(history[1]["message_uid"])
|
|
finally:
|
|
db.close()
|
|
|
|
|
|
def _call(cid):
|
|
return {"id": cid, "type": "function", "function": {"name": "t", "arguments": "{}"}}
|
|
|
|
|
|
def test_a_result_pairs_with_the_assistant_that_named_it_not_the_nearest_one(db):
|
|
"""Restore (results without a stored uid) and the flush walk both skip a nearer assistant that never
|
|
named the result's call, and pair with the one that did."""
|
|
from agent.message_metadata import tool_call_uid_from_history
|
|
|
|
db.create_session("s", "cli")
|
|
msgs = [{"role": "user", "content": "q"},
|
|
{"role": "assistant", "content": "", "tool_calls": [_call("call_1")]},
|
|
{"role": "assistant", "content": "", "tool_calls": [_call("call_2")]},
|
|
{"role": "tool", "content": "r2", "tool_call_id": "call_2"},
|
|
{"role": "tool", "content": "r1", "tool_call_id": "call_1"}]
|
|
db.append_messages_batch("s", msgs)
|
|
first, second = msgs[1]["_tool_call_uids"]["call_1"], msgs[2]["_tool_call_uids"]["call_2"]
|
|
db._conn.execute("UPDATE messages SET tool_call_uid = NULL") # results written by an older build
|
|
db._conn.commit()
|
|
restored = [m for m in db.get_messages_as_conversation("s", repair_alternation=False) if m["role"] == "tool"]
|
|
assert [m.get("_tool_call_uid") for m in restored] == [second, first]
|
|
live = [{k: v for k, v in m.items() if k != "_tool_call_uid"} for m in msgs]
|
|
owners: dict = {}
|
|
assert [tool_call_uid_from_history(live, i, owners) for i in (3, 4)] == [second, first]
|
|
|
|
|
|
def test_one_response_repeating_a_provider_id_shares_one_uid_its_results_all_carry(db):
|
|
"""Two calls sharing a provider id in ONE response share one uid: both results carry it, so neither call
|
|
looks unanswered to a context engine."""
|
|
db.create_session("s", "cli")
|
|
call = {"role": "assistant", "content": "", "tool_calls": [_call("call_x"), _call("call_x")]}
|
|
results = [{"role": "tool", "content": c, "tool_call_id": "call_x", "tool_name": "t"} for c in "ab"]
|
|
db.append_messages_batch("s", [{"role": "user", "content": "q"}, call, *results])
|
|
shared = call["_tool_call_uids"]["call_x"]
|
|
assert isinstance(shared, str) and [r["_tool_call_uid"] for r in results] == [shared, shared]
|
|
|
|
|
|
def test_a_fold_after_a_shared_uid_row_keeps_each_call_aligned_with_its_uid():
|
|
"""The shared uid fills each of the row's own slots before the absorbed turn's occurrence is appended, so
|
|
the absorbed call (third) keeps its own uid instead of sliding onto the second."""
|
|
from agent.agent_runtime_helpers import _merge_consecutive_assistants
|
|
from agent.message_metadata import index_tool_call_uids, resolve_tool_call_uid
|
|
|
|
shared, own = "1" * 32, "2" * 32
|
|
first = {"role": "assistant", "content": "", "tool_calls": [_call("call_x"), _call("call_x")],
|
|
"_tool_call_uids": {"call_x": shared}}
|
|
second = {"role": "assistant", "content": "", "tool_calls": [_call("call_x")], "_tool_call_uids": {"call_x": own}}
|
|
_merge_consecutive_assistants([first, second])
|
|
assert first["_tool_call_uids"] == {"call_x": [shared, shared, own]}
|
|
index: dict = {}
|
|
index_tool_call_uids(index, first)
|
|
assert resolve_tool_call_uid(index, "call_x") == own
|
|
|
|
|
|
def test_a_rewrite_keeps_a_fold_that_extends_a_stored_occurrence_list(db):
|
|
db.create_session("s", "cli")
|
|
a = {"role": "assistant", "content": "a", "tool_calls": [_call("call_0"), _call("call_0")],
|
|
"_tool_call_uids": {"call_0": ["1" * 32, "2" * 32]}}
|
|
b = {"role": "assistant", "content": "b", "tool_calls": [_call("call_0")], "_tool_call_uids": {"call_0": "3" * 32}}
|
|
db.append_messages_batch("s", [{"role": "user", "content": "q"}, a, b])
|
|
survivor = db.get_messages_as_conversation("s", repair_alternation=True, include_row_ids=True)[1]
|
|
survivor.pop("_db_row_snapshot", None)
|
|
db.append_messages_batch("s", [survivor])
|
|
assert survivor["_tool_call_uids"] == {"call_0": ["1" * 32, "2" * 32, "3" * 32]}
|
|
|
|
|
|
def test_a_rewrite_that_drops_a_call_does_not_write_its_uid_back(db):
|
|
db.create_session("s", "cli")
|
|
assistant = {"role": "assistant", "content": "x", "tool_calls": [_call("c1"), _call("c2")]}
|
|
db.append_messages_batch("s", [assistant])
|
|
assistant["tool_calls"] = [_call("c1")]
|
|
assistant["_tool_call_uids"] = {"c1": assistant["_tool_call_uids"]["c1"]}
|
|
db.append_messages_batch("s", [assistant])
|
|
stored = db._conn.execute("SELECT tool_call_uids FROM messages").fetchone()[0]
|
|
assert sorted(json.loads(stored)) == ["c1"]
|
|
|
|
|
|
def test_a_rewrite_keeps_a_folds_per_occurrence_list_over_the_stored_single_uid(db):
|
|
"""Restore folds two stored turns that reuse an id; re-flushing the survivor must not let the stored
|
|
pre-fold map collapse the list back to one uid (the later occurrence's results would pair with nothing)."""
|
|
db.create_session("s", "cli")
|
|
a = {"role": "assistant", "content": "a", "tool_calls": [_call("call_0")]}
|
|
b = {"role": "assistant", "content": "b", "tool_calls": [_call("call_0")]}
|
|
db.append_messages_batch("s", [{"role": "user", "content": "q"}, a, b])
|
|
occurrences = [a["_tool_call_uids"]["call_0"], b["_tool_call_uids"]["call_0"]]
|
|
survivor = db.get_messages_as_conversation("s", repair_alternation=True, include_row_ids=True)[1]
|
|
survivor.pop("_db_row_snapshot", None) # the digest-less re-flush path (replay heal, adopt mismatch)
|
|
assert survivor["_tool_call_uids"] == {"call_0": occurrences}
|
|
db.append_messages_batch("s", [survivor])
|
|
assert survivor["_tool_call_uids"] == {"call_0": occurrences}
|
|
|
|
|
|
def test_a_chunked_copy_pairs_a_result_with_a_call_in_the_previous_chunk(db):
|
|
db.create_session("s", "cli")
|
|
call = {"role": "assistant", "content": "", "tool_calls": [_call("call_1")]}
|
|
result = {"role": "tool", "content": "r", "tool_call_id": "call_1", "tool_name": "t"}
|
|
db.append_messages_batch("s", [{"role": "user", "content": "q"}, call, result], chunk_rows=2)
|
|
stored = db._conn.execute("SELECT tool_call_uid FROM messages WHERE role = 'tool'").fetchone()[0]
|
|
assert stored == call["_tool_call_uids"]["call_1"]
|