From da7aa88b8ee1e3039de8cef79bc5d109af543dca Mon Sep 17 00:00:00 2001 From: Joseph Doherty Date: Sun, 26 Apr 2026 22:24:57 -0400 Subject: [PATCH] refactor: unified record_turn_memory API with you_present kwarg (T84) Extends record_turn_memory_for_present with a you_present: bool = True kwarg so a single entry-point covers both you-scenes (witness_you=1) and meanwhile scenes (witness_you=0). Validates that meanwhile callers provide a guest_bot_id. record_meanwhile_memory becomes a thin backward-compat wrapper that delegates with you_present=False, preserving the call site in chat/web/meanwhile.py without churn. --- chat/services/memory_write.py | 75 ++++++++++++++--------------- tests/test_memory_write.py | 88 +++++++++++++++++++++++++++++++++++ 2 files changed, 122 insertions(+), 41 deletions(-) diff --git a/chat/services/memory_write.py b/chat/services/memory_write.py index 6fc6ecf..12eed5d 100644 --- a/chat/services/memory_write.py +++ b/chat/services/memory_write.py @@ -134,17 +134,34 @@ def record_turn_memory_for_present( chat_clock_at: str | None = None, source: str = "direct", significance: int = 1, + you_present: bool = True, ) -> dict[str, tuple[int, int | None]]: - """Write a ``memory_written`` event for each present bot witness. + """Single entry-point for per-turn memory writes (T84). - Host is always written. Guest is written iff ``guest_bot_id is not - None``. Witness flags are ``[you=1, host=1, guest=1]`` when a guest - is present, ``[you=1, host=1, guest=0]`` otherwise. + Writes one ``memory_written`` event per present bot witness. Host is + always written. Guest is written iff ``guest_bot_id is not None``. + + Witness flags depend on ``you_present``: + + - ``you_present=True`` (default — Phase 1/2/3 you-scenes): the user + is a witness. Mask is ``[you=1, host=1, guest=1]`` when a guest is + present, ``[you=1, host=1, guest=0]`` otherwise. + - ``you_present=False`` (Phase 3 meanwhile scenes): the user is + absent. Mask is ``[you=0, host=1, guest=1]`` for both bots. Both + ``host_bot_id`` and ``guest_bot_id`` are required — a meanwhile + scene by definition has both bots, so passing ``guest_bot_id=None`` + with ``you_present=False`` is a programming error and raises + :class:`ValueError`. Returns a mapping ``{bot_id: (event_id, memory_id)}`` so callers can look up the freshly-projected memory id per owner without re-querying the database. """ + if not you_present and guest_bot_id is None: + raise ValueError("you_present=False requires guest_bot_id") + + witness_you = 1 if you_present else 0 + witness_host = 1 witness_guest = 1 if guest_bot_id is not None else 0 result: dict[str, tuple[int, int | None]] = {} @@ -153,8 +170,8 @@ def record_turn_memory_for_present( owner_id=host_bot_id, chat_id=chat_id, narrative_text=narrative_text, - witness_you=1, - witness_host=1, + witness_you=witness_you, + witness_host=witness_host, witness_guest=witness_guest, scene_id=scene_id, chat_clock_at=chat_clock_at, @@ -167,8 +184,8 @@ def record_turn_memory_for_present( owner_id=guest_bot_id, chat_id=chat_id, narrative_text=narrative_text, - witness_you=1, - witness_host=1, + witness_you=witness_you, + witness_host=witness_host, witness_guest=1, scene_id=scene_id, chat_clock_at=chat_clock_at, @@ -190,46 +207,22 @@ def record_meanwhile_memory( source: str = "direct", significance: int = 1, ) -> dict[str, tuple[int, int | None]]: - """Write per-POV ``memory_written`` events for a meanwhile turn (T64). + """Backward-compat thin wrapper for meanwhile memory writes (T64, T84). - A meanwhile scene runs entirely between host + guest, with "you" - absent. Both bots are present witnesses, so each one gets a row with - witness flags ``[you=0, host=1, guest=1]`` — different from the - normal-turn ``record_turn_memory_for_present`` shape, which assumes - the user is always a witness (``witness_you=1``). - - The ``guest_bot_id`` is required (a meanwhile scene by definition - has both bots) — callers passing ``None`` is a programming error. - - Returns ``{bot_id: (event_id, memory_id)}`` mirroring - :func:`record_turn_memory_for_present` so downstream queues - (significance scoring) can pull memory ids without re-querying. + Equivalent to calling :func:`record_turn_memory_for_present` with + ``you_present=False``. Kept so existing call sites in + :mod:`chat.web.meanwhile` continue to work without churn. New code + should prefer the unified entry-point directly. """ - result: dict[str, tuple[int, int | None]] = {} - result[host_bot_id] = _write_one_memory( + return record_turn_memory_for_present( conn, - owner_id=host_bot_id, chat_id=chat_id, + host_bot_id=host_bot_id, + guest_bot_id=guest_bot_id, narrative_text=narrative_text, - witness_you=0, - witness_host=1, - witness_guest=1, scene_id=scene_id, chat_clock_at=chat_clock_at, source=source, significance=significance, + you_present=False, ) - result[guest_bot_id] = _write_one_memory( - conn, - owner_id=guest_bot_id, - chat_id=chat_id, - narrative_text=narrative_text, - witness_you=0, - witness_host=1, - witness_guest=1, - scene_id=scene_id, - chat_clock_at=chat_clock_at, - source=source, - significance=significance, - ) - return result diff --git a/tests/test_memory_write.py b/tests/test_memory_write.py index 00243b0..77132ae 100644 --- a/tests/test_memory_write.py +++ b/tests/test_memory_write.py @@ -444,3 +444,91 @@ def test_record_for_present_dict_keys_match(tmp_path): narrative_text="Both bots witness this.", ) assert set(result_with_guest.keys()) == {"bot_a", "bot_b"} + + +# --------------------------------------------------------------------------- +# T84: unified record_turn_memory_for_present API with you_present kwarg. +# --------------------------------------------------------------------------- + + +def test_record_turn_memory_you_present_false_writes_meanwhile_witness_mask(tmp_path): + """When ``you_present=False`` the witness mask should be + ``[you=0, host=1, guest=1]`` for both bots — the meanwhile shape.""" + db = tmp_path / "t.db" + apply_migrations(db) + _seed_two_bots(db) + with open_db(db) as conn: + result = record_turn_memory_for_present( + conn, + chat_id="chat_ab", + host_bot_id="bot_a", + guest_bot_id="bot_b", + narrative_text="BotA and BotB confer privately.", + scene_id=None, + chat_clock_at="2026-04-26T20:00:00+00:00", + you_present=False, + ) + + assert set(result.keys()) == {"bot_a", "bot_b"} + + rows = conn.execute( + "SELECT owner_id, witness_you, witness_host, witness_guest " + "FROM memories ORDER BY owner_id" + ).fetchall() + assert len(rows) == 2 + for _owner, w_you, w_host, w_guest in rows: + assert w_you == 0 + assert w_host == 1 + assert w_guest == 1 + + # Two memory_written events were appended. + cur = conn.execute( + "SELECT COUNT(*) FROM event_log WHERE kind = 'memory_written'" + ) + assert cur.fetchone()[0] == 2 + + +def test_record_turn_memory_you_present_true_default_writes_normal_witness_mask(tmp_path): + """Default ``you_present=True`` preserves Phase 2 behaviour: + ``witness_you=1`` for the host POV row.""" + db = tmp_path / "t.db" + apply_migrations(db) + _seed_minimal(db) + with open_db(db) as conn: + # No explicit you_present arg — should default to True. + result = record_turn_memory_for_present( + conn, + chat_id="chat_bot_a", + host_bot_id="bot_a", + guest_bot_id=None, + narrative_text="BotA hums to herself.", + ) + assert set(result.keys()) == {"bot_a"} + + row = conn.execute( + "SELECT witness_you, witness_host, witness_guest " + "FROM memories WHERE owner_id = 'bot_a'" + ).fetchone() + assert row is not None + w_you, w_host, w_guest = row + assert w_you == 1 + assert w_host == 1 + assert w_guest == 0 + + +def test_record_turn_memory_you_present_false_requires_guest(tmp_path): + """Calling with ``you_present=False`` and no ``guest_bot_id`` is a + programming error — meanwhile scenes always have both bots.""" + db = tmp_path / "t.db" + apply_migrations(db) + _seed_minimal(db) + with open_db(db) as conn: + with pytest.raises(ValueError, match="you_present=False requires guest_bot_id"): + record_turn_memory_for_present( + conn, + chat_id="chat_bot_a", + host_bot_id="bot_a", + guest_bot_id=None, + narrative_text="invalid", + you_present=False, + )