feat: meanwhile summary digest surfaces to next you-scene (T65)

This commit is contained in:
Joseph Doherty
2026-04-26 20:59:35 -04:00
parent c9d58b8229
commit a781732ee6
3 changed files with 502 additions and 10 deletions
+323
View File
@@ -1095,3 +1095,326 @@ async def test_thread_detection_emits_events(tmp_path, monkeypatch):
open_threads = list_open_threads(conn, "chat_bot_a")
assert len(open_threads) == 1
assert open_threads[0]["title"] == "Test thread"
# ---------------------------------------------------------------------------
# T65: meanwhile summary digest emitted on meanwhile-scene close, surfaced in
# the next you-scene's prompt as a SHOULD-tier "Meanwhile while you were away:"
# block, then consumed so it never re-renders.
# ---------------------------------------------------------------------------
def _seed_meanwhile_scene(conn) -> None:
"""Seed a parent you-scene + a meanwhile child scene with one assistant
turn so apply_scene_close_summary has dialogue to summarize.
The meanwhile scene id is 2 (parent is scene 1). The meanwhile dialogue
is appended via assistant_turn events under chat_bot_a; the
_read_recent_dialogue helper picks them up by chat_id.
"""
import chat.state.meanwhile # noqa: F401 -- register handlers
append_event(conn, kind="bot_authored", payload=_bot_payload("bot_a", "BotA"))
append_event(conn, kind="bot_authored", payload=_bot_payload("bot_b", "BotB"))
append_event(
conn,
kind="you_authored",
payload={"name": "Me", "pronouns": "they/them", "persona": "engineer"},
)
append_event(
conn,
kind="chat_created",
payload={
"id": "chat_bot_a",
"host_bot_id": "bot_a",
"guest_bot_id": "bot_b",
"initial_time": "2026-04-26T20:00:00+00:00",
"narrative_anchor": "Day 1",
"weather": "",
},
)
append_event(
conn,
kind="container_created",
payload={
"chat_id": "chat_bot_a",
"name": "office",
"type": "workplace",
"properties": {},
},
)
# Parent you-scene (scene_id=1).
append_event(
conn,
kind="scene_opened",
payload={
"chat_id": "chat_bot_a",
"container_id": 1,
"started_at": "2026-04-26T20:00:00+00:00",
"participants": ["you", "bot_a", "bot_b"],
},
)
# Meanwhile child scene (scene_id=2) — bot_a + bot_b only.
append_event(
conn,
kind="meanwhile_scene_started",
payload={
"scene_id": 2,
"chat_id": "chat_bot_a",
"parent_scene_id": 1,
"host_bot_id": "bot_a",
"guest_bot_id": "bot_b",
"started_at": "2026-04-26T20:05:00+00:00",
},
)
# Edges so per-POV apply has rows to update.
append_event(
conn,
kind="edge_update",
payload={
"source_id": "bot_a",
"target_id": "you",
"chat_id": "chat_bot_a",
},
)
append_event(
conn,
kind="edge_update",
payload={
"source_id": "bot_b",
"target_id": "you",
"chat_id": "chat_bot_a",
},
)
# One memory per witness in the meanwhile scene.
append_event(
conn,
kind="memory_written",
payload={
"owner_id": "bot_a",
"chat_id": "chat_bot_a",
"scene_id": 2,
"pov_summary": "Original raw narrative (host, meanwhile)",
"witness_you": 0,
"witness_host": 1,
"witness_guest": 1,
"significance": 1,
},
)
append_event(
conn,
kind="memory_written",
payload={
"owner_id": "bot_b",
"chat_id": "chat_bot_a",
"scene_id": 2,
"pov_summary": "Original raw narrative (guest, meanwhile)",
"witness_you": 0,
"witness_host": 1,
"witness_guest": 1,
"significance": 1,
},
)
# A bot-bot turn happens during the meanwhile scene.
append_event(
conn,
kind="assistant_turn",
payload={
"chat_id": "chat_bot_a",
"speaker_id": "bot_a",
"text": "Did you hear what happened with the missing file?",
"truncated": False,
"user_turn_id": None,
},
)
append_event(
conn,
kind="assistant_turn",
payload={
"chat_id": "chat_bot_a",
"speaker_id": "bot_b",
"text": "I have a theory but no proof yet.",
"truncated": False,
"user_turn_id": None,
},
)
@pytest.mark.asyncio
async def test_meanwhile_close_creates_digest(tmp_path):
"""When apply_scene_close_summary runs on a meanwhile scene
(present_set_kind == 'host_guest'), it emits a meanwhile_digest_created
event after the per-POV summaries land; the meanwhile_digest_pending
table then holds a row with non-empty summary text."""
db = tmp_path / "t.db"
apply_migrations(db)
host_canned = json.dumps(
{
"summary": "BotA confided in BotB about the missing file.",
"knowledge_facts": [],
"relationship_summary": "BotA leaned on BotB.",
}
)
guest_canned = json.dumps(
{
"summary": "BotB listened and offered to help investigate.",
"knowledge_facts": [],
"relationship_summary": "BotB grew protective.",
}
)
digest_canned = json.dumps(
{
"summary": (
"While you were away, BotA confided in BotB about a "
"missing file; BotB offered to help quietly investigate."
),
"knowledge_facts": [],
"relationship_summary": "",
}
)
no_threads = json.dumps({"candidates": []})
with open_db(db) as conn:
_seed_meanwhile_scene(conn)
project(conn)
# Order: host POV summary, guest POV summary, digest summary,
# thread detection.
client = MockLLMClient(
canned=[host_canned, guest_canned, digest_canned, no_threads]
)
await apply_scene_close_summary(
conn,
client,
classifier_model="x",
chat_id="chat_bot_a",
scene_id=2,
host_bot_id="bot_a",
)
# The meanwhile_digest_pending row was written.
from chat.state.meanwhile import list_pending_meanwhile_digests
pending = list_pending_meanwhile_digests(conn, "chat_bot_a")
assert len(pending) == 1
assert pending[0]["scene_id"] == 2
assert pending[0]["summary"]
assert "missing file" in pending[0]["summary"]
# And the meanwhile_digest_created event was logged.
rows = conn.execute(
"SELECT payload_json FROM event_log "
"WHERE kind = 'meanwhile_digest_created'"
).fetchall()
assert len(rows) == 1
payload = json.loads(rows[0][0])
assert payload["chat_id"] == "chat_bot_a"
assert payload["scene_id"] == 2
assert "missing file" in payload["summary"]
def test_pending_digest_renders_in_you_scene_prompt(tmp_path):
"""A pending meanwhile digest (created via direct event append) renders
as a 'Meanwhile while you were away:' SHOULD-tier block in the
assembled you-scene narrative prompt."""
from chat.eventlog.log import append_and_apply
from chat.services.prompt import assemble_narrative_prompt
import chat.state.meanwhile # noqa: F401 -- register handlers
import chat.state.threads # noqa: F401
import chat.state.events # noqa: F401
db = tmp_path / "t.db"
apply_migrations(db)
with open_db(db) as conn:
_seed_single_bot_scene(conn)
project(conn)
digest_text = (
"While you were away, BotA confided in BotB about a missing file."
)
append_and_apply(
conn,
kind="meanwhile_digest_created",
payload={
"chat_id": "chat_bot_a",
"scene_id": 2,
"summary": digest_text,
},
)
msgs = assemble_narrative_prompt(
conn,
chat_id="chat_bot_a",
speaker_bot_id="bot_a",
recent_dialogue=[],
retrieved_memory_summaries=[],
)
body = msgs[0].content
assert "Meanwhile while you were away:" in body
assert digest_text in body
def test_consumed_digest_does_not_render_again(tmp_path):
"""After meanwhile_digest_consumed lands for a digest, reassembling the
you-scene prompt must NOT include that digest's text — the pending
list is filtered by ``consumed_at IS NULL``."""
from chat.eventlog.log import append_and_apply
from chat.services.prompt import assemble_narrative_prompt
import chat.state.meanwhile # noqa: F401 -- register handlers
import chat.state.threads # noqa: F401
import chat.state.events # noqa: F401
db = tmp_path / "t.db"
apply_migrations(db)
with open_db(db) as conn:
_seed_single_bot_scene(conn)
project(conn)
digest_text = (
"While you were away, BotA confided in BotB about a missing file."
)
append_and_apply(
conn,
kind="meanwhile_digest_created",
payload={
"chat_id": "chat_bot_a",
"scene_id": 2,
"summary": digest_text,
},
)
# Sanity: it renders before consumption.
msgs = assemble_narrative_prompt(
conn,
chat_id="chat_bot_a",
speaker_bot_id="bot_a",
recent_dialogue=[],
retrieved_memory_summaries=[],
)
assert digest_text in msgs[0].content
# Look up the pending digest id, then consume it.
from chat.state.meanwhile import list_pending_meanwhile_digests
pending = list_pending_meanwhile_digests(conn, "chat_bot_a")
assert len(pending) == 1
digest_id = pending[0]["id"]
append_and_apply(
conn,
kind="meanwhile_digest_consumed",
payload={
"digest_id": digest_id,
"consumed_at": "2026-04-26T20:30:00+00:00",
},
)
msgs2 = assemble_narrative_prompt(
conn,
chat_id="chat_bot_a",
speaker_bot_id="bot_a",
recent_dialogue=[],
retrieved_memory_summaries=[],
)
body2 = msgs2[0].content
assert "Meanwhile while you were away:" not in body2
assert digest_text not in body2