# 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).
