Adds the Telepathy email communication layer as an OTP subtree: - Telepathy.JMAP: GenServer that discovers Fastmail JMAP session, periodically polls inbox for new emails, and provides send_email/3. Uses :httpc (no external HTTP dep). Deduplicates via seen_ids MapSet seeded from existing MessageStore entries. - Telepathy.MessageStore: JSONL-backed message persistence, compatible with the existing Python messages.jsonl format. Supports store, unread, mark_read, list, count operations. - Telepathy.Supervisor: rest_for_one supervisor (MessageStore then JMAP). - Conditionally enabled via config (:telepathy, :enabled). Disabled in test env. Can be disabled at runtime via TELEPATHY_ENABLED=false. Validated against live Fastmail: session discovery, inbox fetch (20 emails), message storage all working. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
89 lines
3.2 KiB
Elixir
89 lines
3.2 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
|
|
└── 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)
|
|
|
|
telepathy_children =
|
|
if telepathy_enabled do
|
|
[{Symbiont.Telepathy.Supervisor, poll_interval_ms: poll_ms}]
|
|
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
|