Sessions
A session is rho's unit of persistent conversation state. It replaces the older flat Conversation type with a tree-structured model where every turn, tool call, and compaction is an entry in an append-only log.
Why sessions?
The original Conversation type stored messages in a Vec<ChatMessage>. When the context window filled up, the sliding window evicted old messages — including, in some cases, the user's original request. The session model fixes this by:
- Tree structure — branching, not deletion.
/clearand compaction create new branches; the old tree is preserved. - Typed entries — user messages, assistant messages, tool calls, tool results, compaction summaries, and extension entries are all first-class nodes.
- Resolution levels — entries render at a resolution (
Full,Outlined,Summarized,Pinned,Compacted, orAttached) chosen by the context builder. Resolutions are not stored on entries: each cursor holds its own stamps in a sparse overlay, so two branches can resolve the same entry differently. - Persistence — sessions auto-flush to JSONL files, surviving process restarts and crashes.
Shared log, many cursors
A session is an append-only log of Entry nodes, each identified by an EntryId (a UUID). Every entry has a parent_id pointing to its predecessor, forming a single chain (the trunk) with possible branches.
[System] ──→ [User: "fix the bug"] ──→ [Assistant: "ok"] ──→ [User: "read another file"]
│
└──→ [Compaction: "user asked to fix the bug"]
The log is shared by every branch in the file. A cursor is a named position in that log — the branch a given line of work follows — and its leaf is the entry the agent loop is currently building from. fork() creates a new cursor at the current leaf; branch_to(id) moves a cursor's leaf to any existing entry, creating a divergence point. Old branches stay in the log for later inspection or resumption, and each cursor carries its own resolution stamps (see below).
Since 0.93.0 the type is Cursor — Session remains a type alias — wrapping a SessionLog shared behind an Arc<Mutex<…>>: cheap to clone and Sync, so it can be held from more than one place while the agent loop keeps its exclusive cursor.
Persistence
Sessions are stored as append-only JSONL files under ~/.rho/sessions/:
~/.rho/sessions/
└── <project-hash>/
└── <unix-timestamp>_<session-id>.jsonl
- Project hash: first 16 hex characters of SHA-256 of the canonical project root path. This groups sessions by project.
- Auto-flush: every append writes to disk immediately. A crash loses at most the last entry.
- Format: each line is a JSON object with a
typediscriminator —Header,Entry,Resolution, orCursor. The first line is always aHeaderwith session metadata.Resolutionlines record per-cursor resolution changes (last-write-wins per(cursor, entry));Cursorlines record each cursor's leaf and optional branch name (the last line per cursor id wins), and are how a reopened session rebuilds the full branch roster.
Example JSONL file:
{"type":"Header","id":"4b021d5d","version":1,"created_at_secs":1777859536,"cwd":"/home/user/project","parent_session":null}
{"type":"Entry","id":"a1b2c3d4","parent_id":null,"timestamp":{...},"payload":{"Message":{"content":"you are a coding assistant","role":"system"}}}
{"type":"Entry","id":"e5f6a7b8","parent_id":"a1b2c3d4","timestamp":{...},"payload":{"Message":{"content":"fix the bug","role":"user"}}}
{"type":"Cursor","id":"c0","leaf":"e5f6a7b8"}
Session discovery
rho can scan saved sessions without loading the full entry tree. This enables session listing and quick-resume features.
find_latest_session(cwd)
Returns the path to the most recently modified JSONL file for the given project directory. Used by rho -c to auto-resume. Reads only the header line — does not parse entry lines.
list_sessions(cwd)
Returns all saved sessions for the project, sorted by filesystem modification time (most recent first). Each result includes:
| Field | Description |
|---|---|
id | Session ID (8-char hex) |
created_at | Creation timestamp from the JSONL header |
cwd | Working directory the session was started in |
entry_count | Number of entries (lines minus header) |
mtime | Filesystem modification time |
path | Full path to the JSONL file |
The listSessions RPC method returns every saved session for the project — most recent first, as {path, mtimeSecs, sizeKb, entryCount}. No session is marked as "latest": rho -c simply resumes whichever was modified last.
Resuming a session
Use rho -c to resume the most recent session for the current project:
rho -c
Or resume a specific session with --session <path>:
rho --session ~/.rho/sessions/abcdef1234567890/1777859536_4b021d5d.jsonl
The -c, --session, and --ephemeral flags are mutually exclusive (enforced at parse time by clap).
The resumed session picks up where it left off. The model, token budget, redactor, and tool set are updated from the current configuration so a session started with one model can be continued with another.
Ephemeral mode
Use --ephemeral to run without any disk persistence:
rho --ephemeral
All conversation state lives only in memory and is lost when rho exits. Useful for one-shot commands, CI pipelines, or when you don't want session files accumulating.
Startup hint
When rho creates a new persisted session, it checks for previous sessions for the same project. If any exist, it prints a hint:
session: /home/user/.rho/sessions/abcdef12/1777859536_4b021d5d.jsonl
(3 previous session(s) for this project — use rho -c to resume the latest)
Context stats
The ContextStats struct provides a rich snapshot of context window usage, available through Session::context_stats():
#![allow(unused)] fn main() { pub struct ContextStats { // Basic stats pub context_window: usize, pub completion_reserve: usize, pub estimated_used: usize, pub message_count: usize, pub entry_count: usize, pub path_entry_count: usize, // Token distribution pub role_tokens: RoleTokenDistribution, // system, user, assistant, tool pub resolution_tokens: ResolutionTokenDistribution, // full, outlined, summarized, pinned pub phase_tokens: PhaseTokenDistribution, // exploration, execution, verification, conclusion, unclassified // Compaction tracking pub compaction_tokens: usize, pub compacted_entry_count: usize, } }
Key methods:
utilization_percent()— context utilization as 0–100% (based on prompt budget)estimated_remaining()— remaining tokens in the prompt budget (saturates at 0)prompt_budget()— context window minus completion reserve
A consumer can display a compact one-line status bar after every turn:
[████████████░░░░░░░░] 12.3k/32k tokens (50%) │ 12.3k remaining │ 10 messages
Color-coded green (<60%), yellow (60–80%), red (>80%). The getSessionStats RPC method shows a detailed multi-line breakdown including token distribution by role, resolution level, compaction stats, system prompt overhead, tool schema overhead, and conversation token breakdown.
Entry types
Each entry in the tree has a payload that describes what it represents:
| Payload | Description |
|---|---|
Message | A chat message (system, user, assistant, or tool result) |
Compaction | A phase-aware summary replacing older entries |
BranchSummary | A summary created when branching |
CustomMessage | Extension message (sent to model as synthetic user message) |
Custom | Extension state (never sent to model) |
Label | Branch checkpoint label |
LeafMoved | Record of a leaf movement |
ModelChange | Record of a model switch |
SessionInfo | Session metadata |
SessionEnded | Clean shutdown trailer |
Resolution levels
Each entry renders at one of these resolution levels. The level is not stored on the entry — entries are immutable data. Instead, each cursor holds a sparse overlay of stamps, persisted as Resolution JSONL lines and applied last-write-wins per (cursor, entry). Cursor::set_resolution() changes one, and pin_entry() / unpin_entry() set the Pinned level:
Full— complete, verbatim content. Used for recent entries that fit within the context window.Outlined— structural summary: tool name, key arguments, truncated output. Consumes ~10-20% of original tokens.Summarized— prose summary: a one-line description of what the entry represents. Consumes ~5-10% of original tokens.Pinned— protected from eviction and downgrade. Rendered at its native resolution.Compacted— replaced by aCompactionsummary. The original content is still in the tree but the context builder renders the summary instead.Attached— lightweight reference. Rendered as a brief mention rather than full content.
Entries are downgraded through Full → Outlined → Summarized by the selective eviction planner before turn-level eviction is needed. Pinned entries are protected from all downgrade and eviction.
The context builder (Context Management) uses resolution levels to decide what to send to the model — full detail where it matters, summaries where it doesn't.
Bounded tool results
Tool results can be very large (entire file contents, command output). Sessions enforce a bounded output size:
- Tool output exceeding a fraction of the prompt budget is truncated.
- The full output is preserved in
ToolResultDetails::FullOutputfor later retrieval. - Truncation splits on the last newline boundary to avoid breaking UTF-8 sequences.
This prevents a single verbose tool call from consuming the entire context window.
The /clear command
The clear RPC method branches back to the system message entry. This has the same practical effect as clearing the conversation, but the old tree is preserved on disk. You can resume the old branch later with rho -c or --session.
Cursor API
The Cursor type in rho-core (aliased as Session) exposes:
- Constructors:
Cursor::new()(persisted),Cursor::in_memory()(no disk I/O),Cursor::open(path)(resume from JSONL) - Appenders:
append_user_message(),append_assistant_message(),append_tool_result(),append_tool_call() - Navigation:
path_to_root(),children(),leaf(),branch_to(),branch_with_summary() - Branches:
fork()(new cursor at the current leaf),cursors()(the full roster),cursor_id()(this cursor's id),name_cursor()(label a branch) - Resolutions:
set_resolution(),pin_entry(),unpin_entry()(per-cursor overlay; persisted asResolutionlines) - Context:
path_messages()(messages along the current branch),send_current()(send to model with context fitting),context_stats()(usage snapshot),prepare_context()(selective downgrade) - Compaction:
compact_older_than()(compact entries older than a threshold) - Discovery:
find_latest_session(cwd),list_sessions(cwd)(header-only session scanning) - Extensions:
write_custom_state(),read_custom_state(),write_custom_message(),read_custom_message() - Persistence:
save_path(),flush()