From d06a4e45bccb1c75898b8352939ff7eca9c80a62 Mon Sep 17 00:00:00 2001 From: Simbiot Harness Date: Sat, 17 Oct 2026 11:05:09 +0000 Subject: [PATCH] =?UTF-8?q?=D0=BF=D0=B0=D0=BC=D1=8F=D1=82=D1=8C:=20=D1=81?= =?UTF-8?q?=D0=B8=D0=BD=D0=BA=20=D0=B1=D0=B5=D0=B7=20=D0=B7=D0=B0=D0=BF?= =?UTF-8?q?=D0=B8=D1=81=D0=B8=20=D0=B2=20=D1=81=D1=82=D0=BE=D1=80,=20?= =?UTF-8?q?=D0=BA=D1=80=D1=83=D0=B3=D0=B8=20=D0=B8=D0=B7=20=D1=81=D0=BE?= =?UTF-8?q?=D0=B1=D1=8B=D1=82=D0=B8=D0=B9=20(=D0=BD=D0=B5=20=D0=B8=D0=B7?= =?UTF-8?q?=20=D0=BF=D1=80=D0=BE=D0=B5=D0=BA=D1=86=D0=B8=D0=B8),=20=D0=BA?= =?UTF-8?q?=D0=B0=D0=BB=D0=B8=D0=B1=D1=80=D0=BE=D0=B2=D0=BA=D0=B0=20=D0=BF?= =?UTF-8?q?=D0=BE=D1=80=D0=BE=D0=B3=D0=BE=D0=B2=20nomic?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - бэкфилл индекса шёл 13 мин при 4 с CPU: _sync_once звал run_projections() (пишет в стор) и ждал блокировку живой лестницы — теперь только чтение; - круги людей — из событий TrustSet, задачи — из своих событий: отставшая проекция дала бы дефолтный круг 4 — ошибка в сторону утечки; - пороги релевантности по прод-индексу (nomic): релевантное 0.78–0.88, посторонний запрос ≤ 0.75 → MIN_SCORE 0.76 + окно 0.07 от лучшего; «рецепт борща» → пусто, «миграция Gennady» → нужная задача; - прод: индекс 1650 разговоров (личка 1 / правление 2 / агенты 3), 270 задач, 91 дайджест; поиск круга 2 не отдаёт записей круга 1. Co-Authored-By: Claude Opus 5.5 --- harness/memory.py | 35 +++++++++++++++++++++++++++++++---- tests/test_memory.py | 3 +++ 2 files changed, 34 insertions(+), 4 deletions(-) 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