services:
  litellm-db:
    image: postgres:16-alpine
    container_name: litellm-db
    environment:
      - POSTGRES_DB=litellm
      - POSTGRES_USER=litellm
      - POSTGRES_PASSWORD=litellm
    volumes:
      - /mnt/ssd/dbs/litellm/postgres:/var/lib/postgresql/data
    restart: always

  litellm:
    image: ghcr.io/berriai/litellm:main-latest
    container_name: litellm
    ports:
      - "4000:4000"
    volumes:
      - ./litellm-config.yaml:/app/config.yaml
    environment:
      - DATABASE_URL=postgresql://litellm:litellm@litellm-db:5432/litellm
      - LITELLM_MASTER_KEY=sk-fjQC1BxAiGFSMs
      - LANGFUSE_PUBLIC_KEY=${LANGFUSE_PUBLIC_KEY:-changeme}
      - LANGFUSE_SECRET_KEY=${LANGFUSE_SECRET_KEY:-changeme}
      - LANGFUSE_HOST=http://langfuse:3000
      - OPENROUTER_API_KEY=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
    command: ["--config", "/app/config.yaml", "--port", "4000"]
    extra_hosts:
      - "host.docker.internal:host-gateway"
    depends_on:
      - litellm-db
      - langfuse
    restart: always

  kimi-agent:
    build: ./kimi-agent
    container_name: kimi-agent
    volumes:
      - /home/alvis/kimi-workspace:/workspace
      - kimi-agent-home:/root/.kimi-code
    restart: unless-stopped

  langfuse-db:
    image: postgres:16-alpine
    container_name: langfuse-db
    environment:
      - POSTGRES_DB=langfuse
      - POSTGRES_USER=langfuse
      - POSTGRES_PASSWORD=langfuse
    volumes:
      - /mnt/ssd/dbs/langfuse/postgres:/var/lib/postgresql/data
    restart: always

  langfuse:
    image: ghcr.io/langfuse/langfuse:2
    container_name: langfuse
    ports:
      - "3200:3000"
    environment:
      - DATABASE_URL=postgresql://langfuse:langfuse@langfuse-db:5432/langfuse
      - NEXTAUTH_URL=https://lf.alogins.net
      - NEXTAUTH_SECRET=532a746b24ac40afa39f9d317031cab94d4d6881107ea3b1209b28020f1a9761
      - SALT=7927b3b0092afe4542274940b557becea6418a5fed79f7acd25c3a789349fdc9
      - AUTH_DISABLE_SIGNUP=true
    depends_on:
      - langfuse-db
    restart: always

  qdrant:
    image: qdrant/qdrant
    container_name: qdrant
    ports:
      - "6333:6333"
      - "6334:6334"
    restart: always
    volumes:
      - /mnt/ssd/dbs/qdrant:/qdrant/storage:z

  faster-whisper:
    image: fedirz/faster-whisper-server:latest-cuda
    container_name: faster-whisper
    runtime: nvidia
    ports:
      - "8880:8000"
    environment:
      - WHISPER__MODEL=deepdml/faster-whisper-large-v3-turbo-ct2
      - WHISPER__INFERENCE_DEVICE=cuda
      - WHISPER__COMPUTE_TYPE=int8
      - WHISPER__LANGUAGE=ru
      - NVIDIA_VISIBLE_DEVICES=all
      - NVIDIA_DRIVER_CAPABILITIES=compute,utility
    volumes:
      - /mnt/ssd/ai/faster-whisper:/root/.cache/huggingface
    restart: always

  silero-tts:
    build: ./silero-tts
    container_name: silero-tts
    ports:
      - "8881:8881"
    volumes:
      - /mnt/ssd/ai/silero-tts:/cache/torch
    restart: always

  pipecat:
    build: ./pipecat
    container_name: pipecat
    ports:
      - "8882:8882"
    environment:
      - LIVEKIT_URL=ws://host.docker.internal:7880
      - LIVEKIT_PUBLIC_URL=wss://lk.alogins.net
      - LIVEKIT_API_KEY=devkey
      - LIVEKIT_SECRET=ef3ef4b903ca8469b09b2dd7ab6af529c4d2f3c95668f53832fc351cf67777a9
      - ADOLF_URL=http://host.docker.internal:8000/v1
      - STT_URL=http://host.docker.internal:8880/v1
      - TTS_URL=http://host.docker.internal:8881/v1
      - STT_MODEL=deepdml/faster-whisper-large-v3-turbo-ct2
      - TTS_VOICE=onyx
    extra_hosts:
      - "host.docker.internal:host-gateway"
    restart: unless-stopped

  # Adolf — OpenClaw fork (Matrix-first personal assistant). The OpenClaw
  # gateway config (Matrix channel + allow-list, model provider ->
  # adolf-llm:8010, MCP registry, gateway.tools.allow for cron/nodes) is
  # version-controlled at agap_git/adolf/openclaw.json (repo root, alongside
  # this openai/ project, not nested inside it) and bind-mounted read-only
  # over the adolf-state volume (see volumes below), so git is the single
  # source of truth — not a hand-edited volume file. The volume still
  # holds runtime state only (Matrix crypto/devices, credentials, sessions,
  # workspace/SOUL.md, logs). Matrix creds and ADOLF_KEY come from
  # openai/.env (gitignored, never committed). Source tree: /home/alvis/adolf.
  # To change config: edit ../adolf/openclaw.json + restart adolf.
  adolf:
    build:
      context: ../../adolf
      # Matrix is opt-in at build time (see adolf/Dockerfile); without this,
      # the gateway logs "no-channel-owner" and channels.matrix is inert.
      args:
        OPENCLAW_EXTENSIONS: matrix
    image: adolf:local
    container_name: adolf
    environment:
      - HOME=/home/node
      - OPENCLAW_HOME=/home/node
      - OPENCLAW_STATE_DIR=/home/node/.openclaw
      - OPENCLAW_CONFIG_PATH=/home/node/.openclaw/openclaw.json
      - OPENCLAW_CONFIG_DIR=/home/node/.openclaw
      - OPENCLAW_WORKSPACE_DIR=/home/node/.openclaw/workspace
      - OPENCLAW_GATEWAY_TOKEN=${ADOLF_GATEWAY_TOKEN:-}
      - ADOLF_KEY=${ADOLF_KEY:-}
      - MATRIX_HOMESERVER=${MATRIX_HOMESERVER:-}
      - MATRIX_USER_ID=${MATRIX_USER_ID:-}
      - MATRIX_PASSWORD=${MATRIX_PASSWORD:-}
      - MATRIX_DEVICE_NAME=${MATRIX_DEVICE_NAME:-Adolf OpenClaw Gateway}
      # marketplace-mcp bearer token (kb task #61) -- referenced by
      # openclaw.json's mcp.servers.marketplace.headers.Authorization via
      # ${MARKETPLACE_MCP_TOKEN} substitution; never inlined into that file.
      - MARKETPLACE_MCP_TOKEN=${MARKETPLACE_MCP_TOKEN:-}
      - TZ=Europe/Riga
    volumes:
      # Runtime state only (Matrix crypto/devices, credentials, sessions,
      # workspace, logs). The gateway config file itself is overlaid below.
      - adolf-state:/home/node/.openclaw
      # Version-controlled OpenClaw gateway config, mounted read-only on top
      # of the state volume so it is the single source of truth. The gateway
      # reads this JSONC file and snapshots its own .last-good/.rejected
      # copies into the volume dir (writable) — it never rewrites this file,
      # so read-only is safe. Edit the tracked file + restart to change config;
      # runtime/UI edits are intentionally disabled by the ro mount.
      - ../adolf/openclaw.json:/home/node/.openclaw/openclaw.json:ro
      # quota-command plugin (kb #62) — same read-only-bind-over-volume
      # pattern as openclaw.json above, applied to a single external plugin
      # dir instead of the whole state tree. Previously the only precedent
      # (cognee-memory) was docker cp'd straight into the adolf-state volume
      # at runtime with no git backing; this plugin is small enough (no
      # node_modules — only Node built-ins/global fetch) to just bind-mount
      # its tracked source directly at its extensions/<id> path, so git stays
      # the single source of truth the same way it already is for
      # openclaw.json. Activated via plugins.entries.quota-command in that file.
      - ./quota-command-openclaw-plugin:/home/node/.openclaw/extensions/quota-command:ro
      # hindsight-memory plugin (kb #75, H3) — same read-only-bind-over-volume
      # pattern as quota-command above. Structural successor to cognee-memory
      # (still docker cp'd into the adolf-state volume, no git backing; that
      # plugin's activation/container is decommissioned in H4, not here).
      # Forced hooks (before_prompt_build recall / agent_end retain) against
      # the hindsight service (see that service's block below), replacing
      # Cognee as Adolf's memory backend. Activated via
      # plugins.entries.hindsight-memory in openclaw.json.
      - ./hindsight-openclaw-plugin:/home/node/.openclaw/extensions/hindsight-memory:ro
    extra_hosts:
      - "host.docker.internal:host-gateway"
      # mtx.alogins.net's public A record can't hairpin-NAT back through the
      # router from inside a container; route it to the host gateway instead,
      # matching matrix/docker-compose.yml's lk-jwt-service (same problem,
      # same fix). Caddy on the host terminates TLS on :443 and proxies to
      # synapse:8008.
      - "mtx.alogins.net:host-gateway"
    cap_drop:
      - NET_RAW
      - NET_ADMIN
    security_opt:
      - no-new-privileges:true
    init: true
    ports:
      - "18789:18789"
      - "18790:18790"
    command:
      ["node", "dist/index.js", "gateway", "--bind", "lan", "--port", "18789"]
    restart: unless-stopped

  # hindsight-llm — standalone clone of cognee-llm (kb#76, H4 option B): the
  # dedicated Kimi-CLI wrapper that is now Hindsight's LLM, so the whole cognee
  # stack (incl. cognee-llm) can be decommissioned. Own port (:8012) + own
  # kimi-code volume; needs a one-time `kimi login` seeded into hindsight-llm-home.
  hindsight-llm:
    build: ./hindsight-llm
    container_name: hindsight-llm
    ports:
      - "8012:8012"
    volumes:
      - hindsight-llm-home:/root/.kimi-code
    restart: unless-stopped

  # adolf-llm — conversational Kimi-CLI wrapper (:8010), the model backend for
  # the Adolf OpenClaw gateway (P2). Real streaming (SSE), chat_id session-keying
  # + 1:1 kimi resume, media, per-session .mcp.json sourced from the shared
  # shared-mcp.json contract (cognee-mcp P4, openclaw-tools P5). Needs
  # `kimi login` in adolf-llm-home.
  adolf-llm:
    build: ./adolf-llm
    container_name: adolf-llm
    ports:
      - "8010:8010"
    volumes:
      - adolf-llm-workspace:/workspace
      - adolf-llm-home:/root/.kimi-code
      - ./shared-mcp.json:/shared-mcp.json:ro
    extra_hosts:
      # Needed to reach kanboard-mcp-adolf (:3104, network_mode: host, outside
      # this compose project's network) via shared-mcp.json's "kanboard"
      # entry — same host-gateway trick used by adolf/cognee/pipecat above.
      - "host.docker.internal:host-gateway"
    restart: unless-stopped

  # hindsight — Adolf memory backend, replacing cognee/cognee-mcp/cognee-llm
  # (kb#73, migration doc agap_git/adolf/HINDSIGHT-MIGRATION.md, H1). One
  # container: REST API :8888 (also serves the built-in MCP at /mcp/{bank}/),
  # UI :9999, built-in Postgres (pg0) bind-mounted to
  # /mnt/ssd/dbs/hindsight/ (host dir created + chowned 1000:1000 to match
  # the image's non-root `hindsight` user, confirmed via
  # `docker run --entrypoint id`).
  #
  # LLM + embeddings reconfigured 2026-07-15 (kb#84) to fix two wrong H1
  # choices for a Russian/multilingual use case:
  #
  # LLM -> hindsight-llm:8012 (dedicated Kimi-CLI wrapper cloned from the shim cognee
  # uses — see cognee/cognee.env's LLM section for the full precedent,
  # including why LLM_INSTRUCTOR_MODE=json_mode isn't needed here since
  # Hindsight's own client doesn't go through `instructor`). Replaces the
  # H1 choice of LiteLLM + ollama/gemma3:4b (a tiny local model): validated
  # 2026-07-15 that cognee-llm returns clean, JSON-parseable structured
  # extraction for Russian input (see kb#84 probe B) — gemma3:4b's fluency
  # on Russian was never actually verified, it was picked only to dodge
  # qwen3:8b's <think>-token empty-content bug. Kimi is also the flat-rate
  # subscription already paid for, so this isn't a new cost.
  #
  # Embeddings -> ollama's bge-m3 on the GPU (host.docker.internal:11436,
  # separate compose project, same extra_hosts trick as cognee/adolf-llm
  # below), via ollama's OpenAI-compatible /v1/embeddings endpoint
  # (confirmed 200 + 1024-dim vector 2026-07-15, kb#84 probe A). Replaces
  # the H1 choice of Hindsight's built-in `local` provider
  # (BAAI/bge-small-en-v1.5, English-only, 384-d, CPU-bound in-process
  # SentenceTransformers). The hindsight image itself is CPU-only (torch
  # +cpu build, no onnxruntime GPU provider — confirmed 2026-07-15), so its
  # in-process local/onnx embedders can never reach the GPU; routing
  # through ollama's `openai` embeddings provider (HTTP, not the bespoke
  # cognee-style `ollama` provider Hindsight doesn't have) is how GPU
  # serving happens here. Dimensions var matches cognee.env's own bge-m3
  # swap (kb#60): 1024.
  #
  # Runs ALONGSIDE cognee/cognee-mcp/cognee-llm during the migration; those
  # are untouched here and only decommissioned in H4, after H2/H3/H5 prove
  # this service out. Not yet wired into openclaw.json/shared-mcp.json
  # (that's H2, kb#74) — this block only stands the service up and proves
  # retain/recall against a throwaway bank.
  hindsight:
    image: ghcr.io/vectorize-io/hindsight:latest
    container_name: hindsight
    restart: unless-stopped
    environment:
      - HINDSIGHT_API_LLM_PROVIDER=openai
      - HINDSIGHT_API_LLM_BASE_URL=http://hindsight-llm:8012/v1
      - HINDSIGHT_API_LLM_MODEL=openai/hindsight-llm
      # hindsight-llm ignores the key entirely (Kimi CLI wrapper, no real
      # OpenAI auth) — dummy value, non-empty so the client constructs.
      - HINDSIGHT_API_LLM_API_KEY=sk-hindsight-llm-local
      - HINDSIGHT_API_EMBEDDINGS_PROVIDER=openai
      - HINDSIGHT_API_EMBEDDINGS_OPENAI_BASE_URL=http://host.docker.internal:11436/v1
      - HINDSIGHT_API_EMBEDDINGS_OPENAI_MODEL=bge-m3
      - HINDSIGHT_API_EMBEDDINGS_OPENAI_DIMENSIONS=1024
      # ollama doesn't check this value at all (no auth), but the openai
      # embeddings client requires a non-empty key to construct.
      - HINDSIGHT_API_EMBEDDINGS_OPENAI_API_KEY=ollama
      # Stable worker id (compose service name), not the container hostname
      # default -- without this, recreating the container orphans any
      # in-flight async retain/consolidation tasks under the old hostname
      # (startup log warns about exactly this).
      - HINDSIGHT_API_WORKER_ID=hindsight
      # Reranker -> multilingual (kb#84 follow-up). The TEMPR rerank stage
      # defaulted to English cross-encoder/ms-marco-MiniLM, which ranks
      # Russian/multilingual candidates poorly. jina v2 multilingual fixes
      # that. Runs on CPU in this image (no CUDA torch) but only over the
      # small recall candidate set. trust_remote_code: jina ships custom code.
      - HINDSIGHT_API_RERANKER_PROVIDER=local
      - HINDSIGHT_API_RERANKER_LOCAL_MODEL=jinaai/jina-reranker-v2-base-multilingual
      - HINDSIGHT_API_RERANKER_LOCAL_TRUST_REMOTE_CODE=true
    volumes:
      - /mnt/ssd/dbs/hindsight:/home/hindsight/.pg0
      # Persist HuggingFace/sentence-transformers model cache so the jina
      # reranker (~1GB) doesn't re-download on every container recreate.
      - /mnt/ssd/dbs/hindsight-cache:/home/hindsight/.cache
    ports:
      - "8888:8888"
      - "9999:9999"
    extra_hosts:
      # Needed to resolve host.docker.internal from inside the container
      # for the ollama embeddings call above — ollama lives in a separate
      # compose project, same trick as cognee/adolf-llm elsewhere in this
      # file.
      - "host.docker.internal:host-gateway"
    depends_on:
      - hindsight-llm

  # openclaw-tools — MCP bridge (P5) exposing a minimal slice of the Adolf
  # OpenClaw gateway's agent tools (message/cron/nodes/browser) over MCP
  # Streamable HTTP, so Kimi CLI sessions (adolf-llm) can call them instead of
  # bypassing OpenClaw entirely. Proxies each MCP tool call to the gateway's
  # `POST /tools/invoke` HTTP surface (http://adolf:18789). NOTE: `cron` and
  # `nodes` are hard-denied on that surface by default until P6 adds them to
  # `gateway.tools.allow` in the adolf openclaw.json — see openclaw-tools/
  # server.js for the full gate writeup. Not useful until `adolf` (P6) is
  # configured and running; safe to build/run standalone before that.
  openclaw-tools:
    build: ./openclaw-tools
    container_name: openclaw-tools
    environment:
      - OPENCLAW_GATEWAY_URL=http://adolf:18789
      - OPENCLAW_GATEWAY_TOKEN=${ADOLF_GATEWAY_TOKEN:-}
    ports:
      - "8020:8020"
    restart: unless-stopped

volumes:
  kimi-agent-home:
  adolf-state:
  hindsight-llm-home:
  adolf-llm-workspace:
  adolf-llm-home:
