Runbook: OOM loop / engine restart cycle¶
Symptom¶
systemctl status maid-engine@<inst>.serviceshows recent restarts (Restart=on-failure cycling).journalctl -u maid-engine@<inst>repeats:systemd[1]: maid-engine@<inst>.service: Main process exited, code=killed, status=9/KILLorFailed with result 'oom-kill'.dmesg | grep -i 'killed process'shows the engine PID killed by OOM./readyzflaps green/red as the engine restarts.- Players see repeated disconnects every 30-90 s.
Detection¶
maid_engine_restart_count_totalincreases > 2 in 5 min.- systemd
Restart=on-failuretriggering > RestartLimit (engine unit entersfailedstate). - Watchdog metric:
maid_watchdog_unclean_exit_total> 0 in the last 10 min (see M1.2-B watchdog). - No built-in engine webhook fires a named
engine_oom_loopevent. The observability webhook bridge (MAID_OBSERVABILITY_WEBHOOK_*) only POSTs periodic metric snapshots; wire an external Prometheus/Alertmanager rule onmaid_engine_restart_count_total/maid_watchdog_unclean_exit_totalto page, and notify players manually withmaid ops announce.
Blast radius¶
- Players affected: ALL on this instance. Each restart drops every session.
- Data at risk: anything in the persistence dirty queue between two
restarts; the watchdog flushes on SIGTERM but bypasses cleanup on
os._exitfrom the wedged-loop path (see plan R10.B). - AI/external systems: dialogue sessions are torn down on each restart; Redis-cached working set survives but PG-side conversation logs may show truncated turns.
Prerequisites¶
- Interim
maidCLI (MAID_SRC): there is no system-widemaidbinary until M1.4 (install.shleaves/opt/maid/venvempty), so the CLI runs from a source checkout.export MAID_SRC=/path/to/maid-checkoutand build its venv once:sudo -u maid-engine -H uv sync --frozen --project "${MAID_SRC}"(uv:/opt/maid/current/.uv/bin/uv). Commands below run"${MAID_SRC}/.venv/bin/maid"as themaid-engineuser; insidebash -cblocksMAID_SRCis forwarded via--preserve-env=MAID_SRC. - Tools:
maid-admin,systemctl,journalctl,dmesg,py-spy(R9.A.12 — installed viainstall.sh),ps,cat /proc/<PID>/status. - Access: root, or an equivalently broad
sudogrant — this runbook runssudo systemctl/sudo journalctlon the instance units andsudo -u maid-engine …service-user shells, none of which the narrowmaid-adminpriv-helper allowlist (packaging/sudoers/maid-admin.template) grants (it only covers fixedmaid-admin <verb>calls). There is nomaid-opsgroup in the packaging — only themaid-admingroup plus themaid-engineservice user (the restore/cleanup units run asroot). - Env file:
/etc/maid/<inst>.env. - Source checkout (
MAID_SRC):scripts/install.shis repo-only and is not installed at/opt/maid/current/bin/(staging is deferred to M1.4).export MAID_SRC=/path/to/maid-checkoutbefore the rollback step; it runssudo bash "${MAID_SRC}/scripts/install.sh" --rollback-version --yes. - Paths to know:
- Unit drop-in:
/etc/systemd/system/maid-engine@.service.d/ - Engine PID:
cat /run/maid-engine/<inst>/engine.pid(when alive) - Heap dump output:
./engine-heap-<ts>.txt - Audit access:
tail -f /var/log/maid/ops-audit.jsonl. - Escalation: see ./escalation-contacts.md.template.
First 5 minutes (LITERAL commands)¶
# 1. Shed AI load first — it is the most common runaway memory source
# `maid ops ai disable` is an M9 stub; use the functional kill-switch.
sudo -u maid-engine "${MAID_SRC}/.venv/bin/maid" ops kill-switch trip "oom investigation" --confirm --instance <inst>
# 2. Flush the persistence dirty queue so the next restart loses less.
# `maid ops flush` is an M9 stub — force a synchronous save from an
# in-game admin session with `@persistence flush` (IMPLEMENTOR) or `@save`.
# (A clean `systemctl stop`/`restart` also flushes pending saves on SIGTERM.)
# 3. Capture state BEFORE deciding kill-vs-let-restart
sudo journalctl -u maid-engine@<inst>.service -n 500 \
> ./incident-engine-$(date -u +%Y%m%dT%H%M%SZ).log
sudo dmesg -T | grep -i -A2 'oom\|killed process' \
> ./incident-dmesg-$(date -u +%Y%m%dT%H%M%SZ).log
sudo systemctl status maid-engine@<inst>.service --no-pager > /dev/null # just to refresh
# 4. Check MemoryMax headroom in the unit
sudo systemctl show maid-engine@<inst>.service \
--property=MemoryMax,MemoryHigh,MemoryCurrent,TasksMax,TasksCurrent
Investigation¶
Step 1: is it the engine, or the host, that ran out of memory?¶
# Host memory pressure right now
free -h
cat /proc/meminfo | head -5
# Last 10 min of /proc/pressure/memory (Linux PSI)
cat /proc/pressure/memory
- If host
freeshows plenty of RAM but the engine still got killed, the kill is from the cgroupMemoryMaxlimit — bump the limit (see Mitigation) or fix the leak. - If host is exhausted, another tenant on the box (Postgres? a backup job?) is the real culprit. Check:
Step 2: capture a heap dump (while engine is alive between restarts)¶
The engine restart cycle gives you a short window. Catch it on the next start:
# Watch the unit and grab the PID the moment it appears
while :; do
PID=$(systemctl show -p MainPID --value maid-engine@<inst>.service)
if [ "$PID" != "0" ] && kill -0 "$PID" 2>/dev/null; then
echo "engine PID=$PID; dumping"
sudo py-spy dump --pid "$PID" \
> ./engine-heap-$(date -u +%Y%m%dT%H%M%SZ).txt 2>&1 || true
sudo cat /proc/"$PID"/status \
| grep -E 'Vm|Threads|FDSize' \
> ./engine-procstatus-$(date -u +%Y%m%dT%H%M%SZ).txt
break
fi
sleep 0.5
done
If
py-spyis not on the host, install via the procedure documented in ./RB22_python_uv.md — it should be pre-installed byinstall.shper R9.A.12; absence is itself an incident worth noting.
Step 3: decide kill-vs-let-systemd-retry¶
- If the engine is wedged but NOT consuming new memory (rare —
usually means a hot loop without alloc): let systemd retry; the
watchdog will
os._exit(143)within 60 s anyway. - If memory keeps climbing on each restart (warm-cache leak,
unbounded dialogue queue, etc.): stop the unit and contain damage:
sudo systemctl stop maid-engine@<inst>.service # Notify externally via webhook. `maid ops maintenance` is an M9 stub, and # with the engine stopped there is nothing to gate — new logins already fail. # `maid ops announce` reads MAID_BRIDGES_WEBHOOK_URLS from its own process # env, so source the instance env with auto-export (a bare --preserve-env # of a never-exported var forwards nothing): sudo -u maid-engine --preserve-env=MAID_SRC bash -c \ 'set -a; . /etc/maid/<inst>.env; set +a; exec "${MAID_SRC}/.venv/bin/maid" ops announce --severity warn \ --message "engine memory leak; service down, ETA 30m"'
Step 4: identify the leaker from the heap dump¶
- Grep for high-allocation stacks. Common offenders observed in MAID:
EnrichedPromptBuilderretaining conversation context past TTL.MemoryServiceextraction queue without back-pressure.GridManagerholding the full wilderness in RAM (no eviction).- Match the offender against recent deploys (
git log --since "-7d"). Memory loops often correlate with the previous release.
Mitigation¶
- Reduce AI pressure (already done in step 1): keep AI disabled until the leak is fixed.
- Bump cgroup MemoryMax temporarily, only if the host has headroom:
See ./RB19_resource_exhaustion.md for the full resource-cap override procedure.
sudo mkdir -p /etc/systemd/system/maid-engine@.service.d sudo tee /etc/systemd/system/maid-engine@.service.d/95-resource-override.conf <<EOF [Service] MemoryMax=4G MemoryHigh=3.5G EOF sudo systemctl daemon-reload sudo systemctl restart maid-engine@<inst>.service - Roll back to the last known good release if the leak coincided
with a recent deploy:
Note:
sudo bash "${MAID_SRC}/scripts/install.sh" --rollback-version --yes sudo systemctl restart maid-engine@<inst>.service--rollback-versionis a BOOLEAN flag; it reads the prior release from/opt/maid/versions/PREVIOUS(scripts/install.sh:1341-1370) and requires--yesto swap thecurrentsymlink. Do NOT use bare--rollback— that is the per-instance UNINSTALL flag (destructive). See ./rollback.md.
Recovery¶
sudo systemctl status maid-engine@<inst>.service→active (running)with no restart events for ≥ 10 min.maid-admin status --instance <inst> --json | jq .uptime_s≥ 600.MemoryCurrentplateaus instead of climbing:maid-admin doctor --phase runtime --instance <inst>all green.- Re-enable AI if root cause was not AI-side (
maid ops ai enableis an M9 stub — use the functional kill-switch):maid ops kill-switch reset --instance <inst>. - Announce the all-clear to players via the admin broadcast (
maid ops maintenance offis an M9 stub — no login gate to lift).
Post-incident¶
- File ticket with: heap dump, dmesg excerpt, restart timeline, suspect module, was it correlated with a deploy.
- Add a regression test or memory bound to the offending code path.
- If MemoryMax was bumped as a workaround, file follow-up to revert the override once the leak is fixed.
Escalation¶
- Solo path: shed AI → flush → rollback if recent deploy → restart.
- Hosting console URL: see ./escalation-contacts.md.template.
- DNS registrar URL: see ./escalation-contacts.md.template.
- Comms channel URL: see ./escalation-contacts.md.template.
- Peer operator: see ./escalation-contacts.md.template.
- If you cannot recover within 30 minutes:
and consider a clean stop + investigate next day rather than fighting the restart loop in production.
# `maid ops maintenance` is an M9 stub. Notify players externally via # webhook, then do a clean stop. Source the instance env with auto-export # so announce inherits MAID_BRIDGES_WEBHOOK_URLS (a bare --preserve-env of # a never-exported var forwards nothing): sudo -u maid-engine --preserve-env=MAID_SRC bash -c \ 'set -a; . /etc/maid/<inst>.env; set +a; exec "${MAID_SRC}/.venv/bin/maid" ops announce --severity warn \ --message "engine OOM; taking the service down, ETA <ETA>"'
Alternate alerting path (no Prometheus)¶
Operators without a Prometheus / Alertmanager stack get the same
coverage via the maid-doctor timer + alert-script pair shipped in
PR-C. See deploy/monitoring/doctor-alert.sh
and packaging/systemd/maid-doctor@.timer.
Enable per instance:
On every 5-minute tick the timer runs
scripts/maid-doctor.sh --phase runtime --json, pipes the result into
doctor-alert.sh, which then:
- Writes
/var/lib/node_exporter/textfile_collector/maid_doctor.prom(metricsmaid_doctor_last_run,maid_doctor_last_status,maid_doctor_failed_checks_total) so any future scraper picks up the most recent doctor verdict without re-running it. - On
warnorfail, emails the on-call address read from/etc/maid-engine/<inst>/oncall.env(ONCALL_EMAIL).
This is the default monitoring path for solo home-lab installs; Prometheus is the optional add-on for shops that want graphs/history.