[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:
@@ -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 сохранена ниже для ретроспективы.
|
||||
|
||||
Reference in New Issue
Block a user