Phase 3.5 cleanup: 17-item backlog burndown #5

Merged
dohertj2 merged 33 commits from phase-3.5 into main 2026-04-27 01:56:29 -04:00
2 changed files with 107 additions and 2 deletions
Showing only changes of commit d123684f9a - Show all commits
+35 -2
View File
@@ -213,7 +213,11 @@ async def _summarize_and_apply_for_witness(
# Empty default -> skip the memory rewrite; the seeded
# per-turn pov_summary stays in place.
continue
new_value = pov.summary + key_quotes_suffix
# T80.1: a prior close may have already appended a Key quotes
# suffix to this row's pov_summary. Strip it here so the fresh
# rewrite replaces the existing suffix rather than stacking a
# second one on top.
new_value = _strip_key_quotes_suffix(pov.summary) + key_quotes_suffix
append_and_apply(
conn,
kind="manual_edit",
@@ -263,6 +267,31 @@ async def _summarize_and_apply_for_witness(
return pov
# T80.1: header marker shared by the suffix builder and the
# witness-write strip step. Any text starting with this marker is treated
# as a previously-appended Key quotes suffix and stripped before reuse so
# repeated scene closes don't compose recursive bloat.
_KEY_QUOTES_HEADER = "\n\nKey quotes:\n"
def _strip_key_quotes_suffix(text: str) -> str:
"""Remove a previously-appended Key quotes suffix from ``text``.
Returns ``text`` unchanged when the marker is absent, or the prefix
up to (but not including) the marker when present. Used in two
places: (1) when sourcing quote text from a memory row that may
already carry the suffix from a prior close, and (2) when computing
the per-POV rewrite's prior_value so the new write replaces — rather
than stacks on — the old suffix.
"""
if not text:
return text
idx = text.find(_KEY_QUOTES_HEADER)
if idx >= 0:
return text[:idx]
return text
def _build_key_quotes_suffix(conn: Connection, scene_id: int) -> str:
"""If the scene's max-turn-significance is >= 2, build the
"Key quotes:" suffix from the top-3 highest-significance memory rows
@@ -274,6 +303,10 @@ def _build_key_quotes_suffix(conn: Connection, scene_id: int) -> str:
per-turn narrative seeded by T21, since this helper is called BEFORE
the per-POV rewrite. Texts are truncated to 200 chars to bound
memory row growth across many witnesses.
T80.1: candidate text is run through :func:`_strip_key_quotes_suffix`
first so a re-close (whose source memories already carry a suffix from
the prior close) doesn't quote a quote.
"""
row = conn.execute(
"SELECT MAX(significance) FROM memories WHERE scene_id = ?",
@@ -288,7 +321,7 @@ def _build_key_quotes_suffix(conn: Connection, scene_id: int) -> str:
(scene_id,),
)
quotes = [
(r[0] or "")[:200]
_strip_key_quotes_suffix(r[0] or "")[:200]
for r in cur.fetchall()
]
if not quotes:
+72
View File
@@ -1418,3 +1418,75 @@ def test_consumed_digest_does_not_render_again(tmp_path):
body2 = msgs2[0].content
assert "Meanwhile while you were away:" not in body2
assert digest_text not in body2
# ---------------------------------------------------------------------------
# T80: scene_summarize polish bundle.
# ---------------------------------------------------------------------------
@pytest.mark.asyncio
async def test_scene_close_re_run_does_not_double_suffix(tmp_path):
"""T80.1: re-running ``apply_scene_close_summary`` on the same scene
must NOT stack a second "Key quotes:" suffix on each pov_summary. The
builder strips any existing suffix from candidate text before
composing the new one, and the per-POV write replaces (not appends
to) the existing suffix.
"""
db = tmp_path / "t.db"
apply_migrations(db)
canned = json.dumps(
{
"summary": "BotA had a heavy talk with you.",
"knowledge_facts": [],
"relationship_summary": "Things shifted.",
}
)
no_threads = json.dumps({"candidates": []})
with open_db(db) as conn:
_seed_single_bot_scene_no_memory(conn)
# Significance >= 2 triggers the Key quotes suffix path.
_seed_memory(conn, pov_summary="Maya quote one", significance=3)
_seed_memory(conn, pov_summary="Maya quote two", significance=2)
project(conn)
# First close.
client = MockLLMClient(canned=[canned, no_threads])
await apply_scene_close_summary(
conn,
client,
classifier_model="x",
chat_id="chat_bot_a",
scene_id=1,
host_bot_id="bot_a",
)
rows = conn.execute(
"SELECT pov_summary FROM memories WHERE scene_id = 1"
).fetchall()
assert rows
for (pov,) in rows:
assert pov.count("Key quotes:") == 1
# Second close on the same scene with fresh canned responses.
client2 = MockLLMClient(canned=[canned, no_threads])
await apply_scene_close_summary(
conn,
client2,
classifier_model="x",
chat_id="chat_bot_a",
scene_id=1,
host_bot_id="bot_a",
)
rows2 = conn.execute(
"SELECT pov_summary FROM memories WHERE scene_id = 1"
).fetchall()
assert rows2
for (pov,) in rows2:
# Still exactly ONE "Key quotes:" suffix — no recursive bloat.
assert pov.count("Key quotes:") == 1
# And no nested-quote artifacts (the suffix wasn't sourced
# from a row whose text already contained the suffix).
inner_count = pov.count("Key quotes:")
assert inner_count == 1