память: синк без записи в стор, круги из событий (не из проекции), калибровка порогов nomic

- бэкфилл индекса шёл 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 <noreply@anthropic.com>
This commit is contained in:
Simbiot Harness 2026-10-17 11:05:09 +00:00
parent 969d57f14a
commit d06a4e45bc
2 changed files with 34 additions and 4 deletions

View file

@ -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:

View file

@ -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