Phase 3.5 cleanup: 17-item backlog burndown #5
@@ -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:
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user