ワンクリックで
sync-health
Comprehensive sync + vault + LLM + service health checkup. Surfaces what's stale, what's broken, what's about to break.
Codex または Claude でインストール この Prompt をコピーして Codex、Claude、または他のアシスタントに貼り付けると、Skill ページを確認してインストールできます。
メニュー
Comprehensive sync + vault + LLM + service health checkup. Surfaces what's stale, what's broken, what's about to break.
Codex または Claude でインストール この Prompt をコピーして Codex、Claude、または他のアシスタントに貼り付けると、Skill ページを確認してインストールできます。
SOC 職業分類に基づく
| name | sync-health |
| description | Comprehensive sync + vault + LLM + service health checkup. Surfaces what's stale, what's broken, what's about to break. |
| argument-hint | ["--brief|--verbose"] |
Arguments: $ARGUMENTS
Parameters (all optional):
Built after the 2026-05-27 incident (issue #199) and the 2026-05-28 VRAM-exhaustion freeze. Both could have been caught hours earlier with a structured checkup that reads the right tables and sysfs paths instead of eyeballing logs. The data blocks below collect every signal those two debugs taught us to look at; the analysis section weights them.
All paths assume the checkup is invoked from the repo root. Paths use data/ not absolute paths so the checkup also works in worktrees.
!if [ -f data/sync_health.db ]; then sqlite3 data/sync_health.db "SELECT source, status, started_at, completed_at, COALESCE(records_processed,0) as processed, COALESCE(records_created,0) as created, COALESCE(interactions_created,0) as ic, COALESCE(source_entities_created,0) as sec, COALESCE(error_message,'') as err FROM sync_runs WHERE id IN (SELECT MAX(id) FROM sync_runs GROUP BY source) ORDER BY source;" -separator $'\t' 2>/dev/null || echo "DB_ERROR"; else echo "NO_SYNC_HEALTH_DB"; fi
!if [ -f data/sync_health.db ]; then sqlite3 data/sync_health.db "SELECT id, source, started_at, CAST((julianday('now') - julianday(started_at)) * 24 AS INTEGER) AS age_hours FROM sync_runs WHERE status='running' ORDER BY started_at;" -separator $'\t' 2>/dev/null; fi
!if [ -f data/sync_health.db ]; then sqlite3 data/sync_health.db "SELECT source, timestamp, COALESCE(error_type,''), substr(COALESCE(error_message,''),1,200) FROM sync_errors ORDER BY timestamp DESC LIMIT 20;" -separator $'\t' 2>/dev/null; fi
!~/.venvs/lifeos/bin/python -c " import sys; sys.path.insert(0, '.') try: from api.services.sync_health import detect_silent_source_entity_drift warnings = detect_silent_source_entity_drift() if not warnings: print('NO_DRIFT') else: for w in warnings: print(f\"{w['source']}\t{w.get('last_interaction','?')}\t{w.get('last_source_entity','none')}\t{w.get('gap_days','?')}\") except Exception as e: print(f'DETECT_ERROR: {e}') " 2>/dev/null
!~/.venvs/lifeos/bin/python -c " import sys, os; sys.path.insert(0, '.') from config.settings import settings from pathlib import Path try: vault = Path(os.path.expanduser(str(settings.vault_path))) md_count = len(list(vault.rglob('*.md'))) if vault.exists() else 0 except Exception as e: md_count = f'ERROR: {e}' try: from api.services.bm25_index import BM25Index, get_bm25_db_path bm25_count = BM25Index().count() bm25_path = get_bm25_db_path() except Exception as e: bm25_count = f'ERROR: {e}' bm25_path = '?' try: from api.services.vectorstore import VectorStore chroma_count = VectorStore()._collection.count() except Exception as e: chroma_count = f'ERROR: {e}' print(f'md_files={md_count}') print(f'bm25_chunks={bm25_count}') print(f'bm25_db={bm25_path}') print(f'chroma_vectors={chroma_count}') " 2>/dev/null
!if [ -f data/bm25_index.db ]; then sqlite3 data/bm25_index.db "SELECT doc_id FROM chunks_fts ORDER BY RANDOM() LIMIT 5;" 2>/dev/null; fi
!echo "lifeos-llm.service: $(systemctl is-active lifeos-llm.service 2>/dev/null || echo 'absent')"; echo "ollama.service: $(systemctl is-active ollama.service 2>/dev/null || echo 'absent')"; echo "lifeos-llm enabled: $(systemctl is-enabled lifeos-llm.service 2>/dev/null || echo 'absent')"; echo "ollama enabled: $(systemctl is-enabled ollama.service 2>/dev/null || echo 'absent')"
!~/.venvs/lifeos/bin/python -c " import subprocess from pathlib import Path GB = 1024**3 for d in sorted(Path('/sys/class/drm').glob('card*/device')): u_p = d / 'mem_info_vram_used' t_p = d / 'mem_info_vram_total' if u_p.exists() and t_p.exists(): try: u = int(u_p.read_text().strip()) t = int(t_p.read_text().strip()) if t > 0: print(f'VRAM: {u/GB:.1f} / {t/GB:.1f} GB ({u*100//t}%)') except Exception: pass break print('---') try: out = subprocess.run(['rocm-smi', '--showpids'], capture_output=True, text=True, timeout=5).stdout rows = [] for line in out.splitlines(): parts = line.split() if parts and parts[0].isdigit() and len(parts) >= 4: try: vram = int(parts[3]) / GB rows.append((vram, parts[0], parts[1])) except (ValueError, IndexError): pass for vram, pid, name in sorted(rows, reverse=True)[:8]: print(f' PID {pid} ({name}): {vram:.1f} GB VRAM') except Exception as e: print(f'(rocm-smi unavailable: {e})') " 2>/dev/null
!for svc in lifeos-api lifeos-chromadb lifeos-llm lifeos-mcp-http lifeos-agent-worker; do active=$(systemctl is-active $svc.service 2>/dev/null); since=$(systemctl show $svc.service --property=ActiveEnterTimestamp 2>/dev/null | cut -d= -f2-); echo "$svc: $active (since $since)"; done
!pids=$(ss -lntpH 2>/dev/null | grep ':8000 ' | grep -oP 'pid=\K[0-9]+' | sort -u); count=$(echo "$pids" | grep -c .); echo "listener_pids_on_8000=$count"; if [ "$count" -gt 1 ]; then echo "DUPLICATE_LISTENERS: $pids"; fi; echo "(client connections to :8000, for context: $(lsof -ti :8000 2>/dev/null | sort -u | wc -l) total fds)"
!~/.venvs/lifeos/bin/python -c " import subprocess for t in ['lifeos-watchdog', 'lifeos-server-watchdog', 'lifeos-gpu-watchdog', 'lifeos-sync']: try: out = subprocess.run( ['systemctl', 'list-timers', f'{t}.timer', '--no-pager'], capture_output=True, text=True, timeout=5, ).stdout.splitlines() # The output is a header row, data row(s), then footer. Find the data row. data = None for line in out: if t in line: data = line break if not data: print(f'{t}: (timer not installed)') continue # Columns: NEXT (3 cols) | LEFT | LAST (3 cols) | PASSED | UNIT | ACTIVATES # Just dump the line with our prefix; humans can read it. print(f'{t}: {data.strip()}') except Exception as e: print(f'{t}: (error: {e})') " 2>/dev/null
!for f in logs/server-watchdog.log logs/gpu-watchdog.log logs/chromadb-watchdog.log; do if [ -f "$f" ]; then echo "=== $f ==="; tail -5 "$f" 2>/dev/null; fi; done
!if [ -f data/monarch_session.pickle ]; then ~/.venvs/lifeos/bin/python -c " import sys; sys.path.insert(0, '.') try: from api.services.monarch import get_session_status s = get_session_status() print(f\"status={s['status']} age_days={s.get('age_days','?')} msg={s['message']}\") except Exception as e: print(f'MONARCH_ERROR: {e}') " 2>/dev/null else echo "NO_MONARCH_SESSION" fi
!du -sh data/chromadb data/bm25_index.db data/interactions.db data/crm.db data/sync_health.db data/backups 2>/dev/null
!if [ -d data/backups ]; then /bin/ls -lt data/backups/ 2>/dev/null | head -10; fi
!latest=$(/bin/ls -t logs/sync_*.log 2>/dev/null | head -1); if [ -n "$latest" ]; then echo "=== $latest ==="; grep -iE "error|failed|timeout|exception|saturat|oom" "$latest" 2>/dev/null | tail -10 || echo "(no errors found)"; fi
!systemctl list-timers lifeos-sync --no-pager 2>/dev/null | head -3
!~/.venvs/lifeos/bin/python -c " import sys; sys.path.insert(0, '.') from config.settings import settings print(f'llm_backend={settings.llm_backend}') print(f'local_llm_url={settings.local_llm_url}') print(f'anthropic_model={settings.anthropic_model}') print(f'vault_path={settings.vault_path}') print(f'embedding_model={settings.embedding_model}') print(f'agent_worker_autostart={settings.agent_worker_autostart}') print(f'local_llm_autostart={settings.local_llm_autostart}') " 2>/dev/null
You are producing a sync health checkup — a structured report that surfaces problems instead of a narrative of what's running. The data above is your only ground truth; do not speculate beyond it. If a block returned an error or was empty, say so in the corresponding section; don't paper over it.
Look for blocking errors in the data:
NO_SYNC_HEALTH_DB → say so plainly; the rest of the sync section is moot but the LLM / vault / service sections still work.DB_ERROR → database is locked or corrupt; flag and continue.ERROR: → the venv is broken or sys.path couldn't resolve; flag and continue.Walk the data and assign one of:
Use the section headers below. For each section, lead with a single-character status (✅ ⚠️ 🔴) then a one-line summary, then the numbers. Skip the detail tables under --brief. Include the full per-source dumps and the long-tail error log under --verbose.
🟡 Degraded — 2 stale sources, Monarch session 31d old (expired), tonight's nightly will likely fail on
slackagain unless action taken.
One sentence. Pull from Step 2.
Per-source status from the sync_runs block.
success and completed_at is within the source's expected frequency (daily / weekly / monthly).success but last run is stale (older than the frequency threshold).failed, or there's an orphan running row older than 8h.Default output: only sources with ⚠️ or 🔴, plus a footer like (<N> other sources healthy). Under --verbose show all sources.
For each problem source include: source | status | last_completed | stats | error_excerpt.
If the drift detector returned NO_DRIFT → ✅ "no silent regressions detected".
Otherwise → 🔴 with the per-source (last_interaction, last_source_entity, gap_days) table. This is the silent-regression pattern from issue #199 §2 — interactions still flowing but source_entities stopped weeks ago.
Three numbers must roughly align:
md_files — files on diskchroma_vectors — vector store populationbm25_chunks — BM25 populationExpected: chroma_vectors ≈ bm25_chunks ≈ 5–15× md_files (chunks per file vary). Severely under-populated (chroma_vectors < md_files) → 🔴. Within order of magnitude → ✅.
Also inspect the doc_id sample. If any row references a path that doesn't match the current vault path (e.g. /Users/ on Linux, /home/ on macOS), flag a 🔴 for stale BM25 entries with the suggested fix: python scripts/sync_vault_reindex.py --execute --force --clear-bm25.
Read the systemd state + VRAM usage:
enabled (not just active) — it'll come back on next boot. After PR #217, ollama should be disabled.Include the top VRAM consumers in the section body for any state other than ✅.
For each LifeOS service: ✅ if active, 🔴 if it's expected-active and inactive or failed, ⚠️ if activating (in startup).
Expected-active services depend on settings:
lifeos-api, lifeos-chromadb — always expected activelifeos-llm — expected active iff local_llm_autostart=Truelifeos-mcp-http — expected active iff there's a non-empty LIFEOS_MCP_BEARER_TOKENlifeos-agent-worker — expected active iff agent_worker_autostart=True✅ if listener_pids_on_8000=1. 🔴 if DUPLICATE_LISTENERS present — the issue #199 §5 pattern that PR #214 added the pkill fix for. Suggest ./scripts/server.sh restart. The "client connections" count is informational only; agent-worker and mcp-http legitimately keep open FDs to the API.
For each timer in (lifeos-watchdog, lifeos-server-watchdog, lifeos-gpu-watchdog, lifeos-sync):
2 × its scheduled interval2–5 × its interval> 5 × its interval OR if systemctl list-timers returned nothing for it (timer not installed)Echo the status from get_session_status(). 🔴 expired/missing → re-auth needed (point at the procedure in MEMORY.md). ⚠️ expiring_soon → mention how many days until next monthly sync (1st of month). ✅ ok → just the age.
Just the sizes from du, plus the most recent backup of each DB and its age (from the data/backups listing). ⚠️ if any DB backup is older than 7 days.
Combine the prior signals: will the nightly run cleanly?
failed rows in sync_runs).running row from a previous sync that the reaper missed (shouldn't happen post-PR #214, but check anyway).enabled but last_status=failed for the last 3 runs — the dependency cascade will skip downstream sources.End with the timer's "next fire" timestamp so the user knows when to expect it.
End the report with a numbered list of concrete commands to run, ordered by severity. Examples:
~/.venvs/lifeos/bin/python -c "import asyncio; ..." — re-auth Monarch./scripts/server.sh restart — clear stacked uvicorn PIDs~/.venvs/lifeos/bin/python scripts/sync_vault_reindex.py --execute --force --clear-bm25 — drop stale doc_idssudo systemctl disable ollama — keep it off across rebootsOnly include items the data actually supports. Don't add advice that wasn't in the findings.
--brief should cap output at ~30 lines. Skip detail tables, only show the section headers + verdict line + numbers.--verbose adds: full sync_runs table, full sync_errors tail, full storage / backup listing, full configuration dump.Full implementation lifecycle with adversarial review using subagents
Address PR review feedback and verify independently
Draft and create a GitHub issue from a description, investigation, or conversation context. Use when a problem is too large for a quick fix, when you want to propose a change for later, or when the user asks to "file an issue", "create an issue", "draft an issue", "open a ticket", or "log this for later". Also useful when another skill (like tune) determines a change is too large and needs to be tracked as an issue instead.
Merge a PR and update upstream GitHub issues with progress
Validate a PR against PR standards before requesting review
Health-check the LifeOS persona layer (files load, /chat matches the Telegram bots per persona, no personal values leak into committed persona files, frontmatter valid) and open every persona doc in Sublime for editing.