Max treats a chat bot as a correctness problem. Every message, whichever platform it arrives on, lands in one immutable canonical ledger with a single typed message IR; deliveries β including mirroring one conversation across platforms β use bounded process-local queues with persistent receipts and explicit capability fallbacks; and the LLM's context is a rebuildable, integrity-checked projection of that ledger, not a sliding window. The interesting decisions are written down in the ADRs.
chat platforms
β adapters normalize losslessly; nothing degrades at ingest
βΌ
canonical message ledger βββββ¬βββΆ mirror deliveries: native where a
(one phase-indexed IR, β platform can, readable text where
PostgreSQL, delivery receipts, β it cannot; outcomes stay inspectable
echo reconciliation) ββββΆ agent turns: LLM, tools, sandbox,
β browser, files, media
ββββΆ context projections: episodes,
memories, unified recall
- One message IR, capability-tiered delivery. Faces, cards, files, replies, and mentions keep their structure (and raw payloads for native round-trips) all the way to the ledger. Each endpoint declares native/text/drop per feature; a single lowering pass folds whatever an endpoint can't carry into readable text and records every degradation as an auditable note. (ADR 003)
- Mirrors that don't lie. One canonical row per semantic message, per-endpoint delivery receipts and current-process ordering. Ambiguous sends remain unknown unless a platform receipt resolves them. Graceful shutdown drains output within its deadline; a crash does not replay unsent copies.
- Context as a database, not a window. Raw messages are immutable; quiet-period episodes carry layered, sourced summaries with exact, hash-checked source coverage; prompts are token-planned projections that degrade deterministically under budget and expand back to raw text on demand. (ADR 001)
- Memory with an audit trail. Conversation-scoped memories are versioned CAS records with evidence links and actor permissions. Historian proposes updates from conversation evidence; expiry follows recorded dates. Unified recall spans memories, episodes, raw history, and media captions, lexical and semantic, with embedding provenance checked in SQL.
- It reads its own source. An allowlisted snapshot of this repository ships inside the binary; the bot answers questions about itself by searching and reading the exact deployed code, ADRs, and schema.
- SSH fleet operations. Enabled groups load
operationsand run ordinaryssh hostnamefrom their native sandbox through a dedicated Tailscale client. The fleet login ismaxwith passwordless sudo; the bot runs asmax-service. Configuration and lifecycle. - Plus the table stakes: concurrent turns with streaming, cancellation, and mid-turn feedback; multimodal input; persistent per-group sandboxes and browser automation; skills, typed monitors, process-owned background Jobs, proactive participation, and an authenticated local admin panel.
cp .env.example .env
cp max.yaml.example max.yaml
direnv allow # nix-direnv loads the flake devShell
devenv up --detached # PostgreSQL on 127.0.0.1:5433
cabal run maxInteractive development defaults to direnv with use flake . --impure, using
the same devShell as nix develop --impure. Entering the project loads the
environment into your current shell; leaving restores the outer environment.
nix-direnv caches the shell, and changes to flake.nix, flake.lock,
devenv.nix, or .env are picked up at the next prompt.
direnv loads .env with dotenv_if_exists after the Nix environment, so local
values override defaults from devenv.nix. This does not source .env as a
shell script. Plain nix develop --impure and CI require explicit environment
variables: devenv 2.3's built-in dotenv is unavailable through flakes.
The shared nix-config dev profile enables direnv and disables native devenv
auto-activation. When switching from the native hook, run devenv revoke with
the installed CLI in this repository, run direnv allow, and open a fresh
terminal so an existing native subshell or hook does not remain active.
The native CLI >= 2.3 remains an optional entry point through
devenv.yaml/devenv.lock, with dotenv.enable and background shell reload.
Inside the flake environment, devenv is a compatibility wrapper for up,
tasks, and test; use the installed native CLI's full path to invoke shell.
Native dotenv gives explicit env settings precedence and materializes values
in the local Nix store. Source-code compilation and running Max processes are
managed separately from either shell activation mechanism.
devenv.yaml/devenv.lock pin the same nixpkgs and devenv inputs as
flake.lock; after updating either entry point, synchronize the other and run
python3 scripts/check-devenv-pins.py. CI checks the resolved input graphs.
nix build .#max produces the runtime package: max, max-runtime,
and max-adr003-maintenance. The command
nix build .#max-tools --out-link result-tools produces max-prompt-flow and the intent, context, contract, and workflow
evaluation executables in result-tools/bin. Each package builds only its own
executable group. Local Cabal builds include both groups by default, including
cabal run max-prompt-flow before every commit.
Linux builds use mold for linking; both the development shell and the Nix
packages provide it. To verify a Linux binary, run
readelf -p .comment <binary> and look for mold.
The NixOS module provisions native NapCat, browser services and command
sandboxes under max-stack.target. Enable services.max.napcat.enable for QQ,
then open http://localhost:6099 on that host to log in. OneBot uses loopback
port 18080 by default. Migrations and derived-data backfills run automatically.
Local Haskell development does not require Docker; executing systemd sandboxes
requires the NixOS runtime services. See the runtime and migration runbook.
After a NapCat reconnect, Max also makes a bounded, deduplicated history pass over QQ conversations it already knows. Imported rows enrich context and media queues but never trigger replies or mirror as new traffic. This is deliberately best-effort message recovery, not a durable cursor: NapCat may omit messages outside the returned windows and does not reconstruct offline reactions or recalls.
Configuration is layered as CLI flags, environment variables, then YAML;
one LLM API key (OpenAI-compatible, OpenAI Responses, or Anthropic-native)
is the only required value, and optional feature sections stay disabled
when absent. For a real deployment, nix/module.nix
ships the whole thing as a systemd service. Max runs as a single
production instance for its author β a personal agent with
framework-grade plumbing, not a framework.
cabal test max-test
MAX_TEST_DB_URL=postgresql://127.0.0.1:5433/max_test cabal test max-test-db
cabal build all| Document | Contents |
|---|---|
| features.md | behaviour and configuration semantics |
| architecture.md | runtime, context/memory design, and durability |
| SSH operations | dedicated Tailscale, fleet login, recovery and acceptance |
| native runtime | systemd services, sandbox state and service accounts |
| database health | retained failures, memory corrections and release acceptance |
| platforms.md | platform operations, mirroring, and cutover invariants |
| ADR 001 | context/memory invariants and privacy boundaries |
| ADR 002 | historical partial-plan design; see current architecture and Jobs runbook |
| ADR 003 | the message IR, capability-tiered lowering, and prior-art survey |
| ADR 004 | canonical handles for the model, and the identity it addresses |
| ADR 005 | historical replay design; readable scoped digests remain |
| ADR 006 | typed durable monitors and the unified scheduler |
| ADR 007 | historical durable fork/join plans; replaced by process-owned Jobs |
| ADR 008 | historical durable-task design; superseded by runtime simplification |
| development.md | tests, evaluation, versioning, and debugging |
| prompt-flow.md | generated prompt and tool-round wire examples |
MIT Β© Hank Hogan