[2026-06-25] taiga-vault: family/how-to/htpc-emulators-setup.md family/how-to/htpc-gaming-plans.md family/how-to/kraken-access.md family/how-to/openmediavault-rpi5.md family/how-to/time-machine.md family/how-to/wireguard-vpn.md personal/documents/todo-list.md personal/plans/extract-stable-prompt-blocks.md personal/plans/hermes-whale-system-prompt.md personal/plans/thread-scoped-memory.md

This commit is contained in:
Taiga
2026-06-25 05:23:46 +00:00
parent ac0d753ec0
commit 16987d69f3
26 changed files with 2656 additions and 457 deletions
+69 -46
View File
@@ -393,6 +393,10 @@ CACHE="$HOME/.var/app/org.libretro.RetroArch/config/retroarch/system/Mupen64plus
## Ryujinx (Nintendo Switch)
<<<<<<< HEAD
**Версия:** 1.3.3 (Ryubing fork, Flatpak `io.github.ryubing.Ryujinx`)
**Статус:** ✅ Работает (22.06.2026)
=======
**Версия:** 1.3.3 (Ryubing fork, Flatpak `io.github.ryubing.Ryujinx`)
**Статус:** ✅ Работает (10.06.2026)
@@ -446,63 +450,82 @@ CACHE="$HOME/.var/app/org.libretro.RetroArch/config/retroarch/system/Mupen64plus
**Transmission:** `/data/Switch Roms` (case-sensitive, с пробелом) мапится в `/run/media/system/Data/Switch Roms/` через mount `/data``/run/media/system/Data`
>>>>>>> nas/main
→ Подробно: [[switch-emulation-rom-infra]]
### ⚠️ Текущий статус — 21.06.2026
### Компоненты
**Используется:** Flatpak `io.github.ryubing.Ryujinx` 1.3.3 stable.
**AppImage Canary 1.3.287** (`~/Applications/publish/Ryujinx`) — переименован в `Ryujinx.sh.bak`, чтобы launcher не подхватывал его (в 1.3.287 баг `ArgumentOutOfRangeException` в `CheckLaunchState()`, PR #7123/#7116).
| Компонент | Статус | Путь |
|-----------|--------|------|
| prod.keys | ✅ 12KB (22.1.0) | `~/.var/app/io.github.ryubing.Ryujinx/data/Ryujinx/system/` |
| title.keys | ❌ отсутствует | — |
| Firmware | ✅ 19.0.1 (229 .nca) | установлен через GUI |
| ROMs на диске | ✅ | `/run/media/system/Data/Switch Roms/` |
| Symlinks (плоские) | ✅ | `~/Emulation/roms/switch/` |
| В Steam (shortcuts.vdf) | ✅ 38 игр | launchoptions → плоские симлинки |
| Постеры SteamGridDB | ✅ все | `~/.steam/steam/userdata/147839491/config/grid/` |
**Проблема:** после копирования конфигов из AppImage → flatpak, Ryujinx просит prod.keys (хотя файл лежит в `data/Ryujinx/system/prod.keys`, 12KB). firmware 19.0.1 есть (229 .nca). `title.keys` отсутствует.
**Статус:** выясняется — возможно mismatch версий prod.keys (22.1.0) и firmware (19.0.1), либо flatpak sandbox не видит файлы.
### Launcher
**Launcher:** `~/Emulation/tools/launchers/ryujinx.sh` ищет `~/Applications/publish/Ryujinx.sh` → не находит (`.bak`) → запускает `flatpak run io.github.ryubing.Ryujinx`.
**Логирование:** `~/ryujinx-launch.log` — PWD, ARGS, EXE, EXIT_CODE.
`~/Emulation/tools/launchers/ryujinx.sh` ищет `~/Applications/publish/Ryujinx.sh` → не находит (`.bak`) → `flatpak run io.github.ryubing.Ryujinx`. Лог: `~/ryujinx-launch.log`.
**Ryujinx не поддерживает NSZ нативно** — только NSP и XCI. NSZ нужно конвертировать в NSP.
AppImage Canary 1.3.287 (`~/Applications/publish/Ryujinx`) переименован в `.bak` — баг `ArgumentOutOfRangeException` в `CheckLaunchState()`.
**NSZ → NSP конвертация (22.06.2026):**
Установлен `nsz` CLI (v4.6) на HTPC через `pip3 install nsz`.
**Правильная команда:** `nsz -D file.nsz` (decompress mode).
### NSZ конвертация
Проверено на The Disney Afternoon Collection — `nsz -D` верифицировал все NCA хеши (`[VERIFIED]`), файл 407MB PFS0 (7 files), работает в Ryujinx ✅.
**Ryujinx не поддерживает NSZ** — только NSP и XCI. Установлен `nsz` CLI (v4.6) на HTPC: `pip3 install nsz`.
Команда: `nsz -D file.nsz`. NSP создаётся рядом с NSZ. После:
1. Переименовать NSP в slug (короткое имя без пробелов/скобок)
2. Создать симлинк: `ln -sf "/run/media/system/Data/Switch Roms/…/slug.nsp" ~/Emulation/roms/switch/slug.nsp`
NSP создаётся рядом с NSZ, с тем же длинным именем (со скобками/размером). После декомпрессии:
1. Переименовать NSP в slug (короткое имя без пробелов/скобок) — `dk_country_returns_hd.nsp` и т.д.
2. Создать симлинк: `ln -sf /run/media/system/Data/Switch\ Roms/…/slug.nsp ~/Emulation/roms/switch/slug.nsp`
### Игры
**Статус:**
- The Disney Afternoon Collection — ✅ конвертирован, работает
- **NSZ на конвертацию (23 игры):**
| Игра | ROM slug | Статус | Формат | Steam |
|------|----------|--------|--------|-------|
| Animal Crossing New Horizons | `animal_crossing.nsp` | ✅ работает | NSP | ✅ |
| Donkey Kong Country Tropical Freeze | `donkey_kong.nsp` | ✅ работает | NSP | ✅ |
| Super Mario Odyssey | `mario_odyssey.nsp` | ✅ работает | NSP | ✅ |
| Mario Kart 8 Deluxe | `mario_kart_8.nsp` | ✅ работает | NSP | ✅ |
| The Legend of Zelda: Breath of the Wild | `zelda_botw.nsp` | ✅ работает | NSP | ✅ |
| Spyro Reignited Trilogy | `spyro_reignited_trilogy.nsp` | ✅ работает | NSP | ✅ |
| The Disney Afternoon Collection | `the_disney_afternoon_collection.nsp` | ✅ работает | NSP | ✅ |
| Sam & Max Save the World | `sam_and_max_save_the_world.nsp` | ✅ работает | NSP | ✅ |
| It Takes Two | `it_takes_two.nsp` | ⚠️ работает, но как Friends Pass (требуется полная версия) | NSP | ✅ |
| Little Nightmares | `little_nightmares.nsp` | ✅ работает | NSP | ✅ |
| Little Nightmares III | `little_nightmares_iii.nsp` | ✅ работает | NSP | ✅ |
| Luigi Mansion 3 | `luigi_mansion_3.nsp` | ❓ не тестировалась | NSP | ✅ |
| Sonic Mania | `sonic_mania.nsp` | ❓ не тестировалась | NSP | ✅ |
| Super Mario 3D All-Stars | `super_mario_3d_all_stars.nsp` | ❓ не тестировалась | NSP | ✅ |
| Super Mario Bros. Wonder | `super_mario_bros__wonder.nsp` | ❓ не тестировалась | NSP | ✅ |
| Super Mario Party | `super_mario_party.nsp` | ❓ не тестировалась | NSP | ✅ |
| Majora's Mask (2Ship2Harkinian) | `majoras_mask.nsp` | ❌ PAL_SEHException (GPU crash) | NSP | ✅ |
| Constance | `constance.nsp` | ❌ Application not found | NSP | — |
| Hollow Knight | `hollow_knight.nsp` | ❌ битый (hash error) | NSP | ✅ |
| Donkey Kong Country Returns HD | `donkey_kong_country_returns_hd.nsp` | ✅ NSP готов | NSP | ✅ |
| Hollow Knight: Silksong | `hollow_knight__silksong.nsp` | ✅ NSP готов | NSP | ✅ |
| Ori and the Will of the Wisps | `ori_and_the_will_of_the_wisps.nsp` | ✅ NSP готов | NSP | ✅ |
| Plants vs. Zombies: Replanted | `plants_vs__zombies__replanted.nsp` | ✅ NSP готов | NSP | ✅ |
| Super Mario 3D World + Bowser Fury | `super_mario_3d_world___bowser_fury.nsp` | ✅ NSP готов | NSP | ✅ |
| Super Smash Bros. Ultimate | `smash_ultimate.nsp` | ✅ NSP готов | NSP | ✅ |
| Unravel Two | `unravel_two.nsp` | ✅ NSP готов | NSP | ✅ |
| Vasilisa and Baba Yaga | `vasilisa_and_baba_yaga.nsp` | ✅ NSP готов | NSP | ✅ |
| Hunting Simulator 2 | `hunting_simulator_2.nsp` | ✅ NSP готов | NSP | ✅ |
| Carnivores Dinosaur Hunt | `carnivores_dinosaur_hunt.nsp` | ✅ работает | NSP | ✅ |
| America Wild Hunting | `america_wild_hunting.nsp` | ✅ NSP готов | NSP | — |
| Animal Hunting 3D | `animal_hunting_3d.nsp` | ✅ NSP готов | NSP | — |
| Best Sniper Legacy Dino Hunt Shooter 3D | `best_sniper_legacy.nsp` | ✅ NSP готов | NSP | — |
| Big Buck Hunter Arcade | `big_buck_hunter_arcade.nsp` | ✅ NSP готов | NSP | — |
| Big Buck Hunter Ultimate Trophy | `big_buck_hunter_ultimate_trophy.nsp` | ✅ NSP готов | NSP | — |
| Cabela's The Hunt Championship Edition | `cabelas_the_hunt.nsp` | ✅ NSP готов | NSP | — |
| Deer Drive Legends | `deer_drive_legends.nsp` | ✅ NSP готов | NSP | — |
| Duck Hunting Challenge | `duck_hunting_challenge.nsp` | ✅ NSP готов | NSP | — |
| Hunt | `hunt.nsp` | ✅ NSP готов | NSP | — |
| Hunter Simulator Wild Hunting | `hunter_simulator_wild_hunting.nsp` | ✅ NSP готов | NSP | — |
| Hunting Simulator | `hunting_simulator.nsp` | ✅ NSP готов | NSP | — |
| Werewolf Hunter Survive the Howl | `werewolf_hunter.nsp` | ✅ NSP готов | NSP | — |
**В Steam (10):**
1. Donkey Kong Country Returns HD
2. Hollow Knight: Silksong
3. Ori and the Will of the Wisps
4. Plants vs. Zombies: Replanted
5. Super Mario 3D World + Bowser Fury
6. Super Smash Bros. Ultimate
7. Unravel Two
8. Vasilisa and Baba Yaga
9. Hunting Simulator 2
10. Carnivores Dinosaur Hunt
**Не в Steam, в 14 Hunting games (12):**
11. America Wild Hunting
12. Animal Hunting 3D
13. Best Sniper Legacy Dino Hunt Shooter 3D
14. Big Buck Hunter Arcade
15. Big Buck Hunter Ultimate Trophy
16. Cabela's The Hunt Championship Edition
17. Deer Drive Legends
18. Duck Hunting Challenge
19. Hunt
20. Hunter Simulator Wild Hunting
21. Hunting Simulator
22. Werewolf Hunter Survive the Howl
**Битый (Hash error):**
- Hollow Knight — ❌
**Symlink схема:** плоские симлинки без пробелов в `~/Emulation/roms/switch/``/run/media/system/Data/Switch Roms/...`
**Transmission:** `/data/Switch Roms` мапится в `/run/media/system/Data/Switch Roms/` через mount `/data``/run/media/system/Data`
---
+3
View File
@@ -55,6 +55,9 @@
**Полное research + план действий: [[switch-emulation-rom-infra]]**
Приоритет 1: **Switch (DKC: Tropical Freeze)** — Ryujinx (GreemDev fork) через EmuDeck
### TODO:
- [ ] It Takes Two — смержить DLC Full Game (`010092A0172E5001`) с base NSP (сейчас работает как Friends Pass)
Приоритет 2: **Wii (DKC Returns оригинал)** — Dolphin через EmuDeck (проще, без keys)
**Важно:** оригинальный Ryujinx закрылся (окт. 2024). Актуальный — GreemDev fork. Eden — для Android.
+33 -81
View File
@@ -1,99 +1,51 @@
# Kraken — Внешний доступ
> Обновлено: 2026-05-13
> Обновлено: 2026-06-24
## SSH с любой машины
## Как зайти
**Контейнер hermes-kraken:**
- Маппинг `/home/kraken/.ssh:/opt/data/.ssh:ro` в docker-compose.yml
- Кастомный `/etc/passwd` с пользователем `kraken:x:1000:1000` (файл `passwd-with-kraken` рядом с compose)
- `HERMES_UID=1000 HERMES_GID=1000` чтоб entrypoint ремапил пользователя
- SSH из контейнера: `ssh -i /opt/data/.ssh/id_ed25519 kraken@localhost`
- Ключ `kraken` добавлен в `authorized_keys` на хосте
Везде `ssh kraken`. Резолвится через `~/.ssh/config` на Eagle:
`~/.ssh/config` на Eagle:
```
Host kraken
HostName kraken
User kraken
Port 22
IdentityFile ~/.ssh/id_rsa
StrictHostKeyChecking no
ProxyCommand bash -c 'if ifconfig | grep -q 10.99.0.2; then nc 10.99.1.2 22; else nc kraken 22; fi'
```
- **Дома** — напрямую по LAN (`192.168.1.15`)
- **Снаружи** — через WireGuard (`10.99.1.2`, VPN поднимается автоматически `wg-auto.sh`)
**SSH напрямую по LAN:**
```bash
ssh kraken@kraken
```
WireGuard split-tunnel: Eagle (10.99.0.2) ↔ VPS ↔ Kraken (10.99.1.2). Подробнее: [[wireguard-vpn]].
**SSH через VPS** (reverse tunnel, порт 2223):
```bash
ssh -J root@91.207.28.205 kraken@127.0.0.1 -p 2223
```
## Portainer (локально)
`kraken.qentra.top` — Cloudflare Tunnel, только HTTP (Hermes API), SSH не работает.
`kraken.qentra.top` — Cloudflare Tunnel, только HTTP (Hermes API), SSH не работает.
---
## Как устроен туннель
Кракен держит постоянный **reverse SSH tunnel** на VPS через autossh:
```
Кракен (RPi 5, 192.168.1.15)
└── autossh → ssh -R 2223:localhost:22 root@91.207.28.205
VPS (91.207.28.205)
слушает :2223 → перенаправляет на Кракен:22
```
Аналогично Тайге (у неё порт 2222).
### Управление туннелем на Кракене
```bash
sudo systemctl status kraken-tunnel
sudo systemctl restart kraken-tunnel
sudo journalctl -u kraken-tunnel -n 20
```
Файл сервиса: `/etc/systemd/system/kraken-tunnel.service`
SSH-ключ Кракена: `/home/kraken/.ssh/id_ed25519`
Публичный ключ добавлен в `/root/.ssh/authorized_keys` на VPS.
### Проверить с VPS
```bash
ssh root@91.207.28.205 "ss -tlnp | grep 2223"
```
---
## Cloudflared (для будущих сервисов)
Контейнер `cloudflared` запущен на Кракене (`unless-stopped`), туннель `kraken` активен в CF Zero Trust.
Hostname: `kraken.qentra.top → HTTP → localhost:8642` (Hermes API).
---
## Portainer API (локально)
`http://kraken:9000`
API:
```bash
BASE=http://localhost:9000
KEY="ptr_AJY+Ba9A7f6pcAHZfDD5koU4stkKgJCdbTEXLDLxn0g="
EP=3 # endpoint ID на Кракене = 3, не 1!
curl -s -H "X-API-Key: $KEY" "$BASE/api/endpoints/$EP/docker/containers/json?all=true" \
| jq '[.[] | {name: .Names[0], status: .Status}]'
```
---
## Hermes API
`http://kraken:8642/v1/chat/completions` — OpenAI-compatible endpoint.
## Docker контейнеры (2026-06-24)
| Имя | Заметки |
|-----|---------|
| flaresolverr | |
| hermes-kraken | |
| homeassistant | |
| jellyfin | |
| portainer | |
| prowlarr | |
| radarr | |
| rclone | |
| sonarr | |
| transmission | |
| cloudflared | |
| watchtower | |
## Связанные заметки
- [[kraken-portainer-access]] — полная инструкция по деплою cloudflared через Portainer
- [[openmediavault-rpi5]] — настройка Кракена (Docker, Hermes, структура папок)
- [[truenas-remote-access-reverse-proxy]] — аналогичный туннель для Тайги
- [[kraken-network]] — SSH, WG топология
- [[wireguard-vpn]] — полное описание WG
- [[kraken-portainer-access]]
- [[openmediavault-rpi5]]
+67 -3
View File
@@ -5,7 +5,7 @@ tags:
- openmediavault
- how-to
created: '2026-05-11'
updated: '2026-05-12'
updated: '2026-06-24'
status: hermes-pending
ip: 192.168.1.15
---
@@ -26,7 +26,7 @@ ip: 192.168.1.15
| 6 — USB HDD | ✅ | 12TB Seagate ST12000NT001, EXT4, UUID `49e8f586-3839-4c5d-a1e1-58bfc3579ade`, смонтирован через OMV в `/srv/dev-disk-by-uuid-49e8f586-3839-4c5d-a1e1-58bfc3579ade` |
| 7 — omv-extras + Docker | ✅ | omv-extras 7.0.6, Docker 29.4.3, kraken в группе docker |
| 8 — Контейнеры | ✅ | Все через отдельные docker compose, конфиги на HDD (см. ниже) |
| 9 — Time Machine share | ✅ | `mbentley/timemachine:smb`, расписание 10:00-22:00 GMT+6, HDD/timemachine |
| 9 — Time Machine share | ✅ | Samba на хосте (не Docker), шара TimeMachine, квота 2.8 TiB |
| 10 — Перенос данных | N/A | не входит в этот гайд |
| 11 — Hermes агент Кракен | ✅ | Запущен, Telegram @kraken_htpc_bot живой, mcpvault ✅, obsidian RW, SOUL.md, browser/Playwright ✅, Spotify ✅ |
| 11а — Docker на HDD | ✅ | data-root + containerd перенесены на HDD через symlink, SD освободилась до 22% |
@@ -276,11 +276,14 @@ read only = no
inherit acls = yes
fruit:time machine = yes
vfs objects = catia fruit streams_xattr
fruit:time machine max size = 3000000000000 # квота ~2.8 TiB
```
> **TM квота:** установлена 2026-06-24 — `3000000000000` байт (~2.8 TiB). Раньше было `0` (безлимит), из-за чего TM занял 2.2T из 11T HDD. При достижении квоты Mac начнёт удалять старые снепшоты.
**Credentials:**
- Пользователь: `timemachine`
- Пароль: сохранён в bitwarden (или уточнить у Alex)
- Пароль: `timemachine`
**Подключение с Mac:**
1. Finder → `⌘K` → `smb://kraken`
@@ -462,6 +465,67 @@ docker inspect hermes-kraken --format 'Status={{.State.Status}} Restarts={{.Rest
---
## Перенаправление записи с SD на HDD (2026-06-24)
**Проблема (2026-06-24):** Kraken крашился — SD-карта забита под 99% (14G/14G, 40MB свободно). folder2ram держит `/var/log` в tmpfs, но при ребуте/shutdown OMV сбрасывает tmpfs на SD.
**Решение:** bind-mount всех пишущихся каталогов на HDD.
| Каталог | Цель на HDD | Метод |
|---------|-------------|-------|
| `/var/folder2ram/var/log` | `.../var-log` | fstab bind |
| `/var/cache/apt` | `.../apt-cache` | fstab bind |
| `/var/log/journal` | `.../journal` | fstab bind |
| Docker data-root | `.../docker-data` | daemon.json |
| containerd | `.../containerd` | symlink |
### Команды
```bash
HDD="/srv/dev-disk-by-uuid-49e8f586-3839-4c5d-a1e1-58bfc3579ade"
# folder2ram/var/log
sudo mkdir -p "$HDD/var-log"
sudo rsync -a /var/folder2ram/var/log/ "$HDD/var-log/"
sudo mount --bind "$HDD/var-log" /var/folder2ram/var/log
echo "$HDD/var-log /var/folder2ram/var/log none bind,nofail 0 0" | sudo tee -a /etc/fstab
# Apt-кэш
sudo mkdir -p "$HDD/apt-cache"
sudo rsync -a /var/cache/apt/ "$HDD/apt-cache/"
sudo mount --bind "$HDD/apt-cache" /var/cache/apt
echo "$HDD/apt-cache /var/cache/apt none bind,nofail 0 0" | sudo tee -a /etc/fstab
# Journald
sudo mkdir -p "$HDD/journal"
sudo rsync -a /var/log/journal/ "$HDD/journal/" 2>/dev/null
sudo mount --bind "$HDD/journal" /var/log/journal
echo "$HDD/journal /var/log/journal none bind,nofail 0 0" | sudo tee -a /etc/fstab
sudo systemctl restart systemd-journald
```
Проверка:
```bash
df -h /var/folder2ram/var/log /var/log/journal /var/cache/apt
# Все должны показывать /dev/sda1 (HDD), не mmcblk0 (SD)
```
> ⚠️ OMV может перезаписывать `/etc/fstab` при `omv-salt deploy run`. Если bind пропадёт — перепроверить.
### df vs du — расхождение на SD
`df` = 13G/14G, `du -shx /` = 3.6G. Разница ~10G — **блоки ext4, занятые физически на NAND но не привязанные к файлам** (эффект отсутствия TRIM). На SD это норма.
Остаточная запись на SD после переносов: `/etc`, `/var/lib/samba` (8 MB), `/home/kraken` (uv кэш), dpkg — стабильно.
### SD backup
Скрипт: `/usr/local/bin/sd-backup.sh` — образ SD на HDD, хранит 3 последних копии.
```bash
sudo /usr/local/bin/sd-backup.sh # ~11G, ~11 мин
```
Хранится: `$HDD/backup/sd-card/kraken-sd-*.img.gz`. Ротация: auto-removes oldest.
## Шаг 11а — Docker и containerd на HDD
**Статус: ✅ (2026-05-13)**
+32 -4
View File
@@ -10,16 +10,20 @@ Destination: `smb://timemachine@kraken/TimeMachine`
Пароль: `smbpasswd -a timemachine`
passdb.tdb персистентен на хосте — пароль не слетает при рестарте
Бандл лежит на TrueNAS, смонтирован по SMB в Кракен.
Бандл лежит на локальном HDD Кракена: `/srv/dev-disk-by-uuid-49e8f586-3839-4c5d-a1e1-58bfc3579ade/timemachine/`
## Конфигурация OMV Samba
Шара добавлена через `extraoptions` в `conf.service.smb`:
```
[TimeMachine]
path = /mnt/timemachine
valid users = timemachine
...
path = /srv/dev-disk-by-uuid-49e8f586-3839-4c5d-a1e1-58bfc3579ade/timemachine
browseable = yes
read only = no
valid users = timemachine
vfs objects = fruit streams_xattr
fruit:time machine = yes
fruit:time machine max size = 3000000000000
```
Применить конфиг:
@@ -45,9 +49,33 @@ sudo tmutil setdestination -p smb://timemachine@kraken/TimeMachine
## Связанные заметки
- [[family/how-to/wireguard-vpn|WireGuard VPN]] — туннель для бэкапа вне дома
- [[family/how-to/kraken-network|Kraken Network]] — ssh, сетевые адреса
## Важно: связка WireGuard + SMB
Когда mbp-black не дома (IP не `192.168.1.x`), `kraken` резолвится через VPS dnsmasq
в `10.99.1.2` (WG адрес). SMB работает через туннель, но может быть медленнее.
Проверка доступа через WG с mbp-black:
```bash
smbutil view //timemachine@kraken
```
Если дома — лучше через LAN (`smb://192.168.1.15/TimeMachine`), быстрее.
## History
- **До 2026-06-02**: использовался Docker-контейнер `mbentley/timemachine:smb`.
Контейнер снесён, Samba перенесена на хост OMV из-за проблем с persisting паролей
(tdbsam не сохранялся, `ntlm auth = no` ломал backupd при reconnect).
- **2026-06-24**: проверена доступность smbd и бандла.
- **Проблема:** пароль `timemachine` не проходит при подключении через WireGuard (mbp-black
вне дома, IP `10.120.1.143`, Kraken резолвится в WG `10.99.1.2`). `mount_smbfs` и `smbutil`
выдают `Authentication error`.
- **Проверено на Kraken:** smbd слушает на `0.0.0.0:445`, TCP/445 через WG открыт,
`smbstatus` показывает активных сессий 0. Samba конфигурация: `disable netbios = yes`,
`log level = 0` (логи в /var/log/samba/log.smbd пустые — запросы аутентификации даже
не доходят до smbd, значит проблема на клиентской стороне).
- **Диагностика не завершена** — причина Authentication error не выяснена.
- **Доки обновлены:** убрано упоминание TrueNAS, прописан прямой пароль `timemachine`
в credentials в обоих документах (`time-machine.md` и `openmediavault-rpi5.md`).
+2 -1
View File
@@ -55,7 +55,8 @@ Destination обновлён:
- Старый: `smb://timemachine@kraken._smb._tcp.local./TimeMachine` (mDNS, не работает)
- Новый: `smb://timemachine@kraken/TimeMachine` (VPN DNS ✅)
Контейнер: `mbentley/timemachine:smb` на Кракене, работает постоянно (`restart: unless-stopped`).
Samba работает на **хосте OMV** (не в Docker-контейнере). См. [[family/how-to/time-machine]].
Контейнер `mbentley/timemachine:smb` больше не используется.
---
+1
View File
@@ -7,6 +7,7 @@ related:
- "[[personal/documents/deferred-tasks]]"
- "[[personal/documents/russia-tasks]]"
---
- [ ] Cleanup cloud.mail.ru
- [ ] https://x.com/i/status/2056077614113325373 Claude code free
- [x] жене SketchUp установить
- [ ] Eagle/Whale/Balda - в docker контейнеры
@@ -0,0 +1,133 @@
# План: Вынести хардкод stable-блоков system prompt в файлы
**Статус:** Реализовано ✅ (ждёт перезапуска webhook)
## Мотивация
В Hermes Whale все stable-блоки system prompt (identity, guidance, enforcement) захардкожены в `agent/prompt_builder.py` как Python-константы. Невозможно изменить их без редактирования исходного кода Hermes Agent.
## Решение
Вынести каждый блок в отдельный `.md` файл, добавить маппинг в `config.yaml: agent.prompt_overrides`, и модифицировать `agent/system_prompt.py`, чтобы он читал файлы вместо констант.
## Изменяемые файлы
### 1. `agent/system_prompt.py` — замена констант на file-load
Добавлена функция `_load_prompt_block(agent, block_name, default_text)` — строки 51-71 в `/Users/admin/.hermes/hermes-agent/agent/system_prompt.py`:
- Читает `agent._prompt_overrides` (берётся из конфига на старте)
- Если для `block_name` указан путь — читает файл, возвращает его содержимое
- Иначе возвращает `default_text`
Заменены все 7 прямых ссылок на константы в `build_system_prompt_parts()` на вызовы `_load_prompt_block()`.
Добавлены импорты: `import logging`, `import os`, `from pathlib import Path`, `logger = logging.getLogger(__name__)`.
**Константы, которые заменяются (7 блоков):**
| Блок | Константа | Вставляется при условии |
|------|-----------|------------------------|
| `hermes_help` | `HERMES_AGENT_HELP_GUIDANCE` | всегда |
| `task_completion` | `TASK_COMPLETION_GUIDANCE` | всегда |
| `memory_guidance` | `MEMORY_GUIDANCE` | когда есть tool "memory" |
| `session_search_guidance` | `SESSION_SEARCH_GUIDANCE` | когда есть tool "session_search" |
| `skills_guidance` | `SKILLS_GUIDANCE` | когда есть tool "skill_manage" |
| `tool_use_enforcement` | `TOOL_USE_ENFORCEMENT_GUIDANCE` | зависит от модели |
| `execution_discipline` | `OPENAI_MODEL_EXECUTION_GUIDANCE` | зависит от модели |
**Не заменяется (остаётся в коде):**
- `DEFAULT_AGENT_IDENTITY` — это fallback когда нет SOUL.md (у нас есть SOUL.md, не нужно)
- `GOOGLE_MODEL_OPERATIONAL_GUIDANCE` — Google-specific, неактуально для Whale
- `COMPUTER_USE_GUIDANCE` — нет toolset
- `KANBAN_GUIDANCE` — нет kanban
- `PLATFORM_HINTS` — platform-specific, другая логика
### 2. `agent/system_prompt.py` — добавить функцию загрузки
```python
def _load_prompt_block(agent, block_name: str, default: str) -> str:
"""Load a prompt block from a file if configured, else return default."""
overrides = getattr(agent, "_prompt_overrides", None) or {}
path = overrides.get(block_name)
if path:
try:
resolved = os.path.expanduser(path)
content = Path(resolved).read_text(encoding="utf-8").strip()
if content:
return content
except Exception:
logger.debug("Could not load prompt override '%s' from %s", block_name, path)
return default
```
### 3. `agent/agent_init.py` — пробросить конфиг
После загрузки `_agent_cfg` (строка ~1058) добавлено чтение `agent.prompt_overrides`:
```python
agent._prompt_overrides = {}
try:
_po = _agent_cfg.get("agent", {}).get("prompt_overrides", {})
if isinstance(_po, dict):
agent._prompt_overrides = _po
except Exception:
pass
```
### 4. Конфиг Whale — `config.yaml`
Добавить секцию:
```yaml
agent:
prompt_overrides:
hermes_help: ~/.hermes/hermes-whale/review/hermes_help.md
task_completion: ~/.hermes/hermes-whale/review/task_completion.md
memory_guidance: ~/.hermes/hermes-whale/review/memory_guidance.md
session_search_guidance: ~/.hermes/hermes-whale/review/session_search_guidance.md
skills_guidance: ~/.hermes/hermes-whale/review/skills_guidance.md
tool_use_enforcement: ~/.hermes/hermes-whale/review/tool_use_enforcement.md
execution_discipline: ~/.hermes/hermes-whale/review/execution_discipline.md
```
### 5. Файлы блоков
Создать 7 файлов в `~/.hermes/hermes-whale/review/`:
- `hermes_help.md` — содержимое константы `HERMES_AGENT_HELP_GUIDANCE`
- `task_completion.md` — содержимое `TASK_COMPLETION_GUIDANCE`
- `memory_guidance.md` — содержимое `MEMORY_GUIDANCE`
- `session_search_guidance.md` — содержимое `SESSION_SEARCH_GUIDANCE`
- `skills_guidance.md` — содержимое `SKILLS_GUIDANCE`
- `tool_use_enforcement.md` — содержимое `TOOL_USE_ENFORCEMENT_GUIDANCE`
- `execution_discipline.md` — содержимое `OPENAI_MODEL_EXECUTION_GUIDANCE`
### 6. Обратная совместимость
Если `prompt_overrides` не задан или файл не найден — используется хардкод. Никакой код не ломается для других профилей/пользователей.
### 7. Документация
- Обновить `personal/plans/thread-scoped-memory.md` → переименовать или создать отдельный doc
- Создать `personal/plans/extract-stable-prompt-blocks.md` (этот)
## Порядок выполнения
1. ✅ Создать 7 файлов блоков в `~/.hermes/hermes-whale/review/`
2. ✅ Пропатчить `agent/system_prompt.py` — добавить `_load_prompt_block()` и заменить 7 констант
3. ✅ Пропатчить `agent/agent_init.py` — пробросить `prompt_overrides` из конфига
4. ✅ Обновить `config.yaml` — добавить `agent.prompt_overrides` (через копию, т.к. patch блокирован TIRITH)
5. ⏸️ Перезапустить webhook (ждёт команды)
6. ⏸️ Обновить Obsidian docs (этот шаг)
## Проверка
После изменений system prompt должен содержать те же блоки, что и раньше, но загруженные из файлов. При изменении файла и рестарте сессии — новый текст. При удалении файла — fallback на хардкод.
## Pitfalls
- **TIRITH блокирует patch/config.yaml** — `config.yaml` под защитой TIRITH, patch и write_file отказываются писать в него. Решение: `cp` в `/tmp/`, отредактировать там, `cp` обратно (с аппрувом).
- **sed не подходит для YAML конфигов** — сложные многострочные замены с вложенными отступами и escape-символами (`~`, `/`) ломаются в sed. Лучше patch.
- **Конфиг всё равно просит аппрув** — при `cp` обратно TIRITH запрашивает подтверждение (overwrite project env/config file). Это нормально, нужно подтвердить.
- **SOUL.md уже существует** — в Whale он лежит в `/Users/admin/.hermes/hermes-whale/SOUL.md`. Он НЕ выносится через prompt_overrides, т.к. уже является файлом и загружается отдельно через `load_soul_md()`.
- **webhook не в PLATFORM_HINTS** — webhook нет в словаре `PLATFORM_HINTS` в `prompt_builder.py`, поэтому блок platform hints пустой. Это не менялось.
@@ -0,0 +1,104 @@
# Состав system prompt Hermes Whale
**Дата анализа:** 2026-06-24
**Версия:** Текущее состояние на момент разговора
System prompt собирается в `agent/system_prompt.py` (Hermes Agent) из трёх слоёв: **stable**, **context**, **volatile**.
---
## Полный состав
### СТАБИЛЬНЫЙ СЛОЙ (stable) — кешируется на всю сессию
Этот слой не меняется между поворотами. Всё, кроме SOUL.md, захардкожено в `agent/prompt_builder.py`.
| # | Блок | Источник | Описание |
|---|------|----------|----------|
| 1 | **SOUL.md** | `~/.hermes/hermes-whale/SOUL.md` | Кастомная идентичность: «Кит (Whale) — Alex's personal agent». Правила: Obsidian MCP, jq, backup before edit, plan first, stop on стоп. |
| 2 | **Hermes Agent help guidance** | `prompt_builder.py`: `HERMES_AGENT_HELP_GUIDANCE` | docs.hermes-agent.nousresearch.com — source of truth |
| 3 | **Task completion guidance** | `prompt_builder.py`: `TASK_COMPLETION_GUIDANCE` | «Finishing the job» — доводить до конца, не фабриковать |
| 4 | **Memory guidance** | `prompt_builder.py`: `MEMORY_GUIDANCE` | Как сохранять memory (declarative facts, не task progress) |
| 5 | **Session search guidance** | `prompt_builder.py`: `SESSION_SEARCH_GUIDANCE` | session_search для кросс-сессионного контекста |
| 6 | **Skills guidance** | `prompt_builder.py`: `SKILLS_GUIDANCE` | Сохранять сложные подходы как skills, патчить устаревшие |
| 7 | **Tool-use enforcement** | `prompt_builder.py`: `TOOL_USE_ENFORCEMENT_GUIDANCE` | Ты ДОЛЖЕН вызывать инструменты, не описывать планы |
| 8 | **Execution discipline** | `prompt_builder.py`: `OPENAI_MODEL_EXECUTION_GUIDANCE` | Расширенные правила: tool persistence, prerequisite checks, verification |
| 9 | **Skills prompt** | `prompt_builder.py`: `build_skills_system_prompt()` | Список всех доступных skills (автогенерируется) |
| 10 | **Environment hints** | `prompt_builder.py`: `build_environment_hints()` | macOS, home dir, cwd |
| 11 | **Active profile hint** | `system_prompt.py` runtime | «default» + guard не лезть в чужие профили |
| 12 | **Platform hints** | `prompt_builder.py`: `PLATFORM_HINTS` | webhook **нет** в словаре → пусто |
**Note #78:** Т.к. модель `deepseek-chat` попадает под `TOOL_USE_ENFORCEMENT_MODELS = ("gpt", "codex", "gemini", "gemma", "grok", "glm", "qwen", "deepseek")`, то enforcement применяется. DeepSeek НЕ входит в OpenAI-специфичную часть (GPT/Codex/Grok), поэтому блок `OPENAI_MODEL_EXECUTION_GUIDANCE` не вставляется — только `TOOL_USE_ENFORCEMENT_GUIDANCE`.
### КОНТЕКСТНЫЙ СЛОЙ (context) — зависит от cwd
| # | Блок | Что приходит |
|---|------|-------------|
| 13 | **AGENTS.md / CLAUDE.md / .cursorrules / .hermes.md** | Из `TERMINAL_CWD`. В этой сессии `TERMINAL_CWD=/Users/admin` — файлов нет → пусто |
| 14 | **system_message** | Caller-supplied. Webhook не передаёт → пусто |
Поиск контекстных файлов (функция `build_context_files_prompt()`):
1. `.hermes.md` / `HERMES.md` (walk to git root)
2. `AGENTS.md` / `agents.md` (cwd only)
3. `CLAUDE.md` / `claude.md` (cwd only)
4. `.cursorrules` + `.cursor/rules/*.mdc` (cwd only)
— Первый найденный wins, только один проект-контекст загружается.
SOUL.md из HERMES_HOME независим и не участвует в этой очереди — он загружается отдельно через `load_soul_md()`.
### ВОЛАТИЛЬНЫЙ СЛОЙ (volatile) — пересобирается каждый раз
| # | Блок | Источник | Описание |
|---|------|----------|----------|
| 15 | **MEMORY** | `memories/threads/<gateway_session_key>/MEMORY.md` | Thread-scoped memory (включено конфигом) |
| 16 | **USER profile** | `memories/USER.md` | Глобальный профиль пользователя |
| 17 | **Memory instruction** | `~/.hermes/hermes-whale/review/memory_prompt.md` | Кастомная инструкция что запоминать |
| 18 | **Timestamp/model/provider** | Runtime | Дата старта, Model, Provider |
---
## Ключевые файлы
### SOUL.md
- Путь: `~/.hermes/hermes-whale/SOUL.md` (HERMES_HOME/SOUL.md)
- Полностью заменяет `DEFAULT_AGENT_IDENTITY`
- Правила: Obsidian MCP first, jq, backup before edit, plan first, stop on стоп
- Загружается `load_soul_md()` в `agent/prompt_builder.py`
### memory_prompt.md
- Путь: `~/.hermes/hermes-whale/review/memory_prompt.md`
- Конфиг: `memory.prompt_path` в config.yaml
- Загружается в `agent/agent_init.py`, вставляется в volatile слой
### config.yaml (Whale)
- Путь: `~/.hermes/hermes-whale/config.yaml`
- Релевантные секции:
```yaml
memory:
thread_scoped: true
prompt_path: ~/.hermes/hermes-whale/review/memory_prompt.md
```
- Webhook route: `whale` на порту 8645, deliver в zulip
- Webhook платформа **не имеет** platform hint в `PLATFORM_HINTS`
---
## Отсутствующие возможности (opportunity)
Алекс предложил сделать стабильный слой конфигурируемым через файлы так же, как `memory_prompt.md`:
- Вынести каждый хардкод-блок (hermes help, task completion, memory guidance, session search, skills guidance и т.д.) в отдельный `.md` файл
- Добавить `prompt_overrides` секцию в config.yaml, где указывать пути к файлам
- Если файла нет — fallback на хардкод
Связанные файлы Hermes Agent:
- `agent/system_prompt.py` — сборка system prompt, управляющая логика
- `agent/prompt_builder.py` — константы и функции загрузки
- `agent/agent_init.py` — чтение конфига и проброс
---
## Текущие ограничения
- webhook не имеет platform hint → может быть полезно добавить
- Всё хардкоженое в `prompt_builder.py` нельзя переопределить без редактирования кода
- Нет `.hermes.md` / `AGENTS.md` в `/Users/admin/` — контекстный слой пуст
+122
View File
@@ -0,0 +1,122 @@
# Реализация: Thread-scoped memory + custom memory prompt
**Статус:** Реализовано ✅
## Изменённые файлы
### 1. `tools/memory_tool.py` — MemoryStore с thread_key
**`__init__`** — новый параметр `thread_key: Optional[str] = None`, сохраняется как `self.thread_key`.
**`_path_for(target)`** — теперь instance method (был static):
- `target == "user"` → всегда `memories/USER.md`
- `target == "memory"` и `self.thread_key` задан → `memories/threads/<key>/MEMORY.md`
- `target == "memory"` без thread_key → `memories/MEMORY.md` (глобальный fallback)
**`load_from_disk()`** — использует `self._path_for("memory")` и `self._path_for("user")` вместо хардкода.
### 2. `agent/agent_init.py` — конфиг + проброс
Новые поля на агенте:
- `_memory_thread_scoped` — читается из `config.yaml: memory.thread_scoped`
- `_memory_instruction` — читается из `config.yaml: memory.prompt_path` (.md файл)
MemoryStore создаётся с `thread_key=_gateway_session_key` если `thread_scoped: true`.
Кастомная memory instruction логируется: `Loaded custom memory instruction from ...`.
### 3. `agent/system_prompt.py` — вставка в system prompt
В блок MEMORY добавляется:
- Заголовок `MEMORY for thread: <gateway_session_key>` (вместо `MEMORY (your personal notes)`) когда thread_scoped включён и есть ключ.
После memory блока вставляется отдельный блок `MEMORY INSTRUCTION` (с опциональным `for thread: <key>`), содержащий кастомную инструкцию из .md файла.
### 4. `gateway/run.py` — уже пробрасывает
`gateway_session_key` уже передаётся в `AIAgent.__init__` на строке ~17828. Никаких изменений не потребовалось.
### 5. `run_agent.py` — уже принимает
Параметр `gateway_session_key` уже есть в `AIAgent.__init__`. Пробрасывается в `init_agent()` где записывается как `agent._gateway_session_key`.
## Конфиг (Whale)
```yaml
memory:
...
thread_scoped: true
prompt_path: ~/.hermes/hermes-whale/review/memory_prompt.md
```
## Файлы
- **`~/.hermes/hermes-whale/review/memory_prompt.md`** — инструкция что запоминать (документы, команды, конфиги, статус проекта, решения).
- После добавления нового пункта в список patch() не перенумеровывает — нужен второй clean patch.
- **2026-06-24:** Добавлен пункт 2 — после загрузки Obsidian docs (skill_view, mcp_obsidian_read_note) извлекать ключевые факты в memory.
## Файловая структура на диске
```
~/.hermes/hermes-whale/memories/
├── MEMORY.md # глобальная (fallback для CLI/старых сессий)
├── USER.md # глобальная (всегда)
└── threads/
├── agent:main:webhook:webhook:webhook:whale/
│ └── MEMORY.md # память Whale
├── agent:main:zulip:stream:general:thread:123/
│ └── MEMORY.md # память конкретного треда
└── ...
```
## Коммиты
- `hermes-agent`: `98cb69b50` — feat: thread-scoped memory + configurable memory instruction
- `hermes-agent`: `4ebad4f69` — test: thread-scoped memory persistence, drift guard, snapshot, sanitization (+9 тестов, 142 строки)
- `hermes-whale`: `736f8f3` — whale: enable thread-scoped memory and custom memory instruction
## Тесты
9 тестов в `tests/tools/test_memory_tool.py` (всего 76 в файле, 76/76 passed):
**Persistence:**
- `test_thread_scoped_memory_writes_separate_file` — global и thread пишутся в разные файлы
- `test_user_stays_global_with_thread_key` — USER.md всегда глобальный, не залезает в `threads/`
- `test_thread_and_global_are_independent_on_load` — загрузка thread не видит global entries и vice versa
- `test_thread_key_none_falls_back_to_global` — backward compat: без thread_key пишет в `memories/MEMORY.md`
**Snapshot:**
- `test_snapshot_reflects_thread_scoped_path``format_for_system_prompt` берёт данные из thread-файла
- `test_snapshot_from_thread_and_global_are_independent` — thread snapshot изолирован от global
**Drift guard:**
- `test_drift_guard_with_thread_key``_detect_external_drift` работает на thread-scoped MEMORY.md
**Sanitization:**
- `test_load_time_sanitization_with_thread_key` — poisoned entry в thread блокируется на уровне snapshot
**Pitfalls:**
- `pytest-timeout` плагин не установлен, но `pyproject.toml` содержит `addopts = "--timeout=30"`. Запуск падает с `unrecognized arguments`. Используй `-o "addopts="` для override.
- Drift guard на thread: нужен блок > `memory_char_limit` (дефолт 2200), иначе `_detect_external_drift` не находит entry-size overflow. В тесте `"x" * 2300`.
## Тесты
- **76/76 passed** (из них 9 новых для thread_key, добавлены в `4ebad4f69`)
- **9 новых тестов:**
- `test_thread_scoped_memory_writes_separate_file` — разные файлы для global/thread
- `test_user_stays_global_with_thread_key` — USER.md не уходит в threads/
- `test_thread_and_global_are_independent_on_load` — не пересекаются при чтении
- `test_thread_key_none_falls_back_to_global` — backward compat
- `test_snapshot_reflects_thread_scoped_path` — форматирует snapshot из thread файла
- `test_snapshot_from_thread_and_global_are_independent` — не подхватывает global entry
- `test_drift_guard_with_thread_key` — детекция внешней модификации на thread файле
- `test_load_time_sanitization_with_thread_key` — poisoned entry блокируется в thread snapshot
- `test_already_blocked_entry_passes_through` — no double-wrap (расширен)
- Запуск: `cd ~/.hermes/hermes-agent && source venv/bin/activate && python -m pytest tests/tools/test_memory_tool.py -v -o "addopts="`
## Неизменённое
- `run_conversation` / `conversation_loop.py` — не трогали
- `background_review.py` — наследует `_memory_store` от родителя, thread_key приходит автоматически
- `tools/memory_tool.py` schema/MEMORY_SCHEMA — не меняли, кастомная инструкция в system prompt
- External memory providers (honcho/mem0) — не трогали
+160 -172
View File
@@ -1,197 +1,185 @@
# Балда / Валера — эксплуатация
# Балда / Валера — эксплуатация & MCP debug
## Расположение
- Repo: `~/Developer/balda/` (ветка `main`)
- Code: upstream `normahq/balda` + `feat/zulip-transport` влит, `normahq/norma` v0.0.10
- **Рабочий compose**: `~/Docker/balda-agent/docker-compose.yaml` → контейнер `balda-agent-valera-1`
- Config (persistent): `~/Docker/balda-agent/.config/balda/config.yaml`
- Env: `~/Docker/balda-agent/.env`
- Owner: `allowed_owners` в config.yaml (статически, `/start owner=` не нужен)
- **Валера** = `balda-agent-valera-1`, конфиг: `~/Docker/balda-agent/.config/balda/config.yaml`
- **Клавдий** = `claudio-agent-claudio-1`, конфиг: `~/Docker/claudio-agent/.config/balda/config.yaml`
- **Общий образ**: собирается из `~/Docker/claudio-agent/Dockerfile.claudio`, контекст `/Users/admin/Developer`
- **Norma-local** (форк с MCP tools): `~/Developer/norma-local/`
- `go.mod` replace: `github.com/normahq/norma => ./norma-local``~/Developer/balda/go.mod`)
- Модифицирован `pkg/runtime/hostedagent/openai.go` + `pkg/runtime/agentfactory/agentfactory.go`
- **Dockerfile**: `~/Docker/claudio-agent/Dockerfile.claudio`
- Копирует `norma-local/` в `/src/norma-local/`
- `go mod edit -replace github.com/normahq/norma=./norma-local` перед `go mod download`
## Запуск / рестарт
## Архитектура
- **Валера** = DeepSeek (`provider: deepseek`, модель `deepseek-chat`)
- **Клавдий** = Claude через прокси (`provider: claude`, `claude-sonnet-4-6`)
- Валера использует **Balda runtime** — OpenAIModel из openai.go вызывается (hostedagent провайдер).
- `agentfactory.go` используется обоими — там лог версии на старте.
## Версионный лог (norma-tools)
При старте Валеры в логах:
```
norma-tools version=v0.0.10 build=whale-YYYYMMDD-N
```
Где:
- `version` — номер версии нормы (из `openAIVersion`)
- `build` — тег сборки (из `buildTag`)
**Перед каждым билдом апать `buildTag`** в `openai.go`:
```go
const openAIVersion = "v0.0.10"
const buildTag = "whale-YYYYMMDD-N" // ← менять!
```
Файлы где апать:
- `~/Developer/norma-local/pkg/runtime/hostedagent/openai.go``buildTag` константа
## Статус: DeepSeek MCP tools — РАБОТАЕТ
### Что сделано (openai.go)
Добавлена полная поддержка OpenAI tool_calls:
- `openAIToolDefinition`, `openAIFunction`, `openAIToolCall` — структуры
- `Tools []openAIToolDefinition` в `openAIChatRequest`
- `openAIToolsFromConfig()` — конвертация genai.Tool[] → OpenAI definitions
- `parseChatResponse()` — парсинг tool_calls из ответа DeepSeek (ID сохраняется)
- `contentToOpenAI()` — конвертация FunctionCall/FunctionResponse в историю
- `generate()` — TurnComplete=false при tool_calls (ADK делает второй раунд)
### Исправленные проблемы
#### 1. DeepSeek возвращает аргументы с двойной сериализацией ✓ 22.06.2026
**Симптом:** `read_note`/`write_note` падали — Obsidian MCP возвращал `"Cannot read properties of undefined (reading 'replace')"`.
**Корень:** DeepSeek возвращает `function.arguments` как JSON-строку (экранированную), а не как JSON-объект.
**Фикс в `parseChatResponse`:** сперва пробуем распарсить Arguments как строку (`json.Unmarshal(&argsStr)`), потом эту строку как объект (`json.Unmarshal([]byte(argsStr), &args)`).
#### 2. Obsidian MCP: get_vault_stats работает, read/write/delete нет ✓ 22.06.2026
**Корень:** двойная сериализация аргументов (см. проблему 1). После её фикса всё работает.
#### 3. Key `output` не проверялся в contentToOpenAI ✓ 22.06.2026
Obsidian MCP возвращает response как `{"output":"..."}` — добавлена проверка на ключ `output`.
### Текущий билд
- buildTag: `whale-20260622-7`
- Все фиксы закоммичены в `master` (норма-локаль)
- OPENAI-DEBUG стэш дропнут — логи в рабочей копии (не коммитятся)
- Replace на норму работает через `go.mod` + Dockerfile
- `norma-tools` лог подтверждён: `time=2026-06-22T13:00:37.170Z level=INFO msg=norma-tools version=v0.0.10 build=whale-20260622-7`
## Промежуточные статусы — ПОЧИНЕНО (23.06.2026)
**Статус:** работает.
- `RunSessionTurnPayload` (zulip_handler.go) — публикует промежуточный текст и ⚙️ function call статусы для `!ev.TurnComplete` ивентов через `sendPlain`.
- `balda.go` (Telegram) — те же промежуточные публикации.
- `handleAutoClaimMention` / `handleMessage` / `enqueueTurn` — передают `messageID` для точного Zulip threading.
- Коммит: `05159d1` (main), запушен.
## Стэши debug-логов (23.06.2026)
Дебаг логи не коммитятся, хранятся в стэшах.
**balda** (`~/Developer/balda`):
- `stash@{0}`: `debug: whale-20260623 — TASK-ACTOR logging in swarm_task_actor, event count & response_len debug in zulip_handler`
- `internal/apps/balda/actors/swarm_task_actor.go` — TASK-ACTOR: dispatching/dispatch OK/FAILED
- `internal/apps/balda/handlers/zulip_handler.go` — eventCount, response_len, running session turn log
- `stash@{1}`: WIP on feat/zulip-events-polling
**Восстановление balda стэша:**
```bash
cd ~/Docker/balda-agent
docker-compose restart # без пересборки, только конфиг/env не менялись
docker-compose up -d # пересоздать контейнер, перечитать .env
# Rebuild после изменений в коде:
cd ~/Docker/claudio-agent && docker-compose build
docker tag claudio-agent-claudio:latest balda-agent-valera:latest
cd ~/Docker/claudio-agent && docker-compose up -d
cd ~/Docker/balda-agent && docker-compose up -d
cd ~/Developer/balda && git stash pop stash@{0}
```
> Валера и Клавдий — **один образ** (`balda-agent-valera`), собирается из одного Dockerfile (`Dockerfile.claudio`). Разница только в конфиге (`config.yaml`) и `.env`, которые монтируются volumes. Сборка идёт из `~/Docker/claudio-agent/` — там есть build секция.
**norma-local** (`~/Developer/norma-local`):
- OPENAI-DEBUG стэш дропнут — 9x fmt.Fprintf(os.Stderr, "OPENAI-DEBUG:...") в рабочей копии openai.go
## Архитектура получения сообщений
Валера — **outgoing webhook bot** (bot_type=3). Zulip отправляет webhook только для @mention и DM.
## Бранчи (23.06.2026)
- `events_polling.enabled: false` в config.yaml
- Сообщения приходят только через **outgoing webhook** (webhook_token в .env)
- После @mention создаётся сессия; последующие сообщения без @mention обрабатываются
**norma-local:**
- `feat/hostedagent-mcp-tools` — коммит `56a6e1e` (DeepSeek tool_calls фикс), чистая бранча от `origin/master`
- `master` — коммит `56a6e1e` (тот же, DeepSeek фиксы)
**После перезапуска**: написать `@Валера <текст>` чтобы создать сессию.
**balda:**
- `main``05159d1` (intermediate status фикс запушен)
- `backup/our-main-before-upstream` — старый main
- `feat/zulip-transport-intermediate` — от коммита `bc5fd03`
## MCP Obsidian — конфиг
Balda через SSE подключается к obsidian-mcp. **URL обязательно с `/sse`**:
## Сборка
```yaml
runtime:
mcp_servers:
obsidian:
type: sse
url: http://obsidian-mcp:3101/sse # <-- без /sse → 404 Not Found
name: obsidian
```
## Контекст (история сообщений)
**Работает** (на upstream `normahq/balda` с поддержкой hosted LLM agent sessions). ADK inject'ит историю корректно для OpenAI-совместимых провайдеров.
Раньше не работало — исправлено в upstream `32e33a8 fix: support hosted MCP toolsets` / `d774c78 fix: use hosted llmagent sessions`.
## Проблема: MCP не работают у Валеры (provider: type=openai)
### Коренная причина
hostedagent (`pkg/runtime/hostedagent/`) не поддерживает MCP инструменты.
`openAIConstructor` в `agentfactory.go` получает `resolvedMCP map[string]agentconfig.MCPServerConfig`,
но не передаёт их в `hostedagent.Config` — у Config нет поля для MCP.
`openAIConstructor` вызывает `newHostedAgent(hostedagent.Config{...})` без MCPServers.
Фикс: в `hostedagent.Config` добавить `MCPServers map[string]agentconfig.MCPServerConfig`,
а в `openAIConstructor` передавать `toRuntimeMCPServers(resolvedMCP)`.
Важно: hostedagent использует OpenAI-compatible API (не ACP). MCP инструменты нужно
интегрировать через OpenAI tool_calls — модель шлёт tool_call, код выполняет MCP вызов.
## Проблема: MCP не работают у Валеры — попытка фикса
### Что было сделано (2026-06-19)
#### 1. go.mod
Добавлен `replace github.com/normahq/norma => ../norma-local` — для локальной разработки.
#### 2. hostedagent — добавлена поддержка MCP
- `pkg/runtime/hostedagent/agent.go`:
- В Config добавлено `MCPServers map[string]acpagent.MCPServerConfig`
- Сохранено в `runtimeAgent`
- В `run()`: после создания модели вызывается `model.SetTools(openAIToolsFromMCPServers(r.MCPServers))`
- `pkg/runtime/hostedagent/openai.go`:
- В `openAIChatRequest` добавлено `Tools []openAIToolDefinition`
- В `OpenAIModel` добавлены `tools []openAIToolDefinition + `SetTools()`
- Добавлен `tool_calls` в парсинг ответа (`tool_calls` → content string)
#### 3. agentfactory — передача MCP
- В `openAIConstructor` добавлено `MCPServers: toRuntimeMCPServers(resolvedMCP)` в вызов `newHostedAgent()`
#### 4. Dockerfile
В `Dockerfile.claudio` добавлен `COPY norma-local/ ./norma-local/` — чтобы `replace` работал в контейнере.
#### 5. Сборка
Образ собран и запущен как `balda-agent-valera:latest`.
#### 6. Диагностика
- `mcpServerIDs` приходят корректно: `[balda obsidian fast-rlm]`
- Добавлен stderr-лог в `agentfactory.go` в `Build()`:
```go
fmt.Fprintf(os.Stderr, "BALDA-DEBUG: mcpServerIDs=%v resolvedMCP=%v\n", mcpServerIDs, resolvedMCP)
```
- Для просмотра логов нужен `docker logs balda-agent-valera-1 2>&1 | grep BALDA-DEBUG`
### Почему не заработало
Точная причина не установлена — нужно увидеть `resolvedMCP` в логах контейнера. Возможные варианты:
- `resolveMCPServers` возвращает пустой map
- `toRuntimeMCPServers` неправильно конвертирует
- `SetTools` не влияет на уже созданные сообщения в сессии
### Что осталось
1. Прочитать stderr из контейнера: `docker logs balda-agent-valera-1 2>&1 | grep BALDA-DEBUG`
2. Если `resolvedMCP` пустой — исправлять цепочку resolveMCPServers
3. Если не пустой — тестировать response с tool_calls в DeepSeek ответе
## Диагностика молчания
### 1. MCP ошибка: `failed to list MCP tools: failed to connect: Not Found`
**Причина**: в config.yaml url без `/sse` на конце.
**Фикс**: `url: http://obsidian-mcp:3101/sse` → `docker-compose restart`.
### 2. В логах `command running → command handled` за секунду, без `received provider event`
**Причина**: стухший embedded NATS. Swarm внутри процесса не доставляет команду до session/task actor'ов.
**Фикс**: `docker-compose restart` (если не помогло → `up -d`).
### 3. Удалён / пустой state.db
После удаления balda не может зарегистрировать swarm акторы.
**Фикс**: `rm state.db` и `up -d` (balda создаст заново).
### 4. Нет owner
`handleMessage` выходит при `getOwnerID() == 0`.
**Фикс**: прописать `allowed_owners` в config.yaml (не нужен `/start owner=`).
### 5. stream_only_with_session
Сообщение в теме без сессии молча дропается.
**Фикс**: написать `@Валера <текст>` чтобы создать сессию.
### 6. Проверить webhook со стороны
### Полный цикл сборки
```bash
curl -X POST http://localhost:8091/zulip/webhook \
-H "Content-Type: application/json" \
-d '{"type":"test"}'
# 1. Инкремент buildTag в openai.go (строка buildTag)
# ~/Developer/norma-local/pkg/runtime/hostedagent/openai.go
# buildTag = "whale-YYYYMMDD-N"
# 2. Применить OPENAI-DEBUG стэш (если нужно дебажить)
cd ~/Developer/norma-local && git stash pop stash@{0}
# 3. Сборка
docker build --no-cache \
-f ~/Docker/claudio-agent/Dockerfile.claudio \
-t balda-agent-valera:latest \
/Users/admin/Developer
# 4. Деплой
cd ~/Docker/balda-agent && docker-compose up -d --force-recreate
# 5. Проверка версии в логах
docker logs balda-agent-valera-1 2>&1 | grep "norma-tools"
```
## Ключевые грабли
### Особенности сборки
- Dockerfile копирует `norma-local/` в `/src/norma-local/`
- `go.mod` replace: `github.com/normahq/norma => ./norma-local`
- `go mod edit -replace` применяется **до** `go mod download`
- `--no-cache` обязателен при изменении norma-local
- Образ надо таргетировать под нужное имя: `balda-agent-valera` для Валеры, `claudio-agent` для Клавдия
- После сборки `docker-compose up -d --force-recreate` чтобы подхватить новый образ
## Диагностика молчания / проблем
### NORMA-TOOLS не появляется в логах
Причина: агент использует не hostedagent провайдер. openai.go задействован только при provider=deepseek через ADK.
### MCP ошибка: `failed to list MCP tools: failed to connect: Not Found`
Фикс: в config.yaml url должен заканчиваться на `/sse`: `url: http://obsidian-mcp:3101/sse`
### В логах `command running → command handled` за секунду, без `received provider event`
Причина: стухший embedded NATS. Фикс: `docker-compose restart` или `up -d`.
### Удалён / пустой state.db
Фикс: `rm state.db` и `up -d` (balda создаст заново).
### Нет owner
Фикс: прописать `allowed_owners` в config.yaml.
### stream_only_with_session
Фикс: написать `@Валера <текст>` чтобы создать сессию.
## Известные грабли
### OPENAI_BASE_URL — обязательная env var
Norma не читает `base_url` из config.yaml для `provider: deepseek`. Читает env `OPENAI_BASE_URL`.
Без неё запросы уходят на `api.openai.com` (401).
**В `.env` обязательно:**
```
OPENAI_BASE_URL=https://api.deepseek.com/v1
```
Norma не читает `base_url` из config.yaml для `provider: deepseek`. Читает env.
### DOCKER_OPTS — пустая строка убивает старт
Должен быть валидный JSON:
```
DOCKER_OPTS={"max_tokens": 2048, "temperature": 0.7}
```
Должен быть валидный JSON: `DOCKER_OPTS={"max_tokens": 2048}`
### composerestart vs compose up -d
- `restart` — не перечитывает `.env`
- `up -d` — пересоздаёт контейнер с обновлённым `.env`
### docker compose up -d не работает
На этом хосте `docker compose` (без дефиса) не принимает `-d`. Использовать `docker-compose up -d`.
### Docker кеш COPY norma-local/
Слой копирования кешируется. `--no-cache` обязателен при изменении norma-local.
---
### go.mod replace
`replace github.com/normahq/norma => ./norma-local` — путь относительно `/src` в контейнере, куда копируется `norma-local/`.
## Диагноз: MCP не работают — инструменты не передаются модели
### zulip-router owner-форвард падал с 400 (23.06.2026)
**Симптом:** Валера не отвечал на сообщения в закреплённых тредах без @mention. Router лог: `forward failed (owner) error="post ... status 400 body=bad request"`. Валера лог: `invalid zulip webhook payload error="unsupported message.type \"\""`.
**Статус (2026-06-21):** MCP резолвятся, но не вызываются.
**Корень:** В `zulip-router/config.go` структура `Message` не имела поля `Type`. Zulip Events API присылает `message.type` ("stream"/"private"), но роутер его не парсил — в вебхук приходил пустой `""`. Balda-agent валидирует message.type.
**Установлено через BALDA-DEBUG:**
```
BALDA-DEBUG: mcpServerIDs=[balda obsidian fast-rlm]
resolvedMCP=map[balda:{http http://127.0.0.1:33545/mcp}
fast-rlm:{http http://fast-rlm-mcp:3333/mcp}
obsidian:{sse http://obsidian-mcp:3101/sse}]
```
MCP серверы зарезолвлены, тулсеты переданы в `hostedagent.Config`.
**Почему не работают:**
`OpenAIModel.generate()` → `buildChatRequest()` не читает `req.Config.Tools` (поле `genai.GenerateContentConfig`). ADK кладёт инструменты туда, но `buildChatRequest` сериализует только `model`, `messages`, `temperature`, `top_p`, `max_tokens`, `stop`. DeepSeek получает запрос без `tools` → отвечает текстом.
**Логи подтверждают:** `function_call_part_count=0` в ответе DeepSeek.
**Фикс:** добавить передачу tools в OpenAI запрос и парсинг tool_calls из ответа.
**Сделано (2026-06-21):** в `openai.go` добавлены:
- `openAIToolDefinition`, `openAIFunction`, `openAIToolCall` — структуры для API
- Поле `Tools` в `openAIChatRequest`
- Функция `openAIToolsFromConfig()` — конвертирует `genai.Tool[]` → OpenAI tool definitions
- Функция `contentToOpenAI()` — обрабатывает FunctionCall/FunctionResponse из ADK
- Парсинг `tool_calls` в `parseChatResponse()`
- **Тесты:** 17 тестов на new функциональность + 3 существующих = все проходят
1. Добавить поле `Tools` в `openAIChatRequest` + `openAIToolDefinition`
2. В `buildChatRequest` читать `req.Config.Tools` → конвертировать в OpenAI tool definitions
3. В `parseChatResponse` парсить `tool_calls` из ответа (ADK сам выполнит MCP)
**Важно:** upstream `norma` v0.0.10 (`bf2b25d`) уже корректно передаёт MCP тулсеты через `agentfactory.hostedToolsets()`. Проблема только в том, что `OpenAIModel` — кастомная реализация, не умеющая прокидывать tools. Если бы использовался Gemini/ACP provider — MCP бы работали.
**Фикс:** Добавлен `Type string \`json:"type,omitempty"\`` в `Message struct`. Коммит `803ae68` (локальный, без remote). docker-compose up -d --build.
Фикс: messageID int в сигнатуру handleAutoClaimMention, передаётся payload.Message.ID из processMessage.
Файл: `~/Developer/balda/internal/apps/balda/handlers/zulip_handler.go` (в коммите `05159d1`)
+4 -2
View File
@@ -1,10 +1,12 @@
# Balda — Setup & Config
_Последнее обновление: 2026-06-16_
_Последнее обновление: 2026-06-24_
## Хост
Mac (Eagle), Docker Desktop (arm64 native). Контейнеры собираются из исходников в `~/Developer/balda/`.
Mac (Eagle), **Colima** (arm64, 8 CPU, 24 GB RAM, 100 GB sparse disk). Контейнеры собираются из исходников в `~/Developer/balda/`.
**Важно:** `buildx_buildkit_arm64builder0_state` volume — основная причина зависаний Zulip при сборке. См. [[tech/docker-mac-disk-issues]].
## Компоненты
@@ -0,0 +1,355 @@
# Импорт банковских выписок за последний год
**Создано:** 2026-06-23
**Цель:** Получить все транзакции за последний год (середина 2025 — июнь 2026) в Budget App, автоматизируя импорт банковских выписок как можно полнее.
## Проблема
В `Budget.xlsx` данные заканчиваются в **середине 2025** (май-июнь 2025, в зависимости от счёта). Последние ~12 месяцев транзакций не внесены в Excel. Вручную вспомнить каждую трату за год — нереалистично.
## Существующий инструмент: budget-bank-statement-converter
**Путь:** `~/Developer/budget-bank-statement-converter/`
**Язык:** Swift (macOS command-line tool)
**Формат вывода:** CSV с колонками `[дата, сумма, дебет, кредит, категория, комментарий, курс]` — совпадает с форматом Excel.
### Поддерживаемые банки
| Банк | Формат входа | Конфиг | Статус |
|------|-------------|--------|--------|
| Demir | CSV (из PDF → Adobe Extract → CSV) | `demir-config.json` | ✅ Работает |
| Сбер | CSV (выгрузка из СберБизнес) | `sber-config.json` | ✅ Работает |
| Тинькофф | CSV (выгрузка из Тинькофф) | `tinkoff-config.json` | ✅ Работает |
| ВТБ | CSV (из PDF → Adobe Extract → CSV) | `vtb-config.json` | ✅ Работает |
| Альфа | — | — | ❌ `fatalError("Alfa not implemented")` |
### Как работает
1. **PDF → CSV**: использует Adobe PDF Extract API (`pdfservices-api-credentials.json`) — загружает PDF, получает ZIP с CSV-таблицами.
2. **CSV → формат App**: разбирает CSV, маппит категории через regex-конфиг, нормализует double-entry (дебет/кредит/конверсии).
3. Если нужно — автоматически конкатенирует `fileoutpart0001.csv`… файлы.
4. Использует OpenAI GPT-3.5-turbo для AI-категоризации (закомментировано, `aiMaxTokens = 80`).
### Что нужно для использования
- Xcode (для сборки Swift-проекта)
- `OPENAI_API_KEY` в env (не обязательно, выключено)
- `pdfservices-api-credentials.json` для Adobe Extract
- JSON config для каждого банка: `Сбер config`, `Tinkoff config`, `Demir config`, `VTB config`
## План импорта
### Шаг 1: Получить выписки из банков
**Что нужно выгрузить за июнь 2025 — июнь 2026:**
| Счёт | Банк | Как получить выписку |
|------|------|---------------------|
| Нал RUB | Наличные | Ручной ввод (см. ниже про наличные) |
| Нал KGS | Наличные | Ручной ввод |
| Нал USD | Наличные | Ручной ввод |
| Нал KZT | Наличные | Ручной ввод |
| Demir ИП | Demir | CSV (интернет-банк/моб. приложение) |
| Demir ИП USD | Demir | CSV (интернет-банк/моб. приложение) |
| Demir KGS | Demir | CSV (интернет-банк/моб. приложение) |
| Demir USD | Demir | CSV (интернет-банк/моб. приложение) |
| Тинькофф Black | Тинькофф | CSV (выгрузка из Тинькофф) |
| Тинькофф Кредитка | Тинькофф | CSV (выгрузка из Тинькофф) |
| Сбер | Сбер | CSV (СберБизнес / PDF) |
| Сбер Кредитка | Сбер | CSV (СберБизнес / PDF) |
| Альфа | Альфа | CSV — но конвертер Альфу не поддерживает |
| Альфа Кредитка | Альфа | — |
| ВТБ | ВТБ | PDF → Adobe Extract → CSV |
| ВТБ Кредитка | ВТБ | PDF → Adobe Extract → CSV |
### Шаг 2: Конвертировать выписки в CSV формата App
Запуск для каждого банка:
```bash
./budget-bank-statement-converter --bank <bank> --account <account> <input.csv>
```
Выход: `_processed.csv` с колонками `дата, сумма, дебет, кредит, категория, комментарий, курс`.
### Шаг 3: Написать Python импортёр CSV → Budget App DB
Существующий `xlsx_import.py` читает из Excel. Нужен новый: `csv_bank_import.py`, который:
- Читает CSV в формате App (колонки из `csvHeaders` в `Common.swift`)
- Привязывает `дебет`/`кредит` к существующим счетам в БД (по имени)
- Маппит категории из CSV на существующие категории в БД (по имени подкатегории)
- Игнорирует дубликаты (hash по `date + amount + source + dest + comment`)
- Поддерживает несколько CSV-файлов за раз (много выписок)
- Выводит отчёт: сколько добавлено, сколько пропущено (дубликаты), какие категории не найдены
### Шаг 4: Импортировать в Budget App
```bash
cd ~/Developer/budget-app
uv run python src/budget/importers/csv_bank_import.py <output1.csv> <output2.csv> ...
```
### Шаг 5: Дописать недостающее в Swift-конвертере
- **AlfaToCSV**: реализовать парсер для Альфа-банка (CSV выгрузка из моб. банка/СберБизнес)
- **AI-категоризация**: раскомментировать и обновить (GPT-3.5 → DeepSeek/local LLM?)
## Наличные расходы — проблема и решение
### Проблема
Наличные траты не трекались последний год. У нас есть конечный остаток налички на руках сейчас, но нет истории по категориям.
### Подходы
#### A. Снять остаток наличных сейчас → счёт в БД (простой)
- Посчитать физическую наличку сейчас → записать как `initial_balance` для `Нал RUB`, `Нал KGS`, `Нал USD`, `Нал KZT`.
- Все траты наличными за год никогда не будут зафиксированы.
- **Минус:** дыра в данных большого объёма (вероятно значительная часть расходов).
#### B. Экстраполяция по историческим трендам (средний)
- Взять помесячные тренды наличных трат по категориям за 2023–первую половину 2025.
- Экстраполировать на июнь 2025 — июнь 2026 с учётом сезонности.
- Создать транзакции-плейсхолдеры с пометкой `import_id = 'cash_estimate'`.
- **Минус:** неточность, может не отражать реальные изменения.
#### C. Ретроспектива через месяц-два + экстраполяция (предпочтительный)
- **Сейчас:** начать трекать наличные расходы (вручную или через мобильный интерфейс Budget App).
- **Через 1–2 месяца:** по собранным данным наличных трат вычислить реальные помесячные паттерны.
- Экстраполировать на пропущенный год с этими паттернами.
- **Плюс:** база для экстраполяции будет основана на реальных данных, а не на исторических.
#### D. None of the above — принять дыру
- Сделать только безналичный импорт. Наличные начинаем трекать с сегодня.
- В аналитике отмечать периоды как "без наличных".
- **Плюс:** не надо ничего выдумывать.
### Рекомендация: C+D combined
1. Трекать наличку вручную через UI Budget App начиная с сегодня.
2. Через 2 месяца посчитать реальные тренды и решить, стоит ли экстраполировать на прошлый год.
3. Если нет — просто принять дыру и жить с хорошей аналитикой начиная с 2026-06.
## Что уже реализовано в Budget App для импорта
-`xlsx_import.py` — полный импорт из Budget.xlsx (34k строк)
- ✅ Все счета, категории, курсы, транзакции — в БД
- ✅ Идемпотентный UPSERT для счетов и курсов
- ✅ Транзакции добавляются обычным insert (без import_hash после фикса)
## Реализованный скрипт: bank_scraper
**Путь:** `~/Developer/budget-app/scripts/bank_scraper/`
Структура:
```
scripts/bank_scraper/
├── __init__.py
├── .gitignore # config.yaml + data/imports/ не коммитятся
├── config.example.yaml # шаблон для копирования в config.yaml
├── base_driver.py # base class BankDriver + load_config()
├── orchestrator.py # entry point (н.п.)
└── drivers/
└── demir.py # Demir IB драйвер (н.п.)
```
**Статус:** ✅ base + Demir driver написаны, Playwright установлен. **НО — Demir требует QR-логин через мобильное приложение**, не логин/пароль на сайте.
## Реальность Demir IB
Сайт `93.171.215.109``apps.demirbank.kg/ib/`) — **Flutter web SPA** с QR-аутентификацией. Нет формы логина с паролем — нужно сканировать QR мобильным приложением Demir.
**Варианты решения:**
### A. Продолжить с Playwright + session persistence
- Один раз залогиниться руками (QR → моб. приложение)
- Сохранить session cookies/storage в persistent context
- Дальше переиспользовать сессию для выгрузок (пока не протухнет)
- **Плюс:** минимум кода
- **Минус:** сессия рано или поздно протухнет, нужен ручной ре-логин
### B. Appium / ADB — эмуляция мобильного приложения
- Демонстратор Android/iOS эмулятора с мобильным приложением Demir
- Appium для UI automation внутри приложения
- **Плюс:** полный контроль
- **Минус:** сложно, накладно
### C. Заменить Demir на первый банк с логином/паролем
- Тинькофф имеет API для разработчиков (OAuth)
- Сбер — есть API SberBusinessAPI (хотя для юрлиц)
- Можно начать с Тинькофф: Tinkoff API → выписка без браузера
- **Плюс:** самый простой tech-wise
- **Минус:** Demir пока под вопросом
### D. Парсить CSV выписки, которые уже есть в mobile/email
- Возможно Demir присылает выписки на email
- Или можно скачать через мобильное приложение → экспорт → AirDrop/email себе
- Это полу-ручной подход (но быстрее чем QR scraping)
## Решение
**Рекомендация: A + D**
1. Самый ценный банк — **Тинькофф** (есть API) — начинаем с него
2. Demir — разово выгрузить через мобильное приложение (Export CSV/email)
3. Если сессия Demir долго живёт — Playwright persistent context отработает
### Новый порядок разработки
1. ✅ Demir driver (написан, но упирается в QR)
2. **Tinkoff API driver** — следующий приоритет (без браузера, REST API)
3. **Сбер / ВТБ / Альфа** — Playwright или Tinkoff-style API
4. **Parse Demir CSV** — Python-версия DemirToCSV для уже скачанных файлов
## Файл вывода Swift-конвертера
```
csvHeaders = ["дата", "сумма", "дебет", "кредит", "категория", "комментарий", "курс"]
```
- `дата``dd.MM.yyyy HH:mm` (формат EUR)
- `сумма` — строка с суммой (±знак)
- `дебет` — имя счёта-источника (пусто = доход извне)
- `кредит` — имя счёта-получателя (пусто = расход вовне)
- `категория` — имя подкатегории
- `комментарий` — очищенный текст
- `курс` — кросс-курс при внутреннем переводе между валютами
## Автоматизация выгрузки выписок из банков
### 1. Browser automation libraries (CV-driven)
| Библиотека | Язык | Браузеры | CV | 2FA/SMS |
|-----------|------|----------|----|---------|
| **Playwright** (MS) | Python, JS, Java, .NET ⭐ | Chromium, Firefox, WebKit | Есть (locator screenshots) | `page.wait_for_selector` на поле ввода кода |
| **Puppeteer** (Google) | JS (Python через pyppeteer) | Chromium | Есть | — |
| **Selenium** | Python, Java, JS и др. | Все major | Через сторонние утилиты | — |
**Рекомендация: Playwright Python** — де-факто стандарт в 2025, cross-browser, async, видит элементы даже в SPA, встроенные ожидания. Подходит и для РФ-банков (Сбер, Тинькофф, Альфа-клик — все на SPA).
### 2. Готовые решения на GitHub
**AploBankParsers** ([github.com/Zaurrex1/AploBankParsers](https://github.com/Zaurrex1/AploBankParsers)):
- Парсер выписок **СберБизнес** (production-ready) — читает xlsx/сsv из уже выгруженного файла
- Заглушки для Альфа, ВТБ, Тинькофф
- Это парсер **уже скачанных файлов**, не скрапер
**bank_scrapers** ([github.com/eebette/bank_scrapers](https://github.com/eebette/bank_scrapers)):
- Playwright-based для scraping bank websites
- Generic, не специфичен под РФ-банки
**Sber API** — официальный REST API Сбера:
- `developers.sber.ru/docs/ru/sber-api/specifications/statement/transactions`
- Получение выписки по счёту за 5 лет
- **Требует** корпоративного доступа (SberBusinessAPI / ДБО), не подойдёт для личного СберБанк
**Готового решения "под ключ" для РФ-банков** (Playwright → bank login → 2FA → CSV выписка) **нет** в открытом доступе. Каждый банк — свой уникальный UI и flow. Придётся писать самим.
### 3. Архитектура скрипта
```
┌─────────────────────────────────┐
│ Telegram Bot (Hermes/кит) │ ← запрашивает SMS-код
├─────────────────────────────────┤
│ Orchestrator (Python) │ ← запускает по крону / кнопке
│ ┌─────────────────────────┐ │
│ │ Playwright browser │ │ ← drives bank login page
│ │ - headless=false │ │ (visible для отладки)
│ │ - persistent context │ │ (сессия не слетает)
│ └─────────────────────────┘ │
│ ┌─────────────────────────┐ │
│ │ Bank drivers: │ │
│ │ - tinkoff.py │ │
│ │ - sber.py │ │
│ │ - alfa.py │ │
│ │ - demir.py │ │
│ │ - vtb.py │ │
│ └─────────────────────────┘ │
│ ┌─────────────────────────┐ │
│ │ Output: CSV в формате │ │
│ │ budget-bank-statement- │ │
│ │ converter │ │
│ └─────────────────────────┘ │
└─────────────────────────────────┘
```
### 4. Flow для каждого банка
```
1. Запустить headless Playwright (или visible=False для отладки)
2. Открыть страницу логина банка
3. Ввести credentials (из конфига, НЕ скрипта)
4. Если запрошен SMS-код:
→ отправить в Telegram: "Код из смс для {bank}:"
→ ждать ответа (polling/async)
→ ввести полученный код
5. Дождаться загрузки дашборда
6. Перейти на страницу выписок/истории
7. Указать период: 2025-06-01 — 2026-06-23
8. Скачать CSV/Excel
9. Сохранить в ~/Developer/budget-app/data/imports/{bank}/{date}.csv
10. Конвертировать через budget-bank-statement-converter (или Python-версию)
11. Импортировать в БД
12. Закрыть браузер
```
### 5. Обработка SMS-кодов (Telegram)
Скрипт не должен хранить сессию банка, каждый запуск — новая авторизация.
**Варианты:**
1. **Telegram Bot (inline keyboard)**: скрипт ждёт сообщение, когда нужен код — присылает кнопку "Отправить код для {bank}", пользователь вводит → скрипт вставляет
2. **Hermes-агент**: крон-джоб спрашивает в Telegram нужный код, ждёт ответа через webhook
3. **Простой stdin**: скрипт пишет "Введите код для Тинькофф:" и ждёт ввод (если запуск из терминала)
**Рекомендация: вариант 1** — TG bot минимальная зависимость, полный контроль.
Для реализации: существующий Hermes/Zulip может служить relay. Или простой скрипт на Python + python-telegram-bot с `await incoming_message`.
### 6. Чувствительность данных — ограничения
Скрипт будет:
- Знать **логины/пароли** банков (хранятся в локальном конфиге, НЕ в коде)
- Открывать **браузер на машине Алекса** (никаких VPN/прокси)
- Передавать только SMS-коды через TG — пароли не передаются
- Работать **локально**, без LLM/агентов в browser automation
Код пишем так, чтобы ни одна строка credentials не была в скрипте:
```python
# config.yaml (chmod 600)
banks:
tinkoff:
login: "7999..."
password: "..."
phone: "7999..."
sber:
login: "..."
password: "..."
```
### 7. Альтернатива: API банков (без browser)
| Банк | REST API для личных счетов | Комментарий |
|------|---------------------------|-------------|
| Тинькофф | Есть (Tinkoff API для разработчиков) | Требует регистрации приложения, OAuth |
| Сбер | Sber API для юрлиц, нет для личных | Не подходит |
| Альфа | Альфа-Бизнес API (юрлица) | Не подходит |
| Demir | Нет публичного API | — |
| ВТБ | Нет публичного API | — |
Тинькофф — единственный из списка, у кого есть адекватный API для физлиц (Tinkoff API / Tinkoff Invest API). Можно получить выписку через API, без browser. Остальные — только SPA scraping.
**Код:** 10 swift-файлов, ~2 400 строк.
**Что хорошо:**
- Хорошая архитектура: каждый банк = отдельный struct с чётким интерфейсом
- Конфиги вынесены из кода (JSON)
- Regex-маппинг категорий гибкий
- Умеет объединять multi-part CSV и извлекать из PDF через Adobe API
- Формат вывода совпадает со структурой Excel/Budget App
**Чего не хватает:**
- Парсер Альфа-банка (только заглушка)
- AI-категоризация закомментирована (GPT-3.5, устарела)
- Нет интеграции с Budget App (только → CSV, не → БД)
- Нет обработки для Demir ИП USD / Demir USD / Demir KGS отдельно (DemirToCSV один конфиг на все)
- PDF-парсер привязан к Adobe PDF Extract API (платный сервис, credentials нужны)
- Нет обработки для Сбер Кредитка как отдельного счёта (SberToCSV один конфиг)
- Нет автоматического определения новых форматов CSV от банков
@@ -0,0 +1,70 @@
# Остатки счетов из Excel (Budget.xlsx)
Файл: `~/Downloads/Budget.xlsx`
Лист: `транзакции`
Балансы **совпадают** с API после фиксов (2026-06-23).
| Счёт | Баланс | Валюта | Последняя операция |
|---|---|---|---|
| Нал RUB | 815 555.70 | RUB | 2025-05-16 (R6029, 60 000 deb) |
| Нал KGS | 1 794.00 | KGS | 2025-06-17 (R6042, deb) |
| Нал KZT | -22 820.00 | KZT | 2024-12-15 (R5505, deb) |
| Нал USD | 1.00 | USD | 2025-02-04 (R5976, deb) |
| Нал AED | 0.00 | AED | 2023-11-16 (R2335, deb) |
| Нал EUR | — | EUR | нет операций |
| Нал UZS | 18 000.35 | UZS | 2024-03-31 (R3337, deb) |
| Demir ИП | 192 308.54 | KGS | 2025-01-31 (R5943, deb) |
| Demir ИП USD | 284 842.96 | USD | 2026-05-29 (R6053, cred) |
| Demir KGS | 96 013.12 | KGS | 2025-01-31 (R5947, deb) |
| Demir USD | 3 394.29 | USD | 2025-01-31 (R5950, cred) |
| Тинькофф Black | 521 263.65 | RUB | 2025-01-31 (R5959, deb) |
| Тинькофф Кредитка | 7 170.17 | RUB | 2025-01-28 (R5901, deb) |
| Сбер | 266 905.98 | RUB | 2025-01-28 (R5898, deb) |
| Сбер Кредитка | 3 059.00 | RUB | 2025-01-31 (R5946, deb) |
| Альфа | 78 030.00 | RUB | 2025-01-28 (R5925, cred) |
| Альфа Кредитка | 0.50 | RUB | 2025-01-28 (R5924, deb) |
| ВТБ | 265 178.95 | RUB | 2025-01-27 (R5887, deb) |
| ВТБ Кредитка | 297 189.00 | RUB | 2025-01-31 (R5952, deb) |
| Райффайзен | — | RUB | нет операций |
## Формула API
`balance = incoming_transfer + incoming_income - outgoing + initial_balance`
Где:
- `incoming_transfer``SUM(cross_rate * amount)` где source IS NOT NULL (cross_rate конвертирует валюту source в валюту dest)
- `incoming_income``SUM(amount)` где source IS NULL (amount уже в валюте счёта, cross_rate — только для отчёта)
- `outgoing``SUM(amount)` (всегда в валюте счёта-источника)
## Фиксы (2026-06-23)
### 1. incoming для income и transfer — разный расчёт
Было: `incoming = SUM(COALESCE(cross_rate, 1.0) * amount)` — cross_rate применялся ко всем включая income (где amount уже в валюте счёта). Для KGS-счетов с income-пополнениями (например "Конвертация USD по курсу 87") incoming умножался на 87, давая баланс ×87.
Стало: incoming разделён на две части:
- `incoming_transfer (source IS NOT NULL)`: cross_rate применяется (конвертирует валюту source→dest)
- `incoming_income (source IS NULL)`: просто amount (cross_rate — только для отчётной валюты)
### 2. import_hash и on_conflict_do_nothing — удалены
Было: дедупликация по `import_hash = SHA256(date|amount|source|dest|comment)`. В Excel есть 141 дублирующаяся строка с одинаковыми этими полями (реальные повторные списания). `on_conflict_do_nothing` молча пропускал их, но `tx_count` врал что импортировал.
Стало: обычный `session.add(tx)`. Колонка `import_hash` и уникальный индекс дропнуты из таблицы. Функция `_make_import_hash` удалена.
### 3. Accounts — идемпотентность
Было: каждый запуск импорта создавал 20 новых аккаунтов (session.add). После нескольких запусков — 40+ аккаунтов с разными UUID, транзакции привязаны к разным наборам.
Стало: UPSERT — ищет существующий account по `name + user_id`, переиспользует.
### 4. UserSettings — UPSERT
Было: `session.add(UserSettings(...))` — падало с UniqueViolation при повторном запуске.
Стало: `pg_insert(...).on_conflict_do_update(...)`.
### 5. Нюанс Excel col I/L
Для некоторых строк Excel не кеширует вычисленные значения col I (остаток деб) — показывает None. Это не баг, а особенность data_only=True — если Excel не пересчитал формулы перед сохранением, кеш пуст. В таких случаях последний корректный баланс берётся из предыдущей строки минус amount.
@@ -0,0 +1,264 @@
---
aliases:
- Freedom Strategy
- Budget app FIRE
- Budget app strategy
related:
- '[[personal/projects/budget-app/index]]'
- '[[personal/projects/budget-app/fire-investment-strategies-2026]]'
tags:
- personal
- budget-app
- finance
- FIRE
- freedom
- roadmap
title: Финансовая стратегия + FIRE-адаптация для Budget App
updated: '2026-06-23T00:00:00.000Z'
---
# Финансовая стратегия + FIRE-адаптация для Budget App
**Обновлено:** 2026-06-23
**Основание:** анализ данных budget-app + личные вводные Alex
---
## 1. Текущая позиция (June 2026)
### 1.1 Балансы счетов
| Счёт | Валюта | Баланс | Статус |
|------|--------|--------|--------|
| Нал USD | USD | $925,511 | **Накопления** |
| Demir ИП USD | USD | $284,843 | **Накопления** |
| Demir USD | USD | $3,394 | Остаток |
| Нал KGS | KGS | 1,794 | Остаток (текущие) |
| Demir ИП | KGS | -359,861 | На расход |
| Demir KGS | KGS | -3,542,775 | Ушёл в минус |
| RUB счета (Альфа, Сбер, ВТБ, Тинькофф, Нал) | RUB | ~-29.5M | Кредитки + овердрафты |
| Нал RUB | RUB | -12,709,478 | Долг |
| Альфа | RUB | -18,187,976 | Долг |
**Итого накопления:** ~$1,210,000 USD ≈ **112.5M KGS** (по курсу 93)
**Реально свободные:** ~$100k (нал USD), остальное — предпринимательские счета + оборотка
### 1.2 Расходы (из БД, 2024 — июнь 2025, 17 мес)
| Показатель | Значение |
|------------|----------|
| Средние расходы/мес | ~1,075,000 KGS |
| Средний доход/мес | ~851,000 KGS |
| Норма сбережений (по БД) | -26% (данные неполные — часть трат не проведена) |
Категоризация в БД сломана: все расходы свалены в "📉 Расходы" (11.4M), остальное — депозиты, кэшбэк, аренда (~378k). Данные после июня 2025 не вносились, последняя транзакция май 2026 — видимо разовый импорт.
### 1.3 Внешние активы и доходы
| Статья | Цифра |
|--------|-------|
| **Накопления (ликвид)** | ~$100k (Нал USD) |
| **Аренда 2 квартир** | 60-80k KGS/мес |
| **Образование старшей** | $8-10k/год = 65-77k KGS/мес |
| **Младшая (дистант)** | Запуск в этом году — точные цифры появятся |
| **Частный дом** | Текущее содержание |
| **Indie dev** | Проекта пока нет |
---
## 2. Стратегия (Freedom, не FIRE)
Классический FIRE (накопить 25x и сидеть без дела) **тебе не подходит** по нескольким причинам:
1. **Валютный риск** — живёшь в KGS, доход в RUB/USD. KGS волатильна. FIRE-расчёт в KGS ненадёжен
2. **Образование детей** — крупный обязательный платёж на ~10 лет вперёд. Это не "сократить", это фиксированная статья
3. **Ты не хочешь "не работать"** — инди-дев показывает что хочешь заниматься проектами, а не сидеть на пляже
4. **Две квартиры** — актив, который уже почти покрывает образование старшей
### 2.1 Твоя цель: "Freedom Gap"
Не FIRE number, а **Freedom Gap** — разница между расходами и пассивным/полупассивным доходом, которую нужно закрыть капиталом.
```
Freedom Gap = (расходы/мес) − (аренда + дивиденды + проектный доход)
```
| Сценарий | Расходы/мес | Аренда | Проект | Gap/мес | Капитал для 4% |
|----------|------------|--------|--------|---------|----------------|
| **Сейчас** | ~900k KGS | 70k | 0 | **830k KGS** | **249M KGS** ($2.7M) |
| **Аренда → образование** | 900k | 70k→на образование | 0 | 830k | 249M KGS ($2.7M) |
| **+Проект $2k/мес** | 900k | 70k | 186k | **644k KGS** | **193M KGS** ($2.1M) |
| **+Проект $5k/мес** | 900k | 70k | 465k | **365k KGS** | **110M KGS** ($1.2M) |
### 2.2 Реалистичные вехи
| Веха | Условие | Цифра |
|------|---------|-------|
| **🟢 1ая: аренда = образование** | Аренда 80k покрывает старшую ~77k | ✅ **УЖЕ почти** |
| **🟢 2ая: проект = жизнь** | Проект $3k/мес покрывает ~280k KGS | Нужен работающий проект |
| **🟡 3ая: капитал + аренда = всё** | Накопить 110M KGS ($1.2M) при проекте $5k | 10-15 лет |
| **🔴 4ая: полная свобода** | Накопить 249M KGS ($2.7M) | Долгий горизонт |
---
## 3. Инвестиционная стратегия для Alex
### 3.1 Принципы
1. **Core-Satellite** — 70% широкий рынок (VT / VWRA), 30% дивиденды + защита
2. **Мультивалютность** — портфель в USD (защита от девальвации KGS)
3. **Дивиденды как income stream** — снижают sequence-of-returns risk, частично покрывают расходы
4. **Буфер 12 мес** — в USD, не трогать
5. **Ребаланс раз в год** — не дёргаться
### 3.2 Рекомендуемая аллокация
| Класс | % | Инструмент | Назначение |
|-------|---|-----------|------------|
| **Global Equity ETF** | 60% | VWRA (VT) — IRSH / LSE | Рост капитала, мультивалютная диверсификация |
| **Dividend ETF** | 15% | VIG, SCHD, или HDV | Стабильный дивидендный поток |
| **Bonds (TIPS)** | 10% | TIP (iShares TIPS) | Защита от инфляции |
| **Cash USD** | 10% | HYSA / money market | Буфер, ~12 мес расходов |
| **Real Estate (REIT)** | 5% | VNQ / O | Доход от недвижимости без управления |
**Почему не BND:** для тебя облигации в USD не дают премии, а TIPS защищают от инфляции которая в KGS выше номинальной.
### 3.3 Ребаланс
- **Раз в год** в декабре
- **Автоматический триггер:** любая позиция отклонилась >5% от цели
- **Новые деньги:** направляются в самый отстающий класс (автоматический buy-low)
---
## 4. Что внедрить в budget-app (Phase 6 — Roadmap)
### 4.1 Приоритеты (по ценности)
| # | Фича | Зачем | Оценка сложности | Статус |
|---|------|-------|-----------------|--------|
| **P0** | **Savings Rate Dashboard** | Увидеть реальную норму сбережений. Сейчас -26% — надо понять куда уходят деньги | Medium | ❌ Не начато |
| **P0** | **Multi-Currency Net Worth** | Общий капитал в USD: наличка + счета + квартиры + пассивы | Low | ❌ Не начато |
| **P1** | **Investment Snapshots** | Раз в месяц вбить "сколько на Нал USD / IBKR / крипте" — увидеть динамику | Low | ❌ Не начато |
| **P1** | **Freedom Gap Dashboard** | Доход (аренда+дивиденды+проект) - расходы = gap | Medium | ❌ Не начато |
| **P1** | **Passive Income Tracker** | Сколько приносят аренда, дивиденды, депозиты в месяц | Low | ❌ Не начато |
| **P2** | **Projection Engine** | "Если докладываю X/мес и проекты дают Y/мес — через N лет свобода" | Medium+ | ❌ Не начато |
| **P2** | **What-if Simulator** | "Что если аренда упадёт / курс изменится / проект взлетит" | Medium+ | ❌ Не начато |
| **P3** | **Monte Carlo FI Calculator** | Классический 4% vs 3.5% vs variable, probability of success | High | ❌ Не начато |
| **P3** | **Withdrawal Strategy Planner** | Бакетная / guardrails / dividend | High | ❌ Не начато |
### 4.2 Схема расширения БД
```sql
-- Инвестиционные снапшоты (ручной ввод раз в месяц)
CREATE TABLE investment_snapshot (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
user_id UUID NOT NULL REFERENCES "user"(id),
account_id UUID REFERENCES account(id), -- NULL для внешних брокеров
name VARCHAR(128) NOT NULL, -- "Нал USD", "Interactive Brokers", "Крипта"
portfolio_value NUMERIC(16,2) NOT NULL,
currency_code VARCHAR(5) NOT NULL REFERENCES currency(code),
snapshot_date DATE NOT NULL,
asset_class VARCHAR(32), -- cash, bonds, stocks, real_estate, crypto, business
notes TEXT,
UNIQUE(user_id, name, snapshot_date)
);
-- Цели свободы
CREATE TABLE freedom_goal (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
user_id UUID NOT NULL REFERENCES "user"(id),
name VARCHAR(128) NOT NULL, -- "Свобода", "Образование детей", "Ремонт дома"
target_amount NUMERIC(16,2) NOT NULL,
target_currency_code VARCHAR(5) NOT NULL REFERENCES currency(code),
current_amount NUMERIC(16,2) DEFAULT 0, -- ручной или вычисляемый
category VARCHAR(32), -- freedom, education, major_purchase
monthly_contribution NUMERIC(16,2), -- план пополнения
target_date DATE,
expected_return_rate NUMERIC(5,4), -- 0.07 = 7%
is_active BOOLEAN DEFAULT true
);
```
### 4.3 UI макет (какие страницы добавить)
```
/dashboard
├── Savings Rate (график за 12 мес + норма)
├── Net Worth (USD, KGS — два числа)
├── Freedom Gap (прогресс-бар: 0% → 100%)
└── Cash Reserve (мес жизни / норма 12)
/freedom
├── Goals (список целей с прогресс-барами)
├── Investment Snapshots (таблица + график)
├── Projection (график: сегодня → свобода)
└── What-if (слайдеры: аренда, проект, курс)
/finances
├── Passive Income (аренда, дивиденды, депозиты за месяц)
└── Allocations (pie chart портфеля)
```
### 4.4 API эндпоинты (новые)
```
GET /api/freedom/goals — список целей свободы
POST /api/freedom/goals — создать цель
PUT /api/freedom/goals/:id — обновить
GET /api/freedom/goals/:id/projection — проекция к цели
GET /api/investments/snapshots — список снапшотов (с пагинацией)
POST /api/investments/snapshots — добавить снапшот
GET /api/investments/snapshots/latest — последний по каждому инструменту
GET /api/dashboard/net-worth — общий капитал (USD + KGS)
GET /api/dashboard/freedom-gap — gap месяца
GET /api/dashboard/passive-income — аренда + дивиденды за период
```
---
## 5. Первый шаг (что сделать прямо сейчас)
### Step 0: Заполнить данные в budget-app
Без актуальных расходов любой расчёт — гадание. Нужно:
1. Импортировать выписки за июнь 2025 — июнь 2026 (Альфа, Сбер, Demir)
2. Разнести по категориям (хотя бы крупные статьи: дети, дом, стройка, еда)
3. Вбить балансы квартир и других активов как investment_snapshot
### Step 1: Net Worth Dashboard
Показать общий капитал в USD без лишних действий.
Уже можно сделать — данные по счетам есть в БД, квартиры добавляются как manual entry.
### Step 2: Investment Snapshots
Форма: дата, инструмент, сумма, валюта → график роста капитала с проекцией.
Без автоматизации — раз в месяц ввести руками.
### Step 3: Freedom Goal
После того как есть:
- реальные расходы (не -26%)
- актуальный капитал с квартирами
- доход от проекта (хоть какой-то)
→ построить Freedom Gap Dashboard с what-if сценариями.
---
## 6. Что не надо делать (анти-приоритеты)
| Не надо | Почему |
|---------|--------|
| Monte Carlo Simulation | Сложно, данных мало, толку для твоего случая 0 |
| Withdrawal Strategy Planner | Ты не на пенсии, не нужен |
| Roth Conversion Ladder | Не применимо (не US resident) |
| FIRE Number Calculator в классике | Тебе нужен Freedom Gap, не 25x |
| Авто-импорт курсов валют | Не влияет на решение — достаточно раз в месяц |
| Сложные прогнозы в KGS | Курс KGS непредсказуем, считай в USD |
---
## 7. Связанные заметки
- [[personal/projects/budget-app/index|Budget App — главный документ]]
- [[personal/projects/budget-app/fire-investment-strategies-2026|Исходное исследование FIRE + инвестстратегий]]
- [[personal/documents/budget-app-features|Список всех идей фич]]
@@ -0,0 +1,246 @@
---
title: FIRE и инвестиционные стратегии для Budget App
tags:
- personal
- budget-app
- finance
- FIRE
- investments
related: '[[personal/projects/budget-app/index]]'
updated: '2026-06-23T00:00:00.000Z'
---
# FIRE и инвестиционные стратегии для Budget App
**Создано:** 2026-06-23
**Источник:** исследование whale (FIRE guides, Goldman Sachs, PIMCO, Bogleheads, 2026)
---
## 1. FIRE-типы и целевые показатели
### 1.1 Виды FIRE
| Тип | Годовые расходы | Целевой портфель | Суть |
|-----|----------------|-------------------|------|
| **Lean FIRE** | < $40,000 | < $1,000,000 | Минимализм, быстрый выход |
| **Chubby FIRE** | $40k$100k | $1M$2.5M | Комфорт без излишеств |
| **Fat FIRE** | $100k+ | $2.5M$6M+ | Высокий уровень жизни |
| **Barista FIRE** | Переменные | 50–80% от полного | Частичная занятость покрывает часть |
| **Coast FIRE** | Любые | Достаточно чтобы дорасти | Перестать пополнять, дать сложному % работать |
### 1.2 FIRE Number
**Формула:** `FIRE Number = Annual Expenses × 25` (4% rule)
На 2026 год рекомендуется **33.5%** вместо 4%:
- Ранний выход = 40–60 лет на пенсии (Trinity Study считала 30 лет)
- Повышенные оценки рынка (Shiller CAPE выше исторической нормы)
- Низкая доходность облигаций
| Норма сбережений | Годовые расходы | FIRE (4%) | FIRE (3.5%) |
|------------------|----------------|-----------|-------------|
| $30,000 | $30,000 | $750,000 | $857,000 |
| $40,000 | $40,000 | $1,000,000 | $1,143,000 |
| $50,000 | $50,000 | $1,250,000 | $1,429,000 |
| $75,000 | $75,000 | $1,875,000 | $2,143,000 |
| $100,000 | $100,000 | $2,500,000 | $2,857,000 |
### 1.3 Скорость до FIRE от нормы сбережений
| Норма сбережений | Лет до FIRE |
|-----------------|-------------|
| 10% | 51 лет |
| 20% | 37 лет |
| 30% | 28 лет |
| 40% | 22 года |
| 50% | 17 лет |
| 60% | 12.5 лет |
| 70% | 8.5 лет |
| 80% | 5.5 лет |
*Assumes 5% real returns, starting from zero*
---
## 2. Стратегии вывода (Withdrawal Strategies)
### 2.1 4% Rule (Trinity Study)
- 95% success rate для 30 лет с 60/40 портфелем
- **Для FIRE (40+ лет):** риск sequence-of-returns выше → используй 3.5%
### 2.2 Flexible Spending (Variable Withdrawal)
- В плохие годы режешь дискреционные траты на 10–20%
- В хорошие — тратишь больше
- **Guardrails:** увеличиваешь withdrawal когда портфель выше цели, уменьшаешь когда ниже
### 2.3 Bucket Strategy
- **Bucket 1 (12 года):** кэш/деньги — текущие расходы
- **Bucket 2 (37 лет):** облигации
- **Bucket 3 (8+ лет):** акции
- Ребалансируешь только когда акции переросли
### 2.4 Dividend Investing
- Покрываешь расходы дивидендами (не продавая акции)
- Ниже общая доходность, но выше стабильность
### 2.5 Roth Conversion Ladder
- Конвертируешь traditional IRA → Roth IRA ежегодно (1 год расходов)
- Через 5 лет конвертированные средства доступны без штрафа
- Нужен 5-летний bridge из taxable счетов
---
## 3. Инвестиционные стратегии на 2026
### 3.1 Strategic Asset Allocation (классика)
- Фиксированные цели, ребаланс раз в квартал/год
- **Типичный model:** 60% equities / 30% fixed income / 10% alternatives
- **Историческая доходность 60/40:** ~6.5% annualized (20142024)
- **Плюс:** убирает эмоции, дисциплина правил
- **Минус:** не адаптируется к среде
### 3.2 Three-Fund Portfolio (Bogleheads)
- US Total Stock Market (VTI/VTSAX)
- International Total Stock Market (VXUS/VTIAX)
- US Total Bond Market (BND/VBTLX)
- **Allocation:** 60/20/20 (или агрессивнее для молодых)
- **Expense ratios:** 0.030.05%
- **Разница в комиссиях:** 0.03% vs 1.0% на $500k за 20 лет = ~$100k+
### 3.3 Tactical Asset Allocation (2026 context)
- Отклонения 5–20% от стратегической аллокации на основе макро
- **Overweight:** энергия (commodities supercycle), floating-rate securities, инфраструктура
- **Underweight:** long-duration bonds, перегретые US tech
- **Добавить:** emerging market debt, inflation-protected assets
- **Alpha:** ~2.1% annually above static (multi-asset, 2024 study)
### 3.4 Enhanced Passive (Goldman Sachs 2026)
- **Alpha Enhanced:** tracking error 50200 bps, чуть выше комиссии
- **Зачем 2026:** снижение ожидаемой рыночной доходности, концентрация индексов, неопределённость
- **Systematic factor tilts** — небольшие ставки на value, momentum, quality
### 3.5 Insured / Dynamic Allocation
- **Insured:** автоматический переход в консерватив при drawdown > X%
- **Dynamic:** 7.8% annualized vs static с меньшими просадками (2024)
- **Рекомендация 2026:** infrastructure, private credit, low-duration fixed income
### 3.6 Goldman Sachs 2026: активные ETF + alternatives
- **Active ETFs:** AUM растёт 46% CAGR с 2020
- **Derivative-income ETFs:** $47B inflows (Q1Q3 2025)
- **Private assets:** Millennials держат ~20%, Boomers ~6% — поколенческий сдвиг
- **Tail-risk hedging:** нужен более широкий набор инструментов (не только bonds/USD)
---
## 4. Тактики для нашего контекста (Alex, мультивалютный, KGS-based)
### 4.1 Особенности
- **Базовая валюта:** KGS (высокая волатильность, зависимость от переводов РФ)
- **Счета:** RUB, USD, EUR, KGS, KZT, UZS, AED, CNY
- **Доход:** в основном RUB/USD
- **Расходы:** KGS (жизнь), USD (крупные/накопления), RUB (регулярные)
### 4.2 Что адаптировать в budget-app
#### Phase 6 — что строить (приоритеты):
1. **FIRE Number Calculator**
- Поле: годовые расходы (берутся из фактических транзакций за 12 мес)
- Поле: текущий инвестированный капитал
- Поле: expected return (47%)
- Вывод: FIRE number (25x / 28.6x / 33x), years to FIRE
- Вывод: сколько нужно докладывать в месяц для выхода через N лет
2. **Savings Rate Dashboard**
- `Норма сбережений = (Доходы - Расходы) / Доходы`
- График: savings rate по месяцам
- График: накопленный капитал vs FIRE trajectory (projection line)
3. **FireGoal модель**
- `fire_goal` таблица: user_id, target_amount, target_currency_id, expected_return_rate, monthly_contribution, target_date
- Progress bar: сколько % от цели накоплено
- What-if: "что если увеличу savings rate на 5%?"
4. **Multi-currency FIRE number**
- Пересчёт цели в base currency (KGS) и в USD
- Проблема: валютный риск при пенсии в KGS — нужно показывать и USD-эквивалент
5. **Investment Tracking**
- `investment` / `portfolio` таблицы: date, account_id, value, currency_id
- Ввод: ручные snapshots (когда обновляешь брокерский счёт)
- График: капитал по месяцам + проекция 7% CAGR
- Автоматический импорт: нет (ручной ввод раз в месяц)
6. **Withdrawal Simulator (+FI Calc)**
- Monte Carlo simulation (по историческим данным)
- Поля: начальный капитал, годовые расходы, asset allocation, withdrawal rate
- Результат: probability of success (не остаться без денег)
- Модель 4% vs 3.5% vs variable
7. **"Буфер" / Cash Reserve Dashboard**
- Сколько месяцев расходов в кэше (по счетам типа cash/debit)
- Целевой буфер: 6–12 месяцев расходов
- Trigger: < 3 мес → alert
### 4.3 Архитектурные решения для budget-app
```
investment_account (таблица):
id, user_id, account_id FK → account,
portfolio_value, currency_id, snapshot_date,
asset_class {cash, bonds, stocks, real_estate, crypto, other},
notes
fire_goal (уже есть в схеме):
id, user_id, target_amount, target_currency_id,
expected_return_rate (decimal, 0.07 = 7%),
monthly_contribution (decimal),
target_date NULL,
current_portfolio_value (вычисляется из investment_account snapshot)
fire_projection:
computed view: по месяцам от текущей даты
columns: month, contribution, return, portfolio_value, is_fire (bool если >= target)
```
### 4.4 Какие FIRE-варианты реалистичны для Alex
| FIRE-тип | Расходы/мес | Год | FIRE Number (4%) | Годовая норма сбережений | Лет* |
|----------|-------------|-----|------------------|-------------------------|------|
| Lean FIRE | Минимальные | ??? | ??? | ??? | ??? |
| Chubby FIRE | Текущие | ??? | ??? | ??? | ??? |
| Barista FIRE | С part-time | ??? | ??? | ??? | ??? |
*\* — нужно подставить фактические цифры из budget-app*
**Рекомендуемая стратегия для Alex:**
- Core: Three-Fund Portfolio (VT + BNDW) — глобальная диверсификация, не привязана к KGS
- Satellite: TIPS / real assets (инфляция в KGS выше чем в USD)
- Дивидендная составляющая: покрывает часть расходов (снижает sequence-of-returns risk)
- Буфер: 12+ месяцев расходов в USD (не KGS — защита от девальвации)
- Ребаланс: раз в год, или при drift > 5%
---
## 5. Ключевые метрики на дашборд
| Метрика | Откуда берётся | Формула |
|---------|---------------|---------|
| **Норма сбережений** | Транзакции за 12 мес | (Доходы - Расходы) / Доходы |
| **FIRE Number** | Средние расходы × 25 | (AvgExpenses × 12) × 25 |
| **FIRE Progress** | Инвестиционный капитал | PortfolioValue / FIRE_Number × 100% |
| **Years to FIRE** | Калькулятор | (ln(FIRE/R) - ln(FIRE/(R - P×12))) / ln(1+r) — сложно |
| **Safe Withdrawal Amount** | Портфель × 4% | PortfolioValue × 0.04 |
| **Runway (мес)** | Кэш / расходы в месяц | CashBalance / MonthlyExpenses |
| **Dollar-Cost Avg Equity** | Инвестиции / куплено единиц | — |
---
## 6. Связанные ресурсы
- [[personal/projects/budget-app/index|Budget App — главный документ]]
- [[personal/documents/budget-app-features|Исходный список фич]]
- [WealthVieu FIRE Guide 2026](https://wealthvieu.com/retirement/fire/)
- [Goldman Sachs — Portfolio Construction 2026](https://am.gs.com/en-us/advisors/insights/article/investment-outlook/portfolio-construction-2026)
- [Bogleheads Safe Withdrawal Rates](https://www.bogleheads.org/wiki/Safe_withdrawal_rates)
- [PIMCO — Investment Ideas for 2026](https://www.pimco.com/us/en/insights/charting-the-year-ahead-investment-ideas-for-2026)
+189 -10
View File
@@ -81,7 +81,7 @@
- Категоризация — ручная.
- Невозможно нормально работать с мобильного.
- Анализ ограничен сводными в Excel: нет нормальных графиков по подкатегориям, кросс-фильтрации, прогнозов.
- Нет ничего про FIRE: проекций пенсии, моделирования инвестиций, целевых процентов нормы сбережений.
- Нет ничего про Freedom Gap / инвестиции: нет трекинга портфеля, нормы сбережений, проекции "когда аренда + проекты покроют расходы".
- История курсов хранится в строках листа `курсы` — это не нормализованная таблица.
- Нет API: нельзя интегрировать с банковскими экспортами, нельзя автоматически тянуть в дашборд.
@@ -95,7 +95,7 @@
2. Считает аналитику и сводки **на лету** через SQL, а не через `VLOOKUP`.
3. Импортирует банковские выписки (Альфа, Сбер, Demir, …) автоматически + AI-резолвер для неоднозначных случаев.
4. Даёт мобильный UI для быстрого ввода трат на ходу.
5. Моделирует **FIRE-сценарии** (отдельная фаза после готового бюджетирования).
5. **Freedom Gap** — разница между расходами и пассивным/полупассивным доходом (аренда, дивиденды, проекты). График прогресса к нулевому gap. What-if сценарии: "если проект даёт X, аренда Y — через N лет свобода".
6. Прогнозирует траты на N месяцев вперёд по сезонной модели (категории `🧱 Стройка`, `🏖️ Отдых` цикличны).
### Доменная модель (первая итерация)
@@ -144,13 +144,34 @@ Base currency не хардкодится — выбирается в `Settings`
1. **Phase 0 — Discovery & schema**. Полностью разобрать Excel, утвердить доменную модель. *(в процессе — этот док)*
2. **Phase 1 — Skeleton + import**. Создать `~/Developer/budget-app/` (git init), FastAPI + Vue в Docker Compose в `~/docker/budget-app/` с подключением к существующему Postgres-кластеру на хосте (новая БД `budget_app`). Миграция xlsx → БД. Read-only viewer транзакций + остатков + годовые отчёты. Внешний доступ через `budget.qentra.top` с auth (FastAPI Users + JWT).
3. **Phase 2 — Курсы и настройки**. Cron-джоб для НБ КР с back-fill. Базовая валюта в настройках. Все пересчёты привязаны к ней.
4. **Phase 3 — Ручной ввод**. Форма добавления транзакции, CRUD категорий/счетов, ручная правка курсов.
5. **Phase 4 — Импорт банков + AI-резолвер**. Автофетч/парсеры (Альфа, Сбер, Demir). LLM-резолвер. TG-бот для ручных подтверждений.
6. **Phase 5 — Аналитика**. Дашборды: расход по категориям, динамика, бёрндаун по бюджету. Прогнозы.
7. **Phase 6 — Mobile / PWA**. Быстрый ввод с телефона.
8. **Phase 7 — MCP HTTP**. Экспонировать MCP-эндпоинт для Hermes.
9. **Phase 8 — Инвестпортфели + FIRE**. Тикеры/цены/дивы. Калькулятор FIRE. *(отдельное планирование когда бюджетирование готово.)*
3. **Phase 2 — Полноценный Viewer + CRUD**. Довести до уровня Excel по функциональности:
- Валюта у счетов (символ)
- Категории с иерархией (группа → подкатегория)
- Свёртка по годам/месяцам (drill-down как в Excel)
- Dynamic scrolling (infinite scroll вместо кнопок пагинации)
- CRUD транзакций: добавление, редактирование, удаление
- CRUD категорий
- CRUD счетов
- Ручная правка курсов
- Дашборды: расход по категориям, динамика, бёрндаун по бюджету
- Прогнозы (сезонная модель по категориям)
- Multi-currency: отображение балансов в валюте счёта + в base currency
- Сводная таблица по годам (как лист `сводная` в Excel)
- Налоговый учёт (как лист `налоги 22-24` в Excel)
4. **Phase 3 — Импорт банков + AI-резолвер**. Автофетч/парсеры (Альфа, Сбер, Demir). LLM-резолвер. TG-бот для ручных подтверждений.
5. **Phase 4 — Mobile / PWA**. Быстрый ввод с телефона.
6. **Phase 5 — MCP HTTP**. Экспонировать MCP-эндпоинт для Hermes.
7. **Phase 6 — Freedom / Инвестиции**. Замена классического FIRE на Freedom Gap — разница между расходами и пассивным доходом.
- P0: Savings Rate Dashboard (норма сбережений из транзакций)
- P0: Multi-Currency Net Worth (общий капитал USD/KGS)
- P1: Investment Snapshots (ручной ввод раз в месяц, таблица + график)
- P1: Freedom Gap Dashboard (доход-расход-аренда-проект = gap)
- P1: Passive Income Tracker (аренда + дивиденды + депозиты)
- P2: Freedom Goal with Projection Engine (what-if сценарии)
- P3: What-if Simulator (слайдеры: курс, аренда, проект, норма сбережений)
- Новые таблицы: `investment_snapshot`, `freedom_goal`
- Новые страницы: `/freedom`, `/finances`
- *Не делать:* Monte Carlo, Withdrawal Strategy Planner, Roth Conversion Ladder — не применимы
### Multi-tenant readiness (для будущего public SaaS)
@@ -340,6 +361,12 @@ CLOUDFLARE_TUNNEL_TOKEN=...
- `echarts` (скаффолд под Phase 5, не используем активно)
- dev: `vite`, `typescript`, `eslint`, `prettier`
### Правила работы
1. **Тесты — обязательны** для каждого нового API-роута или изменения. Если код не покрыт тестом — он не готов.
2. **Обновление доку** — после каждой завершённой задачи обновлять таблицу прогресса и Acceptance criteria в этом доке.
3. **Комит** — после каждой логически завершённой задачи (не раз в 10 шагов).
### Acceptance criteria Phase 1
- [ ] `https://budget.qentra.top` открывается, login работает.
@@ -364,4 +391,156 @@ CLOUDFLARE_TUNNEL_TOKEN=...
---
**Создано:** 2026-06-21
**Статус:** Phase 0 закрыт, Phase 1 спланирован
**Статус:** Phase 1 — в работе
## Phase 1 progress
| Шаг | Статус | Кем |
| -------------------------------------- | ------ | ---- |
| Init репо | ✅ | Орёл |
| Backend skeleton | ✅ | Орёл |
| Postgres bootstrap — роль + БД | ✅ | Кит |
| Alembic initial migration | ✅ | Кит |
| FastAPI Users + auth routes | ✅ | Кит |
| Bootstrap первого юзера | ✅ | Кит |
| XLSX импортёр (6 045 транзакций) | ✅ | Кит |
| Read-only API (12 тестов) | ✅ | Кит |
| Frontend skeleton + read-only страницы | ✅ | Кит |
| Docker Compose (2 сервиса, работает) | ✅ | Кит |
| Cloudflare Tunnel budget.qentra.top | ✅ | Alex |
| Сверка данных | ✅ | Кит |
| | | |
## Disaster recovery: CASCADE data loss
**Сценарий:** Удалён пользователь (A-click → user delete). Из-за `ON DELETE CASCADE` на `transaction_user_id_fkey` все транзакции этого пользователя удалены мгновенно (6 045 строк). Балансы обнулены.
### Recovery шаги (на будущее)
```bash
# 1. Пересоздать пользователя с тем же email (admin123)
curl -XPOST .../api/auth/register -H... -d'{"email":"alex@qentra.top","password":"admin123"}'
# 2. Переимпортировать транзакции из xlsx
cd ~/Developer/budget-app
uv run python src/budget/importers/__init__.py
# 3. Проверить балансы — все 18 счетов должны совпасть с excel-balances.md
# 4. Пересобрать и передеплоить backend (дата формат) + frontend (любые изменения)
docker-compose build backend && docker-compose up -d backend
cd frontend && npm run build && cd .. && docker-compose build --no-cache frontend && docker-compose up -d frontend
```
### Формат даты на фронте (актуальный)
API возвращает `t.date.isoformat()` → `2026-05-29T17:00:00`.
Фронт режет: `{{ t.date.slice(0, 10) }} {{ t.date.slice(11, 16) }}` → `2026-05-29 17:00`.
Если время = `00:00` — в Excel не было времени для этой транзакции. Это корректно.
### Символы валют вместо колонки
Колонка "Валюта" убрана из таблиц Transactions, Accounts, TaxRecords.
Вместо неё символ валюты показывается непосредственно перед суммой (Transactions, TaxRecords) или в ячейке (Accounts).
Маппинг на фронте (постоянный, не из БД):
| Код | Символ | Валюта |
|-----|--------|--------|
| USD | $ | Доллар |
| EUR | € | Евро |
| RUB | ₽ | Рубль |
| KGS | С̲ | Сом (с с нижней чертой) |
| KZT | ₸ | Тенге (уже есть в Unicode) |
Файлы: `frontend/src/pages/Transactions.vue`, `Accounts.vue`, `TaxRecords.vue` — каждая содержит `CURRENCY_SYMBOLS` маппинг и функцию `getCurrencySymbol`/`currencySymbol`.
### Пароль
- `admin123` — совпадает с `ADMIN_PASSWORD` в `.env`
## Phase 2 progress
| Шаг | Статус | Кем |
| ---------------------------------------------------------------------------- | ------ | -------- |
| Валюта у счетов (символ) | ✅ | Кит |
| Категории с иерархией (API + фронт) | ✅ | Кит |
| Фронт: формы CRUD (транзакции, категории, счета) | ✅ | Кит |
| Свёртка по годам/месяцам (drill-down) | ✅ | Кит |
| Dynamic scrolling (infinite scroll) | ✅ | Кит |
| CRUD транзакций (API + тесты) | ✅ | Кит |
| CRUD категорий (API + тесты) | ✅ | Кит |
| CRUD счетов (API + тесты) | ✅ | Кит |
| Ручная правка курсов + API | ✅ | Кит |
| Дашборды (расход по категориям, динамика, сводная) | ✅ | Кит |
| Multi-currency отображение (баланс в валюте счёта + base currency) | ✅ | Кит |
| Сводная по годам | ✅ | Кит |
| Налоговый учёт | ✅ | Кит |
| DateTime в транзакциях (date → DateTime, datetime-local на фронте, миграция) | ✅ | Кит |
| Символ валюты вместо колонки (Transactions, Accounts, TaxRecords) | ✅ | Кит |
### Phase 2 — что сделано (подробно)
**API (новые эндпоинты):**
- `GET /api/reports/monthly?year=` — помесячная разбивка доходов/расходов
- `GET /api/reports/summary` — сводная по годам
- `GET /api/reports/category-breakdown?year=&month=` — расходы по группам категорий (данные для дашборда)
- `GET/POST/PUT/DELETE /api/exchange-rates` — CRUD курсов валют
- `GET /api/exchange-rates/summary` — группировка по типу налога
- `GET/POST/PUT/DELETE /api/tax-records` — CRUD налоговых записей
- Accounts API теперь возвращает `balance_in_base` и `base_currency` (мультивалютность)
**БД:**
- Новая таблица `tax_record` (alembic migration)
**Фронтенд (новые страницы):**
- `/dashboard` — дашборд с помесячной динамикой (CSS-chart), расходами по категориям, сводной по годам
- `/exchange-rates` — таблица курсов с фильтрами, CRUD через модалку
- `/tax-records` — таблица налогов с фильтрами и сводкой по типам
**Фронтенд (доработки):**
- `/transactions` — infinite scroll вместо пагинации (scroll-based)
- `/accounts` — отображение баланса в валюте счёта + в базовой валюте
- Навигация обновлена — добавлены ссылки на Дашборд, Курсы, Налоги
## Тесты
Запуск всех тестов одной командой (из `backend/`):
```bash
cd backend && uv run pytest
```
Verbose: `cd backend && uv run pytest -v`
### Фикстуры
Общий `conftest.py` в `tests/` предоставляет:
- `engine` — Postgres test DB (`budget_app_test`) с `create_all`/`drop_all` на каждый тест + seed валют
- `client` — ASGI клиент с зарегистрированным тестовым юзером
- `auth_headers` — JWT Bearer token
Все тесты используют **Postgres** (не sqlite). Настройка через `settings.test_database_url`.
### Покрытие
**37 тестов + 1 skipped**:
| Файл | Тестов | Что проверяет |
|------|--------|---------------|
| `test_health.py` | 1 | Health endpoint |
| `test_models.py` | 3 | Импорт моделей, метаданные, create_all в sqlite |
| `test_auth.py` | 2 | Auth flow (register→login→me), unauthorized |
| `test_api.py` | 6 | Транзакции (list, filter, search), accounts, reports, unauthorized |
| `test_crud.py` | 6 | CRUD транзакций |
| `test_categories.py` | 8 | CRUD групп и категорий |
| `test_accounts.py` | 6 | CRUD счетов, удаление с транзакциями |
| `test_account_balances.py` | 6 | **Баланс: доход+расход, переводы, cross_rate, initial_balance** |
### Вычисление баланса
`balance = initial_balance + incoming - outgoing`
- `incoming` = SUM(amount) if cross_rate IS NULL, SUM(amount * cross_rate) если перевод между валютами (для dest_account)
- `outgoing` = SUM(amount) — всегда в валюте источника
@@ -39,7 +39,7 @@ FastAPI + HTMX + Tailwind CDN + Alpine.js. No build step, CDN-only frontend.
├── services.yaml # Service definitions (source of truth)
├── .env # EAGLE_TOKEN=<secret> (not committed)
├── templates/
│ └── index.html # Dashboard UI — 3 tabs: Services, Pages, Files
│ └── index.html # Dashboard UI — 4 tabs: Services, Crons, Pages, Files
└── com.eagle.dashboard.plist
```
@@ -133,14 +133,102 @@ PID-file-based — survives Dashboard restarts without crashing.
---
## Tabs — заглушка урл
Каждый таб имеет свой URL через `?tab=` query-параметр. Переключение через Alpine.js `setUrlTab()`, который использует `history.replaceState()`. При загрузке `activeTab` инициализируется из `URLSearchParams` (с fallback на `'services'`). При клике на таб URL обновляется без перезагрузки страницы. Бэкенд также принимает `?tab=` в `GET /` и передаёт его как `initial_tab` в шаблон (пока не используется).
**Урлы:**
- `http://dashboard.qentra.top/` — Services
- `http://dashboard.qentra.top/?tab=crons` — Crons
- `http://dashboard.qentra.top/?tab=pages` — Pages
- `http://dashboard.qentra.top/?tab=files` — Files
## Tabs
**Services** — health cards with: status badge, PID, uptime, memory (RSS + children), start/stop/restart controls, log tail drawer (last 200 lines).
**Pages** — URL input + iframe for quick local page testing.
**Pages** — URL input + metadata table for `/Library/WebServer/Documents/*.html` test pages. Each row shows filename, title, purpose, contents, modified date, and opens the page in a new browser tab.
**Files** — FileBrowser iframe at `http://localhost:8181`.
**Crons** (2-я вкладка) — управление cron jobs из 3 источников:
- **Hermes Eagle** (22 jobs) — read/write напрямую в `~/.hermes/cron/jobs.json`
- **Hermes Whale** (0 jobs) — read/write в `~/.hermes/hermes-whale/cron/jobs.json`
- **Launchd (User)** — launchd plist-ы из `~/Library/LaunchAgents/` с `StartInterval` или `StartCalendarInterval` (редактируемые через launchctl + plistlib)
Для каждого крона на карточке: name, schedule, enabled/disabled, last run, prompt preview, chips (skills, toolsets, model, workdir). Кнопки:
- **+ Cron** — создать новый крон (выбор source: Hermes Eagle / Hermes Whale / Launchd (User)). При выборе Launchd поля Deliver, Prompt, Model/Provider, Skills/Toolsets динамически скрываются через `x-show="cronModal.source !== 'launchd'"`.
- ✎ — открыть модал редактирования всех полей (name, schedule, deliver, prompt, script, model, provider, skills, toolsets, workdir)
- ⏸/▶ — pause/resume (toggle enabled)
- 🗑 — удалить крон
|**Launchd create** — `POST /api/crons` с `source=launchd` генерирует plist. Принимает: `name` (→ Label, префикс `com.` если не указан), `schedule` (только `every Ns/m/h/d` → StartInterval в секундах), `script` (→ ProgramArguments, разбивка по пробелам), `workdir` (→ WorkingDirectory). Cron-выражения и ISO-даты для launchd **не поддерживаются** — валидатор на фронтенде их отклоняет с сообщением "Launchd: используйте every 30m / every 2h / every 1d". Поля prompt, deliver, skills, toolsets, model, provider игнорируются. Backend парсит `every N` + суффикс s/m/h/d → множитель 1/60/3600/86400.
**`_list_launchd_crons()`** теперь показывает **все** plist-ы из `~/Library/LaunchAgents/`, не только с StartInterval/StartCalendarInterval. Для plist без schedule — `schedule: "none (manual)"`. Это фикс: раньше plist без schedule (например созданный с cron-выражением) не отображался в дашборде.
**Schedule input — формат подсказки и валидация зависят от source:**
- Для **launchd**: подсказка только `every 30s / 30m / 2h / 1d — интервал`. Плейсхолдер `every 30m / every 2h`. Валидатор reject cron и ISO.
- Для **Eagle/Whale**: полная cron-схема (5 полей), пояснения `*`, `*/N`, `N-M`, `A,B`, `N`, плюс `every 30s / 30m / 2h / 1d` и ISO-дата.
**Schedule hint** — сворачиваемый multiline блок над полем ввода, открывается по кнопке `? подсказка`. Финальный рабочий текст:
```
┌── минута (0-59)
│ ┌── час (0-23)
│ │ ┌── день месяца (1-31)
│ │ │ ┌── месяц (1-12)
│ │ │ │ ┌── день недели (0-6, вс=0)
│ │ │ │ │
* * * * *
* — любое значение
*/N — с шагом N (*/30 = каждые 30)
N-M — диапазон (1-5 = пн-пт)
A,B — список (0,6 = вс,сб)
N — точное число (0 = в 0 мин)
every 30s / 30m / 2h / 1d — человеческий формат
2026-07-01T09:00 — ISO, одноразово (только Eagle/Whale)
```
- `scheduleHintOpen` — boolean в `cronModal`, по умолчанию `false`
- При открытии модала сбрасывается в `false`
- ISO-строка скрыта для launchd через `x-if="cronModal.source !== 'launchd'"`
**API эндпоинты:**
- `GET /` — главная страница, опционально `?tab=crons|pages|files|services`
- `GET /api/crons` — список всех кронов
- `POST /api/crons` — создать новый крон (body: source, name, schedule, prompt, script, deliver, skills, enabled_toolsets, model, provider, workdir). Для source=launchd: только name, schedule, script, workdir используются — генерируется plist и загружается через launchctl
- `PATCH /api/crons/{source}/{job_id}` — редактировать поля
- `POST /api/crons/{source}/{job_id}/toggle` — включить/выключить
- `DELETE /api/crons/{source}/{job_id}` — удалить
**Формат Hermes cron jobs.json:**
```json
{
"jobs": [
{
"id": "531b242c30ad",
"name": "data-pipeline",
"prompt": "[SILENT] bash ...",
"skills": [],
"skill": null,
"model": null,
"provider": null,
"script": null,
"schedule": { "kind": "cron", "expr": "*/30 7-21 * * 1-5", "display": "*/30 7-21 * * 1-5" },
"schedule_display": "*/30 7-21 * * 1-5",
"enabled": false,
"state": "paused",
"deliver": "local",
"enabled_toolsets": null,
"workdir": null
}
]
}
```
---
## Deployment
@@ -171,10 +259,32 @@ launchctl load ~/Library/LaunchAgents/com.eagle.dashboard.plist
**Ghost launchctl entries** — after removing plists, use `launchctl remove <label>` (not `bootout`) to clear bootstrap session entries.
**`_launchd_toggle()` known bug** — `_launchd_toggle()` in `main.py` uses `launchctl list <label>` exit code (0 = loaded) to determine "enabled". But `launchctl list` returns 0 even when process is **not running**, as long as plist is registered. After creating a launchd cron via API (`launchctl load -w`), plist is loaded → `enabled=true` → UI shows Pause. Clicking calls unload → `enabled=false` → "✓ paused". Functionally correct but button state appears inverted if user expected Resume. Fix: rewrite `_launchd_toggle()` to check `Disabled` key in plist after unload rather than `launchctl list` exit code.
## Troubleshooting — launchd cron doesn't run (EX_CONFIG)
When a launchd cron is created and `launchctl print gui/<uid>/<label>` shows `last exit code = 78: EX_CONFIG`:
1. **`~` not expanded in ProgramArguments.** launchd does not expand `~`. Use full path: `/Users/admin/scripts/foo.sh`
2. **Script itself exits 78.** Check script logic: `exit 78` is `EX_CONFIG` (configuration error). Fix the script.
## Troubleshooting — sync-vault appeared as "none (manual)"
**Root cause:** Launchd create accepted cron expression `*/5 * * * *` (pre-fix), couldn't parse it into StartInterval, wrote plist without schedule → `_list_launchd_crons()` (pre-fix) filtered it out. Fixed in two ways:
- Backend create now rejects non-`every` for launchd (frontend validator also blocks)
- `_list_launchd_crons()` now shows ALL plists (schedule: "none (manual)" if no StartInterval/StartCalendarInterval)
**Virfield port conflict** — if previously a LaunchAgent (plist deleted but still loaded), stop it: `launchctl stop com.virfield.server && launchctl remove com.virfield.server`.
---
## History
- 2026-06-04: Initial build. Migrated 14 LaunchAgent plist services under Dashboard supervisor. Single `com.eagle.dashboard` Login Item. cloudflared tunnel at `dashboard.qentra.top`. Ghost entries cleaned via `launchctl remove`.
- 2026-06-25 (round 8): **Multiple bugfixes.** `_msg` timer fix — stale object reference replaced with `find` in fresh array (both cron toggle and service action). `_list_launchd_crons()` enabled fix — removed `and pid is not None` (timer-based launchd jobs always had `enabled: false`). `openAddCron()` — source-dependent default schedule (`every 30m` for launchd, `*/30 * * * *` for Hermes), `scheduleErr` init via `validateSchedule()`. `@change` on source selector replaces default if switching hermes→launchd. Backend create for launchd: only `every Ns/m/h/d` parsed (cron-parser removed). `_list_launchd_crons()` now shows ALL plists.
- 2026-06-25 (round 7): **Launchd schedule — source-dependent validation.** `validateSchedule()` now takes `source` param. For `launchd`: only `every Ns/m/h/d` accepted; cron and ISO rejected with explicit message. For Eagle/Whale: all three formats. Hints, placeholders, and `prettySchedule()` also differ by source. `Query` import added to FastAPI.
- 2026-06-25 (round 6): **Tab URL routing.** Each tab now persists in URL via `?tab=` query-param + `history.replaceState()`. Frontend initializes `activeTab` from URL. Backend accepts `?tab=` on `GET /`. Added `setUrlTab()` to Alpine.js data.
- 2026-06-25 (round 4): **Schedule hint final format applied.** Multiline cron reference (no `#` prefix), field labels (m/h/d/m/w), value notation (`*/N`, `N-M`, `A,B`, `N`), human format (`every 30s / 30m / 2h / 1d`), ISO date. Collapsible via `? подсказка` button. ISO line hidden for launchd. Validator also blocks Save on invalid input.
- 2026-06-25 (round 3): **Schedule hint/validator polish.** Replaced `*/30 * * * *` with `every 30m` in hint text. Added `prettySchedule()`. Removed all prefatory labels from hint.
- 2026-06-25 (round 1): Added **launchd create** support — +Cron source selector now includes "⏰ Launchd (User)". Backend generates plist. Fields for Hermes-only dynamically hidden for launchd.
- 2026-06-24: Added **Crons** tab — management of Hermes Eagle/Whale cron jobs + Launchd (User) agents. Tab moved to 2nd position. `+Cron` button with source selector (Eagle/Whale). Full edit modal: name, schedule, prompt, script, deliver, skills, toolsets, model, provider, workdir. Toggle pause/resume, delete. All sections editable — launchd crons toggle via `launchctl unload/load -w`, edit via `plistlib`+reload, delete via unload+rm. API: `GET/POST/PATCH/DELETE /api/crons`. `json` and `datetime` imports added to main.py.
- 2026-06-04: Initial build.
@@ -2,56 +2,88 @@
title: Hermes Cron Jobs
aliases: [hermes cron, scheduled jobs, cron jobs]
tags: [personal-os, hermes, automation, cron]
updated: 2026-06-16
updated: 2026-06-24
---
# Hermes Cron Jobs
All scheduled jobs running in Hermes on Eagle.
All scheduled jobs running in Hermes on Eagle. Managed via **Eagle Dashboard → Crons tab** (`http://localhost:8880`).
## Active Jobs
Управление через Dashboard API: `GET/PATCH/POST/DELETE /api/crons`.
Файл: `~/.hermes/cron/jobs.json` (Eagle), `~/.hermes/hermes-whale/cron/jobs.json` (Whale).
## Job Format (jobs.json)
```json
{
"id": "531b242c30ad",
"name": "data-pipeline",
"prompt": "[SILENT] Run the data pipeline: bash ~/scripts/run-pipeline.sh",
"skills": [],
"script": null,
"schedule": { "kind": "cron", "expr": "*/30 7-21 * * 1-5", "display": "*/30 7-21 * * 1-5" },
"schedule_display": "*/30 7-21 * * 1-5",
"enabled": false,
"state": "paused",
"deliver": "local",
"enabled_toolsets": null,
"workdir": null,
"model": null,
"provider": null,
"created_at": "...",
"last_run_at": "...",
"last_status": "ok"
}
```
Editable fields via API: name, schedule, prompt, script, deliver, skills, model, provider, enabled_toolsets, workdir.
## All 22 Jobs (Eagle)
### Active (1)
| Job | Schedule | Description | Delivery |
|-----|----------|-------------|----------|
| `data-pipeline` | `*/30 7-21 * * 1-5` | Runs `bash ~/scripts/run-pipeline.sh` | local |
| `eod-summary` | `0 18 * * 1-5` | EOD brief with completed/carries-over/blockers | zulip: daily-brief::EOD Summary |
| `inbox-check` | `*/30 9-19 * * 1-5` | Inbox triage via Claude CLI | discord: #inbox |
| `weekly-plan` | `0 8 * * 1` | Weekly plan via `weekly-plan.md` prompt | zulip: daily-brief::Weekly Plan |
| `weekly-review` | `0 17 * * 5` | Weekly review via `weekly-review.md` prompt | zulip: daily-brief::Weekly Review |
| `generate-daily-brief` | `0 7 * * 1-5` | Daily brief via `daily-brief.md` prompt | zulip: daily-brief::Daily Brief |
| `sync-vault` | `*/5 * * * *` | Syncs obsidian vault via `sync-vault.sh` | local |
| `watchlist-sync` | `0 * * * *` | Watchlist sync (TMDB resolver) | local |
## Paused Jobs
### Paused (21)
| Job | Schedule | Notes |
|-----|----------|-------|
| `retrospector` | `30 17 * * 5` | Paused — experiment, not stabilised |
| `data-pipeline` | `*/30 7-21 * * 1-5` | Runs `bash ~/scripts/run-pipeline.sh` |
| `eod-summary` | `0 18 * * 1-5` | EOD brief → zulip:daily-brief::EOD Summary |
| `inbox-check` | `*/30 9-19 * * 1-5` | Inbox triage → discord:#inbox |
| `weekly-plan` | `0 8 * * 1` | Weekly plan → zulip:daily-brief::Weekly Plan |
| `weekly-review` | `0 17 * * 5` | Weekly review → zulip:daily-brief::Weekly Review |
| `generate-daily-brief` | `0 7 * * 1-5` | Daily brief → zulip:daily-brief::Daily Brief |
| `retrospector` | `30 17 * * 5` | Retrospector → discord:#retrospector |
| `executor-autonomous` | `*/30 * * * *` | Paused — replaced by executor-runner |
| `executor-runner` | `*/5 * * * *` | Paused — waiting for new executor arch |
| `executor-analyzer` | `*/5 * * * *` | Paused — waiting for new executor arch |
| `wiki-curation-daily` | `0 2 * * *` | Paused since 2026-05-29 |
| `AI-психолог` | `0 23 * * 1,4` | Paused since 2026-05-29 — experiment |
| `vault-enrichment` | `0 3 * * 0` | Paused since 2026-05-29 |
| `cross-enrichment` | `0 4 * * 0` | Paused since 2026-05-29 |
| `memory-curation` | `0 4 * * 0` | Paused since 2026-05-29 |
| `proactive-research` | `0 5 * * 6` | Paused since 2026-05-29 |
| `wiki-curation-daily` | `0 2 * * *` | wiki curator (skills: llm-wiki, toolsets: file,web,terminal) |
| `AI-психолог` | `0 23 * * 1,4` | Психолог сессия (Пн/Чт) (toolsets: file,web) |
| `vault-enrichment` | `0 3 * * 0` | Vault enrichment (toolsets: file,terminal) |
| `vault-cross-enrichment` | `0 4 * * 0` | Cross-domain enrichment (toolsets: file,web,terminal) |
| `vault-cross-enrichment-overflow` | `30 5 * * 0` | Overflow handler (toolsets: file,web,terminal) |
| `memory-curation` | `0 4 * * *` | Memory curator (toolsets: file,terminal) |
| `proactive-research` | `0 5 * * 6` | Research queue (toolsets: file,web,terminal) |
| `watchlist-nightly` | `0 1 * * *` | Watchlist nightly (toolsets: terminal) |
| `watchlist-discover` | `0 9 * * 0` | Watchlist discover (toolsets: terminal) |
| `claude-auth-login` | `28 6 * * *` | Claude auth refresh |
| `obsidian-inbox-sort` | `0 1 * * *` | Inbox sort (skills: obsidian-inbox-sort) |
## Known Issues
## Whale Jobs
- `weekly-review` — last run (2026-06-12) failed with `HTTP 500: init handshake timed out after 30000ms`
- `generate-daily-brief` — delivery to Zulip hit 502 on 2026-06-16 morning (Zulip was down); run itself succeeded
0 jobs currently. Whale cron state at `~/.hermes/hermes-whale/cron/jobs.json`.
## Delivery Targets
- `local` — saved to `~/.hermes/cron/output/`, not delivered to any chat
- `discord: #inbox` — Discord inbox channel
- `zulip: daily-brief::*` — Zulip stream `daily-brief`, various topics
- `origin` — back to the Zulip thread where the job was created
- `zulip:stream:topic` — Zulip stream+topic
- `discord:#channel` — Discord channel
## Notes
## Known Issues
- Data pipeline and inbox-check run on overlapping intervals during workdays
- `sync-vault` runs every 5 min always (not restricted to workdays)
- Jobs use `claude` CLI (`/Users/admin/.local/bin/claude`) for LLM calls
- `watchlist-sync` added 2026-06-09, uses skill `watchlist-sync-resolver`
- `weekly-review` — last run (2026-06-12) failed with `HTTP 500: init handshake timed out after 30000ms`
- `generate-daily-brief` — delivery to Zulip hit 502 on 2026-06-16 morning (Zulip was down)
@@ -0,0 +1,84 @@
# Obsidian MCP — текущая архитектура
## Факт: что стоит сейчас
**Hermes native MCP client** (встроен в Hermes Agent, не wrapper).
Конфиг в `~/.hermes/config.yaml` (и в `~/.hermes/hermes-whale/config.yaml`):
```yaml
mcp_servers:
obsidian:
command: mcpvault
args:
- /Users/admin/obsidian
```
Пакет: **`@bitbonsai/mcpvault`** v0.12.1 (npm). Команда `mcpvault`.
Hermes на старте:
1. Читает `mcp_servers` из config.yaml
2. Спавнит `mcpvault /Users/admin/obsidian` как subprocess
3. Init + list_tools → регистрирует инструменты как `mcp_obsidian_*`
4. Агент видит инструменты `mcp_obsidian_read_note`, `mcp_obsidian_patch_note`, и т.д.
**Схема:**
```
Hermes (native MCP client) → spawn: mcpvault → reads/writes /Users/admin/obsidian/
```
### Инструменты, доступные через эту связку
`read_note`, `write_note`, `patch_note`, `list_directory`, `delete_note`, `search_notes`, `move_note`, `move_file`, `read_multiple_notes`, `update_frontmatter`, `get_notes_info`, `get_frontmatter`, `manage_tags`, `get_vault_stats`, `list_all_tags`.
## Факт: что такое ~/scripts/obsidian-mcp-wrapper.js и почему он НЕ используется
**Создан** 2026-05-09, когда вместо `mcpvault` стоял `npx obsidian-mcp` (старый пакет, автор Steven, ещё до переименования в `@bitbonsai/mcpvault`). Пакет постоянно падал:
- ZodError на `"id": null` в notifications — `.strict()` валидация
- Race condition при рестарте gateway
- UTF-8 chunk split портил большие JSON
- Зависания без watchdog
Wrapper решал всё это. **Сейчас НЕ используется.** Конфиг в `~/.hermes/config.yaml`:
```yaml
mcp_servers:
obsidian:
command: mcpvault # ← не node wrapper.js
args:
- /Users/admin/obsidian
```
**Почему:** после перехода на `@bitbonsai/mcpvault` (пришёл на смену старому obsidian-mcp), пакет стабильно работает с Hermes native MCP client. Wrapper стал не нужен. Конфиг поменяли, а doc не обновили.
**Файл `~/scripts/obsidian-mcp-wrapper.js`** лежит на диске, не используется. Надо удалить.
## Факт: корень проблем с patch_note
`mcp_obsidian_patch_note` падает с `"String not found"` — это **НЕ проблема транспорта**. Это проблема **самого mcpvault**:
- Файл: `dist/src/filesystem.js`, строка 217
- Механизм: `fullContent.split(oldString).length - 1`
- **Exact string match** — без trim, без fuzzy, без нормализации whitespace
Workaround: использовать Hermes `patch()` (fuzzy matching, 9 стратегий) для сложных строк. `mcp_obsidian_patch_note` — только для простого текста без спецсимволов.
## Эволюция Obsidian MCP у нас
| Период | Что было | Проблемы |
|--------|----------|----------|
| До 2026-05-09 | `npx obsidian-mcp` (пакет Steven, прямой) | ZodError, race condition, UTF-8 chunk split, зависания |
| 2026-05-09 → ? | `npx obsidian-mcp` через wrapper | Wrapper решил проблемы |
| Сейчас | `mcpvault` (Hermes native MCP client) | Стабильно. patch_note exact match — единственная боль |
## Рекомендация по замене (2026-06-24)
При проблемах с mcpvault (зависания, память, exact match) — **cyanheads/obsidian-mcp-server**:
- `obsidian_replace_in_note` с regex + flexible whitespace (решает exact match)
- 9.7K dl/week, dual transport (stdio + HTTP), active
- Требует Obsidian Local REST API plugin (Obsidian должен быть открыт)
- Есть Docker support (`ghcr.io/cyanheads/obsidian-mcp-server`)
Подробнее: `personal/tech/obsidian-mcp-ecosystem.md`
## См. также
- `personal/tech/obsidian-mcp-ecosystem.md` — обзор всех 6 реализаций
- ⚠️ `personal/projects/personal-os/obsidian-mcp-wrapper.md`**УСТАРЕЛ**, не отражает реальность
@@ -1,114 +1,25 @@
# obsidian-mcp-wrapper
> **Файл**: `~/scripts/obsidian-mcp-wrapper.js`
> **Назначение**: прокси-обёртка над `obsidian-mcp`, решает четыре системных бага
---
status: deprecated
superseded_by: personal/projects/personal-os/obsidian-mcp-setup.md
reason: >-
wrapper не используется с 2026-05. Реальность: Hermes native MCP client →
mcpvault
---
## Проблемы, которые решает
# obsidian-mcp-wrapper — УСТАРЕЛ
### 1. ZodError при инициализации (obsidian-mcp v1.0.6)
> **⚠️ Этот документ не отражает реальность.** Wrapper не используется с мая 2026.
> См. `personal/projects/personal-os/obsidian-mcp-setup.md` и
> `personal/tech/obsidian-mcp-ecosystem.md`.
`obsidian-mcp` падал с ZodError сразу после запуска. Причина: Hermes отправляет
`notifications/initialized` с полем `"id": null`, а obsidian-mcp v1.0.6 использует
`.strict()` валидацию и не принимает лишние поля.
Актуальная архитектура: **Hermes native MCP client → spawn `mcpvault /Users/admin/obsidian`**
(пакет `@bitbonsai/mcpvault` v0.12.1). Конфиг в `~/.hermes/config.yaml``mcp_servers.obsidian`.
**Fix**: wrapper перехватывает все notification-сообщения (без `result`/`error`) с `id === null`
и удаляет поле `id` перед передачей в child.
Wrapper (`~/scripts/obsidian-mcp-wrapper.js`) когда-то решал проблемы старого пакета `obsidian-mcp`
(автор Steven), который падал с ZodError, race condition и UTF-8 chunk split.
После смены пакета на `@bitbonsai/mcpvault` — wrapper стал не нужен.
### 2. Race condition при gateway restart
Корень проблем с `patch_note` (`"String not found"`) — exact string match в самом mcpvault,
**не в транспорте**. Workaround: использовать Hermes `patch()` с fuzzy matching.
При рестарте Hermes gateway поднимает новый процесс `obsidian-mcp-wrapper`. Первые
параллельные tool-вызовы приходят пока child ещё инициализируется (~500ms) → они
тайм-аутились, circuit breaker открывался (3 фейла → 60s cooldown).
**Fix**: wrapper буферизует все tool-вызовы до завершения handshake
(`initialize` → ответ → `notifications/initialized`), потом флашит очередь.
### 3. Corrupted large payloads (UTF-8 chunk split)
При больших tool-вызовах (~200KB+) Node.js доставляет stdin в нескольких chunk-ах.
Старый код делал string split — JSON разрезался по байтам → UTF-8 multibyte символы
портились, `JSON.parse` падал, сообщение дропалось молча.
**Симптом**: `edit_note` с большим контентом тихо зависал (30s timeout), в логах:
```
[obsidian-wrapper] Non-JSON from Hermes (forwarding verbatim): {"jsonrpc": "2.0", "method": "tools/call", "id": 3, "params": {"name": "edit-not
```
**Fix**: stdin и stdout читаются через `Buffer.concat` + `Buffer.slice` на `0x0a`.
Строка собирается полностью до передачи в `JSON.parse`.
### 4. Per-call watchdog (зависший child)
Если child не ответил на `tools/call` / `tools/list` / `resources/*` за **5s**
watchdog убивает процесс. После авторестарта call автоматически уходит в голову
очереди и ретраится.
**Fix**: `armWatchdog(callLine)``setTimeout 5000ms``child.kill()``startChild()`.
---
## Как работает
```
Hermes (stdin) → wrapper → obsidian-mcp (child)
↑ auto-restart при краше (до 10 раз)
```
**Состояния**:
- `ready = false` — child стартует, все tool-вызовы в очередь
- `ready = true` — handshake завершён, очередь флашится, всё проходит напрямую
**Restart логика**:
1. Child упал → `ready = false`, `restarts++`
2. Новый child спавнится
3. Wrapper реплеит сохранённый `initialize` → ждёт ответа с `serverInfo`
4. Отправляет `notifications/initialized` (не форвардит Hermes — он не просил)
5. `ready = true` → флаш очереди
**Shutdown**:
На stdin EOF (`Hermes` закрыл процесс) — child убивается без авторестарта, wrapper выходит чисто.
**Логи** (все в stderr с timestamp):
```
[obsidian-wrapper] 2026-05-09T12:00:00.000Z Spawning obsidian-mcp (vault=/Users/admin/obsidian)
[obsidian-wrapper] 2026-05-09T12:00:00.500Z Hermes → child (handshake complete): notifications/initialized
[obsidian-wrapper] 2026-05-09T12:00:00.501Z Child ready — flushing queue (3 items)
[obsidian-wrapper] 2026-05-09T12:00:01.200Z Stripped id:null from notification: notifications/initialized
[obsidian-wrapper] 2026-05-09T12:00:06.000Z WATCHDOG: child did not respond in 5000ms for tools/call #7 — killing and restarting
```
---
## Конфиг Hermes
`~/.hermes/config.yaml`:
```yaml
obsidian:
command: node
args: [/Users/admin/scripts/obsidian-mcp-wrapper.js]
```
Wrapper сам вызывает `/opt/homebrew/bin/obsidian-mcp /Users/admin/obsidian`.
---
## Производительность
- Инициализация: ~587ms (без ZodError)
- Overhead wrapper: negligible (pure Node.js child_process, нет npm-зависимостей)
- MAX_RETRY: 10
- CALL_TIMEOUT_MS: 5000ms (watchdog)
---
## История
**2026-05-09 (1)** — создан после диагностики 58 ошибок `obsidian/... call failed` в логах Hermes.
Корневая причина — ZodError + race condition при старте. Wrapper написан вместо патча
исходников obsidian-mcp (патч не нужен, wrapper чище и не ломается при обновлении пакета).
**2026-05-09 (2)** — фикс large payload: Buffer-based line splitting вместо string split
(`edit_note` с большим контентом молча дропался). Добавлен per-call watchdog (5s timeout → kill & retry).
MAX_RETRY повышен с 5 до 10.
Историческая документация по wrapper сохранена ниже для ретроспективы.
+78
View File
@@ -0,0 +1,78 @@
# Docker на Mac — проблема с диском и сборкой образов
_Последнее обновление: 2026-06-24_
## Сетап
| Параметр | Значение |
|----------|----------|
| Движок | Colima (не Docker Desktop!) |
| Гипервизор | macOS Virtualization.Framework |
| Архитектура | arm64 |
| CPU | **8 ядер** |
| RAM | **24 GiB** |
| Диск VM | **100 GiB** (sparse) |
| Mount type | virtiofs |
## Проблема: контейнеры (особенно Zulip) зависают во время сборки Docker образов для Валеры
**Симптом:** Zulip перестаёт отвечать на вебхуки, RabbitMQ паникует, PostgreSQL тормозит.
## 🔴 Реальный корень — переполнение диска VM (не CPU, не bind mount)
### Структура диска Colima VM
`/dev/vdb1` (98 GiB, data disk) хранит `/var/lib/docker`:
```
17 GB — /var/lib/docker (Colima data)
├── 8.2 GB — rootfs/overlayfs ← слои всех образов
├── 8.9 GB — volumes
│ ├── 8.4 GB — buildx_buildkit_arm64builder0_state ← 🔴 build cache volume
│ ├── 341 MB — zulip_zulip-pgdata
│ ├── 85 MB — dangling volume
│ ├── 46 MB — openclaw_postgres-data
│ └── 38 MB — zulip_zulip-data
└── остальное — containers, buildkit metadata
```
### Build cache volume — 8.4 GB и не чистится сам
Docker BuildKit при использовании `buildx` хранит кеш слоёв в **Docker volume**, а не в overlayfs. Это volume `buildx_buildkit_arm64builder0_state`.
**Почему не чистится:**
- `docker system prune` чистит только build cache records (метаданные), НЕ volume
- BuildKit контейнер (`moby/buildkit:buildx-stable-1`) всегда running — volume считается активным
- BuildKit GC policy не видит этот volume как «своё» хранилище (это Docker volume, не `/var/lib/buildkit`)
- Без ручной очистки volume растёт с каждой сборкой и не уменьшается
**Почему аффектит контейнеры:**
1. Каждая сборка Валеры добавляет новые слои в этот volume
2. Volume растёт (8.4 GB и выше)
3. Когда `/dev/vdb1` заполняется >85-90%:
- Docker overlayfs + containerd snapshotter начинают тормозить
- Запись новых слоёв фейлится или идёт в разы медленнее
- PostgreSQL и RabbitMQ WAL не могут закоммититься
- RabbitMQ Khepri (Raft) при проблемах с WAL сбрасывает состояние (пользователи пропадают)
- Zulip отдаёт 500 на `/api/v1/register`
- Контейнеры тупят или падают в restart loop
### Чистка build cache volume (безопасно)
```bash
# Остановить buildkit контейнер, удалить volume, BuildKit пересоздаст при следующем билде
docker rm -f buildx_buildkit_arm64builder0
docker volume rm buildx_buildkit_arm64builder0_state
```
Мониторинг заполненности диска VM:
```bash
colima ssh -- df -h /dev/vdb1
```
### Связанные документы
- [[tech/docker_colima_setup]] — установка и автостарт Colima
- [[projects/balda/setup]] — Balda агенты (Валера, Клавдий)
- [[how-to/hermes-eagle-mac]] — Hermes на Mac, Zulip Docker стек (RabbitMQ pitfall про Khepri)
- [[how-to/openmediavault-rpi5]] — build cache на RPi (та же проблема ENOSPC)
+11
View File
@@ -1,5 +1,16 @@
# Docker + Colima Autostart Setup (macOS)
_Последнее обновление: 2026-06-24_
## Текущие параметры Colima
```
colima start --cpu 8 --memory 24 --disk 100
```
- virtiofs mount type
- Build cache volume `buildx_buildkit_arm64builder0_state` занимает до 8+ GB и не чистится автоматически. См. [[tech/docker-mac-disk-issues]].
## 1. Install dependencies
``` bash
+112
View File
@@ -0,0 +1,112 @@
# Hermes Memory Architecture
## Обзор
Hermes (Whale) использует **thread-scoped memory** + кастомную memory instruction. Глобальный профиль (`USER.md`) и thread-локальная память (`MEMORY.md`) — два независимых хранилища.
## Состав системного промпта
Собирается в `agent/system_prompt.py` из трёх слоёв: stable (кешируется на сессию), context (зависит от CWD), volatile (каждый раз свежий).
### Stable tier
1. **SOUL.md** — не существует (Whale), используется `DEFAULT_AGENT_IDENTITY`.
2. **HERMES_AGENT_HELP_GUIDANCE** — ссылка на `https://hermes-agent.nousresearch.com/docs`.
3. **TASK_COMPLETION_GUIDANCE** — «Finishing the job»: не фабриковать, не останавливаться на стабах.
4. **Tool-aware guidance**:
- `MEMORY_GUIDANCE` — declarative facts, не imperative, не task progress.
- `SESSION_SEARCH_GUIDANCE` — искать в прошлом session_search.
- `SKILLS_GUIDANCE` — сохранять сложные подходы как skills, патчить устаревшие.
5. **Tool-use enforcement**`TOOL_USE_ENFORCEMENT_GUIDANCE` + per-model operational guidance (deepseek → `OPENAI_MODEL_EXECUTION_GUIDANCE`).
6. **Skills prompt** — список доступных скиллов из `~/.hermes/skills/`.
7. **Environment hints** — macOS, home, cwd.
8. **Active profile hint** — «default», +предупреждение не лезть в чужие профили.
9. **Platform hints** — зависит от платформы (zulip/telegram/webhook/webui...).
### Context tier
10. **system_message** — от платформы (например, содержимое входящего сообщения).
11. **Context files**`AGENTS.md`, `.cursorrules`, `.hermes.md` из `TERMINAL_CWD`.
### Volatile tier (не кешируется)
12. **Memory** — thread-scoped MEMORY.md.
13. **USER.md** — глобальный профиль.
14. **Custom memory instruction**`~/.hermes/hermes-whale/review/memory_prompt.md`.
15. **Таймстемп** — дата старта, session ID, model, provider.
### Файлы конфигурации и кастомные промпты
- `~/.hermes/hermes-whale/review/skill_prompt.md` — инжектится в background review (skill review).
- `~/.hermes/hermes-whale/review/memory_prompt.md` — memory instruction.
- `prefill_messages_file` — не задан.
- `personalities` — пусто.
- `hooks` — пусто.
## Конфигурация
Файл: `~/.hermes/hermes-whale/config.yaml`
```yaml
memory:
memory_enabled: true
user_profile_enabled: true
memory_char_limit: 3000
user_char_limit: 1375
provider: ''
nudge_interval: 10
flush_min_turns: 6
thread_scoped: true # изолированная память по треду
prompt_path: ~/.hermes/hermes-whale/review/memory_prompt.md # кастомная инструкция
```
## Файловая структура
```
~/.hermes/hermes-whale/memories/
├── MEMORY.md # глобальная (fallback для CLI/старых сессий)
├── USER.md # глобальная (всегда)
└── threads/
├── agent:main:webhook:webhook:webhook:whale/
│ └── MEMORY.md # память конкретного треда
└── ...
```
## Memory prompt
Файл: `~/.hermes/hermes-whale/review/memory_prompt.md`
### Что запоминать
1. Документы — file paths, что изменено, почему.
2. **После загрузки Obsidian docs** — извлекать ключевые факты в memory.
3. Команды и инструменты — нетривиальные CLI вызовы, форматы конфигов.
4. Статус проекта — completed, pending, blockers, next steps.
5. Предпочтения пользователя — коррекции, фидбек.
6. Факты окружения — пакеты, порты, пути до credentials (не сами secrets).
7. Решения и обоснование выбора подхода.
8. Конфиг изменения — ключи/значения/пути.
### Что НЕ запоминать
- Временный debugging noise.
- "Сейчас в процессе задачи X" — session_search покрывает.
- Очевидные факты (rediscover за 2 сек).
- Negative claims о сломанных инструментах — становятся persistent self-imposed constraints.
### Особенности редактирования
- `patch()` для точных замен, но не перенумеровывает списки.
- После вставки пункта mid-list — проверить нумерацию, делать второй clean patch.
## Команды
```bash
# Проверить текущую память
cat ~/.hermes/hermes-whale/memories/USER.md
cat ~/.hermes/hermes-whale/memories/MEMORY.md
# Структура файлов памяти
find ~/.hermes/hermes-whale/memories -type f -name "*.md"
# Редактировать prompt (через patch или write_file)
```
## Связанное
- [[thread-scoped-memory]] — план реализации.
- `personal/plans/thread-scoped-memory.md` — детали тестов, pitfalls, коммиты.
@@ -0,0 +1,222 @@
# Hermes Self-Improvement Review (Background Memory/Skill Review)
Source: codebase analysis of `hermes-agent/agent/background_review.py`, `run_agent.py`, `agent/conversation_loop.py`, `agent/agent_init.py`.
## Что это
После каждого юзерского сообщения ([run_conversation](https://github.com/NousResearch/hermes-agent)) AIAgent может запустить **фоновый daemon-thread**, который форкает ещё один AIAgent, даёт ему снимок всего разговора и просит оценить — нужно ли сохранить что-то в память (Memory) или обновить/создать скилл (Skill).
Основной поток продолжается немедленно. Ревью не блокирует пользователя.
## Where it lives
| File | Lines | Role |
|------|-------|------|
| `agent/background_review.py` | 1–597 | Центральный модуль — промпты, `_run_review_in_thread`, `spawn_background_review_thread`, `summarize_background_review_actions` |
| `run_agent.py` | 1360–1400 | Форвардеры: `_spawn_background_review`, `_summarize_background_review_actions`, импорт констант-промптов |
| `agent/conversation_loop.py` | 4779–4805 | Место вызова — после завершения ответа, перед возвратом `result` |
| `agent/codex_runtime.py` | 150–162 | Аналогичный вызов для Codex Response API режима |
| `agent/agent_init.py` | 10671075, 11871190 | Инициализация счётчиков и интервалов |
| `agent/tool_executor.py` | 207209, 776778 | Сброс счётчиков при вызове `memory` или `skill_manage` |
| `tui_gateway/server.py` | 2452–2464 | Подключение `background_review_callback` для TUI (Ink) |
| `gateway/run.py` | 17849–17893 | Подключение `background_review_callback` для gateway-сообщений |
## Как триггерится
Два независимых счётчика:
### Memory review (turn-based)
- Счётчик: `_turns_since_memory`
- Интервал: `_memory_nudge_interval` (default = **10**)
- Задаётся из `config.yaml: memory.nudge_interval`
- Инкрементится **в начале** каждого `run_conversation` (строка 559)
- Проверка: `if _turns_since_memory >= _memory_nudge_interval``_should_review_memory = True`
- Сброс: при вызове `memory` tool (строка 207 в tool_executor.py)
- Условия: `"memory" in valid_tool_names` и `_memory_store != None`
Если `memory_enabled: false` → ревью не триггерится.
### Skills review (iteration-based)
- Счётчик: `_iters_since_skill`
- Интервал: `_skill_nudge_interval` (default = **10**)
- Инкрементится **внутри tool-call цикла** после каждого батча (строка 860)
- Проверка: `if _iters_since_skill >= _skill_nudge_interval``_should_review_skills = True`
- Сброс: при вызове `skill_manage` tool (строки 209, 778 в tool_executor.py)
- Условия: `"skill_manage" in valid_tool_names`
### Финальная проверка
```python
if final_response and not interrupted and (_should_review_memory or _should_review_skills):
agent._spawn_background_review(
messages_snapshot=list(messages),
review_memory=_should_review_memory,
review_skills=_should_review_skills,
)
```
## Что происходит внутри
### `_run_review_in_thread` (background_review.py:327559)
1. **Создаёт форк AIAgent**, наследуя:
- `model`, `provider`, `base_url`, `api_key`, `credential_pool` от родителя (чтобы попасть в тот же prefix cache у Anthropic/OpenRouter)
- `_cached_system_prompt` — byte-идентичный, чтобы не разогревать кеш заново
- `session_id`, `session_start` — те же
- `_memory_store`, `_memory_enabled`, `_user_profile_enabled` — те же
- Но `skip_memory=True` (чтобы форк не создавал свой MemoryManager)
- `_memory_nudge_interval = 0`, `_skill_nudge_interval = 0` (чтобы не рекурсил)
2. **Устанавливает tool whitelist** (строки 459472):
```python
review_whitelist = {
t["function"]["name"]
for t in get_tool_definitions(
enabled_toolsets=["memory", "skills"],
quiet_mode=True,
)
}
```
Вне whitelist — всё блокируется с сообщением `"Background review denied non-whitelisted tool: ..."`
3. **Запускает** `review_agent.run_conversation(...)` с:
- Выбранным промптом (см. ниже) + суффикс `"You can only call memory and skill management tools. Other tools will be denied at runtime — do not attempt them."`
- `conversation_history=messages_snapshot` — весь разговор до этого момента
- `max_iterations=16` (жёстко, не из конфига)
4. **После завершения** — собирает успешные tool-результаты через `summarize_background_review_actions()`, фильтруя те, что уже были в `messages_snapshot` (чтобы не показывать старые как новые).
5. **Выводит** пользователю:
```python
f" 💾 Self-improvement review: {summary}"
# Например: "💾 Self-improvement review: Memory updated · Skill 'xxx' patched"
```
Через `agent._safe_print()` и через `agent.background_review_callback()` (для TUI/gateway).
6. **На ошибках** — пишет в лог `"Background memory/skill review failed: ..."` и шлёт `_emit_auxiliary_failure`. Не ломает основной разговор.
## 3 варианта промпта
Выбираются в `spawn_background_review_thread()` по комбинации флагов:
| `review_memory` | `review_skills` | Промпт | Суть |
|---|---|---|---|
| ✅ | ❌ | `_MEMORY_REVIEW_PROMPT` (~10 строк) | Сохранить факты о пользователе (persona, preferences, style). Если нечего — 'Nothing to save.' |
| ❌ | ✅ | `_SKILL_REVIEW_PROMPT` (~120 строк) | Самая большая инструкция. Искать сигналы: коррекции, frustration, новые техники, устаревшие скиллы. Приоритет: loaded skill → existing umbrella → support file → new umbrella. Запрещено: env-зависимости, негативные фиксации, session-specific. |
| ✅ | ✅ | `_COMBINED_REVIEW_PROMPT` (~80 строк) | Смесь обоих — сначала memory, потом skills с той же логикой. |
## Потенциальные проблемы
- **Нет конфигурируемости для skills interval**`_skill_nudge_interval` хардкожен в 10, не читается из config.yaml (в отличие от memory).
- **Tool whitelist был фиксирован** — теперь настраивается через `review.allowed_toolsets` в config.yaml (см. ниже).
- **Форк не имеет MCP-серверов** — MCP tools (obsidian-mcp, etc.) не регенерируются в форке.
- **max_iterations=16** — жёстко, не из конфига. Может не хватить на комплексное ревью многоскилловых сессий.
## Реализованные расширения (2026-06-24)
Сделаны два изменения в исходном коде Hermes Agent для того, чтобы промпты и whitelist ревью были настраиваемы из `config.yaml`:
### 1. agent_init.py — чтение секции `review:` из config.yaml
Место: `agent/agent_init.py` (после блока skills config).
Читает секцию:
```yaml
review:
# Пути к .md файлам с кастомными промптами.
# Если файл не найден/не читается — тихо падает на встроенный хардкод.
skill_prompt_path: ~/.hermes/review/skill_prompt.md
memory_prompt_path: ~/.hermes/review/memory_prompt.md
combined_prompt_path: ~/.hermes/review/combined_prompt.md
# Какие toolsets доступны форку ревью.
# По дефолту: ["memory", "skills"].
# Чтобы форк мог читать/писать .md файлы Obsidian — добавить "file".
allowed_toolsets:
- memory
- skills
# - file
```
Что делает код:
1. Выставляет `agent._review_allowed_toolsets` (по дефолту `["memory", "skills"]`)
2. Для каждого из трёх путей (`skill_prompt_path`, `memory_prompt_path`, `combined_prompt_path`) — пытается прочитать файл и записать содержимое в `agent._SKILL_REVIEW_PROMPT` / `agent._MEMORY_REVIEW_PROMPT` / `agent._COMBINED_REVIEW_PROMPT` соответственно.
3. Если путь не указан, файл не найден или не читается — тихо игнорируется, остаётся встроенный хардкод.
### 2. background_review.py — использование `_review_allowed_toolsets` с инстанса
Место: `agent/background_review.py`, функция `_run_review_in_thread`, whitelist (строка ~459).
**Было:**
```python
review_whitelist = {
t["function"]["name"]
for t in get_tool_definitions(
enabled_toolsets=["memory", "skills"],
quiet_mode=True,
)
}
```
**Стало:**
```python
_allowed_sets = getattr(agent, "_review_allowed_toolsets", ["memory", "skills"])
review_whitelist = {
t["function"]["name"]
for t in get_tool_definitions(
enabled_toolsets=_allowed_sets,
quiet_mode=True,
)
}
```
А также сообщение для форка динамически подставляет имена toolsets вместо хардкода `"memory and skill"`.
### 3. Примеры промптов
Созданы в `~/.hermes/review/`:
- **`skill_prompt.md`** — копия встроенного `_SKILL_REVIEW_PROMPT` + секция `--- Obsidian documentation update ---`, которая инструктирует форк после каждого обновления скилла читать и патчить соответствующий `.md` в `/Users/admin/obsidian/`.
- **`memory_prompt.md`** — копия `_MEMORY_REVIEW_PROMPT` + инструкция обновлять `personal/profiles/Alex.md` при сохранении user preference.
### Как включить Obsidian-обновление
1. Раскомментировать в `~/.hermes/config.yaml`:
```yaml
review:
skill_prompt_path: ~/.hermes/review/skill_prompt.md
memory_prompt_path: ~/.hermes/review/memory_prompt.md
allowed_toolsets:
- memory
- skills
- file
```
2. Перезапустить Hermes (новый сессионный конфиг прочитается при старте `AIAgent.__init__`).
3. После каждого десятого tool-батча (или любого вызова `skill_manage` раньше) форк будет: создать/обновить скилл **и** прочитать/пропатчить соответствующий Obsidian-документ через `read_file`/`patch`.
### Замечание по архитектуре
File tools работают напрямую с файловой системой, в обход obsidian-mcp. Это значит:
- **Линки** `[[wiki]]` в Obsidian не резолвятся автоматически — форк видит сырой `.md`.
- **Frontmatter** (YAML заголовки) — видит как часть текста, может патчить.
- **Vault search** недоступен — только прямой путь к файлу.
Для большинства задач (пропатчить существующий doc, создать новый .md с frontmatter) file tools достаточно. Если нужен полный Obsidian API — надо дорабатывать подключение MCP в форке (Вариант B из первоначального исследования, пока не реализован).
## Related files
- `~/.hermes/hermes-whale/review/skill_prompt.md` — кастомный skills промпт с Obsidian-инструкцией (для всех артефактов: планы, конфиги, этапы проектов)
- `~/.hermes/hermes-whale/review/memory_prompt.md` — кастомный memory промпт с Obsidian-инструкцией
- `agent/agent_init.py` — чтение секции `review:` из config.yaml
- `agent/background_review.py` — использование `_review_allowed_toolsets` и кастомных промптов
- Config: `config.yaml → review.*` (см. секцию выше)
## Хронология
- 2026-06-24: Исследование и документирование механизма self-improvement review (Whale/Alex).
- 2026-06-24: Реализация настройки через config.yaml: промпты (через пути к .md) + allowed_toolsets. Созданы примеры промптов с Obsidian-инструкцией.
- 2026-06-24: Реализация thread-scoped memory + custom memory instruction (см. `personal/plans/thread-scoped-memory.md`).
- 2026-06-24: Тесты thread-scoped memory: 9 новых тестов (76/76 passed), закоммичено в `4ebad4f69`.
+104
View File
@@ -0,0 +1,104 @@
# Obsidian MCP implementation ecosystem (2026)
Обзор активных реализаций Obsidian MCP серверов по состоянию на июнь 2026. Три архитектурных подхода, ~8 проектов с traction.
## Три архитектуры
| Подход | Примеры | Obsidian нужен? | Плюсы | Минусы |
|--------|---------|----------------|-------|--------|
| **Direct filesystem** | mcpvault, obsidian-mcp (Steven) | Нет | Простота, работает без Obsidian | Нет доступа к внутреннему API Obsidian |
| **Local REST API plugin** | mcp-obsidian (Markus), cyanheads | Да (должен быть открыт) | Obsidian опосредует операции | Нужен плагин + API key |
| **Native Obsidian plugin** | obsidian-mcp-plugin (aaronsb) | Да (должен быть открыт) | Полный API: граф, Dataview, Bases | Менее зрелый, только через BRAT |
## Детальный обзор
### 1. @bitbonsai/mcpvault (наш текущий) — filesystem
- **npm:** `@bitbonsai/mcpvault` (бывший `obsidian-mcp`, переименован март 2026 из-за trademark)
- **Версия:** 0.12.1 (июнь 2026)
- **Язык:** TypeScript
- **Транспорт:** stdio только
- **Инструменты:** 14: read_note, write_note, patch_note, list_directory, delete_note, search_notes, move_note, move_file, read_multiple_notes, update_frontmatter, get_notes_info, get_frontmatter, manage_tags, get_vault_stats, list_all_tags
- **Проблема:** exact string match в patch_note (`fullContent.split(oldString).length - 1`), без fuzzy, без trim, без нормализации whitespace
### 2. cyanheads/obsidian-mcp-server — REST API, самый популярный по загрузкам
- **npm:** `obsidian-mcp-server`
- **Версия:** 3.2.8 (май 2026)
- **★:** 603 | **dl/week:** ~9,776 (самые высокие)
- **Язык:** TypeScript, Bun/Node.js v24+
- **Транспорт:** stdio + Streamable HTTP (dual)
- **Инструменты:** 14
- **Требует:** Obsidian Local REST API plugin v4.0.0+
- **Ключевые фичи:**
- `obsidian_replace_in_note` — regex, whole-word, flexible whitespace, case-sensitivity, capture groups. **Решает проблему exact match**.
- `obsidian_patch_note` — surgical append/prepend/replace по heading/block/frontmatter
- Path policy (folder-scoped permissions через env vars: OBSIDIAN_READ_PATHS, OBSIDIAN_WRITE_PATHS)
- In-memory vault cache (configurable, default 10 min)
- JWT/OAuth аутентификация
- Structured logging с file rotation
- Zod schema validation
- Docker support (`ghcr.io/cyanheads/obsidian-mcp-server`)
- **Вердикт:** самый production-ready
### 3. MarkusPfundstein/mcp-obsidian — REST API, Python
- **npm:** `mcp-obsidian` (Python, через uvx)
- **★:** ~3,700 (самые звёзды)
- **Язык:** Python 100%
- **Транспорт:** stdio
- **Статус:** был 17 месяцев мёртв, вернулся 15 мая 2026. Но npm всё ещё v1.0.0.
- **Инструменты:** 7 (list_files, get_file_contents, search, patch_content, append_content, delete_file)
- **Известные баги:** patch_content timeout/validation (#9), UTF-8 failures (#25), Dataview dependency (#70), нет multi-vault (#63)
- **Вердикт:** watch — viable если выйдет новый npm release
### 4. aaronsb/obsidian-mcp-plugin (Semantic Notes Vault MCP) — native plugin
- **★:** 423
- **Версия:** 0.11.33 (13 релизов с 20 апреля, очень активен)
- **Язык:** TypeScript (плагин Obsidian)
- **Транспорт:** HTTP (порт 3001/3443)
- **Инструменты:** 8 категорий (vault, edit, view, graph, workflow, dataview, bases, system)
- **Ключевые фичи:**
- **Fuzzy text matching** для edits — прямо в описании
- Graph traversal (multi-hop, backlinks, forward-links, path finding)
- Dataview DQL execution
- Bases database operations
- Read-only mode
- mcpb one-click install для Claude Desktop
- **Минус:** Obsidian должен быть открыт. Установка через BRAT (не в community store).
- **Вердикт:** если нужен graph/Dataview/Bases — лучший выбор
### 5. Local REST API v4.0.0+ built-in MCP
- **Плагин:** `coddingtonbear/obsidian-local-rest-api` (★ 2.5k)
- **Версия:** v4.1.3 (июнь 2026)
- С апреля 2026: Local REST API сам стал MCP сервером на `/mcp/`
- **15 tools** (file CRUD, search, tagging, commands, open-in-ui)
- **Транспорт:** Streamable HTTP
- **Требует:** только установку плагина + API key (никаких дополнительных пакетов)
### 6. Minhao-Zhang/obsidian-mcp-server — WIP plugin
- **★:** 13
- **Версия:** v1.1.0 (апрель 2025 — заброшен)
- **Статус:** WIP. Автор: «я не знаю TypeScript». Не рекомендуется.
## Остальные
Всего ~79 Obsidian-связанных MCP серверов на PulseMCP, но ~8 имеют traction. Большинство — клоны/форки mcpvault или mcp-obsidian.
## Рекомендация от 2026-06-24
**Лучший апгрейд без смены архитектуры — cyanheads/obsidian-mcp-server.**
Почему:
1. Решает exact match проблему — `obsidian_replace_in_note` с regex и flexible whitespace
2. Самая высокая загрузка (9.7K dl/week) — стабильность доказана
3. Dual transport (можно через stdio как Hermes native MCP, можно HTTP)
4. Path policy — можно ограничить запись только определёнными папками
5. Docker support
Минус: требует Obsidian открытым (Local REST API плагин).
**Если не хочется ставить плагин:** остаться на mcpvault + Hermes `patch()` для сложных строк.