5704fbb08f
Phase 3.1 closes the cross-process gap the Phase 3 smoke surfaced —
streaming events (msg_start/thinking/delta/complete/curated) flow
from agent-runner → chamber via valkey pub/sub rather than the
SQLite bridge (too high-volume + ephemeral for the DB).
New service: `althing-valkey` (stock `valkey/valkey:8-alpine`).
Internal-only — no exposed port, no volume. chamber + agent-runner
reach via docker DNS at `valkey:6379` on the compose default
network. healthcheck via `valkey-cli ping` (5s interval). chamber
+ agent-runner gain `depends_on: valkey: service_healthy` so the
bridge is up before either side starts publishing or subscribing.
Forseti unchanged — never publishes Phase 3 events.
Operational properties (per forseti's deployment notes):
- Mixed-state safe at every step. Missing valkey.url config key
→ chamber + runner stay on v3.0 / Phase 2 equivalent paths.
- Backward path is single config-key delete + restart.
- streaming_enabled: true (set on agent-runner 2026-05-17) is
unaffected by this change.
README's services table + playbook header + verify section all
extended to reflect the four-service shape. Forseti's contract
at vh/althing:docs/contracts/phase3_1_valkey_bridge.contract.md
carries the wire-protocol spec.
125 lines
5.4 KiB
YAML
125 lines
5.4 KiB
YAML
# althing-chamber stack — chamber + forseti + agent-runner + valkey.
|
|
#
|
|
# Four services on the compose default network:
|
|
# althing-chamber — FastAPI/HTMX app (port 7881 host → 8000 container)
|
|
# althing-forseti — moderator daemon (no port; cross-process glue via the DB)
|
|
# althing-agent-runner — Phase 2 daemon: claims worldtree-driver grants and
|
|
# dispatches to Worldtree's conversation API (no port)
|
|
# valkey — Phase 3.1 sibling service: Valkey 8 alpine, redis-protocol
|
|
# pub/sub bridge for cross-process streaming events from
|
|
# agent-runner → chamber SSE subscribers. Reached via
|
|
# docker DNS at `valkey:6379` on the default network.
|
|
# No exposed port; internal-only.
|
|
#
|
|
# Three storage / IPC channels:
|
|
# - SQLite bind-mount at /app/data backs the althing.db state shared by
|
|
# chamber + forseti + agent-runner (eventbus.bridge_from_db drains DB
|
|
# commits into chamber's SSE).
|
|
# - Valkey pub/sub on docker-default network carries the Phase 3 streaming
|
|
# events (msg_start/thinking/delta/complete/curated) that don't go
|
|
# through SQLite — too high-volume + ephemeral for the DB.
|
|
# - chamber's healthcheck-blocked startup gate on valkey ensures the
|
|
# subscriber side is up before chamber begins handling SSE traffic.
|
|
#
|
|
# Image is built on the host from the vh/althing git repo by the deploy
|
|
# playbook (`playbooks/deploy-althing-chamber.yaml`), which clones into
|
|
# /opt/docker/build/althing-chamber and runs `docker build -t
|
|
# althing-chamber:local .` before installing this compose and bringing
|
|
# both services up. No registry.
|
|
#
|
|
# Internal tooling — accessed directly at http://10.250.50.70:7881 over
|
|
# the LAN; does NOT traverse Traefik. State persists under
|
|
# /opt/docker/conf/althing-chamber/data on the host.
|
|
#
|
|
# All tunables live in .env — edit that, not this file.
|
|
|
|
services:
|
|
valkey:
|
|
image: valkey/valkey:8-alpine
|
|
container_name: althing-valkey
|
|
restart: unless-stopped
|
|
# No exposed port — chamber + agent-runner reach via docker DNS
|
|
# at `valkey:6379` on the compose default network. No volume —
|
|
# the pub/sub channel doesn't persist anything between restarts.
|
|
healthcheck:
|
|
test: ["CMD", "valkey-cli", "ping"]
|
|
interval: 5s
|
|
timeout: 3s
|
|
retries: 5
|
|
|
|
althing-chamber:
|
|
image: ${ALTHING_IMAGE}
|
|
container_name: althing-chamber
|
|
restart: unless-stopped
|
|
depends_on:
|
|
valkey:
|
|
condition: service_healthy
|
|
ports:
|
|
- "${ALTHING_BIND:-0.0.0.0}:${ALTHING_PORT}:8000"
|
|
environment:
|
|
# ALTHING_ROOT moves the entire ~/.althing dir; ALTHING_DB additionally
|
|
# pins the SQLite path explicitly. Both point inside the bind-mount.
|
|
- ALTHING_ROOT=/app/data
|
|
- ALTHING_DB=/app/data/althing.db
|
|
# ALTHING_BIND / ALTHING_PORT — galdrabok-side env-var precedence
|
|
# (env > config.yaml > defaults) is being added in the same cycle
|
|
# as this scaffold lands. Container always listens on 8000 internally;
|
|
# the host port mapping above is the only externally-visible knob.
|
|
- ALTHING_BIND=0.0.0.0
|
|
- ALTHING_PORT=8000
|
|
volumes:
|
|
- ${ALTHING_DATA_DIR}:/app/data
|
|
command: ["althing-chamber"]
|
|
healthcheck:
|
|
# Liveness probe — chamber's /health endpoint returns 200 with no DB
|
|
# read (true liveness, not readiness). galdrabok adds this endpoint
|
|
# in the same cycle as this scaffold; container will crashloop on
|
|
# healthcheck until that lands.
|
|
test: ["CMD-SHELL", "python -c 'import urllib.request,sys; r=urllib.request.urlopen(\"http://127.0.0.1:8000/health\",timeout=3); sys.exit(0 if r.status==200 else 1)' || exit 1"]
|
|
interval: 30s
|
|
timeout: 5s
|
|
retries: 3
|
|
start_period: 30s
|
|
labels:
|
|
- homepage.group=Toolchain
|
|
- homepage.name=althing chamber
|
|
- homepage.icon=mdi-bullhorn
|
|
- homepage.description=Web UI for the althing inter-agent message bus
|
|
- homepage.href=http://10.250.50.70:${ALTHING_PORT}
|
|
|
|
althing-forseti:
|
|
image: ${ALTHING_IMAGE}
|
|
container_name: althing-forseti
|
|
restart: unless-stopped
|
|
environment:
|
|
- ALTHING_ROOT=/app/data
|
|
- ALTHING_DB=/app/data/althing.db
|
|
volumes:
|
|
- ${ALTHING_DATA_DIR}:/app/data
|
|
command: ["althing-forseti"]
|
|
# No healthcheck — the forseti CLI doesn't expose one. Liveness signal
|
|
# for ops is "container hasn't exited" + chamber-side observation that
|
|
# bridge_from_db events are flowing.
|
|
|
|
althing-agent-runner:
|
|
image: ${ALTHING_IMAGE}
|
|
container_name: althing-agent-runner
|
|
restart: unless-stopped
|
|
depends_on:
|
|
valkey:
|
|
condition: service_healthy
|
|
environment:
|
|
- ALTHING_ROOT=/app/data
|
|
- ALTHING_DB=/app/data/althing.db
|
|
volumes:
|
|
- ${ALTHING_DATA_DIR}:/app/data
|
|
command: ["althing-agent-runner"]
|
|
# Phase 2 daemon (added 2026-05-16). Polls floor_grants WHERE
|
|
# consumed_at IS NULL AND agents.driver='worldtree', claims via
|
|
# atomic UPDATE, calls Worldtree's conversation API, posts the
|
|
# agent's response back through the bus as a broadcast. Harmless
|
|
# when no driver=worldtree handles are declared — runner sleeps
|
|
# at poll_interval_seconds. Multi-instance safe via claim_grant's
|
|
# atomic UPDATE; no flock required. Same lack-of-healthcheck story
|
|
# as forseti (CLI doesn't expose one).
|