Describes the current session and persistence model: what is stored, how restore works, and where the key boundaries between runtime and storage live.
Sessions make the app durable across launches without coupling the runtime directly to SQLite.
The current design splits that work across two layers:
app/session.rsowns the bridge between runtime messages and stored messagesstorage/session/owns SQLite schema and CRUD
AppContext uses those pieces to inspect the single most recently updated saved session at startup, restore it only when its stored project_root matches the current runtime project root, and save conversation state after completed submit, approve, and reject requests.
ActiveSession in src/app/session.rs owns:
- the active
SessionStore - the current session ID
- conversion between runtime
Messagevalues and stored message rows
This is the only place that sees both runtime message types and storage message types.
SessionStore in src/storage/session/store.rs owns the SQLite database handle and the basic session CRUD operations.
The storage layer defines:
SessionIdSessionMetaStoredMessageSavedSession
Storage uses plain strings for roles so it stays decoupled from the runtime’s typed Role enum.
The session database lives at data/sessions.db.
Current schema:
sessionsidproject_rootcreated_atupdated_atmsg_count
session_messagessession_idseqrolecontent
Messages are stored in order by (session_id, seq).
SessionStore::save() replaces the stored message set for a session instead of appending deltas.
The runtime provides the full in-memory conversation to ActiveSession::save(), but ActiveSession filters it before writing.
Stored:
- user messages
- assistant messages
- runtime-injected tool result and tool error blocks, because they are stored as user messages
Not stored:
- system messages
- TUI-only status/system lines
- pending approval state
The system prompt is intentionally not persisted. It is rebuilt from current config and tool specs on each startup.
At startup:
app::run()opens the session DBActiveSession::open_or_restore()asksSessionStorefor the single most recently updated session overall- if that session's stored
project_rootexactly matches the current canonical project root, stored messages are converted back into runtime messages - if that single most recent session has a missing or different
project_root, restore does not continue scanning older sessions; a new empty session is created instead - if no prior session exists, a new empty session is created
AppContext::build()loads the restored history into the runtime after creating a fresh system prompt
Restore is intentionally narrower than storage.
Only the most recent 10 stored messages are injected back into the runtime.
Older messages stay in SQLite, but they are not reloaded into the live model context.
Restore also strips runtime-generated tool exchanges:
- user messages starting with
=== tool_result:,=== tool_error:, or[runtime:correction]are dropped - if one of those was immediately preceded by a pure assistant tool call that starts with
[, that assistant message is dropped too
This keeps raw file contents, directory listings, and other tool outputs out of restored context while preserving the full transcript on disk.
Restored history is loaded into the runtime, but it is not replayed into AppState.
That means:
- the model has restored context after startup
- the visible TUI transcript still starts from the fresh welcome message
AppContext::handle() auto-saves after:
RuntimeRequest::SubmitRuntimeRequest::ApproveRuntimeRequest::Reject
This means normal prompt turns and approval decisions are persisted after the runtime finishes handling them. Pending approvals are still in-memory only until the user approves or rejects.
/clear eventually calls AppContext::reset().
That does two things:
- resets the runtime conversation back to the system prompt
- creates a brand new session ID in storage
The old session remains in SQLite; reset does not delete prior sessions.
Session IDs are generated as 16-character lowercase hex strings.
Sessions are considered for restore by updated_at descending, and the app only resumes the most recently updated saved session when its stored project_root exactly matches the current canonical project root.
The docs intentionally treat those timestamp fields as opaque stored ordering values rather than promising a specific unit.
Messages within a session are stored and loaded in ascending seq order.
- Only the single most recently updated session is considered for automatic restore, and it is restored only when its stored
project_rootmatches the current runtime project root. - Pending approvals are not persisted.
- Restore uses a fixed message window rather than token-aware budgeting.
- The full stored transcript can be larger than the context reloaded into the runtime.
- The TUI does not yet show restored transcript history on startup.