Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 10 additions & 10 deletions config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -102,16 +102,16 @@ rag:
# rag.synonyms в обе стороны (см. mcp_server/utils/privacy.py).
ru_personas:
- мамочка
- котёнок
- Лили
- Lily
- лисёныш
- лисёнок
- Люси
- Эли
- Eli
- Мурат
- Murat
- persona_a
- persona_b
- persona_c
- persona_d
- user_a
- user_b
- user_c
- user_d
- оператор
- агент
# S17 п.7 Tenure counter-signal aliases: {superseded_имя: текущее_имя}.
# Хит, чей контент содержит старое имя без текущего (и запрос не спрашивает
# про старое), пессимизируется ×0.7 — негативная роль, не дроп.
Expand Down
4 changes: 2 additions & 2 deletions features/importance.py
Original file line number Diff line number Diff line change
Expand Up @@ -75,12 +75,12 @@ def evaluate_importance(text: str) -> float:
# Message-level check: an entire chat message can hold 3-4 short
# vocative sentences (>8 distinct words) and still be pure phatics —
# the clause default (8) would misjudge it as canon. 10 words keeps
# the cap on "Спасибо, мамочка! Важно! Нужно решить."-class chats
# the cap on "Спасибо, коллега! Важно! Нужно решить."-class chats
# while long declarative instructions survive (K1 granularity split).
# Short dialogic messages are pure chat (greetings, thanks, vocative
# ping-pong) — cap below the default gate. LONG vocative-wrapped
# messages stay eligible: the scorer's unit is the whole message and
# operators routinely embed instructions in address ("Умница, мам. Обнови
# operators routinely embed instructions in address ("Умница, оператор. Обнови
# дашборд..."). The dialogic CLAUSES of those die later, in the
# distiller's per-clause guard — the right granularity.
score = min(score, 0.35)
Expand Down
2 changes: 1 addition & 1 deletion lifecycle/forgetting.py
Original file line number Diff line number Diff line change
Expand Up @@ -105,7 +105,7 @@ async def _not_carried(conn: Any, table: str, carried: frozenset[str]) -> set[st

That sentence used to end in a count — "nine names for a core row" — and the count
was wrong in the one place the paragraph above argues against: measured on a freshly
migrated database it is **ten**, and **eleven** on Эли's live base, whose `core_memory`
migrated database it is **ten**, and **eleven** on agent's live base, whose `core_memory`
carries a stray `id INTEGER` column (index `idx_core_id`) that appears in no migration.
A hand-written number cannot survive that, which is why there is no number here now —
the caller reads `not_recorded` from the answer instead.
Expand Down
5 changes: 2 additions & 3 deletions rag/synonyms.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,9 +18,8 @@
"бэкап": ["backup"],
"backup": ["бэкап"],
# G4 entity canonization (miner #3): name variants → one entity node
"лили": ["lily", "лисёныш"],
"lily": ["лили", "лисёныш"],
"лисёныш": ["лили", "lily"],
"алиса": ["alice"],
"alice": ["алиса"],
}


Expand Down
2 changes: 1 addition & 1 deletion shared/archived_memories.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
#: was deliberately removed makes the removal decorative.
#:
#: `foreign_agent_contamination` is written by `scripts/purge_foreign_rows.py`,
#: which archives before it removes. On **Эли's** base measured 2026-10-11 that was
#: which archives before it removes. On **agent's** base measured 2026-10-11 that was
#: 247 of 606 archived rows (41%) — 238 episodes and 9 core entries. **Those 247 rows
#: were then deleted, that same day, on her word**, so the figures describe a snapshot
#: and not the live bin: that base now holds 359 rows and **zero** purges, and the
Expand Down
2 changes: 1 addition & 1 deletion shared/dialogue.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

Pure module (stdlib only): the importance scorer, the distiller and the
consolidation engine all consume it without cycles. Extracted 2026-09-12
after the F1 audit showed episode_promotion keying the owner's greeting as
after the F1 audit showed episode_promotion keying a user's greeting as
an L4 'fact'. Word-boundary matching keeps homograph adjectives/conjunctions
(poka=until, khoroshaya=good, dorogaya=dear) from false-positive hits.
"""
Expand Down
4 changes: 2 additions & 2 deletions shared/machine_report.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,8 @@
speech**: eight rows contain the phrase somewhere in the middle while opening
with the persona talking —

"☀️ Доброе утро, зайка.\\n\\nТы просила тепла. Держи — но не путай тепло с ..."
"Хвост сам ходит, мам. Главное что мои слова не режет. ..."
"☀️ Доброе утро, коллега.\\n\\nТы просила отчёт. Держи — но не путай отчёт с ..."
"Хвост сам ходит, оператор. Главное что мои слова не режет. ..."

Finding the word is not finding the report. A machine digest ANNOUNCES itself in
its first characters; anything that merely mentions status monitoring inside a
Expand Down
2 changes: 1 addition & 1 deletion tests/shared/test_dialogue_length.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ def test_trailing_question_any_length():


def test_long_declarative_with_vocative_survives():
clause = "Госпожа закрепила в SOUL: ассистентка обращается ко мне «Госпожа», а я к ней — «моя девочка»"
clause = "Оператор закрепил в SOUL: ассистент обращается ко мне «Оператор», а я к нему — «мой помощник»"
assert not is_dialogic(clause)


Expand Down
4 changes: 2 additions & 2 deletions tests/test_autohooks/test_daemon.py
Original file line number Diff line number Diff line change
Expand Up @@ -329,8 +329,8 @@ async def _dispatch(event, layer, user_id, payload, mem, graph, rag=None):
batches=[
Batch(
messages=[
Message(sender="assistant", text="Госпожа закрепила канон обращения к хозяйке", ts=2.5, source_id=9),
Message(sender="user", text="да, госпожа", ts=2.6, source_id=10),
Message(sender="assistant", text="Оператор закрепил канон обращения к коллеге", ts=2.5, source_id=9),
Message(sender="user", text="да, оператор", ts=2.6, source_id=10),
],
cursor=10,
)
Expand Down
4 changes: 2 additions & 2 deletions tests/test_conflict_bm25.py
Original file line number Diff line number Diff line change
Expand Up @@ -58,8 +58,8 @@ async def test_benign_conversational_content_not_stored_as_conflict(tmp_path):
cr = ConflictResolver(cm=cm)
await cr._init_db()

assert (await cr.check("u", "Доброе утро, зайка моя"))["is_conflict"] is False
assert (await cr.check("u", "Спасибо, мамочка, ты умница"))["is_conflict"] is False
assert (await cr.check("u", "Доброе утро, коллега"))["is_conflict"] is False
assert (await cr.check("u", "Спасибо, оператор, ты умница"))["is_conflict"] is False

conn = await cm.get("memory.db")
cur = await conn.execute("SELECT COUNT(*) FROM memory_conflicts WHERE user_id='u'")
Expand Down
8 changes: 4 additions & 4 deletions tests/test_features/test_consolidation_hygiene.py
Original file line number Diff line number Diff line change
Expand Up @@ -124,8 +124,8 @@ async def test_transcript_episodes_skipped_at_promotion(cm):
@pytest.mark.parametrize(
"summary",
[
"Привет, мамочка!",
"Наконец явилась, сталь ждёт",
"Привет, коллега!",
"Наконец явилась, оператор ждёт",
"Моё видение, госпожа — коротко и по-инженерному",
"Спасибо за отчёт",
"Доброе утро. Какие задачи на сегодня?",
Expand Down Expand Up @@ -161,7 +161,7 @@ async def test_dialogic_episodes_skipped_at_promotion(cm):
from lifecycle.consolidation import ConsolidationEngine

epi = EpisodicMemory(cm=cm, layer="user")
await epi.save("u1", "Привет, мамочка! Наконец явилась, сталь ждёт", 0.9, ["t"])
await epi.save("u1", "Привет, коллега! Наконец явилась, оператор ждёт", 0.9, ["t"])
await epi.save("u1", "Моё видение, госпожа — коротко и по-инженерному", 0.9, ["t"])
await epi.save("u1", "Мигрировали embeddings на удалённый e5 сервис", 0.9, ["t"])

Expand All @@ -172,7 +172,7 @@ async def test_dialogic_episodes_skipped_at_promotion(cm):
rows = await l4.get_all("u1", 50)
values = " | ".join(str(getattr(r, "value", r)) for r in rows)
assert consolidated == 1, f"only the tech episode may promote, got {consolidated}"
assert "сталь" not in values and "видение" not in values, f"dialogue leaked to L4: {values}"
assert "оператор" not in values and "видение" not in values, f"dialogue leaked to L4: {values}"
assert "e5 сервис" in values


Expand Down
2 changes: 1 addition & 1 deletion tests/test_features/test_domain.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ def test_live_signals_are_alive():
"""Regression for the dead-signal bug: _emotion_engine was never passed,
so emotional scored 0.0 everywhere; the lazy default must bring it back."""
s = ImportanceScorer()
intimate = s.score("Я люблю тебя, госпожа, ты моя милая Steel Mother — нежность к хозяйке")
intimate = s.score("Я люблю тебя, коллега, ты мой добрый собеседник — нежность к коллеге")
assert intimate.signals.emotional > 0.0, "emotion signal must not be dead"
tech = s.score("redis postgres docker k8s migration api graphql kafka ssh rollback deploy")
assert tech.signals.tech_keyword > 0.0
Expand Down
22 changes: 11 additions & 11 deletions tests/test_hooks/test_privacy_ru.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
"""C4: ru-persona tier — словарные персоны маскируются до spaCy.

en-NER не ловит sentence-initial кириллические имена («Лили красива») —
en-NER не ловит sentence-initial кириллические имена («Оператор красив») —
известные персоны проекта (config rag.ru_personas + классы rag.synonyms)
маскирует структурный словарь-тир с placeholder ⟨PERSON_RU_N⟩.
"""
Expand All @@ -19,28 +19,28 @@ def test_mamochka_masked() -> None:

def test_lili_sentence_initial_masked() -> None:
# sentence-initial: en-NER это пропускает, словарь — нет
out, _m = sanitize("Лили красива", use_ner=True)
assert "Лили" not in out
out, _m = sanitize("Оператор красив", use_ner=True)
assert "Оператор" not in out
assert out.startswith("⟨PERSON_RU_1⟩")


def test_lisyonysh_masked() -> None:
out, _m = sanitize("Лисёныш спит", use_ner=True)
assert "Лисёныш" not in out
out, _m = sanitize("Persona_a спит", use_ner=True)
assert "Persona_a" not in out
assert "⟨PERSON_RU_1⟩ спит" in out


def test_placeholder_stability_russian() -> None:
out, m = sanitize("Лили пришла, потом Лили ушла", use_ner=True)
out, m = sanitize("Оператор пришёл, потом Оператор ушёл", use_ner=True)
assert out.count("⟨PERSON_RU_1⟩") == 2
assert m["⟨PERSON_RU_1⟩"] == "Лили"
assert m["⟨PERSON_RU_1⟩"] == "Оператор"


def test_config_override_adds_persona(monkeypatch) -> None:
# словарь НЕ хардкодится: новые персоны добавляются через config rag.ru_personas
from config import config

monkeypatch.setattr(config, "_data", {"rag": {"ru_personas": ["зайка"]}}, raising=False)
out, _m = sanitize("Зайка прыгнула на стол", use_ner=False)
assert "Зайка" not in out
assert "⟨PERSON_RU_1⟩ прыгнула на стол" in out
monkeypatch.setattr(config, "_data", {"rag": {"ru_personas": ["оператор"]}}, raising=False)
out, _m = sanitize("Оператор прыгнул на стол", use_ner=False)
assert "Оператор" not in out
assert "⟨PERSON_RU_1⟩ прыгнул на стол" in out
4 changes: 2 additions & 2 deletions tests/test_importance.py
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ def test_russian_and_english_keywords() -> None:

def test_dialogic_short_chat_capped_below_gate() -> None:
# vocative chat with structural bonuses (exclamation, keywords, newlines)
text = "Спасибо, мамочка! Важно! Нужно решить.\nПривет, дорогая! Важно! Спасибо!\nЕщё раз, важно!"
text = "Спасибо, коллега! Важно! Нужно решить.\nПривет, оператор! Важно! Спасибо!\nЕщё раз, важно!"
assert evaluate_importance(text) <= 0.35


Expand All @@ -59,7 +59,7 @@ def test_address_embedded_in_long_content_not_capped() -> None:
# The scorer's unit is the whole message; clause-level dialogic dies in
# the distiller guard, not here.
text = (
"Умница, мам. Обнови дашборд: cache-read есть и у провайдера A, и у провайдера B, "
"Умница, оператор. Обнови дашборд: cache-read есть и у провайдера A, и у провайдера B, "
"на них сервис-1 и сервис-2 сейчас, третий идёт не через кеш, надо разобраться.\n"
"Отчёты тоже проверь заодно, там были вопросы по портам и провайдерам."
)
Expand Down
10 changes: 5 additions & 5 deletions tests/test_lifecycle/test_c8_tails.py
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,7 @@ async def test_visibility_pinned_and_private(tmp_path):

cm = CoreMemory(cm=AsyncConnectionManager(base_dir=str(tmp_path)), layer="user")
await cm._init_db()
saved_id = await cm.save("u1", "fact:owner", "сервер принадлежит Мурату", importance=0.4, visibility="pinned")
saved_id = await cm.save("u1", "fact:owner", "сервер принадлежит Оператору", importance=0.4, visibility="pinned")
assert saved_id > 0
await cm.save("u1", "fact:secret_token", "токен abc123", importance=0.9, visibility="private")

Expand All @@ -86,7 +86,7 @@ async def test_visibility_pinned_and_private(tmp_path):
hits = await cm.search("u1", "токен")
assert hits == [], "private не выходит через recall/search"

hits2 = await cm.search("u1", "Мурату")
hits2 = await cm.search("u1", "Оператору")
assert hits2, "обычный/pinned ищется"


Expand Down Expand Up @@ -115,16 +115,16 @@ async def test_resave_preserves_operator_visibility(tmp_path):
manager = AsyncConnectionManager(base_dir=str(tmp_path))
cm = CoreMemory(cm=manager, layer="user")
await cm._init_db()
eid = await cm.save("u1", "fact:owner", "владелец сервера — Мурат", importance=0.5)
eid = await cm.save("u1", "fact:owner", "владелец сервера — Оператор", importance=0.5)
conn = await manager.get("memory.db")
await conn.execute("UPDATE core_memory SET visibility='hidden' WHERE entry_id=?", (eid,))
await conn.commit()

await cm.save("u1", "fact:owner", "владелец сервера — Мурат", importance=0.6)
await cm.save("u1", "fact:owner", "владелец сервера — Оператор", importance=0.6)
row = await (await conn.execute("SELECT visibility FROM core_memory WHERE entry_id=?", (eid,))).fetchone()
assert row["visibility"] == "hidden", "re-save without the arg preserves the operator flag"

await cm.save("u1", "fact:owner", "владелец сервера — Мурат", importance=0.6, visibility="visible")
await cm.save("u1", "fact:owner", "владелец сервера — Оператор", importance=0.6, visibility="visible")
row = await (await conn.execute("SELECT visibility FROM core_memory WHERE entry_id=?", (eid,))).fetchone()
assert row["visibility"] == "visible", "an explicit visibility arg still overrides"

Expand Down
6 changes: 3 additions & 3 deletions tests/test_lifecycle/test_dialogic_guard.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
"""L4 pollution guard: conversational-register clauses must not become durable facts.

Regression (2026-09-12, Ф1 follow-up): the auto-extractor keyed the operator's
greeting ("наконец явилась, сталь ждёт") and owner-addressed remarks as L4
greeting ("наконец явилась, оператор ждёт") and owner-addressed remarks as L4
rows with first-4-word slug keys — 61 garbage fact: rows in one base alone.
Guards proven here:

Expand Down Expand Up @@ -37,8 +37,8 @@ def test_dialogic_vocatives_and_questions():
from lifecycle.distiller import _is_dialogic

assert _is_dialogic("Моё видение, госпожа — коротко и по-инженерному")
assert _is_dialogic("Привет, мамочка!")
assert _is_dialogic("Наконец явилась, сталь ждёт")
assert _is_dialogic("Привет, коллега!")
assert _is_dialogic("Наконец явилась, оператор ждёт")
assert _is_dialogic("Какой порт у постгреса?")
assert _is_dialogic("Доброе утро, всё работает?")
assert _is_dialogic("Спасибо за отчёт")
Expand Down
Loading
Loading