Pabloler21 Claude Fable 5 commited on
Commit
b930fd8
·
1 Parent(s): 1a04c35

fix: drop new_memories that echo Hollow's own reply (recall pollution)

Browse files

On recall turns Hollow retells a memory in first person; extraction
sometimes captured that poetic line back as a new treasure entry.
Deterministic filter: >60% token overlap with the reply = Hollow's
words, not the visitor's.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

Files changed (3) hide show
  1. app.py +1 -1
  2. memory.py +16 -2
  3. tests/test_memory.py +24 -0
app.py CHANGED
@@ -313,7 +313,7 @@ def chat(user_msg: str, state: dict, history: list):
313
  turn_failed = True
314
 
315
  treasure_before = len(state["treasure"])
316
- state = apply_update(state, raw_json)
317
  captured = len(state["treasure"]) > treasure_before
318
 
319
  if do_recall and recall_memory and not turn_failed:
 
313
  turn_failed = True
314
 
315
  treasure_before = len(state["treasure"])
316
+ state = apply_update(state, raw_json, reply=reply)
317
  captured = len(state["treasure"]) > treasure_before
318
 
319
  if do_recall and recall_memory and not turn_failed:
memory.py CHANGED
@@ -17,7 +17,21 @@ def get_tier(affinity: int) -> str:
17
  return "Almost"
18
 
19
 
20
- def apply_update(state: dict, raw_json: str) -> dict:
 
 
 
 
 
 
 
 
 
 
 
 
 
 
21
  try:
22
  start = raw_json.find("{")
23
  end = raw_json.rfind("}") + 1
@@ -31,7 +45,7 @@ def apply_update(state: dict, raw_json: str) -> dict:
31
 
32
  state["affinity"] = max(0, min(100, state["affinity"] + update.affinity_delta))
33
  for mem in update.new_memories:
34
- if mem not in state["treasure"]:
35
  state["treasure"].append(mem)
36
  state["tone"] = max(-100, min(100, state.get("tone", 0) + update.tone_delta))
37
  if update.cruel_quote and update.tone_delta < 0:
 
17
  return "Almost"
18
 
19
 
20
+ def _is_hollows_words(memory: str, reply: str) -> bool:
21
+ """On recall turns Hollow retells a memory in first person; extraction
22
+ sometimes captures THAT poetic line as a new memory. Drop any candidate
23
+ whose words substantially overlap Hollow's reply (>60% shared tokens)."""
24
+ if not reply:
25
+ return False
26
+ mem_words = set(re.findall(r"\w+", memory.lower()))
27
+ if not mem_words:
28
+ return False
29
+ reply_words = set(re.findall(r"\w+", reply.lower()))
30
+ overlap = len(mem_words & reply_words) / len(mem_words)
31
+ return overlap > 0.6
32
+
33
+
34
+ def apply_update(state: dict, raw_json: str, reply: str = "") -> dict:
35
  try:
36
  start = raw_json.find("{")
37
  end = raw_json.rfind("}") + 1
 
45
 
46
  state["affinity"] = max(0, min(100, state["affinity"] + update.affinity_delta))
47
  for mem in update.new_memories:
48
+ if mem not in state["treasure"] and not _is_hollows_words(mem, reply):
49
  state["treasure"].append(mem)
50
  state["tone"] = max(-100, min(100, state.get("tone", 0) + update.tone_delta))
51
  if update.cruel_quote and update.tone_delta < 0:
tests/test_memory.py CHANGED
@@ -324,3 +324,27 @@ class TestSingleGateBranchesByTone:
324
  def test_gate_at_70_branches_only_by_tone(self):
325
  assert decide_ending(_end_state(affinity=70, tone=20)) == "good"
326
  assert decide_ending(_end_state(affinity=70, tone=19)) == "loop"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
324
  def test_gate_at_70_branches_only_by_tone(self):
325
  assert decide_ending(_end_state(affinity=70, tone=20)) == "good"
326
  assert decide_ending(_end_state(affinity=70, tone=19)) == "loop"
327
+
328
+
329
+ class TestHollowWordsFiltered:
330
+ def _s(self):
331
+ return {"affinity": 50, "treasure": [], "claimed": [], "tone": 0, "wounds": []}
332
+
333
+ def test_drops_memory_that_echoes_hollows_reply(self):
334
+ reply = "i remember a voice, soft like the wind through the trees."
335
+ s = apply_update(self._s(),
336
+ '{"affinity_delta": 3, "new_memories": ["a voice soft like the wind through the trees"]}',
337
+ reply=reply)
338
+ assert s["treasure"] == []
339
+
340
+ def test_keeps_genuine_visitor_memory(self):
341
+ reply = "that sounds lovely. tell me more about her."
342
+ s = apply_update(self._s(),
343
+ '{"affinity_delta": 3, "new_memories": ["had a grandmother who kept bees"]}',
344
+ reply=reply)
345
+ assert s["treasure"] == ["had a grandmother who kept bees"]
346
+
347
+ def test_no_reply_keeps_everything(self):
348
+ s = apply_update(self._s(),
349
+ '{"affinity_delta": 3, "new_memories": ["grew up near the sea"]}')
350
+ assert s["treasure"] == ["grew up near the sea"]