mirror of
https://gh-proxy.org/https://github.com/klinsmaya/multica.git
synced 2026-11-08 17:36:45 +08:00
* fix(timeouts): close the two gaps the 2h inactivity budget left open (MUL-6737) Two independent places where a timeout still answered the wrong question. Codex runs a semantic-inactivity timer inside the app-server protocol, and unlike the daemon's watchdog it cannot see that a tool is in flight: a commandExecution that prints nothing for the window trips it anyway. At a fixed 10m ceiling that timer killed a quiet test suite or an output-buffering `docker build` long before the 2h daemon budget meant to protect it, which made "we raised the budget to 2h" false for Codex users. It now derives from the daemon budget, taking the larger of the idle and tool windows because it stands in for both. A tool budget of 0 means "never force-stop during a tool", which this timer cannot express, so it falls back to the idle budget rather than running unbounded; with the watchdog suite disabled entirely Codex keeps its own built-in default, as it always has. The env override and flag are unchanged. Queued tasks expired on a pure wall clock: queued longer than a TTL meant failed. That conflated "nobody is coming for this" with "the queue ahead is long", and only the first is a failure. MUL-6558 was the second — a self-hosted runtime with low concurrency held its own queue past 2h and healthy work died as queued_expired. The TTL knob added then only moved the cliff. Expiry now keys on whether the owning runtime still proves liveness, the same signal FailTasksForOfflineRuntimes already uses for dispatched/running rows, so a daemon going down retires its queued and its in-flight work on one clock instead of two. A busy runtime keeps its backlog for as long as it takes. MULTICA_TASK_QUEUED_TTL is removed with the wall clock it configured. Heartbeat age is read directly instead of gating on runtime.status='online' so a row stuck at 'online' with a dead heartbeat still releases its queue, and rows with no usable runtime binding — runtime_id IS NULL from before migration 251, whose CHECK landed NOT VALID, or a runtime row that is gone — are expired on sight rather than stranded forever by a JOIN that matches nothing. Tests: the queued sweep test now runs both phases against the same rows, asserting the 5h-old task survives while its runtime heartbeats (the MUL-6558 regression) and only expires once that runtime goes silent. Assertions are scoped to the test's own task ids, because a runtime-keyed sweep legitimately also collects queued rows other tests left on the shared fixture runtime. Co-authored-by: multica-agent <[email protected]> * fix(sweeper): give a queued task its own reconnect grace before expiring it Review catch: keying expiry on runtime liveness alone was too aggressive in one direction. Enqueue binds a task to agent.runtime_id without checking that the runtime is up (task.go CreateAgentTask call sites), so for a runtime that has already been dark for longer than the grace, the liveness clause is satisfied the moment a new task is created — assigning an issue to a laptop closed overnight would fail it inside one 30s sweep tick instead of waiting for the machine to come back. That is the opposite of what "reconnect grace" promises. Require the row's own age as well: a queued task now waits a full grace, counted from when it started waiting, before it can be given up on. A heartbeating runtime still never expires its backlog, so the MUL-6558 fix is unchanged. The age bound also stops the sweep re-evaluating the runtime subquery against every queued row on every tick. Also remove the deploy surfaces the previous commit missed — the knob was deleted from the server but helm (values + configmap), the self-host compose file, and the CI-run helm render assertions still advertised it, so operators would have kept setting a variable nothing reads. Same for five orphaned comment lines in .env.example describing the removed variable. Correct the fail-closed comment on the unbound-runtime arms: they are not reachable for "rows predating migration 251" — runtime_id was NOT NULL from migration 004 until 251 replaced it with a CHECK that every insert and update is verified against, and the migration-004 FK is ON DELETE RESTRICT. The arms stay as defence against a future schema change, which is what the comment now says. Tests: a third phase covers the reported case — a task enqueued against an already-dead runtime survives, then expires once it has waited a full grace of its own. Phase 2's expectation is corrected accordingly: the row created at now() no longer expires when its runtime dies, because it has not waited its own grace yet. Co-authored-by: multica-agent <[email protected]> * docs(timeouts): state both queued-expiry conditions and correct the FK claim Review follow-up onb567511. The user docs still described queued expiry as a single condition — "fails once the runtime has been silent past the reconnect grace" — which is the pre-b567511 behaviour and skips exactly the case that commit fixed. A reader assigning work to a machine that went offline last night would conclude the task already qualifies to fail. All four locales of tasks, daemon-runtimes and troubleshooting now state both conditions and why the second one exists. The environment-variables pages contradicted themselves: the env table had been updated to say the Codex semantic budget is derived, while the `multica config` supported-keys table further down still advertised a 10m default. That table now describes the resolution rule instead of a value. The schema comment added inb567511named the wrong constraint. agent_task_queue_runtime_id_fkey (migration 004) is ON DELETE CASCADE; the ON DELETE RESTRICT one is agent_runtime_id_fkey on agent.runtime_id. The conclusion is unchanged — a queued row cannot reference a deleted runtime — but the mechanism is a cascade plus migration 251's explicit unbind of history rows, not a restrict. Also log the resolved codex_semantic_inactivity at daemon startup, for the same reason tool_watchdog is logged: it is now a derived value, so without it the effective Codex budget cannot be read anywhere until a timeout fires. And correct a test comment that still claimed both non-exempt rows fail — the fresh row now stays queued, which is the assertion right below it. Co-authored-by: multica-agent <[email protected]> --------- Co-authored-by: J <[email protected]> Co-authored-by: multica-agent <[email protected]>
216 lines
11 KiB
YAML
216 lines
11 KiB
YAML
# Self-hosting Docker Compose — starts PostgreSQL, backend, and frontend.
|
|
#
|
|
# Services bind to 127.0.0.1 only. For cross-machine or public access, front
|
|
# them with a reverse proxy (Caddy / nginx / Cloudflare Tunnel) that terminates
|
|
# TLS and forwards to 127.0.0.1:8080 (backend) and 127.0.0.1:3000 (frontend).
|
|
# Do NOT change these bindings to 0.0.0.0 — Docker bypasses host firewalls
|
|
# (UFW/iptables) by default, so the raw ports would be exposed to the internet
|
|
# while any deployment credentials remain weak. See:
|
|
# apps/docs/content/docs/self-host-quickstart.mdx
|
|
#
|
|
# Usage:
|
|
# cp .env.example .env
|
|
# # Edit .env — generate JWT_SECRET with `openssl rand -hex 32` (required)
|
|
# docker compose -f docker-compose.selfhost.yml up -d
|
|
#
|
|
# Frontend: http://localhost:${FRONTEND_PORT:-3000}
|
|
# Backend: http://localhost:${BACKEND_PORT:-${API_PORT:-${SERVER_PORT:-${PORT:-8080}}}}
|
|
#
|
|
# The published values above are HOST ports; the containers always listen on
|
|
# 8080 / 3000 internally, so changing them never needs a rebuild. PORT is the
|
|
# variable to edit; BACKEND_PORT, API_PORT and SERVER_PORT are optional aliases
|
|
# that override it in that order. Keep this alias order identical to Makefile
|
|
# and scripts/local-env.sh. The web dev fallback intentionally omits PORT
|
|
# because Next uses that variable for its own frontend listener.
|
|
#
|
|
# Note that *which source* wins differs per entry point — Compose lets the
|
|
# calling environment outrank this file, while make lets the included env file
|
|
# outrank the environment. Nothing re-derives the published port from the inputs
|
|
# any more: `make selfhost` (via scripts/selfhost-wait.sh) and both installers
|
|
# read it back with `docker compose port`, so the health check and the printed
|
|
# URL always match what was actually published.
|
|
|
|
name: multica
|
|
|
|
services:
|
|
postgres:
|
|
image: pgvector/pgvector:pg17
|
|
environment:
|
|
POSTGRES_DB: ${POSTGRES_DB:-multica}
|
|
POSTGRES_USER: ${POSTGRES_USER:-multica}
|
|
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:-multica}
|
|
volumes:
|
|
- pgdata:/var/lib/postgresql/data
|
|
restart: unless-stopped
|
|
healthcheck:
|
|
test:
|
|
[
|
|
"CMD-SHELL",
|
|
"pg_isready -U ${POSTGRES_USER:-multica} -d ${POSTGRES_DB:-multica}",
|
|
]
|
|
interval: 5s
|
|
timeout: 5s
|
|
retries: 5
|
|
|
|
backend:
|
|
image: ${MULTICA_BACKEND_IMAGE:-ghcr.io/multica-ai/multica-backend}:${MULTICA_IMAGE_TAG:-latest}
|
|
depends_on:
|
|
postgres:
|
|
condition: service_healthy
|
|
ports:
|
|
- "127.0.0.1:${BACKEND_PORT:-${API_PORT:-${SERVER_PORT:-${PORT:-8080}}}}:8080"
|
|
volumes:
|
|
- backend_uploads:/app/data/uploads
|
|
environment:
|
|
DATABASE_URL: postgres://${POSTGRES_USER:-multica}:${POSTGRES_PASSWORD:-multica}@postgres:5432/${POSTGRES_DB:-multica}?sslmode=disable
|
|
PORT: "8080"
|
|
METRICS_ADDR: ${METRICS_ADDR:-}
|
|
JWT_SECRET: ${JWT_SECRET:?JWT_SECRET must be set to a strong random value — generate one with 'openssl rand -hex 32'}
|
|
FRONTEND_ORIGIN: ${FRONTEND_ORIGIN:-http://localhost:3000}
|
|
CORS_ALLOWED_ORIGINS: ${CORS_ALLOWED_ORIGINS:-}
|
|
RESEND_API_KEY: ${RESEND_API_KEY:-}
|
|
RESEND_FROM_EMAIL: ${RESEND_FROM_EMAIL:[email protected]}
|
|
SMTP_HOST: ${SMTP_HOST:-}
|
|
SMTP_PORT: ${SMTP_PORT:-25}
|
|
SMTP_USERNAME: ${SMTP_USERNAME:-}
|
|
SMTP_PASSWORD: ${SMTP_PASSWORD:-}
|
|
SMTP_FROM_EMAIL: ${SMTP_FROM_EMAIL:-}
|
|
SMTP_TLS: ${SMTP_TLS:-}
|
|
SMTP_TLS_INSECURE: ${SMTP_TLS_INSECURE:-false}
|
|
SMTP_EHLO_NAME: ${SMTP_EHLO_NAME:-}
|
|
GOOGLE_CLIENT_ID: ${GOOGLE_CLIENT_ID:-}
|
|
GOOGLE_CLIENT_SECRET: ${GOOGLE_CLIENT_SECRET:-}
|
|
GOOGLE_REDIRECT_URI: ${GOOGLE_REDIRECT_URI:-http://localhost:3000/auth/callback}
|
|
S3_BUCKET: ${S3_BUCKET:-}
|
|
S3_REGION: ${S3_REGION:-us-west-2}
|
|
AWS_ENDPOINT_URL: ${AWS_ENDPOINT_URL:-}
|
|
S3_USE_PATH_STYLE: ${S3_USE_PATH_STYLE:-}
|
|
AWS_ACCESS_KEY_ID: ${AWS_ACCESS_KEY_ID:-}
|
|
AWS_SECRET_ACCESS_KEY: ${AWS_SECRET_ACCESS_KEY:-}
|
|
ATTACHMENT_DOWNLOAD_MODE: ${ATTACHMENT_DOWNLOAD_MODE:-auto}
|
|
ATTACHMENT_DOWNLOAD_URL_TTL: ${ATTACHMENT_DOWNLOAD_URL_TTL:-30m}
|
|
CLOUDFRONT_DOMAIN: ${CLOUDFRONT_DOMAIN:-}
|
|
CLOUDFRONT_KEY_PAIR_ID: ${CLOUDFRONT_KEY_PAIR_ID:-}
|
|
CLOUDFRONT_PRIVATE_KEY: ${CLOUDFRONT_PRIVATE_KEY:-}
|
|
COOKIE_DOMAIN: ${COOKIE_DOMAIN:-}
|
|
APP_ENV: ${APP_ENV:-production}
|
|
MULTICA_DEV_VERIFICATION_CODE: ${MULTICA_DEV_VERIFICATION_CODE:-}
|
|
MULTICA_APP_URL: ${MULTICA_APP_URL:-http://localhost:3000}
|
|
MULTICA_DATABASE_STARTUP_TIMEOUT: ${MULTICA_DATABASE_STARTUP_TIMEOUT:-3m}
|
|
MULTICA_DATABASE_CONNECT_TIMEOUT: ${MULTICA_DATABASE_CONNECT_TIMEOUT:-5s}
|
|
MULTICA_SHUTDOWN_HOLD_DURATION: ${MULTICA_SHUTDOWN_HOLD_DURATION:-}
|
|
MULTICA_RUNTIME_RECONNECT_GRACE: ${MULTICA_RUNTIME_RECONNECT_GRACE:-}
|
|
ALLOW_SIGNUP: ${ALLOW_SIGNUP:-true}
|
|
ALLOWED_EMAILS: ${ALLOWED_EMAILS:-}
|
|
ALLOWED_EMAIL_DOMAINS: ${ALLOWED_EMAIL_DOMAINS:-}
|
|
DISABLE_WORKSPACE_CREATION: ${DISABLE_WORKSPACE_CREATION:-}
|
|
# Managed Cloud stays off for self-hosting. All managed Cloud clients use
|
|
# this single URL when it is configured.
|
|
MULTICA_CLOUD_URL: ${MULTICA_CLOUD_URL:-}
|
|
GITHUB_APP_SLUG: ${GITHUB_APP_SLUG:-}
|
|
GITHUB_WEBHOOK_SECRET: ${GITHUB_WEBHOOK_SECRET:-}
|
|
# App API credentials. The pull_request webhook is what mirrors a PR
|
|
# onto a card; these are what let the server authenticate as the App
|
|
# and pull the CI / mergeability snapshot that enriches it, plus
|
|
# Settings -> Repositories -> Choose from GitHub. Left unset,
|
|
# ghsnapshot.NewClientFromEnv returns a nil client and the enrichment
|
|
# degrades off silently - no error and no log line to notice.
|
|
# GITHUB_APP_PRIVATE_KEY is a PEM block: double-quote it in .env so
|
|
# its newlines survive into the container.
|
|
GITHUB_APP_ID: ${GITHUB_APP_ID:-}
|
|
GITHUB_APP_PRIVATE_KEY: ${GITHUB_APP_PRIVATE_KEY:-}
|
|
# Public URL the API is reachable at from the open internet, no
|
|
# trailing slash. Used to mint absolute webhook URLs for autopilot
|
|
# webhook triggers. Leave unset behind a same-origin reverse proxy
|
|
# (e.g. plain localhost dev); the frontend will compose the URL
|
|
# from window.origin + webhook_path in that case. Headers are
|
|
# intentionally NOT used to derive this value, to avoid Host /
|
|
# X-Forwarded-Host spoofing on misconfigured proxies.
|
|
MULTICA_PUBLIC_URL: ${MULTICA_PUBLIC_URL:-}
|
|
# Surface HTML must use a second, cookie-free browser origin routed to
|
|
# this backend. Both settings are optional only in the sense that leaving
|
|
# either empty disables plugin surfaces rather than weakening isolation.
|
|
MULTICA_PLUGIN_SECRET_KEY: ${MULTICA_PLUGIN_SECRET_KEY:-}
|
|
MULTICA_PLUGIN_SURFACE_ORIGIN: ${MULTICA_PLUGIN_SURFACE_ORIGIN:-}
|
|
MULTICA_PLUGIN_API_URL: ${MULTICA_PLUGIN_API_URL:-}
|
|
MULTICA_PLUGIN_DIR: ${MULTICA_PLUGIN_DIR:-}
|
|
# Optional API URL shown in self-host daemon setup commands. Set this
|
|
# when the daemon reaches the API through a different URL than the one
|
|
# used for public webhook URLs; otherwise MULTICA_PUBLIC_URL is used.
|
|
MULTICA_DAEMON_SERVER_URL: ${MULTICA_DAEMON_SERVER_URL:-}
|
|
# Comma-separated CIDRs whose source IP is allowed to set
|
|
# X-Forwarded-For / X-Real-IP for the webhook per-IP rate limiter.
|
|
# Empty default = headers ignored, RemoteAddr used. Set e.g.
|
|
# "127.0.0.1/32" when running behind a same-host reverse proxy.
|
|
MULTICA_TRUSTED_PROXIES: ${MULTICA_TRUSTED_PROXIES:-}
|
|
# Server-side LLM layer, backing internal helpers such as chat title
|
|
# generation. Both the API key and the base URL empty = layer disabled,
|
|
# and callers fall back silently rather than failing. MULTICA_LLM_MAX_RETRIES
|
|
# is the retry budget: unset = 2, 0 = disabled, 1-5 = an exact ceiling.
|
|
# Anything else fails the boot instead of being silently corrected.
|
|
MULTICA_LLM_API_KEY: ${MULTICA_LLM_API_KEY:-}
|
|
MULTICA_LLM_BASE_URL: ${MULTICA_LLM_BASE_URL:-}
|
|
MULTICA_LLM_DEFAULT_MODEL: ${MULTICA_LLM_DEFAULT_MODEL:-}
|
|
MULTICA_LLM_MAX_RETRIES: ${MULTICA_LLM_MAX_RETRIES:-}
|
|
# Lark / Feishu bot integration. MULTICA_LARK_SECRET_KEY is the
|
|
# opt-in: unset = integration disabled. Mainland 飞书 and international
|
|
# Lark are auto-detected per installation and served side by side, so
|
|
# the two base-URL knobs should normally stay EMPTY. They are optional
|
|
# deployment-wide overrides that force every installation onto one host
|
|
# (proxy / mock / single-cloud staging). Upgrading from a setup that
|
|
# used https://open.larksuite.com here? The server relabels existing
|
|
# installs to region=lark on first boot, then you can clear them.
|
|
# See docs/lark-bot-integration.
|
|
MULTICA_LARK_SECRET_KEY: ${MULTICA_LARK_SECRET_KEY:-}
|
|
MULTICA_LARK_HTTP_BASE_URL: ${MULTICA_LARK_HTTP_BASE_URL:-}
|
|
MULTICA_LARK_CALLBACK_BASE_URL: ${MULTICA_LARK_CALLBACK_BASE_URL:-}
|
|
# Slack bot integration. MULTICA_SLACK_SECRET_KEY is the opt-in: unset =
|
|
# integration disabled. It decrypts the per-installation bot/app tokens,
|
|
# which are brought by each workspace via OAuth/BYO and stored encrypted
|
|
# in the database, so this single deployment-wide key is all the operator
|
|
# needs to set here.
|
|
MULTICA_SLACK_SECRET_KEY: ${MULTICA_SLACK_SECRET_KEY:-}
|
|
# Self-hosted Git provider integration (Forgejo / Gitea / GitLab). This
|
|
# is a self-host-only feature, so the compose file turns it on by default;
|
|
# the managed cloud leaves it unset (off). It still needs a valid
|
|
# MULTICA_VCS_SECRET_KEY below to actually work.
|
|
MULTICA_VCS_INTEGRATION_ENABLED: ${MULTICA_VCS_INTEGRATION_ENABLED:-true}
|
|
# VCS integration at-rest encryption key for token-based providers
|
|
MULTICA_VCS_SECRET_KEY: ${MULTICA_VCS_SECRET_KEY:-}
|
|
# WeCom smart-bot integration. MULTICA_WECOM_SECRET_KEY is the
|
|
# opt-in: unset = integration disabled. It encrypts each installation's
|
|
# smart-bot secret at rest. The server needs it to unseal a bot's
|
|
# subscribe credentials at connection time, and to seal the secret when
|
|
# an installation is created from the WeCom settings tab.
|
|
MULTICA_WECOM_SECRET_KEY: ${MULTICA_WECOM_SECRET_KEY:-}
|
|
# Comma-separated ranges the inbound media fetcher may dial even though
|
|
# they look reserved. Empty (the default) keeps the SSRF guard as strict
|
|
# as it ships; set it only behind a fake-IP proxy whose pool would
|
|
# otherwise get every attachment refused. See .env.example.
|
|
MULTICA_WECOM_MEDIA_ALLOW_CIDRS: ${MULTICA_WECOM_MEDIA_ALLOW_CIDRS:-}
|
|
# 1 = log every inbound and outbound WeCom frame, including the first
|
|
# 120 runes of each message body, so a real-device session can be
|
|
# checked against the server afterwards. Off unless set; turn it on only
|
|
# for a debugging session and unset it when that session ends, because
|
|
# what it records is user message content.
|
|
MULTICA_WECOM_TRACE: ${MULTICA_WECOM_TRACE:-}
|
|
restart: unless-stopped
|
|
|
|
frontend:
|
|
image: ${MULTICA_WEB_IMAGE:-ghcr.io/multica-ai/multica-web}:${MULTICA_IMAGE_TAG:-latest}
|
|
depends_on:
|
|
- backend
|
|
ports:
|
|
- "127.0.0.1:${FRONTEND_PORT:-3000}:3000"
|
|
environment:
|
|
HOSTNAME: "0.0.0.0"
|
|
REMOTE_API_URL: ${REMOTE_API_URL:-http://backend:8080}
|
|
DOCS_URL: ${DOCS_URL:-}
|
|
NEXT_PUBLIC_API_URL: ${NEXT_PUBLIC_API_URL:-}
|
|
NEXT_PUBLIC_WS_URL: ${NEXT_PUBLIC_WS_URL:-}
|
|
restart: unless-stopped
|
|
|
|
volumes:
|
|
pgdata:
|
|
backend_uploads:
|