feat(config,curator,recall): memory curator job on opus high, notes head inline in the envelope, portrait in the morning seed

This commit is contained in:
hh
2026-09-01 16:48:25 +02:00
parent 2fc84226f7
commit 548051ea7f
7 changed files with 375 additions and 27 deletions
+80
View File
@@ -0,0 +1,80 @@
from datetime import UTC, datetime, timedelta
from pathlib import Path
from curator import Watched, briefing, last_run, new_replies
def test_last_run_reads_first_line_or_default(tmp_path: Path) -> None:
default = datetime(2026, 9, 1, tzinfo=UTC)
assert last_run(tmp_path / "нет.md", default=default) == default
j = tmp_path / "куратор.md"
j.write_text(
"последний прогон: 2026-09-01T18:00:00+02:00\n\n# журнал\n", encoding="utf-8"
)
assert last_run(j, default=default) == datetime(2026, 9, 1, 16, 0, tzinfo=UTC)
j.write_text("# журнал без строки\n", encoding="utf-8")
assert last_run(j, default=default) == default
def test_briefing_lists_new_replies_and_changed_files_or_nothing(
tmp_path: Path,
) -> None:
vault = tmp_path
beaver = vault / "мета" / "бобер"
replies = beaver / "реплики"
notes = beaver / "наблюдения"
replies.mkdir(parents=True)
notes.mkdir()
(replies / "2026-09.md").write_text(
"# реплики\n\n- 2026-09-01 12:00 · мастер · старое\n- 2026-09-01 19:30 · мастер · новое сообщение\n",
encoding="utf-8",
)
journal = beaver / "куратор.md"
journal.write_text(
"последний прогон: 2026-09-01T18:00:00+02:00\n", encoding="utf-8"
)
old = notes / "Глафира - наблюдения.md"
old.write_text("# Глафира\n", encoding="utf-8")
import os
stale = (datetime(2026, 9, 1, 10, tzinfo=UTC)).timestamp()
os.utime(old, (stale, stale))
fresh = notes / "Зина - наблюдения.md"
fresh.write_text(
"# Зина\n## лента\n- 2026-09-01 · факт · [[2026-09-01]]\n", encoding="utf-8"
)
now = datetime(2026, 9, 1, 20, 0, tzinfo=UTC)
recent = (now - timedelta(minutes=30)).timestamp()
os.utime(fresh, (recent, recent))
text = briefing(
vault=vault,
journal=journal,
replies_dir=replies,
watched=[Watched("наблюдения", notes)],
tz="Europe/Warsaw",
now=now,
)
assert text is not None
assert "прошлый - 2026-09-01 18:00" in text
assert (
"Новые реплики Бобра (1):\n- 2026-09-01 19:30 · мастер · новое сообщение"
in text
)
assert "наблюдения: `мета/бобер/наблюдения/Зина - наблюдения.md`" in text
assert "Глафира" not in text
assert "последний прогон: 2026-09-01T22:00:00+02:00" in text
journal.write_text(f"последний прогон: {now.isoformat()}\n", encoding="utf-8")
assert new_replies(replies, now, "Europe/Warsaw") == []
later = now + timedelta(hours=1)
assert (
briefing(
vault=vault,
journal=journal,
replies_dir=replies,
watched=[Watched("наблюдения", notes)],
tz="Europe/Warsaw",
now=later,
)
is None
)
+38 -6
View File
@@ -4,7 +4,7 @@ from pathlib import Path
from beaver_gateway.core.conversations import UserSaid
from beaver_gateway.core.envelope import RecallContext
from recall import LABEL, Recall, ReplyLog, frontmatter_aliases, mentions
from recall import LABEL, Recall, ReplyLog, frontmatter_aliases, mentions, notes_head
def _card(path: Path, aliases: list[str] | None = None) -> None:
@@ -38,10 +38,17 @@ def _vault(tmp_path: Path) -> Recall:
notes = tmp_path / "мета" / "бобер" / "наблюдения"
notes.mkdir(parents=True)
(notes / "Зина - наблюдения.md").write_text(
"# Зина\n\n- 2026-08-26 · перенесла встречу на час · [[2026-08-26]]\n"
"# Зина - наблюдения\n\n## сейчас\n- просила перезвонить вечером 26.08\n"
"- ждёт ответа про поездку до четверга\n\n## паттерны\n"
"- переносит встречи: [[2026-08-10]], [[2026-08-26]]\n\n## факты\n\n## открытое\n\n"
"## лента\n- 2026-08-26 · перенесла встречу на час · [[2026-08-26]]\n"
"- 2026-08-31 · прислала список покупок · транскрипт\n",
encoding="utf-8",
)
(notes / "Прохор - наблюдения.md").write_text(
"# Прохор - наблюдения\n\n## сейчас\n\n## лента\n- 2026-08-07 · вернулся · [[2026-08-07]]\n",
encoding="utf-8",
)
boards = tmp_path / "📆 доски"
boards.mkdir()
(boards / "организация.md").write_text(
@@ -91,14 +98,24 @@ def test_block_points_to_card_notes_diary_and_deadlines_once(tmp_path: Path) ->
assert block is not None
lines = block.splitlines()
assert lines[0] == LABEL
assert lines[1].startswith(
"👤 Зина - карточка `👤 люди/личное/Зина.md`; записки `мета/бобер/наблюдения/Зина - наблюдения.md`: 2 стр., последняя: 2026-08-31 · четыре смягчения"
assert lines[1] == (
"👤 Зина - карточка `👤 люди/личное/Зина.md`; записки "
"`мета/бобер/наблюдения/Зина - наблюдения.md`: лента 2 стр., паттернов 1; "
"дневник: [[2026-08-31]], [[2026-08-26]], [[2026-08-13]]; сейчас:"
)
assert lines[1].endswith("дневник: [[2026-08-31]], [[2026-08-26]], [[2026-08-13]]")
assert lines[2:] == [
assert lines[2:4] == [
" - просила перезвонить вечером 26.08",
" - ждёт ответа про поездку до четверга",
]
assert lines[4:] == [
"📆 09-01 организация: заполнить файрфлай",
"📆 09-02 организация: витамины",
]
artem = recall.person_line(
next(p for p in recall.people.all() if p.name == "Прохор")
)
assert "лента 1 стр., последняя: 2026-08-07 · вернулся" in artem
assert "сейчас:" not in artem
again = recall.block(RecallContext(text="и ещё про Зину", kind="master", now=now))
assert again is not None and "📆" not in again
branch = recall.block(RecallContext(text="Фёдор звонил", kind="branch", now=now))
@@ -150,3 +167,18 @@ def test_reply_log_appends_one_line_per_message_and_strips_seed(tmp_path: Path)
"- 2026-09-01 14:04 · ветка «крипто-карта» · сравни Trustee и RedotPay\n"
)
assert date.fromisoformat("2026-09-01") # tz shift stays inside the month here
def test_notes_head_picks_sections_for_the_seed(tmp_path: Path) -> None:
recall = _vault(tmp_path)
path = recall.notes_dir / "Зина - наблюдения.md"
head = notes_head(path, ("сейчас", "паттерны"))
assert head == (
"## сейчас\n- просила перезвонить вечером 26.08\n"
"- ждёт ответа про поездку до четверга\n"
"## паттерны\n- переносит встречи: [[2026-08-10]], [[2026-08-26]]"
)
assert notes_head(path, ("факты",)) is None
assert notes_head(recall.notes_dir / "нет.md", ("сейчас",)) is None
short = notes_head(path, ("сейчас", "паттерны"), cap=2)
assert short is not None and short.endswith("… ещё 3 строк в файле")