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
|
# Empty default -> skip the memory rewrite; the seeded
|
||||||
# per-turn pov_summary stays in place.
|
# per-turn pov_summary stays in place.
|
||||||
continue
|
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(
|
append_and_apply(
|
||||||
conn,
|
conn,
|
||||||
kind="manual_edit",
|
kind="manual_edit",
|
||||||
@@ -263,6 +267,31 @@ async def _summarize_and_apply_for_witness(
|
|||||||
return pov
|
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:
|
def _build_key_quotes_suffix(conn: Connection, scene_id: int) -> str:
|
||||||
"""If the scene's max-turn-significance is >= 2, build the
|
"""If the scene's max-turn-significance is >= 2, build the
|
||||||
"Key quotes:" suffix from the top-3 highest-significance memory rows
|
"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
|
per-turn narrative seeded by T21, since this helper is called BEFORE
|
||||||
the per-POV rewrite. Texts are truncated to 200 chars to bound
|
the per-POV rewrite. Texts are truncated to 200 chars to bound
|
||||||
memory row growth across many witnesses.
|
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(
|
row = conn.execute(
|
||||||
"SELECT MAX(significance) FROM memories WHERE scene_id = ?",
|
"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,),
|
(scene_id,),
|
||||||
)
|
)
|
||||||
quotes = [
|
quotes = [
|
||||||
(r[0] or "")[:200]
|
_strip_key_quotes_suffix(r[0] or "")[:200]
|
||||||
for r in cur.fetchall()
|
for r in cur.fetchall()
|
||||||
]
|
]
|
||||||
if not quotes:
|
if not quotes:
|
||||||
|
|||||||
@@ -1418,3 +1418,75 @@ def test_consumed_digest_does_not_render_again(tmp_path):
|
|||||||
body2 = msgs2[0].content
|
body2 = msgs2[0].content
|
||||||
assert "Meanwhile while you were away:" not in body2
|
assert "Meanwhile while you were away:" not in body2
|
||||||
assert digest_text 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