Phase 2 daemon added to the althing-chamber stack per forseti's request
(vh/althing@5cd088a..ad1d025). Polls floor_grants WHERE consumed_at IS
NULL AND agents.driver='worldtree', claims via atomic UPDATE, calls
Worldtree's conversation API, posts the response back through the bus
as a broadcast.
Shape matches the existing forseti daemon:
- Same ${ALTHING_IMAGE} (the binary is already in [project.scripts]
as of ad1d025)
- command: ["althing-agent-runner"]
- Same shared SQLite bind-mount at /app/data
- No port, no healthcheck (CLI doesn't expose one; same liveness
story as forseti)
Safe to enable preemptively per forseti — when no driver=worldtree
handles are declared in config, the runner sleeps at
poll_interval_seconds. Multi-instance safe via the atomic claim
primitive (no flock needed).
Compose top comment, README "Services in this stack" table, playbook
header + verify steps all extended to reflect the three-service
shape. Will land on ana-docker on vh/althing's next push (compose
deployed via the elway playbook's upload step; image already carries
the binary).
althing-chamber
Web UI + moderator daemon + agent-runner for the althing inter-agent
message bus. Three services share a single SQLite store via a
bind-mount; the chamber serves FastAPI/HTMX, the forseti daemon runs
the moderation + curation loops, and the agent-runner daemon claims
floor grants for worldtree-driver agents and dispatches them to
Worldtree's conversation API. Cross-process glue is
eventbus.bridge_from_db polling the same DB all three processes
write.
Server: ana-docker
URL: http://10.250.50.70:7881 (configurable via .env)
Upstream repo: vh/althing
Image: althing-chamber:local — built on the host from the git repo
by the deploy playbook. Not pulled from a registry.
Services in this stack
| Container | Role | Port | Healthcheck |
|---|---|---|---|
althing-chamber |
FastAPI/HTMX web UI; SSE subscribers; /health endpoint |
host 7881 → container 8000 | python urllib /health |
althing-forseti |
Moderator + curator daemon; writes events that chamber's bridge picks up | — (no HTTP) | none (process-up signal only) |
althing-agent-runner |
Phase 2: claims worldtree-driver floor grants and dispatches to Worldtree's conversation API | — (no HTTP) | none (process-up signal only) |
All three use the same ${ALTHING_IMAGE}; the command: line in compose
picks which entrypoint (althing-chamber / althing-forseti /
althing-agent-runner) runs in each container.
The agent-runner is safe to enable preemptively — when no
driver=worldtree handles are declared in config, it polls
floor_grants and sleeps when the query returns empty. Multi-instance
safe via the atomic UPDATE … WHERE consumed_at IS NULL claim primitive.
Deploy
Two paths — automated (preferred) and manual (escape hatch / first-time).
Automated (Gitea Actions, push-to-main)
The vh/althing repo ships .gitea/workflows/deploy.yaml. Every push to
main + manual workflow_dispatch triggers the elway playbook below
pinned to the triggering commit SHA. A reference copy of the workflow
lives next to this README at
gitea-workflow-deploy.yaml.example;
the canonical source is in the vh/althing repo. The example header
lists the two repo secrets required (DEPLOY_SSH_KEY,
MGMT_REPO_TOKEN).
Manual (elway from a workstation)
# First deploy (or update to latest main)
scripts/elway ana-docker --playbook playbooks/deploy-althing-chamber.yaml
# Pin to a specific ref (tag, branch, or commit SHA)
scripts/elway ana-docker --playbook playbooks/deploy-althing-chamber.yaml --var ref=v0.1.0
Path layout (on ana-docker)
| Host path | Container path | Purpose | Restic? |
|---|---|---|---|
/opt/docker/build/althing-chamber/ |
— | git checkout used as docker build context | excluded |
/opt/docker/compose/althing-chamber/ |
— | compose.yaml + .env | included (via /opt/docker) |
/opt/docker/conf/althing-chamber/data/ |
/app/data |
SQLite + state for BOTH services | included |
Network model
Internal tooling, LAN-only. Chamber's container port 8000 is published
on the host at 0.0.0.0:7881 (configurable via ALTHING_BIND /
ALTHING_PORT); access is direct via http://10.250.50.70:7881. No
Traefik, no TLS terminator, no public hostname.
Forseti has no port — it's a daemon. The two services communicate only
via the shared SQLite file under the ${ALTHING_DATA_DIR} bind-mount;
no docker network coupling beyond compose's default bridge.
Env-var contract
| Var | In env? | In ~/.althing/config.yaml? |
Notes |
|---|---|---|---|
ALTHING_ROOT |
✓ (set in compose) | n/a | Overrides the default ~/.althing/ dir. Container sets /app/data. |
ALTHING_DB |
✓ (set in compose) | n/a | Explicit SQLite path. Defaults to ${ALTHING_ROOT}/althing.db. |
ALTHING_BIND |
✓ (deploy-time) | also accepted | Bind address for the chamber HTTP server. Container always sets 0.0.0.0 internally. |
ALTHING_PORT |
✓ (deploy-time) | also accepted | Chamber listens here internally (always 8000 inside the container). |
Env-var support for ALTHING_BIND / ALTHING_PORT was added on the
galdrabok side as part of the container-deploy cycle (env > config.yaml
defaults precedence).
First-deploy sequence (when galdrabok's Dockerfile lands)
- Generate a deploy keypair on ana-docker (private stays on host;
pubkey lands in
~lkraven/.ssh/authorized_keys). - Wire two secrets in
vh/althingActions settings:DEPLOY_SSH_KEY— the private key from step 1.MGMT_REPO_TOKEN— Gitea PAT withread:repositoryon this repo, used by the workflow to clone the management repo for the playbook.
- Push to vh/althing's main branch (or trigger
workflow_dispatch); the Actions runner clones both repos, configures SSH, runsscripts/elway ana-docker --playbook playbooks/deploy-althing-chamber.yaml --var ref=$SHA. - Playbook: clones into
/opt/docker/build/althing-chamber, builds the image, uploads compose + .env (one-time seed), brings both services up, polls/healthuntil 200.