A session is an append-only JSONL file with three record kinds. It is meant to be read by
humans, queried with jq, and committed to git when a change needs explaining.
Where they live
<project>/.crab/sessions/session-<epoch-millis>.jsonl
One file per session, named by creation time. crab --continue resumes the most recent one
in the current project. Interrupted runs are still resumable: text produced and tool results completed
before a Ctrl+C abort are persisted.
The three kinds
kind | Written | Purpose |
|---|---|---|
meta | once, first line | Format version and creation time. A file with no meta row is treated as v1. |
message | per conversation entry | User, assistant and tool content. Has id and parent_id. |
obs | per span | Observation: timing, tokens, digests, outcome. Never part of model-visible history. |
The split is the design. Messages are what the model sees. Observations are what happened to you. Keeping them in one file means a trace and a transcript can never disagree — and because observations are excluded from the request, measuring costs nothing in tokens.
It is a tree on disk, linear in v0
Every message carries parent_id, so branching is expressible. It is worth being precise
about the current state: v0.4.0 implements linear append plus --continue. The
branch, fork and clone UIs are future work. The fields exist now because adding them later would mean
migrating every historical file, and branching UI is a bottomless pit that the acceptance criterion did
not need.
{"kind":"meta","format_version":2,"created":"1792661000878ms"}
{"kind":"message","id":1,"parent_id":null,"message":{"role":"user","content":[
{"type":"text","text":"add a --json flag to crab metrics"}]}}
{"kind":"message","id":3,"parent_id":1,"message":{"role":"assistant","content":[
{"type":"text","text":"Let me read the renderer."},
{"type":"tool_use","id":"call_00_a1b2","name":"read",
"input":{"path":"crates/crab-cli/src/metrics.rs"}}]}}
{"kind":"message","id":4,"parent_id":3,"message":{"role":"user","content":[
{"type":"tool_result","tool_use_id":"call_00_a1b2","content":"..."}]}}
Content blocks are text, tool_use and tool_result. A tool
result refers back to the call by tool_use_id, so a response can request several tools and
the results still line up. The format must be able to express an interrupted assistant message
with a matching tool_result — otherwise --continue would replay an unpaired
tool_use and most providers would answer 400.
Observation records
Four observation types, each carrying run_id, ts and an obs
tag. One run_id is one user message, i.e. one trace.
obs | Extra fields |
|---|---|
turn_start | — |
llm_call | provider, model, duration_ms, usage{prompt,completion,cache_read,cache_write}, outcome |
tool_call | tool, call_seq, args_digest, result_digest, bytes_in, bytes_out, duration_ms, outcome |
turn_end | stop_reason, interrupted, llm_calls, tool_calls |
outcome is Ok, Aborted, or Error(text) truncated to
128 characters — full detail lives in the message log.
Digests, not payloads. A tool observation stores truncated argument and result digests rather than duplicating content already present in the message stream. That keeps the log small and makes tampering detectable, but it means the log is not a place to recover a file's contents — use git for that.
Continuing and reading
crab --continue # resume the latest session in this project
crab # start a new one
crab trace <file> # any session's span tree
crab trace <file> --json # ... as structured JSON
At --continue, crab replays the log to seed exact context occupancy and the token
counters, so the status line is correct from the first prompt rather than being re-estimated.
Compaction
When the last request's prompt crosses auto_compact_threshold (default 0.8)
of the context window — or when you type /compact — crab condenses before the next request,
in two stages:
- Shed — deterministic. Old tool results become short placeholders; long text blocks are truncated. The model can re-run a tool to see the content again.
- Summarize — only if shedding was not enough. Older turns are replaced by an LLM-written summary.
Both are recorded as events with their strategy, so a trace shows how a conversation was condensed, not just that it was.
The session log is never rewritten. Compaction is an event; the model's
view is rebuilt by replaying it. That is why --continue sees exactly what the model saw, and
why you can still read the original messages afterwards.
Set auto_compact_threshold = 0 in ~/.crab/config.toml to disable the
automatic trigger and compact by hand instead.
Should I commit them?
Session files are text, so they diff and review like code. They also contain your prompts and the
agent's reasoning, which is often exactly what a reviewer wants. Two caveats: they grow, and they may
contain snippets of files you would not commit. A .gitignore entry for
.crab/sessions/ is the safe default; commit individual files when a change needs an audit
trail.
Note that .crab/metrics.sqlite is per working directory and written best-effort. Losing
it loses history, not capability.