# `Planck.Agent.Session`
[🔗](https://github.com/alexdesousa/planck/blob/v0.1.0/lib/planck/agent/session.ex#L1)

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}`. `nil` means no more history.

Pass the returned `checkpoint_id` integer back as the cursor for the next page.

# `row`

```elixir
@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.

# `session_id`

```elixir
@type session_id() :: String.t()
```

# `append`

```elixir
@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).

# `find_by_id`

```elixir
@spec find_by_id(Path.t(), String.t()) ::
  {:ok, Path.t(), String.t()} | {:error, :not_found}
```

Resolve a session file by id. Globs `<sessions_dir>/<id>_*.db`.

Returns `{:ok, path, name}` or `{:error, :not_found}`.

# `find_by_name`

```elixir
@spec find_by_name(Path.t(), String.t()) ::
  {:ok, Path.t(), String.t()} | {:error, :not_found}
```

Resolve a session file by name. Globs `<sessions_dir>/*_<name>.db`.

Returns `{:ok, path, session_id}` or `{:error, :not_found}`.

# `get_metadata`

```elixir
@spec get_metadata(session_id()) ::
  {:ok, %{optional(String.t()) =&gt; String.t() | nil}} | {:error, :not_found}
```

Return all metadata for a session as a `%{String.t() => String.t() | nil}` map.

# `messages`

```elixir
@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

# `messages_before_checkpoint`

```elixir
@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

# `messages_from_latest_checkpoint`

```elixir
@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

# `save_metadata`

```elixir
@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.

# `start`

```elixir
@spec start(
  session_id(),
  keyword()
) :: {:ok, pid()} | {:error, term()}
```

Start a session under the SessionSupervisor.

# `stop`

```elixir
@spec stop(session_id()) :: :ok | {:error, :not_found | term()}
```

Stop a running session.

# `truncate_after`

```elixir
@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.

# `whereis`

```elixir
@spec whereis(session_id()) :: {:ok, pid()} | {:error, :not_found}
```

Resolve a session id to its pid via `:global`.

---

*Consult [api-reference.md](api-reference.md) for complete listing*
