Two subcommands read the same session back. trace shows the shape of a run;
metrics shows what it cost. Both have --json, and both are read-only.
crab trace
crab trace <FILE> [--json] # span tree: turn → llm/tool calls
$ crab trace .crab/sessions/session-1792661000878881000.jsonl
run 0 · 63 spans
├── ✓ llm openai/deepseek-chat · 5663ms · ↑254 ↓257.7k (cr0 cw257.7k)
├── ✓ tool read · 4ms │ read {"path":"crates/crab-cli/src/metrics.rs"}
├── ✓ tool bash · 5392ms │ bash {"command":"cargo test -p crab-cli metrics"}
├── ✓ llm openai/deepseek-chat · 2896ms · ↑275 ↓258.8k (cr257.9k cw921)
├── ✓ tool edit · 12ms │ edit {"path":"crates/crab-cli/src/metrics.rs"}
└── ✓ llm openai/deepseek-chat · 3482ms · ↑323 ↓260.4k (cr259.0k cw1.3k)
Read the arrows as ↑ output and ↓ input, with cr /
cw for cache read and write. Two patterns are worth recognising:
- Cheap and fast: an unchanged
↓with a growingcr— the cached prefix is doing its job. - Expensive: a large
↓withcr0on every call — each turn is re-paying for the whole prefix.
A tool line shows the first ~90 characters of the arguments, so you can usually tell what
it did without opening the JSON. Use --json when you want all of it.
crab metrics
crab metrics [DAYS] [--json] # default window: 7 days
$ crab metrics 7
crab metrics · last 7 day(s)
BY DAY
2026-10-22 turns 2 ↑3.6k ↓309.2k cr308.6k cw622 cost $0.0064 tool-err 0% p50 5481ms p90 5481ms
2026-10-21 turns 41 ↑38.1k ↓4.2M cr4.1M cw96.2k cost $0.0917 tool-err 2% p50 2210ms p90 8940ms
BY MODEL
deepseek-chat turns 104 ↑92.1k ↓11.2M cost $0.2318
local-model turns 3 ↑2.1k ↓41.0k cost −
Columns: turns, output tokens, input tokens, cache read, cache write, cost, tool error rate, and
p50/p90 turn latency. The tool-err figure is the one to watch — a rising rate usually means
the agent is guessing at file paths or fighting a test suite.
A model crab has no price for shows −, not $0. That distinction matters when
you are comparing a local model against a hosted one.
The JSON forms
$ crab metrics --json
{
"days": 7,
"daily": [
{ "day_ms": 1792627200000, "turns": 2,
"prompt_tokens": 309230, "completion_tokens": 3646,
"cache_read": 308608, "cache_write": 622,
"cost_usd": 0.006413, "tool_calls": 7, "tool_errors": 0,
"priced_turns": 2 }
],
"models": [ { "model": "deepseek-chat", "turns": 2, "cost_usd": 0.006413 } ]
}
priced_turns is the honesty field: turns whose model had no entry in the price table are
counted for tokens but excluded from cost_usd. If priced_turns is less than
turns, your cost figure is a lower bound.
The ledger
.crab/metrics.sqlite, one row per run, written when the run ends. It is a telemetry sink:
it never enters the model context.
CREATE TABLE turns (
session_id TEXT NOT NULL,
run_id INTEGER NOT NULL,
ts INTEGER NOT NULL,
provider TEXT,
model TEXT,
prompt_tokens INTEGER,
completion_tokens INTEGER,
cache_read INTEGER,
cache_write INTEGER,
cost_usd REAL,
llm_calls INTEGER,
tool_calls INTEGER,
tool_errors INTEGER,
duration_ms INTEGER,
interrupted INTEGER,
PRIMARY KEY (session_id, run_id)
);
CREATE INDEX idx_turns_ts ON turns(ts);
A normal SQLite file. Query it directly for anything the two reports do not cover — cost per repository, cost by hour of day, how often you abort a run:
sqlite3 .crab/metrics.sqlite \
"select model, round(sum(cost_usd),4) as usd, sum(tool_errors)
from turns group by model order by usd desc;
Concurrent runs are fine. The primary key is
(session_id, run_id), so two crabs in two panes write separate rows. WAL mode means a read
while another run is writing will not block it.
What it does not tell you
- Why a turn was slow. You get
duration_msper span; queueing and network time live inside the provider call and are not separable from it. - Your cost if the price changed mid-window. Rows keep the cost computed at write time.
- Anything about a turn that crashed before
turn_end. The span tree shows the gap.
The design constraint behind all of this is one line from the project notes: observability data must not change the observed system. Measurement is a replay over a file, not an extra model call.