fix: enqueue significance for interjection memories (T74.2)
T44's interjection branch wrote interjection memories via record_turn_memory_for_present but never enqueued a SignificanceJob, so the interjection beat could land in memory but never be scored — which meant it could never auto-pin even when it carried a pivotal moment. - Capture the host-POV memory id from the interjection's memory write result and enqueue a SignificanceJob mirroring the primary turn's pattern. One enqueue per beat (host id; guest POV piggybacks on the same score since the prose is identical for v2 — per-POV rewrite happens at scene close in T45). - New test test_interjection_enqueues_significance_job pins the contract by intercepting worker.enqueue and asserting two distinct jobs land per 3-entity turn that fires an interjection.
This commit is contained in:
+28
-1
@@ -616,7 +616,7 @@ async def post_turn(
|
||||
|
||||
# Memory write for the interjection beat — a second pair
|
||||
# of memory_written events (host + guest POVs).
|
||||
record_turn_memory_for_present(
|
||||
interject_memory_results = record_turn_memory_for_present(
|
||||
conn,
|
||||
chat_id=chat_id,
|
||||
host_bot_id=host_bot["id"],
|
||||
@@ -626,6 +626,33 @@ async def post_turn(
|
||||
chat_clock_at=chat.get("time"),
|
||||
)
|
||||
|
||||
# T74.2: enqueue a significance pass for the interjection
|
||||
# memory. Mirrors the primary-turn enqueue pattern above —
|
||||
# we score on the host's memory id since the prose is
|
||||
# identical across both POVs (per-POV rewrite happens at
|
||||
# scene close in T45). Without this enqueue the
|
||||
# interjection beat lands in memory but never gets scored,
|
||||
# so it can never auto-pin even when it carries a pivotal
|
||||
# moment.
|
||||
interject_host_event = interject_memory_results.get(
|
||||
host_bot["id"]
|
||||
)
|
||||
interject_host_memory_id = (
|
||||
interject_host_event[1] if interject_host_event else None
|
||||
)
|
||||
if (
|
||||
worker is not None
|
||||
and interject_host_memory_id is not None
|
||||
):
|
||||
worker.enqueue(
|
||||
SignificanceJob(
|
||||
memory_id=interject_host_memory_id,
|
||||
narrative_text=interjection_text,
|
||||
prior_dialogue=recent_post_interject,
|
||||
host_bot_id=host_bot["id"],
|
||||
)
|
||||
)
|
||||
|
||||
# 9. Scene-close detection (Plan §7.2, T26). Runs AFTER assistant_turn
|
||||
# and the optional interjection so the bots' responses are part of
|
||||
# the closing scene's final beat — closing before narrative would
|
||||
|
||||
@@ -713,3 +713,65 @@ def test_addressee_detection_routes_to_named_bot(app_state_setup, tmp_path):
|
||||
# Interjection follow-on goes to the silent witness — the host.
|
||||
assert interjection_payload["speaker_id"] == "bot_a"
|
||||
assert interjection_payload["interjection_of"] == "bot_b"
|
||||
|
||||
|
||||
def test_interjection_enqueues_significance_job(app_state_setup, tmp_path):
|
||||
"""T74.2: when an interjection fires, the interjection memory is
|
||||
enqueued for significance scoring just like the primary memory.
|
||||
|
||||
Capture enqueued ``SignificanceJob``s by replacing the background
|
||||
worker's ``enqueue`` method with a list-append. Without T74.2, the
|
||||
interjection memory would never be scored — only the primary's
|
||||
enqueue would land. We therefore expect TWO jobs after a turn that
|
||||
has both a primary and an interjection beat: one for the primary
|
||||
memory, one for the interjection memory.
|
||||
"""
|
||||
_seed_chat_with_guest(tmp_path / "test.db")
|
||||
canned_parse = json.dumps(
|
||||
{"segments": [{"kind": "dialogue", "text": "tell me"}]}
|
||||
)
|
||||
canned = [
|
||||
canned_parse,
|
||||
json.dumps(
|
||||
{"addressee_id": "bot_a", "confidence": "medium", "reason": "host"}
|
||||
),
|
||||
"Primary beat.",
|
||||
_zero_state(), _zero_state(), _zero_state(),
|
||||
_zero_state(), _zero_state(), _zero_state(),
|
||||
json.dumps({"should_interject": True, "reason": "jealous"}),
|
||||
"Interjection beat!",
|
||||
_zero_state(), _zero_state(), _zero_state(),
|
||||
_zero_state(), _zero_state(), _zero_state(),
|
||||
json.dumps({"should_close": False, "reason": "no signal"}),
|
||||
]
|
||||
_override_llm(canned)
|
||||
|
||||
captured_jobs: list = []
|
||||
worker = app.state.background_worker
|
||||
# Re-enable enqueue capture even though the worker's loop is disabled
|
||||
# — we want to count enqueues without the loop running classifier work.
|
||||
worker.enabled = True
|
||||
original_enqueue = worker.enqueue
|
||||
worker.enqueue = captured_jobs.append # type: ignore[assignment]
|
||||
|
||||
try:
|
||||
response = app_state_setup.post(
|
||||
"/chats/chat_bot_a/turns", data={"prose": "tell me"}
|
||||
)
|
||||
assert response.status_code == 204
|
||||
finally:
|
||||
worker.enqueue = original_enqueue # type: ignore[assignment]
|
||||
worker.enabled = False
|
||||
app.dependency_overrides.clear()
|
||||
|
||||
# Expect 2 enqueues: 1 for the primary memory + 1 for the
|
||||
# interjection memory.
|
||||
assert len(captured_jobs) == 2
|
||||
|
||||
# Both jobs should reference distinct memory ids — the primary's
|
||||
# host-POV memory and the interjection's host-POV memory.
|
||||
memory_ids = [job.memory_id for job in captured_jobs]
|
||||
assert len(set(memory_ids)) == 2
|
||||
# The two narrative texts should be the two streamed beats.
|
||||
narrative_texts = sorted(job.narrative_text for job in captured_jobs)
|
||||
assert narrative_texts == ["Interjection beat!", "Primary beat."]
|
||||
|
||||
Reference in New Issue
Block a user