Persistent session store backed by SQLite.
One GenServer per session, registered globally so any node in the cluster
can append messages or query history via transparent GenServer calls.
Each session writes to <dir>/<id>_<name>.db.
Both id and name appear in the filename so either can be resolved with
a single directory glob — see find_by_id/2 and find_by_name/2.
Usage
{:ok, _pid} = Planck.Agent.Session.start("a1b2c3d4", name: "crazy-mango", dir: "/path/to/sessions")
:ok = Planck.Agent.Session.append("my-session", "agent-1", message)
{:ok, rows} = Planck.Agent.Session.messages("my-session")
{:ok, rows} = Planck.Agent.Session.messages("my-session", agent_id: "agent-1")Each row is %{db_id: pos_integer(), agent_id: String.t(), message: Message.t(), inserted_at: integer()}.
db_id is the SQLite autoincrement row id — use it with truncate_after/2 to
anchor a truncation to a specific message.
Messages are serialized with :erlang.term_to_binary/1 and read back with
:erlang.binary_to_term/2 (:safe — no new atoms created from DB content).
start/2 requires an explicit :dir option — the sessions directory is
resolved by the caller (typically Planck.Headless from its config).
Distribution
Sessions are registered via :global as {:session, session_id}. Any node
in the Erlang cluster can call append/3 or messages/2 — the call is routed
transparently to the node that owns the session's SQLite file.
Pagination
Messages with role {:custom, :summary} are stored as checkpoints
(checkpoint = 1 in the DB). Two functions support cursor-based pagination
anchored on these checkpoints:
messages_from_latest_checkpoint/2— initial load: latest checkpoint + everything after. Returns{:ok, rows, checkpoint_id | nil}.messages_before_checkpoint/3— load more: the previous chapter. Returns{:ok, rows, prev_checkpoint_id | nil}.nilmeans no more history.
Pass the returned checkpoint_id integer back as the cursor for the next page.
Summary
Functions
Append a message and return its DB row id. Returns nil if the session is
not found (agent has no persistent session).
Resolve a session file by id. Globs <sessions_dir>/<id>_*.db.
Resolve a session file by name. Globs <sessions_dir>/*_<name>.db.
Return all metadata for a session as a %{String.t() => String.t() | nil} map.
Retrieve messages for a session in insertion order.
Return the chapter before a given checkpoint: the previous summary checkpoint
and all messages between it and checkpoint_id.
Return the latest summary checkpoint and all messages after it.
Write key-value metadata for a session. Merges with any existing entries; existing keys are overwritten. Values are stored as strings.
Start a session under the SessionSupervisor.
Stop a running session.
Delete all messages with a DB row id >= db_id, across all agents in the session.
Resolve a session id to its pid via :global.
Types
@type row() :: %{ db_id: pos_integer(), agent_id: String.t(), message: Planck.Agent.Message.t(), inserted_at: integer() }
A row returned by messages/2 and related query functions.
@type session_id() :: String.t()
Functions
@spec append(session_id(), String.t(), Planck.Agent.Message.t()) :: pos_integer() | nil
Append a message and return its DB row id. Returns nil if the session is
not found (agent has no persistent session).
Resolve a session file by id. Globs <sessions_dir>/<id>_*.db.
Returns {:ok, path, name} or {:error, :not_found}.
Resolve a session file by name. Globs <sessions_dir>/*_<name>.db.
Returns {:ok, path, session_id} or {:error, :not_found}.
@spec get_metadata(session_id()) :: {:ok, %{optional(String.t()) => String.t() | nil}} | {:error, :not_found}
Return all metadata for a session as a %{String.t() => String.t() | nil} map.
@spec messages( session_id(), keyword() ) :: {:ok, [row()]} | {:error, :not_found}
Retrieve messages for a session in insertion order.
Options:
agent_id:— filter to messages from a specific agent
@spec messages_before_checkpoint(session_id(), non_neg_integer(), keyword()) :: {:ok, [row()], non_neg_integer() | nil} | {:error, :not_found}
Return the chapter before a given checkpoint: the previous summary checkpoint
and all messages between it and checkpoint_id.
Returns {:ok, rows, prev_checkpoint_id | nil}. When prev_checkpoint_id is
nil there is no further history to load.
Options:
agent_id:— filter to a specific agent
@spec messages_from_latest_checkpoint( session_id(), keyword() ) :: {:ok, [row()], non_neg_integer() | nil} | {:error, :not_found}
Return the latest summary checkpoint and all messages after it.
If no checkpoint exists, returns all messages from the beginning.
The checkpoint_id in the return tuple is the DB row id of the checkpoint —
pass it to messages_before_checkpoint/3 to load the previous page.
Options:
agent_id:— filter to a specific agent
@spec save_metadata(session_id(), map()) :: :ok | {:error, :not_found}
Write key-value metadata for a session. Merges with any existing entries; existing keys are overwritten. Values are stored as strings.
@spec start( session_id(), keyword() ) :: {:ok, pid()} | {:error, term()}
Start a session under the SessionSupervisor.
@spec stop(session_id()) :: :ok | {:error, :not_found | term()}
Stop a running session.
@spec truncate_after(session_id(), pos_integer()) :: :ok | {:error, :not_found}
Delete all messages with a DB row id >= db_id, across all agents in the session.
Used when editing a previous message: truncates the session to strictly before the given row, then the caller re-prompts with new text.
@spec whereis(session_id()) :: {:ok, pid()} | {:error, :not_found}
Resolve a session id to its pid via :global.