File-based session persistence for the web UI.
Sessions are stored as Erlang term binaries under ~/.ex_athena/web/sessions/. A compact JSON index at ~/.ex_athena/web/recent.json tracks the most recently opened working directories so users can jump back to a project quickly.
Summary
Functions
Durably append a completed run's final assistant message to its session.
Delete a session file.
The chat message text for a finished run, surfacing the finish deliverable
so the submitted answer is always visible in the main chat.
List all session headers, newest first.
List session headers for a specific working directory, newest first.
Return the list of recently opened working directories, newest first. Each entry: %{cwd: path, name: basename, opened_at: DateTime.t()}
Load a full session by id.
Build a run's final assistant message from the Result alone (no live UI
state) and durably append it to the session. The durable path for when the
process that owns the run wants the answer saved regardless of whether a
LiveView is currently attached. Idempotent by msg_id (see
append_run_result/4).
Remove a working directory from the recent list.
Persist a session. Overwrites if the id already exists.
Record a working directory as recently opened (deduplicates, caps at 20).
Functions
Durably append a completed run's final assistant message to its session.
Used by the run task so the answer survives even when the LiveView process
that started the run has died (e.g. a websocket reconnect mid-run sends the
final {:athena_done, result} to a now-dead pid). Load-modify-save, and
idempotent by msg_id — a no-op if the LiveView already persisted that
message, so the LiveView's richer (tool-event-bearing) version is preserved.
Delete a session file.
@spec final_message_text(String.t(), ExAthena.Result.t()) :: String.t()
The chat message text for a finished run, surfacing the finish deliverable
so the submitted answer is always visible in the main chat.
When the run ended via finish (finish_reason: :submitted) with a
deliverable: it becomes the message text if nothing was streamed (the
orchestrator delegated and only submitted a deliverable), or is appended to
the streamed text otherwise — unless that text already contains it. Any other
termination keeps the streamed text unchanged.
@spec list() :: [map()]
List all session headers, newest first.
List session headers for a specific working directory, newest first.
@spec list_recent() :: [map()]
Return the list of recently opened working directories, newest first. Each entry: %{cwd: path, name: basename, opened_at: DateTime.t()}
Load a full session by id.
@spec persist_run_result(String.t(), String.t(), ExAthena.Result.t()) :: :ok
Build a run's final assistant message from the Result alone (no live UI
state) and durably append it to the session. The durable path for when the
process that owns the run wants the answer saved regardless of whether a
LiveView is currently attached. Idempotent by msg_id (see
append_run_result/4).
@spec remove_recent(String.t()) :: :ok
Remove a working directory from the recent list.
@spec save(map()) :: :ok
Persist a session. Overwrites if the id already exists.
@spec touch_recent(String.t()) :: :ok
Record a working directory as recently opened (deduplicates, caps at 20).