- New litellm service (pinned v1.98.0 — litellm 1.82.7/1.82.8 on PyPI were
compromised with credential-stealing malware in March 2026; internal-only,
no Traefik route, no reason to expose an LLM gateway with a master key
publicly).
- Replaces !claude/!ai command prefixes with automatic routing: every plain
message in the control room goes through a classifier (router.js) that
decides chat vs code_task. Chat replies use OpenRouter's own auto-router
(openrouter/auto) via LiteLLM; code_task requests go through the existing
runChatTask() flow (Claude Code CLI, unchanged, still using the
subscription token directly).
- Investigated routing Claude itself through LiteLLM via OAuth token
forwarding (general_settings.forward_client_headers_to_llm_api) so the
Pro/Max subscription could be one of the auto-routable options. Confirmed
non-functional: Anthropic returns a generic rate_limit_error for any
direct API call using this token type outside the real Claude Code CLI,
reproduced with plain curl straight to api.anthropic.com. Not included.
- MATRIX_BOT_USER_ID now required and set explicitly (self-message filtering
can no longer rely on a command-prefix mismatch once there isn't one).
The separate Gitea push webhook to Portainer fired in parallel with this
build workflow on the same push event, so it could redeploy before the
new image finished pushing — Portainer would then pull the still-current
:latest tag and silently keep running old code. Removed that webhook;
redeploy now only happens as this workflow's last step, after the image
push actually completes.
Same root cause as the earlier whoami fix: the mixin's initial
/joined_rooms scan 404s against Continuwuity even though the endpoint
works fine when called directly. Only auto-join-on-invite was actually
needed, so a plain room.invite listener replaces the whole mixin.
Not actually needed: self-message filtering was only used to avoid the bot
reacting to its own replies, but those never match the !claude command
pattern anyway, so parseCommand() already filters them out for free.
The bind-mounted relative path resolved to nothing once Portainer's cloned
checkout no longer persisted for the container's runtime, so Docker silently
created an empty directory there and act_runner crash-looped. act_runner
already auto-detects its own docker.sock without any custom config.
Fixes a circular dependency: a full stack redeploy tears every service down
before bringing any back up, but claude-agent's image pull goes through
Traefik->Gitea's registry. Self-hosting Traefik in the same stack it fronts
means redeploying this stack could tear down the very route needed to bring
it back. Traefik now lives in ~/traefik/docker-compose.yml as shared infra,
independent of this stack's lifecycle.