From 11da577104bac4e9eb690bae94fbfdd5e8f7602d Mon Sep 17 00:00:00 2001 From: adarshh347 Date: Thu, 5 Nov 2026 11:05:40 +0530 Subject: [PATCH] Fix nested hint transcript retry stability --- reme/steps/evolve/auto_memory.py | 42 ++++++++----- .../unit/test_auto_memory_session_messages.py | 62 +++++++++++++++++-- 2 files changed, 84 insertions(+), 20 deletions(-) diff --git a/reme/steps/evolve/auto_memory.py b/reme/steps/evolve/auto_memory.py index 47ddcbe1..db1fd339 100644 --- a/reme/steps/evolve/auto_memory.py +++ b/reme/steps/evolve/auto_memory.py @@ -153,26 +153,38 @@ def _canonical_message_json(msg: Msg) -> str: return json.dumps(persisted, ensure_ascii=False, sort_keys=True, separators=(",", ":")) +def _stabilize_content_block(raw_block: dict, path: tuple[int, ...], message_timestamp: str | None) -> dict: + """Stabilize defaults generated while parsing a raw content block.""" + block = dict(raw_block) + if isinstance(block.get("hint"), list): + block["hint"] = [ + _stabilize_content_block(child, (*path, index), message_timestamp) if isinstance(child, dict) else child + for index, child in enumerate(block["hint"]) + ] + + if "id" not in block: + canonical = json.dumps(raw_block, ensure_ascii=False, sort_keys=True, separators=(",", ":"), default=str) + position = str(path[0]) if len(path) == 1 else ".".join(map(str, path)) + block["id"] = hashlib.sha256(f"{position}:{canonical}".encode("utf-8")).hexdigest() + if "created_at" not in block and message_timestamp: + block["created_at"] = message_timestamp + if block.get("type") == "hint" and "finished_at" not in block and message_timestamp: + block["finished_at"] = message_timestamp + return block + + def _stabilize_raw_content_ids(item: dict) -> dict: - """Give raw content blocks stable IDs when the caller did not supply one.""" + """Stabilize generated IDs and timestamps in raw (including nested hint) blocks.""" content = item.get("content") if not isinstance(content, list): return item - stable_content = [] - changed = False - for index, raw_block in enumerate(content): - if not isinstance(raw_block, dict): - stable_content.append(raw_block) - continue - block = dict(raw_block) - if "id" not in block: - canonical = json.dumps(block, ensure_ascii=False, sort_keys=True, separators=(",", ":"), default=str) - block["id"] = hashlib.sha256(f"{index}:{canonical}".encode("utf-8")).hexdigest() - if "created_at" not in block and item.get("created_at"): - block["created_at"] = item["created_at"] - stable_content.append(block) - changed = changed or block != raw_block + timestamp = item.get("created_at") + stable_content = [ + _stabilize_content_block(block, (index,), timestamp) if isinstance(block, dict) else block + for index, block in enumerate(content) + ] + changed = stable_content != content return {**item, "content": stable_content} if changed else item diff --git a/tests/unit/test_auto_memory_session_messages.py b/tests/unit/test_auto_memory_session_messages.py index 6f5e87c7..6d6244b6 100644 --- a/tests/unit/test_auto_memory_session_messages.py +++ b/tests/unit/test_auto_memory_session_messages.py @@ -264,14 +264,29 @@ async def test_metadata_bool_to_int_correction_is_persisted(step, append_new): @pytest.mark.asyncio -async def test_identical_raw_retry_with_generated_content_ids_does_not_rewrite(step, monkeypatch): - """Parsing the same raw messages twice should not create false block-ID changes.""" +@pytest.mark.parametrize( + "content", + [ + [{"type": "text", "text": "same raw payload"}], + [{"type": "hint", "hint": "notification"}], + [ + { + "type": "hint", + "hint": [{"type": "text", "text": "notification"}], + "finished_at": "2026-10-01T10:00:00", + }, + ], + ], + ids=["text", "hint", "nested hint"], +) +async def test_identical_raw_retry_with_generated_content_defaults_does_not_rewrite(step, monkeypatch, content): + """Parsing identical raw messages twice should not churn generated block defaults.""" raw = { "id": "m0", - "name": "Alice", - "role": "user", + "name": "Assistant", + "role": "assistant", "created_at": "2026-10-01T10:00:00", - "content": [{"type": "text", "text": "same raw payload"}], + "content": content, } first = step._build_messages([raw]) await step._save_session_messages(_SESSION, first) @@ -292,6 +307,43 @@ async def test_identical_raw_retry_with_generated_content_ids_does_not_rewrite(s assert step._session_path(_SESSION).read_bytes() == original +def test_raw_hint_conversion_preserves_explicit_nested_ids_and_timestamps(step): + """Caller-provided IDs and timestamps remain authoritative at every hint depth.""" + message = step._to_msg( + { + "id": "m0", + "name": "Alice", + "role": "assistant", + "created_at": "2026-10-01T10:00:00", + "content": [ + { + "type": "hint", + "id": "outer-id", + "created_at": "2026-10-01T10:00:01", + "finished_at": "2026-10-01T10:00:02", + "hint": [ + { + "type": "text", + "text": "notification", + "id": "nested-id", + "created_at": "2026-10-01T10:00:03", + }, + ], + }, + ], + }, + ) + + outer = message.content[0] + nested = outer.hint[0] + assert (outer.id, outer.created_at, outer.finished_at) == ( + "outer-id", + "2026-10-01T10:00:01", + "2026-10-01T10:00:02", + ) + assert (nested.id, nested.created_at) == ("nested-id", "2026-10-01T10:00:03") + + def test_raw_message_conversion_preserves_explicit_content_block_ids(step): """Caller-provided block IDs remain authoritative during normalization.""" message = step._to_msg(