symbiont_ex/lib/symbiont/application.ex

91 lines
3.3 KiB
Elixir

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