feat(memory,jobs): replies and curator runs go to daily files
This commit is contained in:
1 parent
0273f29589
commit
dea793dbd7
6 files changed
+47
-16
No files matched your search
@@ -11,6 +11,7 @@ from beaver_agent.memory.curator import briefing
|
||||
from beaver_agent.memory.watch import CURATOR_WATCH
|
||||
from beaver_agent.vault import (
|
||||
CURATOR_JOURNAL,
|
||||
CURATOR_RUNS,
|
||||
DAYS,
|
||||
LEISURE,
|
||||
REPLIES,
|
||||
@@ -57,6 +58,7 @@ async def curate(run: JobRun) -> None:
|
||||
text = briefing(
|
||||
vault=VAULT,
|
||||
journal=CURATOR_JOURNAL,
|
||||
runs=CURATOR_RUNS,
|
||||
replies_dir=REPLIES,
|
||||
watched=CURATOR_WATCH,
|
||||
tz=TZ,
|
||||
|
||||
@@ -3,8 +3,9 @@
|
||||
Крон дёргает детерминированный код, модель судит: здесь собирается брифинг
|
||||
для служебного тёрна `beaver-curator` - новые реплики Бобра целиком и имена
|
||||
файлов, изменившихся с прошлого прогона. Время прошлого прогона куратор
|
||||
сам пишет первой строкой `мета/бобер/куратор.md`; нет ничего нового -
|
||||
брифинга нет и джоб не спавнится.
|
||||
сам пишет первой строкой `мета/бобер/куратор.md`, запись о прогоне - в файл
|
||||
дня `мета/бобер/куратор/YYYY-MM-DD.md`; нет ничего нового - брифинга нет и
|
||||
джоб не спавнится.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
@@ -46,13 +47,16 @@ def last_run(journal: Path, *, default: datetime) -> datetime:
|
||||
|
||||
|
||||
def new_replies(replies_dir: Path, since: datetime, tz: str) -> list[str]:
|
||||
"""Строки `реплики/YYYY-MM.md` позже ``since`` (время в строках локальное)."""
|
||||
"""Строки `реплики/YYYY-MM-DD.md` позже ``since`` (время в строках локальное)."""
|
||||
zone = ZoneInfo(tz)
|
||||
local_since = since.astimezone(zone).replace(tzinfo=None)
|
||||
out: list[str] = []
|
||||
if not replies_dir.exists():
|
||||
return out
|
||||
for path in sorted(replies_dir.glob("????-??.md"))[-2:]:
|
||||
first = f"{local_since:%Y-%m-%d}.md"
|
||||
for path in sorted(replies_dir.glob("????-??-??.md")):
|
||||
if path.name < first:
|
||||
continue
|
||||
for line in path.read_text(encoding="utf-8", errors="ignore").splitlines():
|
||||
m = REPLY_RE.match(line)
|
||||
if not m:
|
||||
@@ -91,6 +95,7 @@ def briefing(
|
||||
*,
|
||||
vault: Path,
|
||||
journal: Path,
|
||||
runs: Path,
|
||||
replies_dir: Path,
|
||||
watched: list[Watched],
|
||||
tz: str,
|
||||
@@ -111,12 +116,14 @@ def briefing(
|
||||
if not replies and not changed:
|
||||
return None
|
||||
zone = ZoneInfo(tz)
|
||||
local = now.astimezone(zone)
|
||||
head = (
|
||||
f"Прогон куратора {now.astimezone(zone):%Y-%m-%d %H:%M} ({tz}); прошлый - "
|
||||
f"Прогон куратора {local:%Y-%m-%d %H:%M} ({tz}); прошлый - "
|
||||
f"{since.astimezone(zone):%Y-%m-%d %H:%M}. Ниже - что изменилось с тех пор; "
|
||||
"остальное читай сам по слоям. В конце обнови первую строку "
|
||||
f"`{journal.relative_to(vault)}`: `последний прогон: "
|
||||
f"{now.astimezone(zone).isoformat(timespec='seconds')}`."
|
||||
"остальное читай сам по слоям. В конце запись о прогоне - в "
|
||||
f"`{(runs / f'{local:%Y-%m-%d}.md').relative_to(vault)}`, а в "
|
||||
f"`{journal.relative_to(vault)}` только первая строка: `последний прогон: "
|
||||
f"{local.isoformat(timespec='seconds')}`."
|
||||
)
|
||||
parts = [head]
|
||||
if replies:
|
||||
|
||||
@@ -6,7 +6,7 @@ Gateway путей не знает; здесь - что искать по тек
|
||||
дни дневника, где он встречался, и раз в день - подошедшие сроки из досок и
|
||||
ближайшая неделя календарей.
|
||||
``ReplyLog.write`` кладёт каждую реплику Бобра из мастера и веток в
|
||||
`мета/бобер/реплики/YYYY-MM.md` - единственное место, где они grep-абельны:
|
||||
`мета/бобер/реплики/YYYY-MM-DD.md` - единственное место, где они grep-абельны:
|
||||
транскрипт живёт в Postgres, а глубокие чаты и так лежат файлами в `💬 чаты/`.
|
||||
"""
|
||||
|
||||
@@ -388,7 +388,7 @@ def event_line(event: Event, zone: ZoneInfo) -> str:
|
||||
|
||||
@dataclass
|
||||
class ReplyLog:
|
||||
"""`мета/бобер/реплики/YYYY-MM.md`: строка на реплику Бобра из мастера и веток."""
|
||||
"""`мета/бобер/реплики/YYYY-MM-DD.md`: строка на реплику из мастера и веток."""
|
||||
|
||||
dir: Path
|
||||
tz: str = "Europe/Warsaw"
|
||||
@@ -416,11 +416,11 @@ class ReplyLog:
|
||||
if seeded:
|
||||
# не человек сказал, а механизм спавна: видно по источнику, не по тексту
|
||||
where = f"{where} (бриф)"
|
||||
path = self.dir / f"{local:%Y-%m}.md"
|
||||
path = self.dir / f"{local:%Y-%m-%d}.md"
|
||||
self.dir.mkdir(parents=True, exist_ok=True)
|
||||
if not path.exists():
|
||||
path.write_text(
|
||||
f"# реплики Бобра, {local:%Y-%m}\n\n"
|
||||
f"# реплики Бобра, {local:%Y-%m-%d}\n\n"
|
||||
"> пишет gateway: что Бобёр сказал в мастере и ветках, "
|
||||
"строка на сообщение. Глубокие чаты лежат в `💬 чаты/` сами.\n\n",
|
||||
encoding="utf-8",
|
||||
|
||||
@@ -18,6 +18,7 @@ CHATS = VAULT / "💬 чаты"
|
||||
DIARY = VAULT / "📅 дни"
|
||||
PEOPLE = VAULT / "👤 люди"
|
||||
BOARDS = VAULT / "📆 доски"
|
||||
THOUGHTS = VAULT / "🧠 мысли"
|
||||
|
||||
BEAVER = VAULT / "мета" / "бобер"
|
||||
"""Дом агента: всё, что он ведёт сам, лежит здесь."""
|
||||
@@ -31,6 +32,12 @@ NOTES = BEAVER / "наблюдения"
|
||||
PORTRAIT = NOTES / "Бобёр - наблюдения.md"
|
||||
REPLIES = BEAVER / "реплики"
|
||||
CURATOR_JOURNAL = BEAVER / "куратор.md"
|
||||
"""Первая строка - время прошлого прогона; сами записи лежат в `CURATOR_RUNS`."""
|
||||
CURATOR_RUNS = BEAVER / "куратор"
|
||||
OWN_DIARY = BEAVER / "дневник"
|
||||
"""Его собственный дневник, файл на день: то, что он думает, без логов."""
|
||||
JOURNAL = BEAVER / "журнал"
|
||||
"""Рабочий журнал, файл на день: следы тиков, логи, разбор промахов."""
|
||||
LEISURE = BEAVER / "досуг.md"
|
||||
"""Раскладка суток по тикам досуга: пишет её модель, механизм только читает."""
|
||||
STATE_DIR = Path(os.environ.get("BEAVER_STATE_DIR", BEAVER / ".состояние"))
|
||||
|
||||
@@ -25,7 +25,10 @@ def test_briefing_lists_new_replies_and_changed_files_or_nothing(
|
||||
notes = beaver / "наблюдения"
|
||||
replies.mkdir(parents=True)
|
||||
notes.mkdir()
|
||||
(replies / "2026-09.md").write_text(
|
||||
(replies / "2026-08-31.md").write_text(
|
||||
"# реплики\n\n- 2026-08-31 23:50 · мастер · вчерашнее\n", encoding="utf-8"
|
||||
)
|
||||
(replies / "2026-09-01.md").write_text(
|
||||
"# реплики\n\n- 2026-09-01 12:00 · мастер · старое\n- 2026-09-01 19:30 · мастер · новое сообщение\n",
|
||||
encoding="utf-8",
|
||||
)
|
||||
@@ -49,6 +52,7 @@ def test_briefing_lists_new_replies_and_changed_files_or_nothing(
|
||||
text = briefing(
|
||||
vault=vault,
|
||||
journal=journal,
|
||||
runs=beaver / "куратор",
|
||||
replies_dir=replies,
|
||||
watched=[Watched("наблюдения", notes)],
|
||||
tz="Europe/Warsaw",
|
||||
@@ -63,6 +67,7 @@ def test_briefing_lists_new_replies_and_changed_files_or_nothing(
|
||||
assert "наблюдения: `мета/бобер/наблюдения/Зина - наблюдения.md`" in text
|
||||
assert "Глафира" not in text
|
||||
assert "последний прогон: 2026-09-01T22:00:00+02:00" in text
|
||||
assert "запись о прогоне - в `мета/бобер/куратор/2026-09-01.md`" in text
|
||||
|
||||
journal.write_text(f"последний прогон: {now.isoformat()}\n", encoding="utf-8")
|
||||
assert new_replies(replies, now, "Europe/Warsaw") == []
|
||||
@@ -71,6 +76,7 @@ def test_briefing_lists_new_replies_and_changed_files_or_nothing(
|
||||
briefing(
|
||||
vault=vault,
|
||||
journal=journal,
|
||||
runs=beaver / "куратор",
|
||||
replies_dir=replies,
|
||||
watched=[Watched("наблюдения", notes)],
|
||||
tz="Europe/Warsaw",
|
||||
|
||||
+12
-3
@@ -168,8 +168,8 @@ def test_reply_log_appends_one_line_per_message_and_strips_seed(tmp_path: Path)
|
||||
at=at,
|
||||
)
|
||||
)
|
||||
text = (tmp_path / "реплики" / "2026-09.md").read_text(encoding="utf-8")
|
||||
assert text.startswith("# реплики Бобра, 2026-09\n")
|
||||
text = (tmp_path / "реплики" / "2026-09-01.md").read_text(encoding="utf-8")
|
||||
assert text.startswith("# реплики Бобра, 2026-09-01\n")
|
||||
assert text.endswith(
|
||||
"- 2026-09-01 14:04 · мастер · привет ⏎ ты меня любишь\n"
|
||||
"- 2026-09-01 14:04 · ветка «крипто-карта» (бриф) · "
|
||||
@@ -180,7 +180,16 @@ def test_reply_log_appends_one_line_per_message_and_strips_seed(tmp_path: Path)
|
||||
assert new_replies(tmp_path / "реплики", since, "Europe/Warsaw") == [
|
||||
line for line in text.splitlines() if line.startswith("- ")
|
||||
]
|
||||
assert date.fromisoformat("2026-09-01") # tz shift stays inside the month here
|
||||
late = datetime(2026, 9, 1, 22, 30, tzinfo=UTC)
|
||||
log.write(
|
||||
UserSaid(conversation_id="c1", kind="master", title=None, text="ночь", at=late)
|
||||
)
|
||||
night = (tmp_path / "реплики" / "2026-09-02.md").read_text(encoding="utf-8")
|
||||
assert night.endswith("- 2026-09-02 00:30 · мастер · ночь\n")
|
||||
assert new_replies(tmp_path / "реплики", late, "Europe/Warsaw") == []
|
||||
assert new_replies(
|
||||
tmp_path / "реплики", datetime(2026, 9, 1, 13, tzinfo=UTC), "Europe/Warsaw"
|
||||
) == ["- 2026-09-02 00:30 · мастер · ночь"]
|
||||
|
||||
|
||||
def test_notes_head_picks_sections_for_the_seed(tmp_path: Path) -> None:
|
||||
|
||||
Reference in new issue
Block a user