defmodule Symbiont.Application do @moduledoc """ OTP Application for Symbiont — the self-sustaining AI orchestrator. Supervision tree (dev/prod): Symbiont.Supervisor ├── Task.Supervisor (Symbiont.TaskSupervisor) ├── Symbiont.Ledger — append-only cost log ├── Symbiont.Queue — persistent task queue ├── Symbiont.Engram — cross-session memory (SQLite) ├── Symbiont.Heartbeat — periodic health checks + queue processing ├── Symbiont.Telepathy.Supervisor — email communication layer (if enabled) │ ├── Symbiont.Telepathy.MessageStore — JSONL-backed message persistence │ ├── Symbiont.Telepathy.JMAP — Fastmail JMAP client + polling │ └── Symbiont.Telepathy.SMTP — gen_smtp listener on port 25 └── Bandit (Symbiont.API) — HTTP API In test mode, the supervisor starts empty — tests manage their own processes. ## Graceful shutdown Bandit is configured with a long ThousandIsland `shutdown_timeout` so that in-flight `/task` requests (which can take up to 600s while Claude runs) are not killed when systemd restarts the service. The Bandit child spec also carries a matching `:shutdown` value so the OTP supervisor itself waits long enough before forcing a kill. The matching systemd unit must set `TimeoutStopSec=620` for this to take effect end to end. """ use Application # 615s for Bandit to drain its sockets, 620s for the OTP child shutdown # window. Keep these slightly larger than the Claude CLI timeout (600s) # used by Muse's SMTP dispatcher. @bandit_drain_ms 615_000 @bandit_child_shutdown_ms 620_000 @impl true def start(_type, _args) do if Application.get_env(:symbiont, :port) == 0 do # Test mode: start an empty supervisor, tests manage their own processes Supervisor.start_link([], strategy: :one_for_one, name: Symbiont.Supervisor) else start_full() end end defp start_full do data_dir = Application.get_env(:symbiont, :data_dir, "data") File.mkdir_p!(data_dir) port = Application.get_env(:symbiont, :port, 8111) engram_db = Application.get_env(:symbiont, :engram_db, "/data/symbiont/engram.db") bandit_spec = Supervisor.child_spec( {Bandit, plug: Symbiont.API, port: port, scheme: :http, thousand_island_options: [shutdown_timeout: @bandit_drain_ms]}, shutdown: @bandit_child_shutdown_ms ) telepathy_cfg = Application.get_env(:symbiont, :telepathy, []) telepathy_enabled = Keyword.get(telepathy_cfg, :enabled, true) poll_ms = Keyword.get(telepathy_cfg, :poll_interval_ms, 60_000) smtp_port = Keyword.get(telepathy_cfg, :smtp_port, 25) telepathy_children = if telepathy_enabled do [{Symbiont.Telepathy.Supervisor, poll_interval_ms: poll_ms, smtp_port: smtp_port}] else [] end children = [ {Task.Supervisor, name: Symbiont.TaskSupervisor}, {Symbiont.Ledger, data_dir: data_dir}, {Symbiont.Queue, data_dir: data_dir}, {Symbiont.Engram, db_path: engram_db}, {Symbiont.Heartbeat, []} ] ++ telepathy_children ++ [bandit_spec] opts = [strategy: :rest_for_one, name: Symbiont.Supervisor] Supervisor.start_link(children, opts) end end