Supervises many Omni.Session processes and provides id-based
lifecycle management.
The Manager is an app-level Supervisor. Apps define their own module
that uses it — following the Ecto.Repo convention — naming the
host application via otp_app: and dropping the module into a
supervision tree:
defmodule MyApp.Sessions do
use Omni.Session.Manager, otp_app: :my_app
end
# config/config.exs
config :my_app, MyApp.Sessions,
store: {Omni.Session.Store.FileSystem, base_path: "priv/sessions", otp_app: :my_app}
# application.ex
children = [MyApp.Sessions]Supervisor start-opts override app-env values when both are set, for cases where a value must be computed at boot:
children = [
{MyApp.Sessions, store: dynamic_store()}
]Call sites go through the use-generated shorthand:
{:ok, pid} = MyApp.Sessions.create(agent: [model: {:anthropic, "claude-sonnet-4-5"}])
{:ok, pid, :started} = MyApp.Sessions.open("abc-123")
:ok = MyApp.Sessions.close("abc-123")
{:ok, sessions} = MyApp.Sessions.list(limit: 50)What the Manager supervises
MyApp.Sessions (Supervisor, :rest_for_one)
├── MyApp.Sessions.Registry (Registry, keys: :unique)
├── MyApp.Sessions.DynamicSupervisor (DynamicSupervisor)
└── MyApp.Sessions.Tracker (GenServer)Sessions live under the DynamicSupervisor with restart: :temporary —
on crash they do not auto-restart. The Registry maps session ids to
pids; on close/2 or crash, entries are removed automatically. The
Tracker observes every running session and powers list_open/1
plus the Manager-level subscribe/1 feed.
Running sessions outlive the caller that created them. The caller is
auto-subscribed as a :controller by default, so idle-shutdown kicks
in once the caller drops off (see :idle_shutdown_after).
Cross-session view
list_open/1 returns a snapshot of all running sessions; each
entry is %{id, title, status, pid}. subscribe/1 atomically returns
the same snapshot and starts streaming live events to the caller:
{:manager, MyApp.Sessions, :opened, %{id, title, status, pid}}
{:manager, MyApp.Sessions, :status, %{id, status}}
{:manager, MyApp.Sessions, :title, %{id, title}}
{:manager, MyApp.Sessions, :closed, %{id}}The second element is the Manager module — what the caller already holds — so subscribers watching multiple Managers route events by pattern-matching.
Configuration
Options resolve in this order (highest priority first): supervisor
start-opts → Application.get_env(otp_app, ManagerModule, []) → the
defaults below.
:store— required.{module, keyword()}— the store adapter tuple. Used by every session this Manager starts, and bylist/2anddelete/2.:idle_shutdown_after—nil | non_neg_integer(). Default for sessions this Manager starts; overridden per-call. Defaults to300_000(5 minutes) when absent. Passnilto disable Manager-wide.:name— overrides the registered name. Defaults to theuse-ing module.
open/3 return shape
open/3 tells you whether the Manager actually started the session or
attached to an already-running one:
{:ok, pid, :started} # Manager started the process; opts applied
{:ok, pid, :existing} # process was already up; opts silently droppedOn the :existing branch, start-time opts (:agent, :title,
:idle_shutdown_after, :subscribers) are dropped because mutating a
live session's configuration safely requires knowing its agent status.
Callers who genuinely need fresh config use close/2 + open/3.
:subscribe is honored in both branches — it is a subscription, not a
state mutation.
Summary
Functions
Stops a running session. Idempotent — returns :ok if the session is
not running. The store is untouched.
Starts a fresh session under this Manager.
Stops the session if running, then deletes it from the store.
Lists sessions from the Manager's store.
Returns the list of sessions currently running under this Manager.
Returns a pid for the session with the given id.
Starts the Manager supervisor and its children.
Subscribes the caller to Manager-level session events.
Unsubscribes the caller from Manager-level events.
Registry lookup — returns the session pid for id, or nil.
Types
@type entry() :: %{ id: id(), title: String.t() | nil, status: :idle | :busy | :paused, pid: pid() }
Per-session entry returned by list_open/1 and subscribe/1.
@type id() :: Omni.Session.Store.session_id()
@type manager() :: module()
Functions
Stops a running session. Idempotent — returns :ok if the session is
not running. The store is untouched.
@spec create( manager(), keyword() ) :: {:ok, pid()} | {:error, :already_exists} | {:error, {:invalid_opt, atom()}} | {:error, term()}
Starts a fresh session under this Manager.
Options:
:id— explicit session id (binary). Auto-generated when omitted.:subscribe— boolean, defaulttrue. Auto-subscribes the caller as:controller.:agent,:title,:subscribers,:idle_shutdown_after— passed through toOmni.Session.start_link/1.
Rejects Manager-owned opts (:store, :name, :new, :load) with
{:error, {:invalid_opt, key}}. Returns {:error, :already_exists}
when an explicit :id collides with a running session or one in the
store.
Stops the session if running, then deletes it from the store.
Propagates the underlying Omni.Session.Store.delete/3 error.
@spec list( manager(), keyword() ) :: {:ok, [Omni.Session.Store.session_info()]}
Lists sessions from the Manager's store.
Pass-through to Omni.Session.Store.list/2. Honours adapter-level opts
like :limit and :offset.
Returns the list of sessions currently running under this Manager.
Each entry is a %{id, title, status, pid} map. Ordering is
unspecified — callers sort client-side.
Complements list/2 (store-backed, may include sessions that are not
running). The two are commonly composed to render an "all sessions
with running indicator" view.
@spec open(manager(), id(), keyword()) :: {:ok, pid(), :started | :existing} | {:error, :not_found} | {:error, {:invalid_opt, atom()}} | {:error, term()}
Returns a pid for the session with the given id.
The trailing atom tells you what happened:
{:ok, pid, :started}— session wasn't running; Manager loaded it from the store, and start-time opts (:agent,:title,:idle_shutdown_after,:subscribers) were applied.{:ok, pid, :existing}— session was already running. Start-time opts are silently dropped (:subscribestill applies).
Returns {:error, :not_found} when no session with the id exists in
the store.
The caller is auto-subscribed as :controller by default. Opt out
with subscribe: false.
@spec start_link(keyword()) :: Supervisor.on_start()
Starts the Manager supervisor and its children.
Required opt: :name (when called directly without use, pass the
module name to register under).
Subscribes the caller to Manager-level session events.
Returns an atomic snapshot of currently-running sessions. After the call returns, the caller receives messages of shape:
{:manager, manager_module, :opened, %{id, title, status, pid}}
{:manager, manager_module, :status, %{id, status}}
{:manager, manager_module, :title, %{id, title}}
{:manager, manager_module, :closed, %{id}}The second element is the Manager module — the same atom the caller passed in — so a subscriber watching multiple Managers can route events by pattern-matching.
Idempotent per pid: subscribing a second time returns a fresh snapshot without registering duplicate delivery.
@spec unsubscribe(manager()) :: :ok
Unsubscribes the caller from Manager-level events.
Registry lookup — returns the session pid for id, or nil.