Skip to main content

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
For catastrophic-loss scenarios (lost DB volume, lost secret key), see 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 in infrastructure/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.
AlertSeveritySection
Worker Offlinecritical§ Worker Offline
Brain Daemon Stalecritical§ Brain Daemon Stale
GPU Temperature Highcritical§ GPU Temperature High
High Error Ratecritical§ High Error Rate
Stale Taskswarning§ Stale Tasks
Embedding Sync Lagwarning§ Embedding Sync Lag
DB Size Warningwarning§ DB Size Warning
Daily Cost Spikewarning§ Daily Cost Spike
Content Quality Dropwarning§ Content Quality Drop
Traffic Anomalywarning§ Traffic Anomaly
Page-View Capture Deadwarning§ Page-View Capture Dead
Pipeline Stalledwarning§ Pipeline Stalled
Ollama Unresponsivewarning§ Ollama Unresponsive
Zero Published Posts This Weekwarning§ Zero Published Posts This Week
GPU Metrics Stalewarning§ GPU Metrics Stale
Disk Space Low (Prometheus)warning§ Disk Space Low
Site DOWN (brain)critical§ Site DOWN
Wan Server DEGRADEDwarning§ Wan Server DEGRADED
(Unknown alert)varies§ Unknown alert — generic triage

Worker Offline

Means. No brain_decisions row in the last 10 minutes AND no pipeline_tasks activity in the last 30 minutes. The worker is probably crashed. Triage.
Fix.
If the container won’t start (crashloop), see 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. No brain_decisions rows in the last 15 minutes. The 5-minute brain cycle has stopped. Triage.
Fix.
The brain runs as the 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:
Escalation. If brain restarts but immediately exits, look at the logs for a DB connection error — likely a DATABASE_URL problem. See disaster-recovery.md CONFIG-1.

GPU Temperature High

Means. The latest gpu_metrics row has temperature > 85°C. Alert is stable for 5 min. Triage.
Fix.
  1. If it’s a runaway pipeline: pause it.
  2. If it’s an image-gen or Wan render: stop the in-flight job.
  3. If it’s a third-party workload (gaming, a stray process): kill that process or accept the temperature.
  4. If GPU temp stays high after killing all GPU loads: physical cooling problem. Check fan curve, intake/exhaust airflow, ambient temp.
Verification.
Escalation. Sustained > 90°C is hardware damage territory. Power-off the workstation if it stays there.

High Error Rate

Means. More than 5 audit_log rows with severity='error' in the last hour. Triage.
Fix. Depends on the category. Common ones:
  • category='ollama' → see § Ollama Unresponsive
  • category='pipeline' → see Stale Tasks / troubleshooting.md
  • category='webhook' → likely a 4xx from a third-party (Vercel, Telegram, Discord) — check secret rotation

Stale Tasks

Means. One or more pipeline_tasks rows have status='in_progress' and updated_at older than 2 hours. Triage.
Fix.
Then check what hung — see troubleshooting.md “Pipeline task stuck in_progress for more than 10 minutes.”

Embedding Sync Lag

Means. Newest row in embeddings is more than 6 hours old. Auto-embed daemon is probably dead. Triage + fix.
If the daemon is alive but embeddings still aren’t progressing, check Ollama (the embedding model is nomic-embed-text, served by Ollama). See § Ollama Unresponsive.

DB Size Warning

Means. poindexter_brain is now > 1 GB. Triage.
Fix. Usual suspects:
  • 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. See poindexter retention --help for archival commands.
Escalation. Sustained growth > 10 GB needs a long-term plan — partition the high-churn tables, archive cold rows to a separate DB.

Daily Cost Spike

Means. cost_logs SUM in the last 24h exceeds $5. Triage.
Fix. If a paid provider is spiking:
For the budget to actually enforce, the cost guard must be enabled — verify with poindexter settings get cost_guard_enabled. Escalation. 5/day=5/day = 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 average quality_score is below 70. Pipeline is producing weaker content. Triage.
Fix. Most common cause — pipeline_writer_model was flipped off the intended model. See troubleshooting.md “Approval rate drops to ~0%” entry.

Traffic Anomaly

Means. Today’s page_views count is less than 50% of yesterday’s. Possible site issue. Triage.
Fix. If site is down → see § Site DOWN. If site is up but traffic still dropped:
  • Check Search Console for new manual penalties or indexing drops
  • Check Google Analytics for referrer changes
  • Check whether ViewTracker beacon is firing — open the site in a browser, watch the Network tab for the sendBeacon POST 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 fresh page_views rows — the own-analytics capture chain has stalled somewhere between the frontend beacon and the DB. Triage.
Fix. Walk the chain front to back:
  • Beacon not firing (frontend): open a post in a browser, watch the Network tab for the sendBeacon POST to the Cloudflare Worker (NEXT_PUBLIC_BEACON_URL). If it’s missing, the frontend regressed — check web/public-site/components/ViewTracker.tsx and that NEXT_PUBLIC_BEACON_URL is set in Vercel.
  • Sync job stalled (backend): the worker imports Cloudflare Analytics Engine into page_views every 5 minutes via services/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 new pipeline_tasks rows in the last 48 hours. Triage.
Fix.
  • If pipeline_paused=truepoindexter 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. no cost_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 on cost_usd = 0 (local inference is free — cost_guard stamps is_local calls at $0), not provider = 'ollama'. Since the 2026-05-16 LiteLLM router cutover, local inference is logged provider='litellm' (the real Ollama model is in the model column), so the old provider = 'ollama' literal matched zero rows and the alert false-fired on every active task. cost_usd = 0 also 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.
Triage.
Fix.

Zero Published Posts This Week

Means. No posts rows with status='published' and published_at > NOW() - 7d. Content publishing has stopped. Triage. Three possible causes:
  1. Pipeline isn’t generating → see § Pipeline Stalled
  2. Pipeline is generating but everything’s getting rejected → see “Approval rate drops to ~0%” in troubleshooting.md
  3. Posts are reaching awaiting_approval but Matt hasn’t approved any → operator action needed
Fix. Whichever case applies — drain the queue, fix the writer model, or unpause the pipeline.

GPU Metrics Stale

Means. No gpu_metrics rows in the last 30 minutes. The scraper crashed. Triage + fix.
If running via the GPU scraper service:

Disk Space Low

Means. A host volume is running low on free disk space. This is now a Prometheus alert, defined in infrastructure/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.
Both are sourced from windows_exporter’s 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.
Fix. Free up headroom on the named volume:
Other space to reclaim: clear old local model files, or archive high-churn tables (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_bytes under 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.
Both thresholds are DB-tunable (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.
Fix. In order of leverage:
  • 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-cadvisor reclaims it instantly).
  • Unpin idle VRAM: a KEEP_ALIVE=-1 model (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.
Fix.
  • 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 — /generate returns 503 forever”
  • poindexter-wan-server container restart-loops every ~30 seconds”
Quick recovery:
TBD — needs operator confirmation: No Grafana alert for this exists yet (per 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

Find the firing rule. Read its 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):
After maintenance:

Escalation

This is a single-operator system. There is no on-call rotation.

See also

  • troubleshooting.md — symptom-driven debugging
  • disaster-recovery.md — catastrophic-loss playbooks
  • secret-rotation.md — secret rotation procedures
  • infrastructure/grafana/provisioning/alerting/alert-rules.yml — the alert rule source of truth
  • brain/ — brain daemon source (synthetic alerts fired directly from here)