Devin
← all harnesses · parser: crates/core/src/session/devin.rs
The only harness here that keeps its conversation in two places: a SQLite database the CLI is working from, and a transcript it writes beside it for other tools to read. Neither half is complete, so most of what is on this page is about which of the two a given field comes out of — and about the one number Devin records nowhere, which is the price.
Where it lives
~/.local/share/devin/cli/ — the platform data directory plus devin/cli
(config::DEVIN_CLI_DIR) — holding sessions.db and transcripts/<id>.json
beside it.
$CHISEL_SESSION_DB overrides it, and it is the one override that names a
file rather than a directory: cctop takes the parent of the database it
points at as the CLI directory, because the transcript has to sit beside the
database it belongs to (see db_for). Pointing it at a database somewhere else
therefore moves the transcripts with it, which is the only way that override can
be right.
Every other home’s copy is found by convention, through devin_cli_dirs(). Only
this user’s own database honours the variable; another user’s is always
~/…/devin/cli/sessions.db, since an override cannot speak for a home it is not
in.
The configuration is somewhere else entirely. ~/.config/devin
(DEVIN_CONFIG_DIR) holds AGENTS.md, config.json, mcp_config.json and
skills, none of which is next to the sessions. access.rs spent a while
looking in the data directory for a config.toml that has never existed there,
so the two are separate constants on purpose and the Access panel reads the
config one.
The format
The database, opened read-only (SQLITE_OPEN_READ_ONLY | NO_MUTEX):
| Table | What cctop takes |
|---|---|
sessions | id, working_directory, model, created_at, last_activity_at, title, hidden, and agent_mode for the permission column |
message_nodes | chat_message (a JSON blob per node: role, tool_calls) and metadata (num_tokens_preceding), newest by row_id |
tool_call_state | tool_call_json and tool_call_update_json per call: the arguments, and the status each update reported |
The transcript is one ATIF-v1.7 JSON document per session — not JSONL, and
not a database. schema_version, session_id, agent (with model_name and
the tool definitions, which are the toolset and not its usage), then steps:
each with source (system, user or agent), timestamp, model_name,
metrics, and tool_calls. Totals live at the end under final_metrics.
The traps
The two stores answer different questions. The transcript is the artefact
other tools are meant to read; the database is the live state. Which is why the
tool outcomes are only in the second: a step records that a call was made,
and tool_call_state records how it ended. tool_statuses joins them on
tool_call_id, and a call with no row in it counts as not failed rather than
as unknown — a still-running call is not a failure, and guessing either way would
put a wrong number in ERR%.
The transcript is one document, so nothing can tail it. Every other JSONL
harness here answers “what is it doing” by reading the last 64 KB; a single
pretty-printed JSON file has no records to walk, and a seek into its middle
lands in the middle of a string. So live_state, extract_last_tool and
extract_context all read the database instead, and session::live_state hands
Devin over before the tail walk starts. The conversation reader reads the whole
document for the same reason, through its own read_devin rather than
extract::for_each_jsonl.
One model, two spellings, in one file. Most steps record the slug the API
was called with (swe-2-high); a few carry the display name the database knows
(SWE-2 High). Bucketing on the raw string splits one model into two rows in
the Cost panel, so canonical_model folds to letters and digits lowercased and
the slug is the spelling shown — it is what the pricing tables key on.
agent steps only. system and user steps carry no metrics and no tool
calls; counting from them would double the tokens against final_metrics.
created_at is seconds, not milliseconds. Every other harness stamps
milliseconds, so ms_to_rfc3339(created_at * 1000) is the whole conversion. Read
as-is, every Devin session is dated 1970.
ATIF keeps no structured patch. An edit or write diff lives in the call’s
arguments, so the report’s Changes view has to be built from
extract::edit_delta rather than read off the step.
Cost
There is none. Neither the transcript nor the database records a dollar
figure, so list_sessions sets cost_available = false and total_cost = None, and the $, $/1H and $/24H columns read ─. Tokens are a different
question and are recorded: Provider::records_token_usage is true for Devin,
which is why doctor’s unpriced-model check skips it — a Devin $0.00 is a
blank, not a price.
--plan max counts Devin as bundled (Plan::includes), alongside Claude. On
screen that changes nothing: incl stands for a figure cctop computed and then
withheld, and cost_available = false is tested before it, so a Devin row reads
─ under every plan.
Context
message_nodes.metadata.num_tokens_preceding on the newest node is what the
next request would carry in. The ceiling is LiteLLM’s max_input_tokens for
the model, or nothing — the same qualification as OpenCode’s, and a model
LiteLLM has never heard of shows a blank CTX%. compacted is always false:
ATIF records no compaction, so the CTX% chart has no markers.
Tokens
final_metrics is the session’s ledger — total_prompt_tokens,
total_completion_tokens, total_cached_tokens. The per-step metrics are
what fill the hourly and daily buckets, so a session whose steps carry no
timestamp contributes to the total and to no window, rather than to an invented
one.
What the format does not settle: whether total_cached_tokens is part of
total_prompt_tokens or additional to it. cctop keeps them in separate buckets
and prices them separately, on the reading that the cached part of a prompt is
billed cheaper rather than twice. A real transcript is the only thing that can
settle it.
Liveness
devin acp is the agent backend, spawned per session by the CLI and by editor
integrations; proc::could_be_agent recognises the binary, and provider_of
requires the acp subcommand so that devin auth, devin mcp and the rest
never become a phantom session row. devin --resume <id> puts the id on the
command line, and Session::resume_argv is that command — so this is one of the
harnesses with a real process and real CPU and memory columns.
agent_mode on the session row answers the permission column, translated into
the four buckets: normal and auto are ask, accept-edits is edits,
plan is plan, and dangerous/yolo/bypass/autonomous are BYPASS.
smart — edits auto-approved plus a fast model running clearly-safe actions —
has no honest bucket, so it reports nothing rather than picking one.
cctop installs no hooks into Devin: it is not among hook::HARNESSES, and
Devin’s own hooks.v1.json is a shape cctop hook does not speak. Everything
on a Devin row is forensic, read out of what the CLI left behind.
What else it can reach
The conversation reader works (serve/chat.rs::read_devin), because ATIF keeps a
call and its result together on one step under observation.results, keyed by
source_call_id. Two Devin-specific details in it: system steps are mostly
prompt assembly — the sysprompt and rules telemetry sources, plus
<available_skills> and <system_info> blocks re-injected each turn — and only
a tagged block reporting an event is shown as a turn; and step_id is a
number, not a string, so two consecutive steps stay two replies.
delete is the one destructive path that writes to the harness’s own store. It
sets hidden = 1 and removes the transcript, rather than deleting the row: the
live CLI holds the database open, and hiding is what the CLI itself does.