Skip to content

Session Format

Sessions are stored as newline-delimited JSON (JSONL) files. Each line is a self-contained JSON object with a type discriminant field.

{data_dir}/sessions/{project_hash}/{session_id}.jsonl
Platformdata_dir
Linux~/.local/share/clido
macOS~/Library/Application Support/clido

project_hash is a lowercase hex string derived from the absolute project path (SHA-256 prefix). session_id is a UUID4 without hyphens.

Example:

~/.local/share/clido/sessions/a3f8b2c1d4e5678f/a1b2c3d4e5f6789abcdef0123456789a.jsonl

The current schema version is 1 (constant SCHEMA_VERSION in clido-storage).

Every line in the file has a "type" field. Lines appear in chronological order.

{
"type": "meta",
"session_id": "a1b2c3d4e5f6789abcdef0123456789a",
"schema_version": 1,
"start_time": "2026-03-21T14:30:00Z",
"project_path": "/home/user/projects/my-app"
}
FieldTypeDescription
session_idstringUUID4 (no hyphens)
schema_versionintegerAlways 1 for current sessions
start_timestringISO 8601 UTC
project_pathstringAbsolute path of the working directory
{
"type": "user_message",
"role": "user",
"content": [
{
"type": "text",
"text": "Refactor the parse() function to return Result<T, ParseError>"
}
]
}
FieldTypeDescription
rolestringAlways "user"
contentarrayContent blocks (see below)

assistant_message — A response from the LLM

Section titled “assistant_message — A response from the LLM”
{
"type": "assistant_message",
"content": [
{
"type": "text",
"text": "I'll refactor the parse() function now. Let me start by reading it."
},
{
"type": "tool_use",
"id": "toolu_01abc123",
"name": "Read",
"input": { "file_path": "src/parser.rs" }
}
]
}
FieldTypeDescription
contentarrayMix of text and tool_use content blocks

tool_call — A tool call extracted from an assistant message

Section titled “tool_call — A tool call extracted from an assistant message”
{
"type": "tool_call",
"tool_use_id": "toolu_01abc123",
"tool_name": "Read",
"input": { "file_path": "src/parser.rs" }
}

::: tip tool_call lines are written for each tool use in an assistant message. They are a convenience index — the same information is in the assistant_message content. Resume logic uses the assistant_message directly. :::

{
"type": "tool_result",
"tool_use_id": "toolu_01abc123",
"content": "fn parse(input: &str) -> Option<Ast> {\n // ...\n}",
"is_error": false,
"duration_ms": 8,
"path": "src/parser.rs",
"content_hash": "sha256:a1b2c3d4...",
"mtime_nanos": 1742560200000000000
}
FieldTypeRequiredDescription
tool_use_idstringYesMatches the tool_use.id in the assistant message
contentstringYesTool output text
is_errorbooleanYesWhether the tool returned an error
duration_msintegerNoTool execution time
pathstringNoFile path (for Read/Write/Edit tools; used for stale detection)
content_hashstringNosha256:<hex> of the file at read time
mtime_nanosintegerNoFile modification time at read time

path, content_hash, and mtime_nanos are set by file-reading tools (Read, Glob, Grep) for stale file detection on resume.

{
"type": "system",
"subtype": "compaction",
"message": "Context compacted: 42 turns → 1 summary (saved ~18,000 tokens)"
}
{
"type": "system",
"subtype": "error",
"message": "Provider error: rate limit exceeded, retrying in 5s"
}
FieldTypeDescription
subtypestring"compaction", "error", "warning", "info"
messagestring (optional)Human-readable description

result — Last line of a completed session

Section titled “result — Last line of a completed session”
{
"type": "result",
"exit_status": "success",
"total_cost_usd": 0.0034,
"num_turns": 4,
"duration_ms": 8312
}
FieldTypeDescription
exit_statusstring"success", "max_turns", "max_budget", "error", "interrupted"
total_cost_usdfloatAccumulated cost for the session
num_turnsintegerNumber of completed turns
duration_msintegerTotal wall time from start to end

Content blocks appear inside user_message.content and assistant_message.content arrays.

{ "type": "text", "text": "The function has 42 lines." }
{
"type": "tool_use",
"id": "toolu_01abc123",
"name": "Bash",
"input": { "command": "cargo check" }
}

tool_result (user messages only — for feeding results back to the LLM)

Section titled “tool_result (user messages only — for feeding results back to the LLM)”
{
"type": "tool_result",
"tool_use_id": "toolu_01abc123",
"content": " Compiling my-app v0.1.0\n Finished dev target in 1.2s",
"is_error": false
}
{"type":"meta","session_id":"a1b2c3d4e5f6789abcdef0123456789a","schema_version":1,"start_time":"2026-03-21T14:30:00Z","project_path":"/home/user/projects/my-app"}
{"type":"user_message","role":"user","content":[{"type":"text","text":"How many lines is src/main.rs?"}]}
{"type":"assistant_message","content":[{"type":"tool_use","id":"toolu_01abc","name":"Read","input":{"file_path":"src/main.rs"}}]}
{"type":"tool_call","tool_use_id":"toolu_01abc","tool_name":"Read","input":{"file_path":"src/main.rs"}}
{"type":"tool_result","tool_use_id":"toolu_01abc","content":"fn main() {\n...","is_error":false,"duration_ms":5,"path":"src/main.rs","content_hash":"sha256:deadbeef...","mtime_nanos":1742560200000000000}
{"type":"assistant_message","content":[{"type":"text","text":"src/main.rs has 312 lines."}]}
{"type":"result","exit_status":"success","total_cost_usd":0.0009,"num_turns":1,"duration_ms":2100}
use clido_storage::{SessionReader, SessionLine};
let lines: Vec<SessionLine> = SessionReader::open(&session_path)?
.collect::<anyhow::Result<Vec<_>>>()?;
for line in &lines {
match line {
SessionLine::UserMessage { content, .. } => { /* ... */ }
SessionLine::AssistantMessage { content } => { /* ... */ }
SessionLine::ToolResult { tool_name, content, .. } => { /* ... */ }
SessionLine::Result { total_cost_usd, .. } => { /* ... */ }
_ => {}
}
}

Or with shell tools:

Terminal window
# Extract all user messages
jq -r 'select(.type == "user_message") | .content[] | select(.type == "text") | .text' session.jsonl
# Total cost across all sessions
jq -r 'select(.type == "result") | .total_cost_usd' ~/.local/share/clido/sessions/*/*.jsonl | paste -sd+ | bc