diff --git a/harness/memory.py b/harness/memory.py index b95006a..6717801 100644 --- a/harness/memory.py +++ b/harness/memory.py @@ -42,7 +42,11 @@ except ImportError: KINDS = ("conv", "out", "task", "digest") BUDGET = {2: (8, 5000), 3: (15, 12000)} # глубина → (записей, символов) SYNC_BATCH = 400 -MIN_SCORE = float(os.environ.get("SIMBIOT_MEMORY_MIN_SCORE", "0.45")) # ниже — шум +# калибровка на прод-индексе (nomic, 2026-10-17): релевантное 0.78–0.88, +# посторонний запрос («рецепт борща») — шум ≤ 0.75 → абсолютный порог 0.76 + +# окно от лучшего совпадения (берём только близкое к лучшему) +MIN_SCORE = float(os.environ.get("SIMBIOT_MEMORY_MIN_SCORE", "0.76")) +REL_WINDOW = float(os.environ.get("SIMBIOT_MEMORY_REL_WINDOW", "0.07")) _LOCK = threading.Lock() _LAST = {} # уровень → последнее решение о глубине (для DepthChosen) @@ -140,7 +144,15 @@ def _sync_once(limit: int, log=None) -> tuple: (last, limit)).fetchall() if not rows: return 0, 0 - model = es.run_projections() + # круги — прямо из событий TrustSet, только чтение: run_projections() + # пишет в стор и ждал блокировку живой лестницы (бэкфилл 13 мин + # при 4 с CPU), а отставшая проекция дала бы дефолтный круг 4 — + # ошибку в сторону утечки (запись видна шире, чем надо) + tmap = {} + for (td,) in src.execute("SELECT data FROM events WHERE event_type='TrustSet' ORDER BY id"): + td = json.loads(td) + tmap[td["subject"]] = {"circle": td["circle"]} + model = {"trust": tmap} senders = json.loads((db.execute("SELECT v FROM meta WHERE k='senders'") .fetchone() or ["{}"])[0]) senders = {k: set(v) for k, v in senders.items()} @@ -175,7 +187,7 @@ def _sync_once(limit: int, log=None) -> tuple: chat_circle(d.get("chat_id"), model, senders), "Коди", t[:1500])) elif et == "TaskCompleted": - t = (model.get("tasks") or {}).get(aid) or {} + t = _task_info(src, aid) ans = (d.get("answer") or d.get("result") or "") if t.get("title") or ans: pending.append((f"task:{aid}", "task", ts, "", trust.norm(t.get("circle")), @@ -205,6 +217,20 @@ def _sync_once(limit: int, log=None) -> tuple: db.close() +def _task_info(src, tid: str) -> dict: + """Заголовок и круг задачи — из её событий (по индексу агрегата).""" + t = {} + for et, d in src.execute("SELECT event_type, data FROM events WHERE aggregate='task' " + "AND aggregate_id=? AND event_type IN ('TaskSubmitted','TaskCircleSet') " + "ORDER BY id", (tid,)): + d = json.loads(d) + if et == "TaskSubmitted": + t.update(title=d.get("title"), circle=d.get("circle")) + else: + t["circle"] = d.get("circle") + return t + + def _conv_item(src, db, aid, d, ts, model, senders, pending=()): row = src.execute("SELECT data FROM events WHERE aggregate='inbox' AND aggregate_id=? " "AND event_type='MessageReceived' LIMIT 1", (aid,)).fetchone() @@ -334,8 +360,9 @@ def context_block(depth: int, level: str, inbox: list, batch: list, circle: int) except Exception as e: return f"ПАМЯТЬ: поиск недоступен ({str(e)[:80]}) — отвечай из контекста тика." lines, used = [], 0 + top = hits[0]["score"] if hits else 0.0 for h in hits: - if h["score"] < MIN_SCORE: + if h["score"] < MIN_SCORE or h["score"] < top - REL_WINDOW: continue line = f"- [{h['kind']} {h['ts'][:10]}] {h['text'][:900]}" if used + len(line) > budget: diff --git a/tests/test_memory.py b/tests/test_memory.py index db05ae5..52eb1c6 100644 --- a/tests/test_memory.py +++ b/tests/test_memory.py @@ -27,6 +27,9 @@ os.environ["SIMBIOT_MEMORY_DIR"] = os.path.join(_tmp, "memory") os.environ["SIMBIOT_CORE_DIR"] = os.path.join(_tmp, "core") for _v in ("SIMBIOT_MEMORY", "SIMBIOT_DECISION"): os.environ.pop(_v, None) +# «мешок слов» даёт оценки ниже nomic — пороги под него (калибровка nomic — в коде) +os.environ["SIMBIOT_MEMORY_MIN_SCORE"] = "0.2" +os.environ["SIMBIOT_MEMORY_REL_WINDOW"] = "1.0" import numpy as np