Incident Response Runbook
Last reviewed: 2026-04-30 Audience: solo operator (Matt) at 2am during an incident Prereqs: Local PC online, Docker running, gh CLI authed, poindexter CLI installed, Grafana access When an alert fires (Telegram or Discord), this is the playbook. Find the alert by name in the index, jump to its section, follow the steps. If something doesn’t match a known alert, fall through to Unknown alert — generic triage.Scenarios this covers
- A Telegram or Discord notification just fired and you need to know what to do
- You woke up to multiple alerts and need to triage by severity
- A symptom is happening but no alert fired (Grafana / brain daemon may be down)
- You need to escalate / mute an alert during planned maintenance
disaster-recovery.md. For known-pattern symptoms, see troubleshooting.md. For routine secret rotation, see secret-rotation.md.
Quick triage flowchart
Alert index — alert name to runbook section
These are the active Grafana alert rules ininfrastructure/grafana/provisioning/alerting/alert-rules.yml (15 rules as of 2026-06-13). Brain daemon also fires a few synthetic alerts directly to Telegram, and host-infrastructure alerts (e.g. disk space) are sourced from Prometheus (infrastructure/prometheus/alerts/).
No-data posture (Glad-Labs/poindexter#581). 14 of the 15 rules use noDataState: Alerting: every page-worthy rule returns a row in the healthy state (a count(*) / SUM / AVG always does; GPU Temperature High reads the latest gpu_metrics row), so “no data” can only mean the underlying table/view was renamed or dropped, or the datasource is down — i.e. the rule went blind. Those surface loudly rather than silently resolving green (fail-loud, no silent fallbacks). Query errors (renamed column, dropped table) surface independently via execErrState, which is Error (or Alerting) on every rule — never OK. The single exception is DB Size Warning, intentionally kept on noDataState: OK: it is a non-page-worthy capacity warning whose only no-data condition (datasource unreachable) is already paged by the two critical rules on the same datasource (Worker Offline, Brain Daemon Stale). The posture is pinned by src/cofounder_agent/tests/unit/infrastructure/test_grafana_alert_no_data_state.py.
| Alert | Severity | Section |
|---|---|---|
| Worker Offline | critical | § Worker Offline |
| Brain Daemon Stale | critical | § Brain Daemon Stale |
| GPU Temperature High | critical | § GPU Temperature High |
| High Error Rate | critical | § High Error Rate |
| Stale Tasks | warning | § Stale Tasks |
| Embedding Sync Lag | warning | § Embedding Sync Lag |
| DB Size Warning | warning | § DB Size Warning |
| Daily Cost Spike | warning | § Daily Cost Spike |
| Content Quality Drop | warning | § Content Quality Drop |
| Traffic Anomaly | warning | § Traffic Anomaly |
| Page-View Capture Dead | warning | § Page-View Capture Dead |
| Pipeline Stalled | warning | § Pipeline Stalled |
| Ollama Unresponsive | warning | § Ollama Unresponsive |
| Zero Published Posts This Week | warning | § Zero Published Posts This Week |
| GPU Metrics Stale | warning | § GPU Metrics Stale |
| Disk Space Low (Prometheus) | warning | § Disk Space Low |
| Site DOWN (brain) | critical | § Site DOWN |
| Wan Server DEGRADED | warning | § Wan Server DEGRADED |
| (Unknown alert) | varies | § Unknown alert — generic triage |
Worker Offline
Means. Nobrain_decisions row in the last 10 minutes AND no pipeline_tasks activity in the last 30 minutes. The worker is probably crashed.
Triage.
troubleshooting.md “Test suite fails in the worker container” or check the logs for a Python traceback. Most common: a recent migration added a column that the deployed code doesn’t expect — roll the migration back or pull/rebuild.
Escalation. If the container is healthy but tasks still aren’t processing, jump to Stale Tasks below.
Brain Daemon Stale
Means. Nobrain_decisions rows in the last 15 minutes. The 5-minute brain cycle has stopped.
Triage.
poindexter-brain-daemon container with restart: unless-stopped, so Docker relaunches it automatically if the process crashes. (The legacy OS-level watchdog — the Task Scheduler “Poindexter Brain Watchdog” — predates containerization and was disabled 2026-06-09; Docker’s restart policy is the recovery path now.) If the container is up but the cycle is still stale, it’s hung rather than crashed — force a clean cycle:
DATABASE_URL problem. See disaster-recovery.md CONFIG-1.
GPU Temperature High
Means. The latestgpu_metrics row has temperature > 85°C. Alert is stable for 5 min.
Triage.
- If it’s a runaway pipeline: pause it.
- If it’s an image-gen or Wan render: stop the in-flight job.
- If it’s a third-party workload (gaming, a stray process): kill that process or accept the temperature.
- If GPU temp stays high after killing all GPU loads: physical cooling problem. Check fan curve, intake/exhaust airflow, ambient temp.
High Error Rate
Means. More than 5audit_log rows with severity='error' in the last hour.
Triage.
category='ollama'→ see § Ollama Unresponsivecategory='pipeline'→ see Stale Tasks /troubleshooting.mdcategory='webhook'→ likely a 4xx from a third-party (Vercel, Telegram, Discord) — check secret rotation
Stale Tasks
Means. One or morepipeline_tasks rows have status='in_progress' and updated_at older than 2 hours.
Triage.
troubleshooting.md “Pipeline task stuck in_progress for more than 10 minutes.”
Embedding Sync Lag
Means. Newest row inembeddings is more than 6 hours old. Auto-embed daemon is probably dead.
Triage + fix.
nomic-embed-text, served by Ollama). See § Ollama Unresponsive.
DB Size Warning
Means.poindexter_brain is now > 1 GB.
Triage.
audit_log— runs forever by default. Add a retention policy:embeddings— high churn. Check whether old post embeddings are being re-generated unnecessarily.pipeline_tasks— old failed rows accumulate. Seepoindexter retention --helpfor archival commands.
Daily Cost Spike
Means.cost_logs SUM in the last 24h exceeds $5.
Triage.
poindexter settings get cost_guard_enabled.
Escalation. 150/month. If this is sustained without commensurate revenue, kill the paid provider entirely (poindexter settings set <provider>_enabled false) and run on local Ollama only.
Content Quality Drop
Means. 7-day averagequality_score is below 70. Pipeline is producing weaker content.
Triage.
pipeline_writer_model was flipped off the intended model. See troubleshooting.md “Approval rate drops to ~0%” entry.
Traffic Anomaly
Means. Today’spage_views count is less than 50% of yesterday’s. Possible site issue.
Triage.
- Check Search Console for new manual penalties or indexing drops
- Check Google Analytics for referrer changes
- Check whether
ViewTrackerbeacon is firing — open the site in a browser, watch the Network tab for thesendBeaconPOST to the Cloudflare Worker (NEXT_PUBLIC_BEACON_URL). If it’s missing, the analytics beacon is broken (frontend issue, not a real traffic drop) — see § Page-View Capture Dead.
Page-View Capture Dead
Means. No freshpage_views rows — the own-analytics capture chain has stalled somewhere between the frontend beacon and the DB.
Triage.
- Beacon not firing (frontend): open a post in a browser, watch the Network tab for the
sendBeaconPOST to the Cloudflare Worker (NEXT_PUBLIC_BEACON_URL). If it’s missing, the frontend regressed — checkweb/public-site/components/ViewTracker.tsxand thatNEXT_PUBLIC_BEACON_URLis set in Vercel. - Sync job stalled (backend): the worker imports Cloudflare Analytics Engine into
page_viewsevery 5 minutes viaservices/jobs/sync_cloudflare_analytics.py. Confirm the worker is up and that job is scheduled. - Real traffic drop, not a capture failure: cross-check Search Console / GA — if they dropped too, treat it as § Traffic Anomaly, not a capture bug.
Pipeline Stalled
Means. Zero newpipeline_tasks rows in the last 48 hours.
Triage.
- If
pipeline_paused=true→poindexter settings set pipeline_paused false - If approval queue is at the cap → drain it (approve / reject pending posts)
- If worker is healthy and queue is empty but no new tasks fire → topic discovery may be stuck. See
troubleshooting.md“Topic discovery keeps generating the same rejected topic genre.”
Ollama Unresponsive
Means. No local LLM inference in the last 6 hours WHILE tasks are pending — i.e. nocost_logs rows with cost_type='inference', cost_usd = 0, and provider IN ('litellm','ollama','ollama_native'). Ollama is probably down.
Why the predicate looks the way it does. It keys onTriage.cost_usd = 0(local inference is free —cost_guardstampsis_localcalls at $0), notprovider = 'ollama'. Since the 2026-05-16 LiteLLM router cutover, local inference is loggedprovider='litellm'(the real Ollama model is in themodelcolumn), so the oldprovider = 'ollama'literal matched zero rows and the alert false-fired on every active task.cost_usd = 0also means a cloud fallback through LiteLLM (cost_usd > 0) is deliberately excluded, so a dead Ollama masked by a paid fallback still surfaces. Do not “simplify” it back to a provider literal.
Zero Published Posts This Week
Means. Noposts rows with status='published' and published_at > NOW() - 7d. Content publishing has stopped.
Triage. Three possible causes:
- Pipeline isn’t generating → see § Pipeline Stalled
- Pipeline is generating but everything’s getting rejected → see “Approval rate drops to ~0%” in
troubleshooting.md - Posts are reaching
awaiting_approvalbut Matt hasn’t approved any → operator action needed
GPU Metrics Stale
Means. Nogpu_metrics rows in the last 30 minutes. The scraper crashed.
Triage + fix.
Disk Space Low
Means. A host volume is running low on free disk space. This is now a Prometheus alert, defined ininfrastructure/prometheus/alerts/infrastructure.yml. (It used to be a Grafana SQL rule, but that rule was mislabeled — it actually queried pg_database_size, duplicating § DB Size Warning — so it was removed 2026-06-03 and replaced with real host-disk monitoring.) Two thresholds fire:
PoindexterDiskSpaceLow(warning) — a drive-letter volume has under 20 GB free for 10 minutes.PoindexterDiskSpaceCritical(critical) — under 10 GB free for 5 minutes; Postgres writes, Docker pulls, and image generation will start failing.
windows_logical_disk_free_bytes (the windows scrape job on host.docker.internal:9182), filtered to drive-letter volumes ([A-Z]:). The {{ $labels.volume }} label in the alert names the affected drive.
Triage.
cost_logs / page_views). For Postgres-specific growth, see § DB Size Warning.
Host Memory Pressure
Means. The worker host is running out of physical RAM. The box is triple-booked — the container stack (WSL2, memory-capped in~/.wslconfig), the host-native Ollama/inference fleet, and the operator desktop all draw on the same physical RAM. When available RAM approaches zero the OS pages the working set to the pagefile; the desktop compositor (which shares the GPU and RAM) stalls and the UI freezes — occasionally hard enough to force a dirty reset (Windows Kernel-Power 41). Two Prometheus alerts (DB-rendered from app_settings.prometheus.*, sourced from windows_exporter — the windows scrape job on host.docker.internal:9182) cover it:
PoindexterHostMemoryLow(warning) —windows_memory_available_bytesunder 4 GB for 10 minutes. A headroom heads-up: the next allocation spike will page.PoindexterHostMemoryThrashing(critical) —rate(windows_memory_swap_pages_written_total[5m])over 2000 pages/s for 2 minutes. Active memory-pressure eviction — the freeze-in-progress signal. Idle median is ~0.08 pages/s, so it fires only during a genuine episode.
prometheus.threshold.host_memory_available_warning_gb, prometheus.threshold.host_memory_paging_critical_pages_per_sec). Neither carries an absent() guard — a bare comparison yields no series when the exporter is down, so exporter death routes to WindowsExporterDown, not a false memory page. Visualize on the Hardware & Power board → “Host Memory — pressure” row (available RAM, commit-vs-RAM, page-out rate).
Triage.
- Immediate: close idle browser/Electron apps (they hold both RAM and display-GPU memory), or pause heavy pipeline work (image/video generation).
- Reclaim a leak:
docker restart <container>on a bloated one — cAdvisor has leaked before (docker restart poindexter-cadvisorreclaims it instantly). - Unpin idle VRAM: a
KEEP_ALIVE=-1model (e.g. the vision instance on:11435) that WDDM has paged to system RAM is holding host RAM; unpin it if vision QA is idle. - Durable: the box is structurally oversubscribed — move the container tier onto separate hardware (bare-metal Linux) or add physical RAM.
Not the PSU. A recurring Kernel-Power 41 reset here reads like a power fault, but the HX1500i telemetry (sensor_samples) shows normal power (~380 W) and temperatures before each reset, and host telemetry goes dark 28–30 min ahead of the reset — the freeze signature, not an instantaneous power trip. Chase memory pressure first.
Site DOWN
Means. Brain daemon’s site probe failed for > 5 minutes.https://www.gladlabs.io is returning non-2xx.
Triage.
- 404 on routes that should exist → ISR cache issue, see
troubleshooting.md“Post is Not Found” + “Static export writes succeed but the frontend still shows stale data” - 500 → Vercel build broken; check the deploy logs in Vercel dashboard
- Connection refused → DNS / Vercel domain issue; check Vercel project settings
Wan Server DEGRADED
Means.curl http://localhost:9840/health returned degraded:true. Video generation will silently fall back to image-gen/Pexels stills.
Fix. See troubleshooting.md entries:
- “Wan-server enters DEGRADED state —
/generatereturns 503 forever” - “
poindexter-wan-servercontainer restart-loops every ~30 seconds”
troubleshooting.md). The brain daemon should probe wan-server /health and alert; for now, this section assumes you noticed manually.
Unknown alert — generic triage
If an alert fired with a name that’s not in the index above:Step 1 — Find it in Grafana
summary and description annotations.
Step 2 — Open the alert’s source query
In Grafana → click the alert → “View rule” → copy the SQL. Run it directly:Step 3 — Decide if it’s real
- Value just barely over threshold → likely flapping; consider raising the threshold (it’s in
infrastructure/grafana/provisioning/alerting/alert-rules.yml) - Value way over threshold → real issue; investigate the table the SQL queries
Step 4 — Document it
Add a section above with the alert name, the meaning, and the fix you applied.Planned maintenance — muting alerts
If you’re doing intentional work that will fire alerts (e.g., bringing the worker down for an upgrade):Escalation
This is a single-operator system. There is no on-call rotation.- Critical bugs in product code (Poindexter): file a GitHub issue at https://github.com/Glad-Labs/poindexter/issues
- Glad Labs business / operator issues: file in the private tracker at https://github.com/Glad-Labs/poindexter/issues
- Hardware issues: consult the operator’s own hardware notes (
~/.claude/projects/<your-project>/memory/if using Claude Code memory, or wherever you keep your install spec)
See also
troubleshooting.md— symptom-driven debuggingdisaster-recovery.md— catastrophic-loss playbookssecret-rotation.md— secret rotation proceduresinfrastructure/grafana/provisioning/alerting/alert-rules.yml— the alert rule source of truthbrain/— brain daemon source (synthetic alerts fired directly from here)