[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
@@ -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 сохранена ниже для ретроспективы.