crab0.4.0
crab/docs/Sessions & compaction

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

kindWrittenPurpose
metaonce, first lineFormat version and creation time. A file with no meta row is treated as v1.
messageper conversation entryUser, assistant and tool content. Has id and parent_id.
obsper spanObservation: 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.

obsExtra fields
turn_start—
llm_callprovider, model, duration_ms, usage{prompt,completion,cache_read,cache_write}, outcome
tool_calltool, call_seq, args_digest, result_digest, bytes_in, bytes_out, duration_ms, outcome
turn_endstop_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:

  1. 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.
  2. 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.

esc