"""harness 產生的文字不是一輪對話,不該抵達召回。 2026-09-24 在正式資料的 1,020 個 turn 裡量到 137 個(13.5%)的 query 其實是 harness 產生的文字,最大宗是背景任務完成通知。它們同時污染三個地方:turns 表、 對話軌跡,以及召回。 召回那邊的後果看得見:這種文字剛好跟 openclaw 類的卡片用字重疊,所以餘弦給 0.57~0.62,高過 0.55 的地板,整批被注入進 context。同一批候選 jev 給 0.03~0.10 ——也就是說,這不是門檻調錯,是這些文字根本不該被拿去查。 **這份測試用的是資料庫裡真實出現過的字串。** 今天已經犯過一次「測試覆蓋的是我 想像的情況,不是實際發生的那個」,所以下面每一個樣本都是從 turns 表撈出來的。 最重要的一條是誤殺:`` 是 Pan 貼東西進來時的包裝,資料 裡有 5 筆。用「以 < 開頭」這種寬鬆規則會把它一起擋掉,等於他貼的東西全部變成 看不見——那比多注入幾筆雜訊糟得多。 """ from __future__ import annotations import sys import tempfile import unittest from pathlib import Path from unittest import mock ROOT = Path(__file__).resolve().parents[1] sys.path.insert(0, str(ROOT / "scripts")) import xkb_agent_hook # noqa: E402 from conversation_state_parser import is_harness_text, noise_kind # noqa: E402 # 以下四段都是 turns 表裡真實出現過的開頭。 REAL_TASK_NOTIFICATION = ( "\na063252154283d8bf\n" "/tmp/x.output\ncompleted\n" "") REAL_OPENCLAW_CONTEXT = ( "<<>>\n" "OpenClaw runtime context (internal):\n" "This context is runtime-generated, not user-authored.\n" "<<>>") REAL_PASTED_CONTENT = ( ' 畫面由 GPS 研究流程拉回到 REALI 呈現品牌資料、' "不同 AI Agent、團隊分工") REAL_QUESTION = "食品展的客戶通常怎麼找" class ItRecognisesWhatTheHarnessWrote(unittest.TestCase): def test_the_two_shapes_that_actually_occurred(self): self.assertTrue(is_harness_text(REAL_TASK_NOTIFICATION)) self.assertTrue(is_harness_text(REAL_OPENCLAW_CONTEXT)) def test_the_marker_must_be_at_the_start(self): """137 筆全部是開頭就帶標記,沒有夾在中間的。 不錨定的話,一句提到這些標記的真話會被當成 harness 文字——而這份 對話本身就是這樣的一句。 """ self.assertFalse(is_harness_text( "為什麼 會被當成查詢送進召回?")) def test_leading_whitespace_does_not_hide_it(self): self.assertTrue(is_harness_text("\n " + REAL_TASK_NOTIFICATION)) def test_it_is_reported_as_its_own_kind(self): """跟問候/應答分開,因為要修的東西不一樣。 問候是判斷問題(之後交給 jev),這個是接線問題——這種文字根本不該 抵達召回。混成同一種的話,報告上看不出哪一種在發生。 """ self.assertEqual(noise_kind(REAL_TASK_NOTIFICATION), "harness_text") self.assertEqual(noise_kind("早安"), "greeting") self.assertEqual(noise_kind("好的"), "acknowledgement") class ItMustNotSwallowWhatPanPasted(unittest.TestCase): """最貴的誤殺。資料裡有 5 筆 ``,那是他貼進來的東西。""" def test_pasted_content_is_not_harness_text(self): self.assertFalse(is_harness_text(REAL_PASTED_CONTENT)) self.assertEqual(noise_kind(REAL_PASTED_CONTENT), "") def test_a_plain_question_is_untouched(self): self.assertFalse(is_harness_text(REAL_QUESTION)) self.assertEqual(noise_kind(REAL_QUESTION), "") def test_an_angle_bracket_alone_means_nothing(self): for text in ("
這段 HTML 要怎麼改", "<3", "<= 這個符號怎麼念"): with self.subTest(text=text): self.assertFalse(is_harness_text(text)) class TheHookDoesNotOpenATurnForIt(unittest.TestCase): """不是「開了 turn 但不召回」——它根本不是一輪對話。""" def setUp(self): self.tmp = tempfile.TemporaryDirectory() self.addCleanup(self.tmp.cleanup) def _run(self, prompt: str) -> list[str]: calls: list[str] = [] def spy(path, body, cfg): calls.append(path) return {"session_id": "s", "turn_id": "t", "retrieval": {"records": []}} state = Path(self.tmp.name) / "state.json" with mock.patch.object(xkb_agent_hook, "call", spy), \ mock.patch.object(xkb_agent_hook, "session_key", return_value="k"), \ mock.patch.object(xkb_agent_hook, "next_ordinal", return_value=1), \ mock.patch.object(xkb_agent_hook, "STATE_DIR", Path(self.tmp.name)), \ mock.patch.object(xkb_agent_hook, "state_path", return_value=state), \ mock.patch.object(xkb_agent_hook, "emit"): xkb_agent_hook.on_prompt({"prompt": prompt}, {"source": "test"}) return calls def test_harness_text_leaves_no_state_behind(self): """不開 turn,也不該留下狀態——留下的話會讓下一輪對不上。""" self._run(REAL_TASK_NOTIFICATION) self.assertEqual(list(Path(self.tmp.name).glob("*.json")), []) def test_a_task_notification_opens_nothing(self): self.assertEqual(self._run(REAL_TASK_NOTIFICATION), [], "連 session 都不該開——它不是一輪對話") def test_the_openclaw_internal_context_opens_nothing(self): self.assertEqual(self._run(REAL_OPENCLAW_CONTEXT), []) def test_a_real_prompt_still_opens_a_turn(self): calls = self._run(REAL_QUESTION) self.assertIn("/v1/sessions/open", calls) self.assertIn("/v1/turns/start", calls) def test_pasted_content_still_opens_a_turn(self): calls = self._run(REAL_PASTED_CONTENT) self.assertIn("/v1/turns/start", calls) class OneDefinitionForBothEnds(unittest.TestCase): """hook 擋一次、召回再擋一次,但判斷只能有一份。 兩邊各寫一份就會各自漂移——這個專案的「多寫入者一讀取者」記錄在案,而 _skip_reason 的註解裡也寫著它從 parser 拿清單的理由:它自己那份拷貝曾經 漏掉複合應答,於是「ok 收到」撈了十筆進一個什麼都沒問的對話。 """ def test_the_hook_imports_the_definition(self): import inspect source = inspect.getsource(xkb_agent_hook) self.assertIn("from conversation_state_parser import is_harness_text", source) self.assertNotIn("task-notification", source, "標記清單不要在 hook 裡再寫一份") def test_recall_skips_it_through_the_same_function(self): import inspect from xkb_memory_service import Store source = inspect.getsource(Store._skip_reason) self.assertIn("_noise_kind", source) self.assertEqual(noise_kind(REAL_TASK_NOTIFICATION), "harness_text", "召回端的 skip_reason 會直接變成這個字串") if __name__ == "__main__": unittest.main() class TheOldRowsStillCrowdTheRecallWindow(unittest.TestCase): """在 hook 端擋掉之前累積的那些列,還躺在資料庫裡。 對話召回只看**最近 500 筆** turn。2026-09-24 的資料庫裡有 137 筆 turn 的 query 是背景任務通知——也就是視窗的 27% 被佔掉,真正的對話歷史被擠出去。在 Python 端 過濾救不了這件事:它們已經佔掉名額了,所以排除要發生在 SQL 裡。 刻意不刪那些列:它們的 answer 是真實的回答內容。代價是那些答案從此不會被對話 召回撈到——真有知識價值的內容,它的家是卡片或 wiki,不是一個問句是系統通知的 對話軌跡。 """ def setUp(self): from xkb_memory_service import Store self.tmp = tempfile.TemporaryDirectory() self.addCleanup(self.tmp.cleanup) self.store = Store(Path(self.tmp.name) / "memory.sqlite") # 用真正的 API 建資料,不要自己寫 INSERT——第一版我猜了 sessions 的欄位, # 猜錯了。走公開路徑的話,schema 改了測試會跟著壞,那是對的。 session = self.store.open_session({ "source": "test", "agent_id": "test", "session_key": "k", "namespace": "private"}) rows = [ ("t1", "食品展的客戶通常怎麼找", "展前拿名單、展中面對面、展後跟催"), ("t2", REAL_TASK_NOTIFICATION, "食品展的客戶開發我剛做完了"), ("t3", REAL_OPENCLAW_CONTEXT, "食品展相關的內部通知"), ] for turn_id, query, answer in rows: self.store.start_turn({"session_id": session["session_id"], "turn_id": turn_id, "query": query}) self.store.complete_turn(turn_id, { "session_id": session["session_id"], "query": query, "answer": answer}) def test_harness_turns_never_enter_the_window(self): result = self.store.recall("食品展的客戶通常怎麼找", limit=10, namespace="private") queries = [m["query"] for m in result["memories"]] self.assertEqual(queries, ["食品展的客戶通常怎麼找"], "harness 產生的列不該佔用最近 500 筆的名額") def test_a_real_turn_is_still_recalled(self): result = self.store.recall("展前拿名單", limit=10, namespace="private") self.assertEqual(len(result["memories"]), 1) def test_the_rows_are_not_deleted(self): """排除的是召回,不是資料。那些 answer 是真實的回答內容。""" with self.store.connect() as db: count = db.execute("SELECT COUNT(*) FROM turns").fetchone()[0] self.assertEqual(count, 3)