paulandClaude Sonnet 4.6 e14c21fab5 Fix 429 (model pin) and bot ignoring encrypted admin replies
Two independent bugs:

1. 429 "long context" — agentic sessions read enough files to exceed the
   200K standard window; on the default [1m] model Synapse escalates into
   the paid 1M tier. Pin claude_model to claude-opus-4-8 (200K window)
   so Claude Code compacts context instead. Updated config.example.json
   and the maintainer.py default / comment accordingly.

2. Bot silently ignores operator replies — the DM room was created with
   is_direct:true, causing Element to auto-enable E2E encryption. This
   stdlib-only bot cannot decrypt m.room.encrypted events, so all admin
   messages were dropped without any log output. Fix:
   - register-matrix-bot.sh now creates rooms with is_direct:false and
     preset:private_chat (Synapse does not force encryption on non-DM
     rooms).
   - matrix_poll_admin() now logs a loud WARN if an encrypted event
     arrives, instead of silently skipping it.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-21 18:46:33 +12:00

semprini-maintainer — autonomous stack maintenance agent

A standalone systemd service that keeps the container stack healthy and current, and talks to the operator over Matrix (chat.semprini.me).

It is the unattended driver for the canonical stack-support agent that lives in the semprini-core repo (agents/stack-support.agent.md): the daemon gathers state and hands the judgement calls (diagnosis, remediation, upgrade planning) to that agent, run headless via Claude Code.

Relationship to semprini-core

This is a separate project from the stack it maintains. It does not bundle its own copy of the stack logic — at runtime it reads, from the semprini-core checkout pointed to by repo_dir in config.json:

  • agents/stack-support.agent.md — injected as the agent's system prompt
  • docs/stack-support-runbook.md — the remediation runbook
  • core_stack/compose*.yml — the compose files it operates on

So a semprini-core checkout and the running core stack must be present on the host (they are what this agent maintains). The canonical stack-support agent definition stays in semprini-core to avoid drift. Other projects that register Uptime Kuma monitors (e.g. semprini-blog) are also maintained — their repos are made available to the agent via extra_dirs.

Prerequisites

  • Docker (host user in the docker group)
  • Python 3 (stdlib only — no venv needed)
  • Claude Code CLI on PATH
  • A semprini-core checkout + the core stack running (incl. Uptime Kuma)

What it does

Cadence Action
Every 5 min (health_interval_seconds) Read Uptime Kuma to learn which services are tracked right now and their up/down status. If a service is confirmed down across down_confirmations cycles, invoke the stack-support agent to diagnose + remediate, then report to Matrix.
Every 7 days (upgrade_interval_seconds) Resolve the containers behind tracked services, find the owning compose file, and have the agent research + apply upgrades (test, pin, rollback on failure). Breaking/major upgrades are escalated to Matrix for a decision.
Continuous (matrix_poll_seconds) Poll the Matrix room for the admin's replies — to answer escalations, or to take ad-hoc instructions.
Daily (heartbeat_interval_seconds) Quiet "N/M services up" heartbeat when all is well.

Scope is dynamic

Scope is whatever Uptime Kuma is tracking at the moment the check runs — the monitor list is re-read every cycle, nothing is hard-coded. This deliberately covers services from other projects that register Kuma monitors (e.g. semprini-blog), not just semprini-core. Other project repos are made available to the agent via extra_dirs.

Kuma's own heartbeat result is the health signal, so the daemon never re-implements probes — it trusts the monitoring system already in the stack.

Architecture

                 ┌─────────────────────────────────────────┐
                 │           maintainer.py (daemon)         │
   Uptime Kuma ──┤  read monitors+status (sqlite in volume) │
   (kuma.db)     │                                          │
                 │  on down / on schedule:                  │
                 │     claude -p  --append-system-prompt     │──▶ stack-support
                 │        (agents/stack-support.agent.md)    │     agent does the
                 │                                          │     diagnosis/fix
                 │  parse JSON result envelope              │◀──  + returns JSON
                 │                                          │
   Matrix  ◀─────┤  send summaries / ask questions          │
   (@paul)  ─────┤  poll replies → resume escalations       │
                 └─────────────────────────────────────────┘

The agent returns a small JSON envelope the daemon relays:

{"status":"ok|action_taken|escalate|blocked","summary":"…","question":"…","options":["…"]}

escalate posts the question (numbered options) to Matrix and pauses upgrades until the admin replies; the reply is fed back to the agent to carry out.

Autonomy

Set autonomy in config.json:

  • full (default) — remediate and apply safe upgrades autonomously (test + auto-rollback); escalate only major/breaking upgrades.
  • fix-only — remediate health issues; report upgrades but never apply them.
  • notify — never change anything; only report to Matrix.

Why it runs as paul, not root

  • Claude Code refuses --permission-mode bypassPermissions under root.
  • paul is in the docker group, so the agent can drive Docker/Compose.
  • Kuma's root-owned kuma.db is read via a throwaway keinos/sqlite3 container mounting the named volume read-only — no host file access needed.

Install

cd /home/paul/Dev/semprini-maintainer

# 1. Config
cp config.example.json config.json
#   set repo_dir to the semprini-core checkout; edit paths/intervals if needed
#   (defaults match this host)

# 2. Provision the Matrix bot + private room with the admin
./register-matrix-bot.sh
#   then, one-time: accept the room invite in Element as @paul:semprini.me

# 3. Install + start the service
./install.sh

config.json and .bot-secrets hold the bot token/password and are gitignored.

Operate

journalctl -u semprini-maintainer -f          # logs
systemctl status semprini-maintainer
sudo systemctl restart semprini-maintainer

# One-off cycles without the service (for testing):
python3 maintainer.py --config config.json --once health
python3 maintainer.py --config config.json --once upgrade
python3 maintainer.py --config config.json --once replies

Files

File Purpose
maintainer.py The daemon. Stdlib only.
config.example.json Template — copy to config.json.
register-matrix-bot.sh Creates the @maintainer account + room, writes token to config.
semprini-maintainer.service systemd unit (runs as paul).
install.sh Syntax-check, install, enable, start.

See docs/architecture.md for the architecture decision record.

S
Description
No description provided
Readme
120 KiB
Languages
Python 71.1%
Shell 28.9%