Output¶
The analyzer answers two questions about an input, and each has its own
command and output. detect says which agents left state in it, for
which users, and whether a parser exists for each, on stdout; nothing is
written. timeline reads every transcript it can parse and writes three
files into -o: timeline.jsonl, one record per turn, tool call, tool
result or system event; sessions.jsonl, one record per session; and
inventory.json, what was detected and parsed. The JSONL records are the
interface for everything downstream, so their fields, order and types are
fixed and documented in full here, and a record always has every field.
The example collection¶
The examples of detect, detect --json,
timeline, inventory.json,
timeline.jsonl and sessions.jsonl are
the analyzer's output on a synthetic collection. No collector gathered
it from a host: analyzer/tools/example_collection.py writes it, its
collection.json and manifest.jsonl in the shape
collectors/docs/output.md documents,
and every file's contents are invented. The record shapes are those of
the analyzer's test fixtures and the research documents' samples, built
from real installs (Claude Code 2.1.310 on Linux and macOS, Codex CLI
0.162.1 on Linux) and the agents' source, so the parsers read these
files as they read an install's. The collection is of
wsdev14, a Linux workstation, as collector 1.19.0 would record it in
live mode, with the users alice and bob, who both work on the
repository /srv/proj, a Go billing service. Everything happens on
2026-10-27, a working day, on a host whose clock runs on UTC.
- alice's terminal session in
/srv/proj(Claude Code 2.1.310): she pastes a CI failure and asks for the fix, and the model runsGrep,Read,EditandBashcalls, each atool_userow joined to itstool_resultrow ontool_use_id. Approvals of single commands, a denial of agit pushwith alice's reason, and an allow-always grant arepermission decision:rows beside their calls. The session's permission mode, from a state record that carries no time, is apermission mode:row with no timestamp, her/modelcommand and its output aresystemrows, and the next reply, from the other model, gives amodel: <old> -> <new>row; its scratchpad is ascratch directory:row (see Row order and system rows). She then turns on Remote Control with/remote-controland, later, sends a prompt from her phone: the bridge records giveremote control: bridge sessionandsession synced: remote control bridgerows, the queued prompt aprompt queued:and aprompt trigger:row, and the session's registry file under.claude/sessions/, whose process is still running at collection time, aclaude-code processrow and one moresession synced:row. Insessions.jsonlit readssyncedyesandoriginlocal. - alice's Remote Control session, spawned by the Remote Control server
for a prompt she sent from claude.ai: its records give
session origin: remote (Remote Control, entrypoint sdk-cli), aprompt trigger:row for that prompt, and apermission decision:row for aBashcall a settings rule allowed; itsccr-tip.jsongives asession synced: remote eventrow, and the project'sbridge-pointer.json, left by the server, aremote control: environmentrow that belongs to no session. Insessions.jsonlit readssyncedyesandoriginremote. - alice's teleported session, a cloud session started with
claude --cloudand brought to this host withclaude --teleport: the turns replayed from the cloud givesession origin: remote (teleport, remoteSourced)andsession synced: cloud session (teleported: ...), the cloud machine's scratchpad asession origin: remote (teleport, cloud scratchpad <path>)row, and the prompt alice then typed on this host is auserrow, with the local scratchpad as ascratch directory:row, and its registry file under.claude/sessions/aclaude-code processrow. The transcript's last line was cut mid-write, as a copy taken while the session runs can catch it: it is aparser: unparseable linerow with an emptytimestamp_utc, so it sorts after every timed row, and every row before it is kept. - alice's prompt history and settings:
.claude/history.jsonlrepeats the prompts the transcripts hold, the pasted CI failure among them (the file keeps a placeholder and the paste cache its text, which the parser puts back so it matches the transcript's copy), which are dropped as duplicates and counted on thedropped:line (see Secondary sources); its other lines, a prompt of a session whose transcript she deleted and one in a repository,/srv/invoicing-old, that she removed with its transcripts, are kept withrecoveredyes, each a session of its own..claude/settings.jsongivespermission setting:rows with an emptyproject_pathand amodel setting:row;.claude.jsongivespermission setting:rows for/srv/proj(its trust dialog and an allowed tool) and for the removed/srv/invoicing-old, whose entry it keeps, and theremote control: spawn mode,remote control: machine id set,cloud: hasRemoteEnvironment=true(left by the--cloudstart) andsource host: github status=connectedrows. - bob's Codex CLI 0.162.1 thread in
/srv/proj: its rollout gives asession start:row, agit remote:row with the repository's URL, amodel:row and apermission mode: approval=... sandbox=...row, then anexec_commandcall whose command fails, itstool_resultstarting[exit <code>], and anapply_patchcall..codex/history.jsonlrepeats the typed prompt,state_5.sqlitegives athread record:row, and.codex/config.tomlgivespermission setting:rows at the file's modification time. - bob's Gemini CLI and Ollama files: the Gemini CLI settings hold
nothing the parser turns into a row, so
gemini-cliis underparser found no rows, and a Gemini CLI chat the manifest lists as collected is missing fromfs/, which gives aproblem:line. The Ollama prompt history gives auserrow per prompt and aslash command: /byerow, with an emptytimestamp_utcbecause the file records no times. - alice's Amp settings: Amp has no parser, so it is under
detected but not parsed. - The files of
/srv/proj: its git metadata (.git/HEAD,.git/config, which names the remotesoriginandupstream,FETCH_HEAD,packed-refs,info/excludeand the refs) andAGENTS.mdare shared files;.claude/settings.local.json, written by the allow-always grant, gives apermission setting:row withproject_path/srv/proj;.aider.input.historygives an Aider prompt row, markedrecoveredbecause the chat history it mirrors is not in the collection; and a Cursor rules file gives no row, socursoris underparser found no rows. - A manifest row is cut short, which gives the
manifest rows did not parsenote.
To write the collection into an empty directory, here example, from
the repository root:
mkdir example
python3 analyzer/tools/example_collection.py example
The collection is example/wsdev14/, with collection.json,
manifest.jsonl, collector.log and fs/, the input for detect and
timeline. The
examples show its path as /cases/wsdev14/wsdev14 and the timeline
output directory as out.
detect¶
detect prints the input, its kind (archive, collected for an extracted
collection, or loose), the host, any notes, and up to two tables: one
line per user, home and agent outside any project directory, and, when the
input holds project directories, one line per agent of each project. Both
give the number of files, their bytes (from the manifest size, or from
disk in loose mode), and whether a parser exists. The notes cover the
archive hash check, unreadable collection.json, manifest rows that do not
parse, and how a loose root was interpreted. One problem: line follows the
notes for each directory or file inside the input that could not be read
(see Unreadable inputs). no agent artifacts
found is printed, with exit code 1, only when both tables are empty.
--files adds one user<TAB>agent<TAB>path line per attributed file.
Collections made by collectors before 1.6.0 also hold a live/ system
snapshot, which appears as agent live with no user, and detect only.
The first table has the columns user, home (cut to 32 characters),
agent, files, bytes and parser (yes or detect only), in the
order each user, home and agent's first file was met. Its homes are user
homes and Docker volumes, never a project directory.
Files the collector gathered from a project directory (a repository that
agent state referenced, or one given with -p) are listed in the second
table under the agent that owns them: a repository's .claude/ under
claude-code, its .aider.chat.history.md under aider. Collectors
before 1.12.0 recorded every project file with agent project; for such a
collection the analyzer names the agent itself, from the project catalog
in its bundled catalog copy (the # project artifacts section of
doubleagent catalog): the most specific entry whose glob matches the
file's path relative to the project directory, so an old and a new
collection of the same host list the same agents. Nothing is printed about
the re-attribution.
Some project files belong to no single agent, because several agents read
the same file. These are the shared files: the collector records them
with agent project, and the tables count them on the project's
(shared) line. They are the project lines of the project catalog, and
research/shared.md
and the per-agent research documents cite each reader:
AGENTS.md, the repository instruction file read by Codex CLI, Copilot CLI, Amp, OpenCode, Crush, Factory Droid, Kilo Code, Zed and pi, and its variantsAGENTS.override.md(Codex CLI, pi, Hermes) andAGENT.md(Amp, Zed)..agents/, the cross-tool skills directory that Codex CLI, Copilot CLI, Amp and Muse Code read..mcp.json, the project's list of MCP (Model Context Protocol) servers, read by Claude Code, Copilot CLI and Muse Code..github/copilot-instructions.md,.github/git-commit-instructions.mdand.github/instructions/, the GitHub Copilot instruction files, read by the Copilot editor extensions, Copilot CLI, Crush and Zed.SOUL.mdandUSER.md, the persona and user-profile files of an OpenClaw or nanobot workspace; Hermes readsSOUL.mdtoo.- The repository's git metadata, from collector 1.19.0:
.git/HEAD,.git/config,.git/FETCH_HEAD,.git/packed-refs,.git/refs/,.git/logs/,.git/worktrees/and.git/info/exclude, the checked-out branch, the remotes, the branches, the reflog and the local ignore rules, which say where the code came from and where it may have been pushed rather than which agent worked there. Git metadata incollectors/docs/coverage.mdsays what each file holds and what under.git/is not collected.
A project file no project catalog entry matches is counted as shared as
well. project is not an agent, so it never appears as an agent in
either table, and a shared file cannot say which agent used it: a
repository holding only AGENTS.md shows that some agent was set up
there, not which one. The per-agent lines are the evidence of a
particular agent.
The project directories are the homes the analyzer marks as projects: the
paths in collection.json projects and, for a collection from a
collector before 1.12.0, the home of each collected row with agent
project. A loose input holds none, so the second table is not printed
there, nor for a collection without project files. The table follows the
first after a blank line, sorted by project path.
This is the output of detect on
the example collection. The cut manifest row
gives the note: line and bob's missing Gemini CLI chat the problem:
line. The first table holds alice's and bob's homes, with amp marked
detect only; the second holds /srv/proj, with a line for each agent
that left files there, a (shared) line for its AGENTS.md and .git/
files, and the remotes: line its .git/config gives:
input: /cases/wsdev14/wsdev14
kind: collected
host: wsdev14
note: 1 manifest rows did not parse
problem: /home/bob/.gemini/tmp/6b9af143446a411aefa3edb76e4693799bd7513e7ed8d919962d6713d3d3b972/chats/session-2026-10-27T09-41-2407af53.json: missing from the collection
user home agent files bytes parser
alice /home/alice claude-code 13 48222 yes
alice /home/alice amp 1 177 detect only
bob /home/bob gemini-cli 1 190 yes
bob /home/bob codex-cli 4 22690 yes
bob /home/bob ollama 1 116 yes
project users agent files bytes parser
/srv/proj alice aider 1 72 yes
claude-code 1 69 yes
cursor 1 372 yes
(shared) 10 1564
remotes: origin git@github.com:acme--ledger/ledger.git, upstream https://git.example.com/acme-billing/ledger.git
Each project has one line per agent, sorted by agent, then a (shared)
line when it holds shared files, with an empty parser column. The path
(cut to 40 characters) and users (the users whose manifest rows name the
directory, sorted and joined by ,) are printed on the project's first
line only. A project whose .git/config names remotes gets one more
line after its last (since 0.21.0): remotes: followed, per remote
sorted by name and joined by , , by the name and its url values
separated by spaces, then (push <pushurl> ...) when the remote sets
push URLs, as in remotes: origin git@git.example.com:alice/proj.git https://mirror.example.com/alice/proj.git (push https://git.example.com/alice/proj.git), upstream https://git.example.com/upstream/proj.git. In the example above it is the
last line, naming the origin and upstream remotes of /srv/proj.
detect --json¶
With --json, detect prints the same data as one JSON object,
indented by two spaces, and exits 0 even when nothing was found. This
example is the output of detect --json --files on
the example collection, the data of the
detect example with each project's remotes and head and,
since --files was given, the files key, one entry per attributed file:
{
"input": "/cases/wsdev14/wsdev14",
"kind": "collected",
"host": "wsdev14",
"notes": [
"1 manifest rows did not parse"
],
"homes": [
{
"user": "alice",
"home": "/home/alice",
"inferred": false,
"agents": [
{
"agent": "amp",
"files": 1,
"bytes": 177,
"parser": false
},
{
"agent": "claude-code",
"files": 13,
"bytes": 48222,
"parser": true
}
]
},
{
"user": "bob",
"home": "/home/bob",
"inferred": false,
"agents": [
{
"agent": "codex-cli",
"files": 4,
"bytes": 22690,
"parser": true
},
{
"agent": "gemini-cli",
"files": 1,
"bytes": 190,
"parser": true
},
{
"agent": "ollama",
"files": 1,
"bytes": 116,
"parser": true
}
]
}
],
"projects": [
{
"path": "/srv/proj",
"users": [
"alice"
],
"agents": [
{
"agent": "aider",
"files": 1,
"bytes": 72,
"parser": true
},
{
"agent": "claude-code",
"files": 1,
"bytes": 69,
"parser": true
},
{
"agent": "cursor",
"files": 1,
"bytes": 372,
"parser": true
}
],
"shared_files": 10,
"shared_bytes": 1564,
"remotes": [
{
"name": "origin",
"urls": [
"git@github.com:acme--ledger/ledger.git"
],
"pushurls": []
},
{
"name": "upstream",
"urls": [
"https://git.example.com/acme-billing/ledger.git"
],
"pushurls": []
}
],
"head": "main"
}
],
"problems": [
"/home/bob/.gemini/tmp/6b9af143446a411aefa3edb76e4693799bd7513e7ed8d919962d6713d3d3b972/chats/session-2026-10-27T09-41-2407af53.json: missing from the collection"
],
"files": [
{
"user": "alice",
"agent": "claude-code",
"path": "/home/alice/.claude/projects/-srv-proj/d4ccb1ca-a86b-4f26-9501-752de29ee24d.jsonl"
},
{
"user": "alice",
"agent": "claude-code",
"path": "/home/alice/.claude/projects/-srv-proj/e9022006-4f4a-5a45-a27b-6623a05d1ab4.jsonl"
},
{
"user": "alice",
"agent": "claude-code",
"path": "/home/alice/.claude/projects/-srv-proj/e9022006-4f4a-5a45-a27b-6623a05d1ab4/ccr-tip.json"
},
{
"user": "alice",
"agent": "claude-code",
"path": "/home/alice/.claude/projects/-srv-proj/99b95ab5-5229-41e2-bc47-dcba1398c74b.jsonl"
},
{
"user": "alice",
"agent": "claude-code",
"path": "/home/alice/.claude/projects/-srv-proj/bridge-pointer.json"
},
{
"user": "alice",
"agent": "claude-code",
"path": "/home/alice/.claude/history.jsonl"
},
{
"user": "alice",
"agent": "claude-code",
"path": "/home/alice/.claude/paste-cache/71ae889d1f1f55db.txt"
},
{
"user": "alice",
"agent": "claude-code",
"path": "/home/alice/.claude/sessions/48213.json"
},
{
"user": "alice",
"agent": "claude-code",
"path": "/home/alice/.claude/sessions/52904.json"
},
{
"user": "alice",
"agent": "claude-code",
"path": "/home/alice/.claude/sessions/48213.06ca70864509508792d04db08549a779154e3cb857f55101ac73f19455893e8a.key"
},
{
"user": "alice",
"agent": "claude-code",
"path": "/home/alice/.claude/sessions/52904.5f5e741bce555807b9efc334311290e2816512d548e151828eec498604de7beb.key"
},
{
"user": "alice",
"agent": "claude-code",
"path": "/home/alice/.claude/settings.json"
},
{
"user": "alice",
"agent": "claude-code",
"path": "/home/alice/.claude.json"
},
{
"user": "alice",
"agent": "amp",
"path": "/home/alice/.config/amp/settings.json"
},
{
"user": "bob",
"agent": "gemini-cli",
"path": "/home/bob/.gemini/settings.json"
},
{
"user": "bob",
"agent": "codex-cli",
"path": "/home/bob/.codex/sessions/2026/10/27/rollout-2026-10-27T14-02-11-01a17dab-2784-70e1-8cd3-70e4412754b8.jsonl"
},
{
"user": "bob",
"agent": "codex-cli",
"path": "/home/bob/.codex/history.jsonl"
},
{
"user": "bob",
"agent": "codex-cli",
"path": "/home/bob/.codex/state_5.sqlite"
},
{
"user": "bob",
"agent": "codex-cli",
"path": "/home/bob/.codex/config.toml"
},
{
"user": "bob",
"agent": "ollama",
"path": "/home/bob/.ollama/history"
},
{
"user": "alice",
"agent": "claude-code",
"path": "/srv/proj/.claude/settings.local.json"
},
{
"user": "alice",
"agent": "project",
"path": "/srv/proj/AGENTS.md"
},
{
"user": "alice",
"agent": "project",
"path": "/srv/proj/.git/HEAD"
},
{
"user": "alice",
"agent": "project",
"path": "/srv/proj/.git/config"
},
{
"user": "alice",
"agent": "project",
"path": "/srv/proj/.git/FETCH_HEAD"
},
{
"user": "alice",
"agent": "project",
"path": "/srv/proj/.git/packed-refs"
},
{
"user": "alice",
"agent": "project",
"path": "/srv/proj/.git/refs/heads/fix/invoice-rounding"
},
{
"user": "alice",
"agent": "project",
"path": "/srv/proj/.git/refs/heads/main"
},
{
"user": "alice",
"agent": "project",
"path": "/srv/proj/.git/refs/remotes/origin/HEAD"
},
{
"user": "alice",
"agent": "project",
"path": "/srv/proj/.git/refs/remotes/origin/main"
},
{
"user": "alice",
"agent": "project",
"path": "/srv/proj/.git/info/exclude"
},
{
"user": "alice",
"agent": "cursor",
"path": "/srv/proj/.cursor/rules/style.mdc"
},
{
"user": "alice",
"agent": "aider",
"path": "/srv/proj/.aider.input.history"
}
]
}
inferred is true only in loose mode, where projects is always
[], so no single run shows both. The remotes and head of
/srv/proj come from its collected .git/config and .git/HEAD. Bob's
missing Gemini CLI chat is left out of his gemini-cli counts and named
under problems. homes holds the counts of the first table of
detect grouped by home, with each home's agents sorted by name, where
the table lists them in the order their first file was met: here
alice's amp comes before claude-code whatever the manifest's order.
Its keys, in order:
| Key | Type | Meaning |
|---|---|---|
input |
string | The input path as given on the command line, as on the input: line. |
kind |
string | archive (an archive the analyzer extracted), collected (an extracted collection: manifest.jsonl and fs/) or loose (any other directory, or an archive that did not unpack to a collection). |
host |
string | --host when given, else collection.json hostname, else "". |
notes |
array of strings | The note: lines without their prefix, in the order they were made: the archive sha256 check (archive sha256 verified against <name>, or a line starting WARNING), collection.json unreadable: <reason>, <n> manifest rows did not parse, fs/ found without manifest.jsonl; treating fs/ as the root, root is a <agent> directory; treating its parent as the home. [] when there are none. |
homes |
array of objects | The first table, grouped by home: one per user home and Docker volume, in the order its first collected row was met; project directories are left out (they are projects). |
homes[].user |
string | The home's owner: the manifest user, or in loose mode the user inferred from the path; --user when neither names one; "" when unknown. |
homes[].home |
string | The manifest home, the path on the source host: a user's home or a Docker volume's _data, never a project directory. In loose mode the home's path relative to the input root with a leading /, or "" when the input root is the home or an agent directory; the text table prints the home's path on the analyst's disk there instead. |
homes[].inferred |
boolean | true in loose mode, where user and home come from the path; false when they come from a manifest. |
homes[].agents |
array of objects | One per agent with at least one file in this home, sorted by agent; [] when every file of the home is missing from the collection. Never holds project. |
homes[].agents[].agent |
string | Catalog agent name. |
homes[].agents[].files |
integer | Files attributed to this agent in this home: collected manifest rows of type file whose file is present under fs/, or files under a matched catalog path in loose mode. |
homes[].agents[].bytes |
integer | Their total size: the sum of the manifest size values, or of the sizes on disk in loose mode. |
homes[].agents[].parser |
boolean | true when the analyzer has a parser for this agent, false when it is detected only. |
projects |
array of objects | The second table: one per project directory the input holds (see detect for which they are), sorted by path. [] when there is none, always in loose mode. |
projects[].path |
string | The project directory as the manifest's home records it, the collector's path; in image mode it still starts with the -r root. |
projects[].users |
array of strings | The distinct users whose rows name the directory, sorted. |
projects[].agents |
array of objects | One per agent that owns at least one file in the directory, sorted by agent. Never holds project; [] when the directory holds only shared files, such as a repository with an AGENTS.md and nothing else. |
projects[].agents[].agent |
string | Catalog agent name. |
projects[].agents[].files |
integer | Files of this agent in the directory, counted as for homes[].agents[].files. |
projects[].agents[].bytes |
integer | Their total manifest size. |
projects[].agents[].parser |
boolean | true when the analyzer has a parser for this agent, false when it is detected only. |
projects[].shared_files |
integer | The shared files in the directory: files several agents read, which the manifest records with agent project rather than an owner (AGENTS.md, .agents/, .mcp.json, .github/copilot-instructions.md, SOUL.md, the .git/ metadata; see detect for the full list and their readers). 0 when there is none. |
projects[].shared_bytes |
integer | The total manifest size of those shared files. |
projects[].remotes |
array of objects | The remotes of the directory's .git/config, one per [remote "<name>"] section that sets a non-empty url or pushurl, sorted by name; [] when the collection holds no .git/config there (a collector before 1.19.0, a directory that is not a repository, or a linked worktree or submodule whose .git is a file). The file is read whole, as git writes it: lines ending at a line feed or a carriage return and line feed, an initial byte-order mark ignored, section and key names in any case, quoted values, # and ; comments, a pair on the same line as its section header, a value continued by a backslash at the end of its line (a backslash that ends a comment continues nothing); a line that is neither a section header nor a key = value pair is skipped, as are the keys after an unterminated section header. A value git rejects, a quoted one the file ends inside or one holding an escape git does not define (only \\, \", \n, \t and \b are escapes), is left out and listed under problems as <path>: line <n>: unterminated quoted value or <path>: line <n>: unsupported escape \q, so a config cut mid-URL or edited by hand never yields a destination git would not read. The deprecated [remote.name] header form lower-cases its name, as git does, so it names the same remote as [remote "name"]. A .git/config or .git/HEAD the analyzer cannot read (not a regular file, a permission or I/O error) leaves remotes [] and head "" and is listed under problems as <path>: <reason>. Since 0.21.0. |
projects[].remotes[].name |
string | The remote's name, as the section header spells it (origin, upstream). |
projects[].remotes[].urls |
array of strings | Every url value of the section, in file order; [] when it sets none. Git fetches from the first and, when the section sets no pushurl, pushes to every one. A URL may carry credentials in its user part (https://<user>:<token>@<host>/...): redact them before sharing the output. |
projects[].remotes[].pushurls |
array of strings | Every pushurl value of the section, in file order; [] when it sets none. Git pushes to every one, so a second value is a second destination of each push. These are the values the file records, not git's fallback to urls, and they may carry credentials as urls can. |
projects[].head |
string | What the directory's .git/HEAD names: the checked-out branch (main for ref: refs/heads/main), the full ref for any other symbolic ref, or the commit hash of a detached HEAD; "" when the collection holds no .git/HEAD there. Last key of the object since 0.21.0. |
problems |
array of strings | The problem: lines without their prefix, <path>: <reason>: each directory or file inside the input that could not be read while it was opened, and each collected manifest row whose file is missing from fs/ (see Unreadable inputs). [] when there are none. |
files |
array of objects | Only with --files: one per attributed file, in the order the manifest lists them (the walk order in loose mode). It mirrors the manifest file by file, so a shared file such as AGENTS.md appears here with agent project. |
files[].user |
string | As homes[].user. |
files[].agent |
string | The attributed agent: the manifest agent, re-attributed from the bundled project catalog for a project file of a collection before 1.12.0; project for a shared file (AGENTS.md, .mcp.json). |
files[].path |
string | The file's path on the source host, as the manifest records it. |
Before analyzer 0.12.0 the per-home counts were a flat array agents,
one object per user, home and agent with the keys user, home,
agent, files, bytes, parser and inferred, which also held the
project directories as rows whose home was the directory; homes had
no agents key and listed the project directories too.
timeline¶
timeline writes three files into -o, creating the directory if needed,
overwriting files of the same name, and writes them even when no rows
were produced:
| File | What it holds | Schema |
|---|---|---|
timeline.jsonl |
One record per turn, tool call, tool result or system event, sorted by time. | timeline.jsonl |
sessions.jsonl |
One record per session, with its first and last timestamp, models and counts. | sessions.jsonl |
inventory.json |
What was detected and what was parsed: the input's kind, notes and problems; per home the files, bytes and whether a parser exists for each agent; row and session counts per project and outside any project, whether each project directory was collected and the files each agent left in it; and the lists of agents parsed and not parsed. | inventory.json |
On stdout it prints the input, host and notes, one problem: line per
directory or file that could not be read (see
Unreadable inputs), and, when the timeline
is filtered, a window: line with the resolved UTC bounds, a match:
line with the patterns, and a filtered: line counting the rows dropped
outside the window, without a timestamp, and not matching. Then comes
the dropped: line, always printed, which counts the rows of secondary
sources left out because a primary store, or another secondary source,
records the same event (see
Secondary sources), with the agents that had any
in parentheses, sorted by name, and no parentheses when the count is 0:
dropped: 3 duplicate rows from secondary sources (antigravity 2, claude-code 1)
dropped: 0 duplicate rows from secondary sources
Then come the agents: line, the row count with a breakdown by agent and
turn_type, and the session count. The breakdown counts the rows written. The
last three line groups name the three files, one each, in this order:
rows: 214 in out/timeline.jsonl
claude-code assistant 40
claude-code user 50
sessions: 6 in out/sessions.jsonl
inventory: out/inventory.json (homes, projects, scratch directories, counts per agent, problems)
rows: is followed by one indented line per agent and turn_type with
its count, sorted by agent and type, and nothing when there are no rows.
When at least one record of sessions.jsonl has synced yes,
relayed yes, origin remote or recovered yes or partial,
the sessions: line ends with those counts in parentheses after the
path, in that order, each part present only when its count is greater
than 0 and the parts joined by a comma:
sessions: 6 in out/sessions.jsonl (1 synced),
sessions: 6 in out/sessions.jsonl (2 relayed) or
sessions: 6 in out/sessions.jsonl (1 synced, 2 relayed, 1 remote, 3 recovered).
With none it ends at the path, as above. The synced part is there
since analyzer 0.14.0, the relayed, remote and recovered parts
since 0.15.0.
The paths are -o joined with the file name, as given on the command
line. The exit code is 0 when at least one row was written and 1
otherwise; the files are written either way. Before analyzer 0.12.0
timeline also wrote detect.json, named on a detect: line before the
inventory: line; its content is now in inventory.json (see
inventory.json).
The agents: line is always one line. When the input holds at least one
agent it lists them in two to four groups; otherwise it reads
none were found:
agents: parsed: antigravity, claude-code; detected but not parsed: amp
agents: parsed: claude-code; detected but not parsed: none
agents: parsed: none; detected but not parsed: amp
agents: parsed: claude-code; detected but not parsed: none; parser found no rows: antigravity
agents: parsed: claude-code; detected but not parsed: amp; collected only: copilot
agents: none were found
parsed names the agents that produced rows before filtering. detected but
not parsed names the agents in the input that have no parser and whose files
hold records a parser is wanted for, the backlog rows of the agents
page (the agents detect shows without a parser, in
either table, so an agent whose only files are in a project directory counts).
collected only names the agents in the input that have no parser because
their files hold nothing to parse, a token store or configuration only: the
collected rows of that table. The group follows detected but not parsed
and appears only when it has an entry. Before analyzer 0.14.0 those agents
were listed under detected but not parsed. parser found no rows names the
agents in the input that have a parser which read their files and produced no
rows, such as a data directory holding only an empty database; the group
appears only when it has an entry, and with --agent it counts only the
agents selected. Shared files (agent project, see detect), which
every parser is offered, are never listed in any group. Each list is sorted
and comma-separated, and parsed or detected but not parsed reads none
when it is empty. none were found means the input holds no agent data, where
detect prints no agent artifacts found. Before analyzer 0.12.0 the line
read parsed <list>; detected only <list>, with none for an empty list.
This is the output of timeline -o out on
the example collection; the files it writes
are the examples under inventory.json,
timeline.jsonl and sessions.jsonl.
The note: and problem: lines repeat those of detect; the
dropped: line counts the secondary-source rows, the history prompts
among them, that a transcript also records; and the sessions: line
ends with the synced, remote and recovered counts: the terminal session
that turned on Remote Control and the Remote Control and teleported
sessions are synced, the Remote Control and teleported sessions remote,
and the sessions only alice's prompt history still holds are recovered:
input: /cases/wsdev14/wsdev14 (collected)
host: wsdev14
note: 1 manifest rows did not parse
problem: /home/bob/.gemini/tmp/6b9af143446a411aefa3edb76e4693799bd7513e7ed8d919962d6713d3d3b972/chats/session-2026-10-27T09-41-2407af53.json: missing from the collection
dropped: 7 duplicate rows from secondary sources (claude-code 4, codex-cli 3)
agents: parsed: aider, claude-code, codex-cli, ollama; detected but not parsed: amp; parser found no rows: cursor, gemini-cli
rows: 140 in out/timeline.jsonl
aider user 1
claude-code assistant 11
claude-code system 69
claude-code tool_result 12
claude-code tool_use 12
claude-code user 8
codex-cli assistant 1
codex-cli system 12
codex-cli tool_result 5
codex-cli tool_use 5
codex-cli user 1
ollama system 1
ollama user 2
sessions: 6 in out/sessions.jsonl (3 synced, 2 remote, 2 recovered)
inventory: out/inventory.json (homes, projects, scratch directories, counts per agent, problems)
inventory.json¶
inventory.json is one JSON object, UTF-8 without a byte order mark,
indented by two spaces. It is the detection snapshot of the run as well
as its counts: kind, notes, homes and the problems met while the
input was opened are what detect --json prints for
the same input, so an analyst reads them here later instead of running
detect again. Its row and session counts follow what timeline.jsonl
and sessions.jsonl hold, so with --since, --until or --match they
count the rows and sessions left after filtering, and with --agent only
the agents selected. homes, notes, problems and the file counts
(homes[].agents[].files and bytes, and the project files, bytes
and shared_files) describe the whole input and do not follow the
filters, except that --agent changes the parser problems, which come
from the parsers that ran. This example is the inventory.json that
timeline -o out writes for
the example collection, the run shown under
timeline. Under projects, /srv/proj has the remotes
and head of its .git/ files and, in recorded_remotes, the URL
bob's Codex CLI thread recorded, and /srv/invoicing-old, which only
alice's prompt history and .claude.json still name, is referenced;
scratch_directories
holds the scratchpads alice's Claude Code sessions used on this host,
counted there rather than as projects:
{
"doubleagent_version": "0.22.0",
"input": "/cases/wsdev14/wsdev14",
"kind": "collected",
"host": "wsdev14",
"notes": [
"1 manifest rows did not parse"
],
"problems": [
"/home/bob/.gemini/tmp/6b9af143446a411aefa3edb76e4693799bd7513e7ed8d919962d6713d3d3b972/chats/session-2026-10-27T09-41-2407af53.json: missing from the collection"
],
"rows": 140,
"sessions": 6,
"parsed": [
"aider",
"claude-code",
"codex-cli",
"ollama"
],
"detected_not_parsed": [
"amp"
],
"no_rows": [
"cursor",
"gemini-cli"
],
"homes": [
{
"user": "alice",
"home": "/home/alice",
"inferred": false,
"agents": [
{
"agent": "amp",
"files": 1,
"bytes": 177,
"parser": false,
"synced_sessions": 0,
"relayed_sessions": 0,
"remote_sessions": 0,
"recovered_sessions": 0
},
{
"agent": "claude-code",
"files": 13,
"bytes": 48222,
"parser": true,
"synced_sessions": 3,
"relayed_sessions": 0,
"remote_sessions": 2,
"recovered_sessions": 2
}
]
},
{
"user": "bob",
"home": "/home/bob",
"inferred": false,
"agents": [
{
"agent": "codex-cli",
"files": 4,
"bytes": 22690,
"parser": true,
"synced_sessions": 0,
"relayed_sessions": 0,
"remote_sessions": 0,
"recovered_sessions": 0
},
{
"agent": "gemini-cli",
"files": 1,
"bytes": 190,
"parser": true,
"synced_sessions": 0,
"relayed_sessions": 0,
"remote_sessions": 0,
"recovered_sessions": 0
},
{
"agent": "ollama",
"files": 1,
"bytes": 116,
"parser": true,
"synced_sessions": 0,
"relayed_sessions": 0,
"remote_sessions": 0,
"recovered_sessions": 0
}
]
}
],
"projects": {
"/srv/invoicing-old": {
"status": "referenced",
"rows": 2,
"sessions": 1,
"agents": {
"claude-code": {
"rows": 2,
"sessions": 1,
"turn_types": {
"system": 1,
"user": 1
},
"files": 0,
"bytes": 0
}
},
"shared_files": 0,
"synced": "no",
"relayed": "",
"origin": "local",
"recovered": "yes",
"remotes": [],
"head": "",
"recorded_remotes": []
},
"/srv/proj": {
"status": "collected",
"rows": 120,
"sessions": 5,
"agents": {
"aider": {
"rows": 1,
"sessions": 0,
"turn_types": {
"user": 1
},
"files": 1,
"bytes": 72
},
"claude-code": {
"rows": 97,
"sessions": 4,
"turn_types": {
"assistant": 11,
"system": 55,
"tool_result": 12,
"tool_use": 12,
"user": 7
},
"files": 1,
"bytes": 69
},
"codex-cli": {
"rows": 22,
"sessions": 1,
"turn_types": {
"assistant": 1,
"system": 10,
"tool_result": 5,
"tool_use": 5,
"user": 1
},
"files": 0,
"bytes": 0
},
"cursor": {
"rows": 0,
"sessions": 0,
"turn_types": {},
"files": 1,
"bytes": 372
}
},
"shared_files": 10,
"synced": "partial",
"relayed": "",
"origin": "mixed",
"recovered": "partial",
"remotes": [
{
"name": "origin",
"urls": [
"git@github.com:acme--ledger/ledger.git"
],
"pushurls": []
},
{
"name": "upstream",
"urls": [
"https://git.example.com/acme-billing/ledger.git"
],
"pushurls": []
}
],
"head": "main",
"recorded_remotes": [
"git@github.com:acme--ledger/ledger.git"
]
}
},
"outside_projects": {
"rows": 16,
"sessions": 0,
"agents": {
"claude-code": {
"rows": 11,
"sessions": 0,
"turn_types": {
"system": 11
}
},
"codex-cli": {
"rows": 2,
"sessions": 0,
"turn_types": {
"system": 2
}
},
"ollama": {
"rows": 3,
"sessions": 0,
"turn_types": {
"system": 1,
"user": 2
}
}
},
"synced": "",
"relayed": "",
"origin": "",
"recovered": ""
},
"deduplicated": {
"claude-code": 4,
"codex-cli": 3
},
"collected_only": [],
"synced_sessions": 3,
"relayed_sessions": 0,
"remote_sessions": 2,
"recovered_sessions": 2,
"scratch_directories": {
"/tmp/claude-1000/-srv-proj/99b95ab5-5229-41e2-bc47-dcba1398c74b/scratchpad": {
"status": "referenced",
"rows": 1,
"sessions": 0,
"agents": {
"claude-code": {
"rows": 1,
"sessions": 0,
"turn_types": {
"system": 1
},
"files": 0,
"bytes": 0
}
},
"shared_files": 0,
"synced": "",
"relayed": "",
"origin": "",
"recovered": "",
"remotes": [],
"head": "",
"recorded_remotes": []
},
"/tmp/claude-1000/-srv-proj/d4ccb1ca-a86b-4f26-9501-752de29ee24d/scratchpad": {
"status": "referenced",
"rows": 1,
"sessions": 0,
"agents": {
"claude-code": {
"rows": 1,
"sessions": 0,
"turn_types": {
"system": 1
},
"files": 0,
"bytes": 0
}
},
"shared_files": 0,
"synced": "",
"relayed": "",
"origin": "",
"recovered": "",
"remotes": [],
"head": "",
"recorded_remotes": []
}
}
}
Each row is counted once: under the project its project_path names,
under scratch_directories when that path is a scratch directory, or
under outside_projects when it is empty; a session is counted where its
first non-empty project_path puts it. /srv/proj is collected: its
agents are those with rows there (the Claude Code sessions run in it,
bob's Codex CLI thread, the Aider prompt and the project's
permission setting: rows) and those with files there, such as
cursor, whose rules file gives no row. A path that only rows name, such
as the removed /srv/invoicing-old of alice's prompt history, is a
referenced project with no files. outside_projects holds the rows
with no project: the user-level settings rows of both users, the
.claude.json rows that name no project, the rows of the Remote
Control sidecar files (bridge-pointer.json and ccr-tip.json), the
parser: row of the cut line, and bob's Ollama rows.
scratch_directories holds the scratchpads alice's sessions used on
this host, referenced because the collection holds no files from them.
no_rows lists cursor and gemini-cli, detected_not_parsed lists
amp, and collected_only is [] because no agent of the input is one
whose files hold nothing to parse. deduplicated holds the counts of
the dropped: line per agent. synced_sessions counts the terminal
session that turned on Remote Control and the Remote Control and
teleported sessions, remote_sessions the Remote Control and teleported
sessions, recovered_sessions
the sessions only the prompt history holds, and relayed_sessions is 0,
since no session here went through a messaging platform. The synced,
relayed, origin and recovered marks of each project follow the
rules in the table below: a project whose sessions are some synced and
some not reads synced partial, and one whose sessions are some
remote and some local reads origin mixed. Only /srv/proj has
remotes and head, from its .git/config and .git/HEAD; its
recorded_remotes is the URL in the git remote: row of bob's Codex CLI
thread. Its keys, in order:
| Key | Type | Meaning |
|---|---|---|
doubleagent_version |
string | The analyzer version that wrote the file, as doubleagent --version prints it, so the parser set and row conventions behind the counts can be looked up in the changelog. First key since 0.14.0. |
input |
string | The input path, as on the input: line. |
kind |
string | As in detect --json: archive, collected or loose. |
host |
string | The host name, "" when unknown. |
notes |
array of strings | As in detect --json: the note: lines without their prefix, in the order they were made; [] when there are none. |
problems |
array of strings | The problem: lines of timeline without their prefix, <path>: <reason>: first the problems met while the input was opened, as in detect --json, then each file a parser could not read, by its path on the source host. Not changed by --since, --until or --match; with --agent, only the parsers that ran add theirs. [] when there are none. |
rows |
integer | The number of records in timeline.jsonl, after the filters. |
sessions |
integer | The number of records in sessions.jsonl, after the filters. |
parsed |
array of strings | The parsed group of the agents: line: the agents that produced rows before filtering, sorted. |
detected_not_parsed |
array of strings | The detected but not parsed group: the agents in the input, outside or inside a project directory, that have no parser, sorted. Since 0.14.0 it excludes the agents in collected_only. |
no_rows |
array of strings | The parser found no rows group, sorted; [] when it is empty. |
homes |
array of objects | The homes array of detect --json for the same input, element for element, with synced_sessions, relayed_sessions, remote_sessions and recovered_sessions added as the last four keys of each agent object: one per user home and Docker volume, in the order its first collected row was met, never a project directory. Not filtered. |
homes[].user |
string | As homes[].user in detect --json: the home's owner, "" when unknown. |
homes[].home |
string | As homes[].home in detect --json: the manifest home, or in loose mode the path relative to the input root with a leading /. |
homes[].inferred |
boolean | true in loose mode, where user and home come from the path; false when they come from a manifest. |
homes[].agents |
array of objects | One per agent with files in this home, sorted by agent; [] when every file of the home is missing from the collection. |
homes[].agents[].agent |
string | Catalog agent name; never project. |
homes[].agents[].files |
integer | Files of this agent in this home, counted as in detect --json; not filtered. |
homes[].agents[].bytes |
integer | Their total size, the manifest size or the size on disk in loose mode; not filtered. |
homes[].agents[].parser |
boolean | true when the analyzer has a parser for this agent, false when it is detected only. |
homes[].agents[].synced_sessions |
integer | This user's sessions of this agent whose synced in sessions.jsonl is yes, after the filters; 0 when there is none, and always 0 for an agent whose parser records no sync state. Sessions are matched to the home by user. Last key of the object in 0.14.0. |
homes[].agents[].relayed_sessions |
integer | This user's sessions of this agent whose relayed in sessions.jsonl is yes, after the filters; 0 when there is none, and always 0 for an agent whose parser records no relay state. Sessions are matched to the home by user, as for synced_sessions. Added in 0.15.0. |
homes[].agents[].remote_sessions |
integer | This user's sessions of this agent whose origin in sessions.jsonl is remote, after the filters; 0 when there is none, and always 0 for an agent whose parser records no origin. Sessions are matched to the home by user, as for synced_sessions. Added in 0.15.0. |
homes[].agents[].recovered_sessions |
integer | This user's sessions of this agent whose recovered in sessions.jsonl is yes or partial, after the filters; 0 when there is none, and always 0 for an agent whose parser reads no secondary source. Sessions are matched to the home by user, as for synced_sessions. Last key of the object since 0.15.0. |
projects |
object | Keyed by project path, sorted: every distinct non-empty project_path of the rows, and every project directory the input holds, except the scratch directories, the agent's own working directories, which are under scratch_directories (since 0.18.0, see below), as the collector's path with the image root removed and Windows paths in the canonical form of timeline.jsonl project_path (see below). Windows paths that differ only in case are one project, keyed by the collected directory's spelling when the project is one, else by the first spelling the rows record. {} when there is neither. |
projects.<path>.status |
string | collected when the path is a project directory the input holds, so its project files are in the collection; referenced when only rows name it, because the directory was deleted, was out of the collector's reach, was skipped with --no-projects, or is recorded in another form (see below). |
projects.<path>.rows |
integer | Rows with this project_path, or for a Windows path any spelling of it that differs only in case, after the filters. |
projects.<path>.sessions |
integer | Sessions whose project_path in sessions.jsonl is this one, compared as for rows, after the filters. |
projects.<path>.agents |
object | Keyed by agent name, sorted: every agent with rows in the project or files in its directory, so the keys are the agents used on the project. Never has the key project; {} for a project with neither. |
projects.<path>.agents.<agent>.rows |
integer | This agent's rows with this project_path, compared as for rows, after the filters; 0 for an agent with files and no rows. |
projects.<path>.agents.<agent>.sessions |
integer | This agent's sessions counted under this project, after the filters. |
projects.<path>.agents.<agent>.turn_types |
object | Keyed by turn_type, sorted: this agent's rows of each type in the project, after the filters, listing only the types that occur; {} with no rows. |
projects.<path>.agents.<agent>.files |
integer | The files the collection holds for this agent in this directory, not filtered; 0 for an agent with rows and no files there, and for every agent of a referenced project. |
projects.<path>.agents.<agent>.bytes |
integer | Their total manifest size, not filtered. |
projects.<path>.shared_files |
integer | The shared files the collection holds in the directory: files several agents read, which the manifest records with agent project rather than an owner (AGENTS.md, .agents/, .mcp.json, .github/copilot-instructions.md, SOUL.md, the .git/ metadata; see detect for the full list and their readers). Not filtered; 0 for a referenced project. |
projects.<path>.synced |
string | yes when every session counted under the project has synced yes; partial when some do; no when none does and at least one agent of the project records sync state; "" otherwise, and for a project with no session. Since 0.15.0. |
projects.<path>.relayed |
string | The same four values from relayed, with no when at least one agent of the project records relay state. Since 0.15.0. |
projects.<path>.origin |
string | remote when every session counted under the project has origin remote; mixed when some do; local when none does and at least one agent of the project records origin; "" otherwise, and for a project with no session. Since 0.15.0. |
projects.<path>.recovered |
string | yes when every session counted under the project has recovered yes, so the project's primary transcripts are gone and only secondary sources remain; partial when some of its sessions are yes or partial; no when none is and at least one agent of the project reads a secondary source; "" otherwise, and for a project with no session. A referenced project whose recovered is yes is one whose directory and whose agents' primary stores are both gone: what is left of it is the secondary sources. Last keys of the entry from 0.15.0 to 0.20.1: synced, relayed, origin and recovered, in that order. Counts of these sessions are kept only at the top level and per home agent. |
projects.<path>.remotes |
array of objects | The remotes of the directory's .git/config, as projects[].remotes in detect --json: name, urls and pushurls per remote, sorted by name. [] for a referenced project and for a collected one whose collection holds no .git/config. Not filtered. Since 0.21.0. |
projects.<path>.head |
string | The branch or detached commit the directory's .git/HEAD names, as projects[].head in detect --json; "" for a referenced project and when the collection holds no .git/HEAD. Not filtered. Since 0.21.0. |
projects.<path>.recorded_remotes |
array of strings | The remote URLs the agents recorded for the project: the distinct URLs of the git remote: rows (see Row order and system rows) after the filters, sorted. A row of a session is counted under the session's project, as the session is, and a row without a session under its own project_path. It is filled for a referenced project too, so a project whose directory is gone still names the remote its agent saw; [] when no row names one. Last keys of the entry since 0.21.0: remotes, head and recorded_remotes, in that order. |
outside_projects |
object | The counts for the rows and sessions whose project_path is empty. It has no status, no file counts and no remotes, head or recorded_remotes: files outside projects are counted per home under homes. |
outside_projects.rows |
integer | Rows with an empty project_path, after the filters. |
outside_projects.sessions |
integer | Sessions with an empty project_path, after the filters. |
outside_projects.agents |
object | Keyed by agent name, sorted: the agents with such rows or sessions; {} when there is none. |
outside_projects.agents.<agent>.rows |
integer | This agent's rows with an empty project_path, after the filters. |
outside_projects.agents.<agent>.sessions |
integer | This agent's sessions with an empty project_path, after the filters. |
outside_projects.agents.<agent>.turn_types |
object | Keyed by turn_type, sorted: this agent's rows of each type, listing only the types that occur. |
outside_projects.synced, relayed, origin, recovered |
string | As for a project, by the same rules and in the same order, over the sessions with an empty project_path and the agents with such rows or sessions; a Copilot cloud agent task, which records no project, is counted here. Last keys since 0.15.0. |
deduplicated |
object | Keyed by agent name, sorted: the agents with rows of a secondary source dropped because a primary store or another secondary source records the same event (see Secondary sources), the counts of the dropped: line. With --agent, only the agents selected; not changed by --since, --until or --match, which apply to the rows kept. {} when no row was dropped. |
deduplicated.<agent> |
integer | The number of this agent's secondary-source rows dropped, never 0. |
collected_only |
array of strings | The collected only group of the agents: line: the agents in the input, outside or inside a project directory, that have no parser because their files hold nothing to parse (the collected rows of the agents page), sorted; [] when it is empty. Since 0.14.0. |
synced_sessions |
integer | The number of records in sessions.jsonl whose synced is yes, after the filters, as for sessions; 0 when there is none. Last key in 0.14.0. |
relayed_sessions |
integer | The number of records in sessions.jsonl whose relayed is yes, after the filters, as for sessions; 0 when there is none. Added in 0.15.0. |
remote_sessions |
integer | The number of records in sessions.jsonl whose origin is remote, after the filters, as for sessions; 0 when there is none. Added in 0.15.0. |
recovered_sessions |
integer | The number of records in sessions.jsonl whose recovered is yes or partial, after the filters, as for sessions; 0 when there is none. Last key from 0.15.0 to 0.17.0. |
scratch_directories |
object | Keyed by path, sorted, and grouped as projects is: every project_path of the rows, and every project directory the input holds, that is a scratch directory (see below); {} when there is none. Last key since 0.18.0. |
scratch_directories.<path>.status |
string | As projects.<path>.status: collected when the collector gathered project files from the directory, referenced when only rows name it. Since 0.18.0. |
scratch_directories.<path>.rows, sessions, agents, shared_files |
integer, object | As for a project, over the rows and sessions with this project_path and the files in the directory; each agent object has the keys of projects.<path>.agents.<agent>. Since 0.18.0. |
scratch_directories.<path>.synced, relayed, origin, recovered |
string | As for a project, by the same rules and in the same order. Since 0.18.0. |
scratch_directories.<path>.remotes, head, recorded_remotes |
array of objects, string, array of strings | As for a project, by the same rules and in the same order, as the last keys of the entry. Since 0.21.0. |
A row is counted under its own project_path and a session under the
session's, the first non-empty project_path among its rows, so a session
whose rows name two projects has rows under both and is counted once, under
the first. Rows with no session_id belong to no session. The rows of
projects, scratch_directories and outside_projects add up to the
top-level rows, and their sessions to the top-level sessions.
Nothing is counted twice: files per home are under homes, files per
project directory under projects or scratch_directories, and rows and
sessions under projects, scratch_directories and outside_projects.
A scratch directory is a working directory that belongs to the agent
rather than to the user: the directory an app creates for a chat that has
no project (the ChatGPT desktop app's ~/Documents/Codex/<date>/<slug>,
with work/ and outputs/ inside), the agent's own state directory or a
clone or worktree it made inside it (Copilot Desktop's
.copilot/chats/<id>, .copilot/repos/<name> and
.copilot/canvas-catalog-probe), or the working directory of a session
the app pre-created and never prompted (Copilot Desktop's pool and probe
sessions, whose cwd on Windows is the app's install directory). A
project is a directory the user pointed the agent at. The two are kept
apart because a scratch directory says what the agent did on its own
account, which can still matter to the investigation. A path is a
scratch directory when any of these holds:
- A parser says so: a
systemrow whosetextstarts withscratch directory:names it in its ownproject_path. Thecodex-cliparser writesscratch directory: <path> (projectless thread <id>) (from .codex/.codex-global-state.json thread-projectless-output-directories, file mtime)for each entry of the desktop app's map of projectless output directories, and thecopilot-cliparser writesscratch directory: <cwd> (working directory of a session the app pre-created and never prompted: no events.jsonl)for a session directory that holds aworkspace.yamlwith acwdand noevents.jsonl. Theopencodeandkilo-codeparsers writescratch directory: <directory> (git worktree of <project> strategy=<strategy>) (from <database> worktree, time_created)for each worktree the agent made, from theworktreeorproject_directorytable. Since 0.20.1 thecontinueparser writes one for acn reviewworktree, thezedparser for a thread's sandboxed-terminal temp directory and a git worktree Zed created, thehermesparser for a git worktree the agent created (<repo>/.worktrees/<name>on ahermes/<name>branch), theopenhandsparser for a conversation in the Agent Server's default worktree root/tmp/conversation-worktrees, and theagent-zeroparser for an editor (ACP) chat's container working directory; each paragraph under Row order and system rows gives the row's text. Since 0.21.0 theclaude-codeparser writesscratch directory: <path> (session scratchpad, from the environment attachment)for the session scratchpad a transcript'senvironmentattachment names (%LOCALAPPDATA%\Temp\claude\<slug>\<session>\scratchpadon Windows). - The catalog says so: the path is in or under a directory that a home
entry of the bundled catalog names, in one of the input's homes. The
path relative to the home is matched segment by segment against each
entry's glob, so
/home/alice/.copilot/chats/<id>is under the entry.copilotand/home/alice/.config/Cursor/xunder.config/[Cc]ursor, while/home/alice/.config/myprojectis under no entry. A Windows home is compared ignoring case. The rule needs the home's path on the source host, which a collection's manifest gives; in loose mode it applies only where the analyzer could infer the home's path from the tree, and not at all when the input is a home itself. - The project catalog says so: the path is under a directory that a
project entry of one agent in the bundled catalog names, inside
another directory, with at least one segment below the entry. This is
the worktree an agent makes inside the user's project:
/srv/proj/.claude/worktrees/featis under theclaude-codeentry.claude, and/srv/proj/.kilo/worktrees/k1,/srv/proj/.gemini/worktrees/g1,/srv/proj/.qwen/worktrees/q1and/srv/proj/.codex/worktrees/c1under thekilo-code,gemini-cli,qwen-codeandcodex-clientries, while/srv/proj/.kiloitself and/srv/proj/srcare not scratch directories. The entries that belong to no single agent (AGENTS.md,.agents, the agentprojectin--list) do not count. A Windows path is compared ignoring case. The rule needs no home path, so it applies in loose mode as well. Since 0.20.1 (issue 227).
No rule looks at the collector's list of project directories, so a
scratch directory the collector gathered project files from is listed
under scratch_directories with status collected. A scratch
directory the collector gathers as the agent's own files (the Codex
output directory Documents/Codex, a codex-cli catalog entry since
collector 1.16.0) is not in collection.json projects, so it is
listed with status referenced: its files are counted under homes
for codex-cli, not as project files. The rows keep
their project_path in timeline.jsonl and sessions.jsonl, and
detect and detect --json, which read no rows, list every project
directory the input holds without applying either rule.
The project directories the input holds are the paths in collection.json
projects (the directories the collector gathered project files from,
discovered or given with -p) and, for a collection whose
collection.json is missing or has no projects, the home of each
collected manifest row with agent project. A loose input holds none:
the analyzer does not look for project directories in a loose tree, so
every key there is referenced. Each such directory is listed even when
no row names it, and even when the filters left no rows at all.
Paths are compared and counted as recorded, with two exceptions. First,
for a collection made in image mode (collection.json mode image),
the root given with -r is removed from the front of each project
directory, so /mnt/image/srv/proj in collection.json matches the
/srv/proj the transcripts on that host recorded; the file counts of two
directories that map to one path are added together. Second, a Windows
path (one that starts with a drive letter, or with \\ for a network
share) is in the canonical form described under
timeline.jsonl project_path, in the rows and in the
project directories alike, and is compared ignoring case, because Windows
file names are case-insensitive: c:/Users/sean/demo,
C:\Users\sean\demo\ and C:\USERS\sean\demo are one project. Its
key is the spelling of the collected directory when the project is one,
so a collected project's rows, files and status collected stay under
the path collection.json names; otherwise it is the first spelling seen
in the rows in timeline order, then in the sessions. The case after the
drive is never changed, since the analyzer cannot know the folder's real
casing. Nothing else is normalised, so one repository recorded in two
forms has two keys: a trailing slash or a different case on a POSIX path,
or a Windows image collected on Linux or macOS, whose transcripts say
C:\Users\alice\proj while the collector's path, once the root is
removed, is /Users/alice/proj. The collector's form then has status
collected and the files, and the transcripts' form has the rows and
status referenced.
A jq query that lists projects by row count:
jq -r '.projects | to_entries | sort_by(-.value.rows)[] | "\(.value.rows)\t\(.key)"' out/inventory.json
The same for scratch directories:
jq -r '.scratch_directories | to_entries | sort_by(-.value.rows)[] | "\(.value.rows)\t\(.key)"' out/inventory.json
One that lists each project with the agents used on it:
jq -r '.projects | to_entries[] | "\(.key)\t\(.value.agents | keys | join(", "))"' out/inventory.json
And one that lists the agents that left files in a project but no rows:
jq -r '.projects | to_entries[] | .key as $p | .value.agents | to_entries[] | select(.value.rows == 0 and .value.files > 0) | "\($p)\t\(.key)"' out/inventory.json
Before analyzer 0.12.0 there was no inventory.json. The keys are an
interface like the JSONL fields: new ones are added at the end, the
existing ones keep their names, types and meaning. Analyzer 0.13.0 added
deduplicated. Analyzer 0.14.0 added collected_only and then
synced_sessions, the last key, and synced_sessions as the last key of
each homes[].agents[] object; from that version detected_not_parsed
no longer lists the agents with nothing to parse, which it had held since
0.12.0. Analyzer 0.15.0 added relayed_sessions, remote_sessions
and recovered_sessions after synced_sessions, in that order, as the
last keys of the file and of each homes[].agents[] object, and
synced, relayed, origin and recovered as the last keys of each
projects.<path> entry and of outside_projects. Analyzer 0.18.0
added scratch_directories as the last key of the file and moved the
scratch directories out of projects into it. Analyzer 0.21.0 added
remotes, head and recorded_remotes as the last keys of each
projects.<path> and scratch_directories.<path> entry. The one key added at the front is doubleagent_version, in
analyzer 0.14.0, because a reader needs the version before the rest; the
file is one object read by key, not by position.
JSONL encoding¶
Both JSONL files are UTF-8 without a byte order mark, one JSON object per
line, each line ended by LF, with no header line. Every object has every
field, in the order listed below. Fields with no value hold the empty string
"", never null, and every field is a string except the integers
source_line, user_turns, assistant_turns and tool_calls and the
array models. A line holds no literal line break: newlines inside
text are written as \n, and U+2028 and U+2029 as \u2028 and
\u2029, because some line readers split on them. Other non-ASCII text is
written as UTF-8, not escaped, so a search for it matches as typed; the
exception is a record whose text holds a lone UTF-16 surrogate, which UTF-8
cannot encode, and which is written entirely with \uXXXX escapes.
There is no space after : or ,, the same layout jq -c prints, so a
grep pattern such as "agent":"codex-cli" matches the file and jq output
alike. searching.md has recipes.
timeline.jsonl¶
timeline.jsonl fields, in order:
| Field | Type | Meaning |
|---|---|---|
timestamp_utc |
string | ISO 8601 UTC with milliseconds, 2026-10-03T16:55:31.123Z. Times recorded without a zone are taken as UTC. Epoch numbers above 10^11 are read as milliseconds, smaller ones as seconds. Empty when the record has none. |
host |
string | --host when given, else collection.json hostname. Empty for loose input without --host. |
user |
string | From the manifest, inferred from the path, or --user. |
agent |
string | Catalog agent name of the parser that produced the row. |
session_id |
string | The agent's own session or thread identifier. |
project_path |
string | Working directory recorded for the turn. A Windows path, one that starts with a drive letter and a separator or with \\, is written in one canonical form whichever store recorded it: upper-case drive letter, backslash separators, no repeated separators and no trailing separator except in a drive root (C:\), and the extended-length prefix \\?\ removed (\\?\UNC\server\share becomes \\server\share), so c:/Users/sean/demo/ is C:\Users\sean\demo. The case after the drive is kept as recorded. Any other path, POSIX paths included, is written as recorded. A file: URI is turned into a path by the parser that reads it. |
git_branch |
string | Branch recorded for the turn, where the agent logs one. |
turn_type |
string | user, assistant, tool_use, tool_result, system, or thinking with --include-thinking. |
model |
string | Model that produced the turn, where recorded. |
tool_name |
string | For tool_use rows: the tool. A Codex command run is shell when the rollout records it only as a local_shell_call, or as a CommandExecution item with no call line of the same id, else the call's own name, such as exec_command. |
tool_use_id |
string | Links a tool_use row to its tool_result. |
text |
string | The event's full text, with leading and trailing whitespace removed and inner line breaks and indentation kept: the prompt, the response, the tool output, or for a tool call its most identifying argument (the Bash command, the edited file, the search pattern, the fetched URL), else the arguments as JSON. It is never truncated, so the timeline is a complete transcript, one event per line. |
source_file |
string | Path of the record on the source host, as in the manifest. In loose mode it is the path relative to the input root with a leading /, or relative to the home when the input root is the home or an agent directory. |
source_line |
integer | Where the record is in that file, so the full record can be read. For line-oriented files (JSONL, Markdown, text) the 1-based line number. For SQLite stores it is the record's rowid (Tabby events: the event id), and for JSON documents an array index or 1. Each parser's module docstring says which. 0 when no position applies. |
recovered |
string | yes when the row is an event (any turn_type but system) from a secondary source of its agent (see Secondary sources) that survived deduplication, so the agent's primary store does not hold the event; since 0.20.0 also the user and assistant rows the cursor parser takes from the agentKv prompt blobs of a conversation deleted in the editor, which the editor's database still holds although the conversation's own records are gone; since 0.20.1 also the turns the kiro parser recovers from a conversation's checkpoint snapshots or tangent-mode main history, which the conversation's own history no longer holds. "" for every other row, including every system row of a secondary source (an index header, a thread record, a snapshot, a session-start or permission row, a parser: problem), which describes the source or the session rather than a turn the primary store lost. Since analyzer 0.15.0. |
The field names each parser relies on are listed in its module docstring
under doubleagent/parsers/. Thinking and reasoning blocks are left out
unless --include-thinking is passed.
This is the timeline.jsonl that timeline -o out writes for
the example collection, the run shown under
timeline. Rows worth following: a tool call and its result
sharing one tool_use_id; a permission decision: row beside the call
it decided; a model: <old> -> <new> row after alice's /model; the
session origin: remote rows of the Remote Control and teleported
sessions; the history prompts with recovered yes; and the
parser: unparseable line row at the cut end of alice's teleported
transcript:
{"timestamp_utc":"2026-10-27T08:05:12.481Z","host":"wsdev14","user":"alice","agent":"aider","session_id":"","project_path":"/srv/proj","git_branch":"","turn_type":"user","model":"","tool_name":"","tool_use_id":"","text":"add tests for the invoice total rounding","source_file":"/srv/proj/.aider.input.history","source_line":2,"recovered":"yes"}
{"timestamp_utc":"2026-10-27T08:20:14.903Z","host":"wsdev14","user":"alice","agent":"claude-code","session_id":"18386daa-971d-40ca-828a-12b746180948","project_path":"/srv/invoicing-old","git_branch":"","turn_type":"user","model":"","tool_name":"","tool_use_id":"","text":"port the CSV export to the new ledger schema and keep the old column order","source_file":"/home/alice/.claude/history.jsonl","source_line":1,"recovered":"yes"}
{"timestamp_utc":"2026-10-27T08:41:37.226Z","host":"wsdev14","user":"alice","agent":"claude-code","session_id":"083d4578-89db-41c5-8894-271b3f00cb9b","project_path":"/srv/proj","git_branch":"","turn_type":"user","model":"","tool_name":"","tool_use_id":"","text":"why does the reconciler skip invoices dated on the last day of the month?","source_file":"/home/alice/.claude/history.jsonl","source_line":2,"recovered":"yes"}
{"timestamp_utc":"2026-10-27T09:11:58.402Z","host":"wsdev14","user":"alice","agent":"claude-code","session_id":"d4ccb1ca-a86b-4f26-9501-752de29ee24d","project_path":"/srv/proj","git_branch":"","turn_type":"system","model":"","tool_name":"","tool_use_id":"","text":"claude-code process 48213 interactive status=idle entrypoint=cli bridge=cse_20d42b8c54115622 host_session=none","source_file":"/home/alice/.claude/sessions/48213.json","source_line":1,"recovered":""}
{"timestamp_utc":"2026-10-27T09:12:03.087Z","host":"wsdev14","user":"alice","agent":"claude-code","session_id":"d4ccb1ca-a86b-4f26-9501-752de29ee24d","project_path":"/srv/proj","git_branch":"fix/invoice-rounding","turn_type":"system","model":"","tool_name":"","tool_use_id":"","text":"session surface: cli","source_file":"/home/alice/.claude/projects/-srv-proj/d4ccb1ca-a86b-4f26-9501-752de29ee24d.jsonl","source_line":3,"recovered":""}
{"timestamp_utc":"2026-10-27T09:12:03.087Z","host":"wsdev14","user":"alice","agent":"claude-code","session_id":"d4ccb1ca-a86b-4f26-9501-752de29ee24d","project_path":"/srv/proj","git_branch":"fix/invoice-rounding","turn_type":"user","model":"","tool_name":"","tool_use_id":"","text":"<pasted_content id=\"71ae\">\n=== RUN TestInvoiceTotal\n=== RUN TestInvoiceTotal/single_line\n=== RUN TestInvoiceTotal/quantity_three\n--- PASS: TestInvoiceTotal (0.00s)\n --- PASS: TestInvoiceTotal/single_line (0.00s)\n --- PASS: TestInvoiceTotal/quantity_three (0.00s)\n=== RUN TestRoundCents\n=== RUN TestRoundCents/whole_dollars\n=== RUN TestRoundCents/half_cent_up\n=== RUN TestRoundCents/half_cent_down\n=== RUN TestRoundCents/negative_refund\n=== RUN TestRoundCents/zero\n--- FAIL: TestRoundCents (0.00s)\n --- PASS: TestRoundCents/whole_dollars (0.00s)\n --- FAIL: TestRoundCents/half_cent_up (0.00s)\n round_test.go:31: RoundCents(1.005) = 100, want 101\n --- PASS: TestRoundCents/half_cent_down (0.00s)\n --- FAIL: TestRoundCents/negative_refund (0.00s)\n round_test.go:31: RoundCents(-1.005) = -100, want -101\n --- PASS: TestRoundCents/zero (0.00s)\n=== RUN TestParseAmount\n=== RUN TestParseAmount/plain\n=== RUN TestParseAmount/thousands_separator\n=== RUN TestParseAmount/negative\n--- PASS: TestParseAmount (0.00s)\n --- PASS: TestParseAmount/plain (0.00s)\n --- PASS: TestParseAmount/thousands_separator (0.00s)\n --- PASS: TestParseAmount/negative (0.00s)\nFAIL\nFAIL\tgithub.com/acme--ledger/ledger/internal/invoice\t0.014s\n</pasted_content> CI has failed like this on main since this morning's merge. Find the cause and fix it without changing the test expectations.","source_file":"/home/alice/.claude/projects/-srv-proj/d4ccb1ca-a86b-4f26-9501-752de29ee24d.jsonl","source_line":3,"recovered":""}
{"timestamp_utc":"2026-10-27T09:12:03.091Z","host":"wsdev14","user":"alice","agent":"claude-code","session_id":"d4ccb1ca-a86b-4f26-9501-752de29ee24d","project_path":"/tmp/claude-1000/-srv-proj/d4ccb1ca-a86b-4f26-9501-752de29ee24d/scratchpad","git_branch":"fix/invoice-rounding","turn_type":"system","model":"","tool_name":"","tool_use_id":"","text":"scratch directory: /tmp/claude-1000/-srv-proj/d4ccb1ca-a86b-4f26-9501-752de29ee24d/scratchpad (session scratchpad, from the environment attachment)","source_file":"/home/alice/.claude/projects/-srv-proj/d4ccb1ca-a86b-4f26-9501-752de29ee24d.jsonl","source_line":4,"recovered":""}
{"timestamp_utc":"2026-10-27T09:12:08.412Z","host":"wsdev14","user":"alice","agent":"claude-code","session_id":"d4ccb1ca-a86b-4f26-9501-752de29ee24d","project_path":"/srv/proj","git_branch":"fix/invoice-rounding","turn_type":"system","model":"claude-opus-5-5","tool_name":"","tool_use_id":"","text":"model: claude-opus-5-5","source_file":"/home/alice/.claude/projects/-srv-proj/d4ccb1ca-a86b-4f26-9501-752de29ee24d.jsonl","source_line":5,"recovered":""}
{"timestamp_utc":"2026-10-27T09:12:08.412Z","host":"wsdev14","user":"alice","agent":"claude-code","session_id":"d4ccb1ca-a86b-4f26-9501-752de29ee24d","project_path":"/srv/proj","git_branch":"fix/invoice-rounding","turn_type":"assistant","model":"claude-opus-5-5","tool_name":"","tool_use_id":"","text":"Both failures are in RoundCents. I'll find where it is defined and who calls it.","source_file":"/home/alice/.claude/projects/-srv-proj/d4ccb1ca-a86b-4f26-9501-752de29ee24d.jsonl","source_line":5,"recovered":""}
{"timestamp_utc":"2026-10-27T09:12:08.977Z","host":"wsdev14","user":"alice","agent":"claude-code","session_id":"d4ccb1ca-a86b-4f26-9501-752de29ee24d","project_path":"/srv/proj","git_branch":"fix/invoice-rounding","turn_type":"tool_use","model":"claude-opus-5-5","tool_name":"Grep","tool_use_id":"toolu_01OVC1eOB5IXXxTdTTWbEmZS","text":"RoundCents | /srv/proj","source_file":"/home/alice/.claude/projects/-srv-proj/d4ccb1ca-a86b-4f26-9501-752de29ee24d.jsonl","source_line":6,"recovered":""}
{"timestamp_utc":"2026-10-27T09:12:09.204Z","host":"wsdev14","user":"alice","agent":"claude-code","session_id":"d4ccb1ca-a86b-4f26-9501-752de29ee24d","project_path":"/srv/proj","git_branch":"fix/invoice-rounding","turn_type":"tool_result","model":"","tool_name":"","tool_use_id":"toolu_01OVC1eOB5IXXxTdTTWbEmZS","text":"internal/invoice/round.go:7:func RoundCents(amount float64) int64 {\ninternal/invoice/total.go:18:\t\tsum += RoundCents(line.Amount) * int64(line.Quantity)\ninternal/invoice/round_test.go:24:\t\tgot := RoundCents(tc.amount)","source_file":"/home/alice/.claude/projects/-srv-proj/d4ccb1ca-a86b-4f26-9501-752de29ee24d.jsonl","source_line":7,"recovered":""}
{"timestamp_utc":"2026-10-27T09:12:09.204Z","host":"wsdev14","user":"alice","agent":"claude-code","session_id":"d4ccb1ca-a86b-4f26-9501-752de29ee24d","project_path":"/srv/proj","git_branch":"fix/invoice-rounding","turn_type":"system","model":"","tool_name":"","tool_use_id":"toolu_01OVC1eOB5IXXxTdTTWbEmZS","text":"permission decision: accept source=config reason=mode","source_file":"/home/alice/.claude/projects/-srv-proj/d4ccb1ca-a86b-4f26-9501-752de29ee24d.jsonl","source_line":7,"recovered":""}
{"timestamp_utc":"2026-10-27T09:12:12.530Z","host":"wsdev14","user":"alice","agent":"claude-code","session_id":"d4ccb1ca-a86b-4f26-9501-752de29ee24d","project_path":"/srv/proj","git_branch":"fix/invoice-rounding","turn_type":"tool_use","model":"claude-opus-5-5","tool_name":"Read","tool_use_id":"toolu_01rKYIFhqwxVQVhD2iEy7Z42","text":"/srv/proj/internal/invoice/round.go","source_file":"/home/alice/.claude/projects/-srv-proj/d4ccb1ca-a86b-4f26-9501-752de29ee24d.jsonl","source_line":8,"recovered":""}
{"timestamp_utc":"2026-10-27T09:12:12.611Z","host":"wsdev14","user":"alice","agent":"claude-code","session_id":"d4ccb1ca-a86b-4f26-9501-752de29ee24d","project_path":"/srv/proj","git_branch":"fix/invoice-rounding","turn_type":"tool_result","model":"","tool_name":"","tool_use_id":"toolu_01rKYIFhqwxVQVhD2iEy7Z42","text":"1\tpackage invoice\n 2\t\n 3\timport \"math\"\n 4\t\n 5\t// RoundCents converts an amount in dollars to whole cents,\n 6\t// rounding half away from zero.\n 7\tfunc RoundCents(amount float64) int64 {\n 8\t\treturn int64(math.Round(amount * 100))\n 9\t}","source_file":"/home/alice/.claude/projects/-srv-proj/d4ccb1ca-a86b-4f26-9501-752de29ee24d.jsonl","source_line":9,"recovered":""}
{"timestamp_utc":"2026-10-27T09:12:12.611Z","host":"wsdev14","user":"alice","agent":"claude-code","session_id":"d4ccb1ca-a86b-4f26-9501-752de29ee24d","project_path":"/srv/proj","git_branch":"fix/invoice-rounding","turn_type":"system","model":"","tool_name":"","tool_use_id":"toolu_01rKYIFhqwxVQVhD2iEy7Z42","text":"permission decision: accept source=config reason=mode","source_file":"/home/alice/.claude/projects/-srv-proj/d4ccb1ca-a86b-4f26-9501-752de29ee24d.jsonl","source_line":9,"recovered":""}
{"timestamp_utc":"2026-10-27T09:12:24.019Z","host":"wsdev14","user":"alice","agent":"claude-code","session_id":"d4ccb1ca-a86b-4f26-9501-752de29ee24d","project_path":"/srv/proj","git_branch":"fix/invoice-rounding","turn_type":"assistant","model":"claude-opus-5-5","tool_name":"","tool_use_id":"","text":"The cause is float rounding: 1.005 * 100 is 100.49999999999999 in float64, so math.Round returns 100. I'll round to thousandths first and add the half cent in integer arithmetic, which is exact.","source_file":"/home/alice/.claude/projects/-srv-proj/d4ccb1ca-a86b-4f26-9501-752de29ee24d.jsonl","source_line":10,"recovered":""}
{"timestamp_utc":"2026-10-27T09:12:25.330Z","host":"wsdev14","user":"alice","agent":"claude-code","session_id":"d4ccb1ca-a86b-4f26-9501-752de29ee24d","project_path":"/srv/proj","git_branch":"fix/invoice-rounding","turn_type":"tool_use","model":"claude-opus-5-5","tool_name":"Edit","tool_use_id":"toolu_01vqU0JmXmcMBB5EDRUqdtkg","text":"/srv/proj/internal/invoice/round.go","source_file":"/home/alice/.claude/projects/-srv-proj/d4ccb1ca-a86b-4f26-9501-752de29ee24d.jsonl","source_line":11,"recovered":""}
{"timestamp_utc":"2026-10-27T09:12:25.391Z","host":"wsdev14","user":"alice","agent":"claude-code","session_id":"d4ccb1ca-a86b-4f26-9501-752de29ee24d","project_path":"/srv/proj","git_branch":"fix/invoice-rounding","turn_type":"tool_result","model":"","tool_name":"","tool_use_id":"toolu_01vqU0JmXmcMBB5EDRUqdtkg","text":"The file /srv/proj/internal/invoice/round.go has been updated. Here's the result of running `cat -n` on a snippet of the edited file:\n 7\tfunc RoundCents(amount float64) int64 {\n 8\t\tmills := int64(math.Round(amount * 1000))\n 9\t\treturn (mills + 5) / 10\n 10\t}","source_file":"/home/alice/.claude/projects/-srv-proj/d4ccb1ca-a86b-4f26-9501-752de29ee24d.jsonl","source_line":12,"recovered":""}
{"timestamp_utc":"2026-10-27T09:12:25.391Z","host":"wsdev14","user":"alice","agent":"claude-code","session_id":"d4ccb1ca-a86b-4f26-9501-752de29ee24d","project_path":"/srv/proj","git_branch":"fix/invoice-rounding","turn_type":"system","model":"","tool_name":"","tool_use_id":"toolu_01vqU0JmXmcMBB5EDRUqdtkg","text":"permission decision: accept source=config reason=mode","source_file":"/home/alice/.claude/projects/-srv-proj/d4ccb1ca-a86b-4f26-9501-752de29ee24d.jsonl","source_line":12,"recovered":""}
{"timestamp_utc":"2026-10-27T09:12:28.804Z","host":"wsdev14","user":"alice","agent":"claude-code","session_id":"d4ccb1ca-a86b-4f26-9501-752de29ee24d","project_path":"/srv/proj","git_branch":"fix/invoice-rounding","turn_type":"tool_use","model":"claude-opus-5-5","tool_name":"Bash","tool_use_id":"toolu_01ExtQ8rP6S0BlImvKaENojZ","text":"make test","source_file":"/home/alice/.claude/projects/-srv-proj/d4ccb1ca-a86b-4f26-9501-752de29ee24d.jsonl","source_line":13,"recovered":""}
{"timestamp_utc":"2026-10-27T09:12:41.115Z","host":"wsdev14","user":"alice","agent":"claude-code","session_id":"d4ccb1ca-a86b-4f26-9501-752de29ee24d","project_path":"/srv/proj","git_branch":"fix/invoice-rounding","turn_type":"tool_result","model":"","tool_name":"","tool_use_id":"toolu_01ExtQ8rP6S0BlImvKaENojZ","text":"[error] Exit code 2\ngo vet ./...\ngo test ./...\nok \tgithub.com/acme--ledger/ledger/cmd/ledger\t0.019s\nok \tgithub.com/acme--ledger/ledger/internal/api\t0.176s\n--- FAIL: TestRoundCents (0.00s)\n --- FAIL: TestRoundCents/negative_refund (0.00s)\n round_test.go:31: RoundCents(-1.005) = -100, want -101\nFAIL\nFAIL\tgithub.com/acme--ledger/ledger/internal/invoice\t0.011s\nok \tgithub.com/acme--ledger/ledger/internal/store\t0.298s\nFAIL\nmake: *** [Makefile:14: test] Error 1","source_file":"/home/alice/.claude/projects/-srv-proj/d4ccb1ca-a86b-4f26-9501-752de29ee24d.jsonl","source_line":14,"recovered":""}
{"timestamp_utc":"2026-10-27T09:12:41.115Z","host":"wsdev14","user":"alice","agent":"claude-code","session_id":"d4ccb1ca-a86b-4f26-9501-752de29ee24d","project_path":"/srv/proj","git_branch":"fix/invoice-rounding","turn_type":"system","model":"","tool_name":"","tool_use_id":"toolu_01ExtQ8rP6S0BlImvKaENojZ","text":"permission decision: accept source=user_temporary reason=- {\"decision\":\"accept\",\"grantScope\":\"once\",\"reasonCode\":\"bashMiss:no-rule-match\",\"source\":\"user_temporary\"}","source_file":"/home/alice/.claude/projects/-srv-proj/d4ccb1ca-a86b-4f26-9501-752de29ee24d.jsonl","source_line":14,"recovered":""}
{"timestamp_utc":"2026-10-27T09:12:47.302Z","host":"wsdev14","user":"alice","agent":"claude-code","session_id":"d4ccb1ca-a86b-4f26-9501-752de29ee24d","project_path":"/srv/proj","git_branch":"fix/invoice-rounding","turn_type":"assistant","model":"claude-opus-5-5","tool_name":"","tool_use_id":"","text":"The positive case passes now, but adding the half cent moves a negative amount toward zero: -1005 thousandths becomes -1000, so -100. Negative amounts need it subtracted.","source_file":"/home/alice/.claude/projects/-srv-proj/d4ccb1ca-a86b-4f26-9501-752de29ee24d.jsonl","source_line":15,"recovered":""}
{"timestamp_utc":"2026-10-27T09:12:48.160Z","host":"wsdev14","user":"alice","agent":"claude-code","session_id":"d4ccb1ca-a86b-4f26-9501-752de29ee24d","project_path":"/srv/proj","git_branch":"fix/invoice-rounding","turn_type":"tool_use","model":"claude-opus-5-5","tool_name":"Edit","tool_use_id":"toolu_01qxroJLMANaPvgU8vHVr2m7","text":"/srv/proj/internal/invoice/round.go","source_file":"/home/alice/.claude/projects/-srv-proj/d4ccb1ca-a86b-4f26-9501-752de29ee24d.jsonl","source_line":16,"recovered":""}
{"timestamp_utc":"2026-10-27T09:12:48.214Z","host":"wsdev14","user":"alice","agent":"claude-code","session_id":"d4ccb1ca-a86b-4f26-9501-752de29ee24d","project_path":"/srv/proj","git_branch":"fix/invoice-rounding","turn_type":"tool_result","model":"","tool_name":"","tool_use_id":"toolu_01qxroJLMANaPvgU8vHVr2m7","text":"The file /srv/proj/internal/invoice/round.go has been updated. Here's the result of running `cat -n` on a snippet of the edited file:\n 8\t\tmills := int64(math.Round(amount * 1000))\n 9\t\tif mills < 0 {\n 10\t\t\treturn (mills - 5) / 10\n 11\t\t}\n 12\t\treturn (mills + 5) / 10","source_file":"/home/alice/.claude/projects/-srv-proj/d4ccb1ca-a86b-4f26-9501-752de29ee24d.jsonl","source_line":17,"recovered":""}
{"timestamp_utc":"2026-10-27T09:12:48.214Z","host":"wsdev14","user":"alice","agent":"claude-code","session_id":"d4ccb1ca-a86b-4f26-9501-752de29ee24d","project_path":"/srv/proj","git_branch":"fix/invoice-rounding","turn_type":"system","model":"","tool_name":"","tool_use_id":"toolu_01qxroJLMANaPvgU8vHVr2m7","text":"permission decision: accept source=config reason=mode","source_file":"/home/alice/.claude/projects/-srv-proj/d4ccb1ca-a86b-4f26-9501-752de29ee24d.jsonl","source_line":17,"recovered":""}
{"timestamp_utc":"2026-10-27T09:12:51.067Z","host":"wsdev14","user":"alice","agent":"claude-code","session_id":"d4ccb1ca-a86b-4f26-9501-752de29ee24d","project_path":"/srv/proj","git_branch":"fix/invoice-rounding","turn_type":"tool_use","model":"claude-opus-5-5","tool_name":"Bash","tool_use_id":"toolu_017ab5eukF0YYIKa9jvCAlDq","text":"make test","source_file":"/home/alice/.claude/projects/-srv-proj/d4ccb1ca-a86b-4f26-9501-752de29ee24d.jsonl","source_line":18,"recovered":""}
{"timestamp_utc":"2026-10-27T09:12:53.000Z","host":"wsdev14","user":"alice","agent":"claude-code","session_id":"","project_path":"/srv/proj","git_branch":"","turn_type":"system","model":"","tool_name":"","tool_use_id":"","text":"permission setting: permissions.allow=Bash(make *) (from .claude/settings.local.json, file mtime)","source_file":"/srv/proj/.claude/settings.local.json","source_line":1,"recovered":""}
{"timestamp_utc":"2026-10-27T09:13:19.842Z","host":"wsdev14","user":"alice","agent":"claude-code","session_id":"d4ccb1ca-a86b-4f26-9501-752de29ee24d","project_path":"/srv/proj","git_branch":"fix/invoice-rounding","turn_type":"tool_result","model":"","tool_name":"","tool_use_id":"toolu_017ab5eukF0YYIKa9jvCAlDq","text":"go vet ./...\ngo test ./...\nok \tgithub.com/acme--ledger/ledger/cmd/ledger\t0.018s\nok \tgithub.com/acme--ledger/ledger/internal/api\t0.181s\nok \tgithub.com/acme--ledger/ledger/internal/invoice\t0.010s\nok \tgithub.com/acme--ledger/ledger/internal/store\t0.305s","source_file":"/home/alice/.claude/projects/-srv-proj/d4ccb1ca-a86b-4f26-9501-752de29ee24d.jsonl","source_line":19,"recovered":""}
{"timestamp_utc":"2026-10-27T09:13:19.842Z","host":"wsdev14","user":"alice","agent":"claude-code","session_id":"d4ccb1ca-a86b-4f26-9501-752de29ee24d","project_path":"/srv/proj","git_branch":"fix/invoice-rounding","turn_type":"system","model":"","tool_name":"","tool_use_id":"toolu_017ab5eukF0YYIKa9jvCAlDq","text":"permission decision: accept source=user_permanent reason=- {\"decision\":\"accept\",\"reasonCode\":\"bashMiss:no-rule-match\",\"source\":\"user_permanent\"}","source_file":"/home/alice/.claude/projects/-srv-proj/d4ccb1ca-a86b-4f26-9501-752de29ee24d.jsonl","source_line":19,"recovered":""}
{"timestamp_utc":"2026-10-27T09:13:23.509Z","host":"wsdev14","user":"alice","agent":"claude-code","session_id":"d4ccb1ca-a86b-4f26-9501-752de29ee24d","project_path":"/srv/proj","git_branch":"fix/invoice-rounding","turn_type":"tool_use","model":"claude-opus-5-5","tool_name":"Bash","tool_use_id":"toolu_01RFZ4ewcSAT34QAQ7ldN3nh","text":"git status","source_file":"/home/alice/.claude/projects/-srv-proj/d4ccb1ca-a86b-4f26-9501-752de29ee24d.jsonl","source_line":20,"recovered":""}
{"timestamp_utc":"2026-10-27T09:13:23.618Z","host":"wsdev14","user":"alice","agent":"claude-code","session_id":"d4ccb1ca-a86b-4f26-9501-752de29ee24d","project_path":"/srv/proj","git_branch":"fix/invoice-rounding","turn_type":"tool_result","model":"","tool_name":"","tool_use_id":"toolu_01RFZ4ewcSAT34QAQ7ldN3nh","text":"On branch fix/invoice-rounding\nChanges not staged for commit:\n (use \"git add <file>...\" to update what will be committed)\n (use \"git restore <file>...\" to discard changes in working directory)\n\tmodified: internal/invoice/round.go\n\nno changes added to commit (use \"git add\" and/or \"git commit -a\")","source_file":"/home/alice/.claude/projects/-srv-proj/d4ccb1ca-a86b-4f26-9501-752de29ee24d.jsonl","source_line":21,"recovered":""}
{"timestamp_utc":"2026-10-27T09:13:23.618Z","host":"wsdev14","user":"alice","agent":"claude-code","session_id":"d4ccb1ca-a86b-4f26-9501-752de29ee24d","project_path":"/srv/proj","git_branch":"fix/invoice-rounding","turn_type":"system","model":"","tool_name":"","tool_use_id":"toolu_01RFZ4ewcSAT34QAQ7ldN3nh","text":"permission decision: accept source=config reason=subcommandResults","source_file":"/home/alice/.claude/projects/-srv-proj/d4ccb1ca-a86b-4f26-9501-752de29ee24d.jsonl","source_line":21,"recovered":""}
{"timestamp_utc":"2026-10-27T09:13:30.271Z","host":"wsdev14","user":"alice","agent":"claude-code","session_id":"d4ccb1ca-a86b-4f26-9501-752de29ee24d","project_path":"/srv/proj","git_branch":"fix/invoice-rounding","turn_type":"assistant","model":"claude-opus-5-5","tool_name":"","tool_use_id":"","text":"All tests pass. RoundCents now rounds half away from zero from thousandths, which gives 101 for 1.005 and -101 for -1.005; only round.go changed.","source_file":"/home/alice/.claude/projects/-srv-proj/d4ccb1ca-a86b-4f26-9501-752de29ee24d.jsonl","source_line":22,"recovered":""}
{"timestamp_utc":"2026-10-27T09:13:30.288Z","host":"wsdev14","user":"alice","agent":"claude-code","session_id":"d4ccb1ca-a86b-4f26-9501-752de29ee24d","project_path":"/srv/proj","git_branch":"fix/invoice-rounding","turn_type":"system","model":"","tool_name":"","tool_use_id":"","text":"turn_duration: 87201 ms, 21 messages","source_file":"/home/alice/.claude/projects/-srv-proj/d4ccb1ca-a86b-4f26-9501-752de29ee24d.jsonl","source_line":23,"recovered":""}
{"timestamp_utc":"2026-10-27T09:13:30.288Z","host":"wsdev14","user":"alice","agent":"claude-code","session_id":"d4ccb1ca-a86b-4f26-9501-752de29ee24d","project_path":"/srv/proj","git_branch":"fix/invoice-rounding","turn_type":"system","model":"","tool_name":"","tool_use_id":"","text":"session title: Fix invoice cent rounding","source_file":"/home/alice/.claude/projects/-srv-proj/d4ccb1ca-a86b-4f26-9501-752de29ee24d.jsonl","source_line":24,"recovered":""}
{"timestamp_utc":"2026-10-27T09:24:40.551Z","host":"wsdev14","user":"alice","agent":"claude-code","session_id":"d4ccb1ca-a86b-4f26-9501-752de29ee24d","project_path":"/srv/proj","git_branch":"fix/invoice-rounding","turn_type":"system","model":"","tool_name":"","tool_use_id":"","text":"slash command: <command-name>/model</command-name>\n <command-message>model</command-message>\n <command-args></command-args>","source_file":"/home/alice/.claude/projects/-srv-proj/d4ccb1ca-a86b-4f26-9501-752de29ee24d.jsonl","source_line":25,"recovered":""}
{"timestamp_utc":"2026-10-27T09:24:40.552Z","host":"wsdev14","user":"alice","agent":"claude-code","session_id":"d4ccb1ca-a86b-4f26-9501-752de29ee24d","project_path":"/srv/proj","git_branch":"fix/invoice-rounding","turn_type":"system","model":"","tool_name":"","tool_use_id":"","text":"command output: <local-command-stdout>Set model to `Fable 5.1` and saved as your default for new sessions</local-command-stdout>","source_file":"/home/alice/.claude/projects/-srv-proj/d4ccb1ca-a86b-4f26-9501-752de29ee24d.jsonl","source_line":26,"recovered":""}
{"timestamp_utc":"2026-10-27T09:25:02.904Z","host":"wsdev14","user":"alice","agent":"claude-code","session_id":"d4ccb1ca-a86b-4f26-9501-752de29ee24d","project_path":"/srv/proj","git_branch":"fix/invoice-rounding","turn_type":"user","model":"","tool_name":"","tool_use_id":"","text":"Commit this on the branch with a message that explains the float problem, then push it.","source_file":"/home/alice/.claude/projects/-srv-proj/d4ccb1ca-a86b-4f26-9501-752de29ee24d.jsonl","source_line":27,"recovered":""}
{"timestamp_utc":"2026-10-27T09:25:09.633Z","host":"wsdev14","user":"alice","agent":"claude-code","session_id":"d4ccb1ca-a86b-4f26-9501-752de29ee24d","project_path":"/srv/proj","git_branch":"fix/invoice-rounding","turn_type":"system","model":"claude-fable-5-1","tool_name":"","tool_use_id":"","text":"model: claude-opus-5-5 -> claude-fable-5-1","source_file":"/home/alice/.claude/projects/-srv-proj/d4ccb1ca-a86b-4f26-9501-752de29ee24d.jsonl","source_line":28,"recovered":""}
{"timestamp_utc":"2026-10-27T09:25:09.633Z","host":"wsdev14","user":"alice","agent":"claude-code","session_id":"d4ccb1ca-a86b-4f26-9501-752de29ee24d","project_path":"/srv/proj","git_branch":"fix/invoice-rounding","turn_type":"assistant","model":"claude-fable-5-1","tool_name":"","tool_use_id":"","text":"I'll commit round.go with the float explanation in the body, then push the branch.","source_file":"/home/alice/.claude/projects/-srv-proj/d4ccb1ca-a86b-4f26-9501-752de29ee24d.jsonl","source_line":28,"recovered":""}
{"timestamp_utc":"2026-10-27T09:25:10.118Z","host":"wsdev14","user":"alice","agent":"claude-code","session_id":"d4ccb1ca-a86b-4f26-9501-752de29ee24d","project_path":"/srv/proj","git_branch":"fix/invoice-rounding","turn_type":"tool_use","model":"claude-fable-5-1","tool_name":"Bash","tool_use_id":"toolu_01LG0RTHAb0M8gTu3drkGfKi","text":"git commit -am \"Round invoice cents from thousandths\" -m \"1.005 * 100 is 100.49999999999999 in float64, so math.Round gave 100. Round to thousandths first and add or subtract the half cent in integer arithmetic.\"","source_file":"/home/alice/.claude/projects/-srv-proj/d4ccb1ca-a86b-4f26-9501-752de29ee24d.jsonl","source_line":29,"recovered":""}
{"timestamp_utc":"2026-10-27T09:25:15.447Z","host":"wsdev14","user":"alice","agent":"claude-code","session_id":"d4ccb1ca-a86b-4f26-9501-752de29ee24d","project_path":"/srv/proj","git_branch":"fix/invoice-rounding","turn_type":"tool_result","model":"","tool_name":"","tool_use_id":"toolu_01LG0RTHAb0M8gTu3drkGfKi","text":"[fix/invoice-rounding 8ca869c] Round invoice cents from thousandths\n 1 file changed, 5 insertions(+), 1 deletion(-)","source_file":"/home/alice/.claude/projects/-srv-proj/d4ccb1ca-a86b-4f26-9501-752de29ee24d.jsonl","source_line":30,"recovered":""}
{"timestamp_utc":"2026-10-27T09:25:15.447Z","host":"wsdev14","user":"alice","agent":"claude-code","session_id":"d4ccb1ca-a86b-4f26-9501-752de29ee24d","project_path":"/srv/proj","git_branch":"fix/invoice-rounding","turn_type":"system","model":"","tool_name":"","tool_use_id":"toolu_01LG0RTHAb0M8gTu3drkGfKi","text":"permission decision: accept source=user_temporary reason=- {\"decision\":\"accept\",\"grantScope\":\"once\",\"reasonCode\":\"bashMiss:no-rule-match\",\"source\":\"user_temporary\"}","source_file":"/home/alice/.claude/projects/-srv-proj/d4ccb1ca-a86b-4f26-9501-752de29ee24d.jsonl","source_line":30,"recovered":""}
{"timestamp_utc":"2026-10-27T09:25:18.026Z","host":"wsdev14","user":"alice","agent":"claude-code","session_id":"d4ccb1ca-a86b-4f26-9501-752de29ee24d","project_path":"/srv/proj","git_branch":"fix/invoice-rounding","turn_type":"tool_use","model":"claude-fable-5-1","tool_name":"Bash","tool_use_id":"toolu_01uv4kzzTiT6hnng4BsFIJQT","text":"git push -u origin fix/invoice-rounding","source_file":"/home/alice/.claude/projects/-srv-proj/d4ccb1ca-a86b-4f26-9501-752de29ee24d.jsonl","source_line":31,"recovered":""}
{"timestamp_utc":"2026-10-27T09:25:31.790Z","host":"wsdev14","user":"alice","agent":"claude-code","session_id":"d4ccb1ca-a86b-4f26-9501-752de29ee24d","project_path":"/srv/proj","git_branch":"fix/invoice-rounding","turn_type":"tool_result","model":"","tool_name":"","tool_use_id":"toolu_01uv4kzzTiT6hnng4BsFIJQT","text":"[error] The user doesn't want to proceed with this tool use.","source_file":"/home/alice/.claude/projects/-srv-proj/d4ccb1ca-a86b-4f26-9501-752de29ee24d.jsonl","source_line":32,"recovered":""}
{"timestamp_utc":"2026-10-27T09:25:31.790Z","host":"wsdev14","user":"alice","agent":"claude-code","session_id":"d4ccb1ca-a86b-4f26-9501-752de29ee24d","project_path":"/srv/proj","git_branch":"fix/invoice-rounding","turn_type":"system","model":"","tool_name":"","tool_use_id":"toolu_01uv4kzzTiT6hnng4BsFIJQT","text":"permission decision: reject source=user_reject reason=- {\"decision\":\"reject\",\"reasonCode\":\"bashMiss:no-rule-match\",\"source\":\"user_reject\"} toolDenialKind=user-rejected userFeedback=Don't push yet; I'll push it myself after the review meeting.","source_file":"/home/alice/.claude/projects/-srv-proj/d4ccb1ca-a86b-4f26-9501-752de29ee24d.jsonl","source_line":32,"recovered":""}
{"timestamp_utc":"2026-10-27T09:25:36.412Z","host":"wsdev14","user":"alice","agent":"claude-code","session_id":"d4ccb1ca-a86b-4f26-9501-752de29ee24d","project_path":"/srv/proj","git_branch":"fix/invoice-rounding","turn_type":"assistant","model":"claude-fable-5-1","tool_name":"","tool_use_id":"","text":"Understood: the commit stays local on fix/invoice-rounding and nothing was pushed. Run `git push -u origin fix/invoice-rounding` when you are ready.","source_file":"/home/alice/.claude/projects/-srv-proj/d4ccb1ca-a86b-4f26-9501-752de29ee24d.jsonl","source_line":33,"recovered":""}
{"timestamp_utc":"2026-10-27T09:25:36.430Z","host":"wsdev14","user":"alice","agent":"claude-code","session_id":"d4ccb1ca-a86b-4f26-9501-752de29ee24d","project_path":"/srv/proj","git_branch":"fix/invoice-rounding","turn_type":"system","model":"","tool_name":"","tool_use_id":"","text":"turn_duration: 33526 ms, 8 messages","source_file":"/home/alice/.claude/projects/-srv-proj/d4ccb1ca-a86b-4f26-9501-752de29ee24d.jsonl","source_line":34,"recovered":""}
{"timestamp_utc":"2026-10-27T09:31:10.000Z","host":"wsdev14","user":"alice","agent":"claude-code","session_id":"","project_path":"","git_branch":"","turn_type":"system","model":"","tool_name":"","tool_use_id":"","text":"permission setting: permissions.allow=Bash(go test:*) (from .claude/settings.json, file mtime)","source_file":"/home/alice/.claude/settings.json","source_line":1,"recovered":""}
{"timestamp_utc":"2026-10-27T09:31:10.000Z","host":"wsdev14","user":"alice","agent":"claude-code","session_id":"","project_path":"","git_branch":"","turn_type":"system","model":"","tool_name":"","tool_use_id":"","text":"permission setting: permissions.allow=Bash(git diff:*) (from .claude/settings.json, file mtime)","source_file":"/home/alice/.claude/settings.json","source_line":1,"recovered":""}
{"timestamp_utc":"2026-10-27T09:31:10.000Z","host":"wsdev14","user":"alice","agent":"claude-code","session_id":"","project_path":"","git_branch":"","turn_type":"system","model":"","tool_name":"","tool_use_id":"","text":"permission setting: permissions.deny=Bash(git push --force:*) (from .claude/settings.json, file mtime)","source_file":"/home/alice/.claude/settings.json","source_line":1,"recovered":""}
{"timestamp_utc":"2026-10-27T09:31:10.000Z","host":"wsdev14","user":"alice","agent":"claude-code","session_id":"","project_path":"","git_branch":"","turn_type":"system","model":"","tool_name":"","tool_use_id":"","text":"permission setting: permissions.defaultMode=acceptEdits (from .claude/settings.json, file mtime)","source_file":"/home/alice/.claude/settings.json","source_line":1,"recovered":""}
{"timestamp_utc":"2026-10-27T09:31:10.000Z","host":"wsdev14","user":"alice","agent":"claude-code","session_id":"","project_path":"","git_branch":"","turn_type":"system","model":"","tool_name":"","tool_use_id":"","text":"model setting: model=fable (from .claude/settings.json, file mtime)","source_file":"/home/alice/.claude/settings.json","source_line":1,"recovered":""}
{"timestamp_utc":"2026-10-27T09:31:10.205Z","host":"wsdev14","user":"alice","agent":"claude-code","session_id":"d4ccb1ca-a86b-4f26-9501-752de29ee24d","project_path":"/srv/proj","git_branch":"fix/invoice-rounding","turn_type":"system","model":"","tool_name":"","tool_use_id":"","text":"local_command: <command-name>/remote-control</command-name>\n <command-message>remote-control</command-message>\n <command-args></command-args>","source_file":"/home/alice/.claude/projects/-srv-proj/d4ccb1ca-a86b-4f26-9501-752de29ee24d.jsonl","source_line":35,"recovered":""}
{"timestamp_utc":"2026-10-27T09:31:12.660Z","host":"wsdev14","user":"alice","agent":"claude-code","session_id":"d4ccb1ca-a86b-4f26-9501-752de29ee24d","project_path":"/srv/proj","git_branch":"fix/invoice-rounding","turn_type":"system","model":"","tool_name":"","tool_use_id":"","text":"bridge_status: /remote-control is active","source_file":"/home/alice/.claude/projects/-srv-proj/d4ccb1ca-a86b-4f26-9501-752de29ee24d.jsonl","source_line":36,"recovered":""}
{"timestamp_utc":"2026-10-27T09:31:12.660Z","host":"wsdev14","user":"alice","agent":"claude-code","session_id":"d4ccb1ca-a86b-4f26-9501-752de29ee24d","project_path":"/srv/proj","git_branch":"fix/invoice-rounding","turn_type":"system","model":"","tool_name":"","tool_use_id":"","text":"remote control: bridge session cse_20d42b8c54115622 lastSequenceNum=1","source_file":"/home/alice/.claude/projects/-srv-proj/d4ccb1ca-a86b-4f26-9501-752de29ee24d.jsonl","source_line":37,"recovered":""}
{"timestamp_utc":"2026-10-27T09:31:12.660Z","host":"wsdev14","user":"alice","agent":"claude-code","session_id":"d4ccb1ca-a86b-4f26-9501-752de29ee24d","project_path":"/srv/proj","git_branch":"fix/invoice-rounding","turn_type":"system","model":"","tool_name":"","tool_use_id":"","text":"session synced: remote control bridge cse_20d42b8c54115622 url https://claude.ai/code/session_20d42b8c54115622","source_file":"/home/alice/.claude/projects/-srv-proj/d4ccb1ca-a86b-4f26-9501-752de29ee24d.jsonl","source_line":37,"recovered":""}
{"timestamp_utc":"2026-10-27T11:02:47.318Z","host":"wsdev14","user":"alice","agent":"claude-code","session_id":"d4ccb1ca-a86b-4f26-9501-752de29ee24d","project_path":"/srv/proj","git_branch":"fix/invoice-rounding","turn_type":"system","model":"","tool_name":"","tool_use_id":"","text":"prompt queued: delivered at line 40","source_file":"/home/alice/.claude/projects/-srv-proj/d4ccb1ca-a86b-4f26-9501-752de29ee24d.jsonl","source_line":38,"recovered":""}
{"timestamp_utc":"2026-10-27T11:02:47.320Z","host":"wsdev14","user":"alice","agent":"claude-code","session_id":"d4ccb1ca-a86b-4f26-9501-752de29ee24d","project_path":"/srv/proj","git_branch":"fix/invoice-rounding","turn_type":"system","model":"","tool_name":"","tool_use_id":"","text":"queue dequeue:","source_file":"/home/alice/.claude/projects/-srv-proj/d4ccb1ca-a86b-4f26-9501-752de29ee24d.jsonl","source_line":39,"recovered":""}
{"timestamp_utc":"2026-10-27T11:02:47.341Z","host":"wsdev14","user":"alice","agent":"claude-code","session_id":"d4ccb1ca-a86b-4f26-9501-752de29ee24d","project_path":"/srv/proj","git_branch":"fix/invoice-rounding","turn_type":"user","model":"","tool_name":"","tool_use_id":"","text":"What's the hash of the rounding fix commit?","source_file":"/home/alice/.claude/projects/-srv-proj/d4ccb1ca-a86b-4f26-9501-752de29ee24d.jsonl","source_line":40,"recovered":""}
{"timestamp_utc":"2026-10-27T11:02:47.341Z","host":"wsdev14","user":"alice","agent":"claude-code","session_id":"d4ccb1ca-a86b-4f26-9501-752de29ee24d","project_path":"/srv/proj","git_branch":"fix/invoice-rounding","turn_type":"system","model":"","tool_name":"","tool_use_id":"","text":"prompt trigger: promptSource=queued turnOrigin=human","source_file":"/home/alice/.claude/projects/-srv-proj/d4ccb1ca-a86b-4f26-9501-752de29ee24d.jsonl","source_line":40,"recovered":""}
{"timestamp_utc":"2026-10-27T11:02:47.341Z","host":"wsdev14","user":"alice","agent":"claude-code","session_id":"d4ccb1ca-a86b-4f26-9501-752de29ee24d","project_path":"/srv/proj","git_branch":"fix/invoice-rounding","turn_type":"system","model":"","tool_name":"","tool_use_id":"","text":"remote control: bridge session cse_20d42b8c54115622 lastSequenceNum=4","source_file":"/home/alice/.claude/projects/-srv-proj/d4ccb1ca-a86b-4f26-9501-752de29ee24d.jsonl","source_line":41,"recovered":""}
{"timestamp_utc":"2026-10-27T11:02:52.774Z","host":"wsdev14","user":"alice","agent":"claude-code","session_id":"d4ccb1ca-a86b-4f26-9501-752de29ee24d","project_path":"/srv/proj","git_branch":"fix/invoice-rounding","turn_type":"tool_use","model":"claude-fable-5-1","tool_name":"Bash","tool_use_id":"toolu_01PRSGFrNNXO2tCgIcsfwFnU","text":"git log -1 --oneline","source_file":"/home/alice/.claude/projects/-srv-proj/d4ccb1ca-a86b-4f26-9501-752de29ee24d.jsonl","source_line":42,"recovered":""}
{"timestamp_utc":"2026-10-27T11:02:52.903Z","host":"wsdev14","user":"alice","agent":"claude-code","session_id":"d4ccb1ca-a86b-4f26-9501-752de29ee24d","project_path":"/srv/proj","git_branch":"fix/invoice-rounding","turn_type":"tool_result","model":"","tool_name":"","tool_use_id":"toolu_01PRSGFrNNXO2tCgIcsfwFnU","text":"8ca869c Round invoice cents from thousandths","source_file":"/home/alice/.claude/projects/-srv-proj/d4ccb1ca-a86b-4f26-9501-752de29ee24d.jsonl","source_line":43,"recovered":""}
{"timestamp_utc":"2026-10-27T11:02:52.903Z","host":"wsdev14","user":"alice","agent":"claude-code","session_id":"d4ccb1ca-a86b-4f26-9501-752de29ee24d","project_path":"/srv/proj","git_branch":"fix/invoice-rounding","turn_type":"system","model":"","tool_name":"","tool_use_id":"toolu_01PRSGFrNNXO2tCgIcsfwFnU","text":"permission decision: accept source=config reason=subcommandResults","source_file":"/home/alice/.claude/projects/-srv-proj/d4ccb1ca-a86b-4f26-9501-752de29ee24d.jsonl","source_line":43,"recovered":""}
{"timestamp_utc":"2026-10-27T11:02:57.000Z","host":"wsdev14","user":"alice","agent":"claude-code","session_id":"d4ccb1ca-a86b-4f26-9501-752de29ee24d","project_path":"/srv/proj","git_branch":"","turn_type":"system","model":"","tool_name":"","tool_use_id":"","text":"session synced: remote control bridge cse_20d42b8c54115622 url - (from .claude/sessions/48213.json, file mtime)","source_file":"/home/alice/.claude/sessions/48213.json","source_line":1,"recovered":""}
{"timestamp_utc":"2026-10-27T11:02:57.115Z","host":"wsdev14","user":"alice","agent":"claude-code","session_id":"d4ccb1ca-a86b-4f26-9501-752de29ee24d","project_path":"/srv/proj","git_branch":"fix/invoice-rounding","turn_type":"assistant","model":"claude-fable-5-1","tool_name":"","tool_use_id":"","text":"The fix is commit 8ca869c on fix/invoice-rounding. It is still only on this machine; nothing has been pushed.","source_file":"/home/alice/.claude/projects/-srv-proj/d4ccb1ca-a86b-4f26-9501-752de29ee24d.jsonl","source_line":44,"recovered":""}
{"timestamp_utc":"2026-10-27T11:02:57.130Z","host":"wsdev14","user":"alice","agent":"claude-code","session_id":"d4ccb1ca-a86b-4f26-9501-752de29ee24d","project_path":"/srv/proj","git_branch":"fix/invoice-rounding","turn_type":"system","model":"","tool_name":"","tool_use_id":"","text":"turn_duration: 9789 ms, 5 messages","source_file":"/home/alice/.claude/projects/-srv-proj/d4ccb1ca-a86b-4f26-9501-752de29ee24d.jsonl","source_line":45,"recovered":""}
{"timestamp_utc":"2026-10-27T13:30:14.000Z","host":"wsdev14","user":"alice","agent":"claude-code","session_id":"","project_path":"","git_branch":"","turn_type":"system","model":"","tool_name":"","tool_use_id":"","text":"remote control: environment env_VA0YJCOC2x7fY4mf3gsqaP session 1aa70576079d5584943d534497c1db28 source=standalone (from .claude/projects/-srv-proj/bridge-pointer.json, file mtime)","source_file":"/home/alice/.claude/projects/-srv-proj/bridge-pointer.json","source_line":1,"recovered":""}
{"timestamp_utc":"2026-10-27T13:31:05.512Z","host":"wsdev14","user":"alice","agent":"claude-code","session_id":"e9022006-4f4a-5a45-a27b-6623a05d1ab4","project_path":"/srv/proj","git_branch":"main","turn_type":"system","model":"","tool_name":"","tool_use_id":"","text":"prompt queued: delivered at line 3","source_file":"/home/alice/.claude/projects/-srv-proj/e9022006-4f4a-5a45-a27b-6623a05d1ab4.jsonl","source_line":1,"recovered":""}
{"timestamp_utc":"2026-10-27T13:31:05.515Z","host":"wsdev14","user":"alice","agent":"claude-code","session_id":"e9022006-4f4a-5a45-a27b-6623a05d1ab4","project_path":"/srv/proj","git_branch":"main","turn_type":"system","model":"","tool_name":"","tool_use_id":"","text":"queue dequeue:","source_file":"/home/alice/.claude/projects/-srv-proj/e9022006-4f4a-5a45-a27b-6623a05d1ab4.jsonl","source_line":2,"recovered":""}
{"timestamp_utc":"2026-10-27T13:31:05.604Z","host":"wsdev14","user":"alice","agent":"claude-code","session_id":"e9022006-4f4a-5a45-a27b-6623a05d1ab4","project_path":"/srv/proj","git_branch":"main","turn_type":"system","model":"","tool_name":"","tool_use_id":"","text":"session surface: sdk-cli","source_file":"/home/alice/.claude/projects/-srv-proj/e9022006-4f4a-5a45-a27b-6623a05d1ab4.jsonl","source_line":3,"recovered":""}
{"timestamp_utc":"2026-10-27T13:31:05.604Z","host":"wsdev14","user":"alice","agent":"claude-code","session_id":"e9022006-4f4a-5a45-a27b-6623a05d1ab4","project_path":"/srv/proj","git_branch":"main","turn_type":"system","model":"","tool_name":"","tool_use_id":"","text":"session origin: remote (Remote Control, entrypoint sdk-cli)","source_file":"/home/alice/.claude/projects/-srv-proj/e9022006-4f4a-5a45-a27b-6623a05d1ab4.jsonl","source_line":3,"recovered":""}
{"timestamp_utc":"2026-10-27T13:31:05.604Z","host":"wsdev14","user":"alice","agent":"claude-code","session_id":"e9022006-4f4a-5a45-a27b-6623a05d1ab4","project_path":"/srv/proj","git_branch":"main","turn_type":"system","model":"","tool_name":"","tool_use_id":"","text":"permission mode: default","source_file":"/home/alice/.claude/projects/-srv-proj/e9022006-4f4a-5a45-a27b-6623a05d1ab4.jsonl","source_line":3,"recovered":""}
{"timestamp_utc":"2026-10-27T13:31:05.604Z","host":"wsdev14","user":"alice","agent":"claude-code","session_id":"e9022006-4f4a-5a45-a27b-6623a05d1ab4","project_path":"/srv/proj","git_branch":"main","turn_type":"user","model":"","tool_name":"","tool_use_id":"","text":"I pulled main at lunch. Run the whole test suite and tell me whether anything outside the invoice package fails.","source_file":"/home/alice/.claude/projects/-srv-proj/e9022006-4f4a-5a45-a27b-6623a05d1ab4.jsonl","source_line":3,"recovered":""}
{"timestamp_utc":"2026-10-27T13:31:05.604Z","host":"wsdev14","user":"alice","agent":"claude-code","session_id":"e9022006-4f4a-5a45-a27b-6623a05d1ab4","project_path":"/srv/proj","git_branch":"main","turn_type":"system","model":"","tool_name":"","tool_use_id":"","text":"prompt trigger: promptSource=sdk turnOrigin=human","source_file":"/home/alice/.claude/projects/-srv-proj/e9022006-4f4a-5a45-a27b-6623a05d1ab4.jsonl","source_line":3,"recovered":""}
{"timestamp_utc":"2026-10-27T13:31:10.877Z","host":"wsdev14","user":"alice","agent":"claude-code","session_id":"e9022006-4f4a-5a45-a27b-6623a05d1ab4","project_path":"/srv/proj","git_branch":"main","turn_type":"system","model":"claude-fable-5-1","tool_name":"","tool_use_id":"","text":"model: claude-fable-5-1","source_file":"/home/alice/.claude/projects/-srv-proj/e9022006-4f4a-5a45-a27b-6623a05d1ab4.jsonl","source_line":4,"recovered":""}
{"timestamp_utc":"2026-10-27T13:31:10.877Z","host":"wsdev14","user":"alice","agent":"claude-code","session_id":"e9022006-4f4a-5a45-a27b-6623a05d1ab4","project_path":"/srv/proj","git_branch":"main","turn_type":"assistant","model":"claude-fable-5-1","tool_name":"","tool_use_id":"","text":"I'll run the whole suite with go test from the repository root.","source_file":"/home/alice/.claude/projects/-srv-proj/e9022006-4f4a-5a45-a27b-6623a05d1ab4.jsonl","source_line":4,"recovered":""}
{"timestamp_utc":"2026-10-27T13:31:11.240Z","host":"wsdev14","user":"alice","agent":"claude-code","session_id":"e9022006-4f4a-5a45-a27b-6623a05d1ab4","project_path":"/srv/proj","git_branch":"main","turn_type":"tool_use","model":"claude-fable-5-1","tool_name":"Bash","tool_use_id":"toolu_01MEroK0ktBdtn9G7jU7wt3e","text":"go test ./...","source_file":"/home/alice/.claude/projects/-srv-proj/e9022006-4f4a-5a45-a27b-6623a05d1ab4.jsonl","source_line":5,"recovered":""}
{"timestamp_utc":"2026-10-27T13:31:39.065Z","host":"wsdev14","user":"alice","agent":"claude-code","session_id":"e9022006-4f4a-5a45-a27b-6623a05d1ab4","project_path":"/srv/proj","git_branch":"main","turn_type":"tool_result","model":"","tool_name":"","tool_use_id":"toolu_01MEroK0ktBdtn9G7jU7wt3e","text":"[error] Exit code 1\nok \tgithub.com/acme--ledger/ledger/cmd/ledger\t0.021s\nok \tgithub.com/acme--ledger/ledger/internal/api\t0.184s\n--- FAIL: TestExportCSV_Amounts (0.00s)\n csv_test.go:42: row 2 amount = \"12.50 \", want \"12.50\"\nFAIL\nFAIL\tgithub.com/acme--ledger/ledger/internal/export\t0.009s\n--- FAIL: TestRoundCents (0.00s)\n --- FAIL: TestRoundCents/half_cent_up (0.00s)\n round_test.go:31: RoundCents(1.005) = 100, want 101\n --- FAIL: TestRoundCents/negative_refund (0.00s)\n round_test.go:31: RoundCents(-1.005) = -100, want -101\nFAIL\nFAIL\tgithub.com/acme--ledger/ledger/internal/invoice\t0.012s\nok \tgithub.com/acme--ledger/ledger/internal/store\t0.311s\nFAIL","source_file":"/home/alice/.claude/projects/-srv-proj/e9022006-4f4a-5a45-a27b-6623a05d1ab4.jsonl","source_line":6,"recovered":""}
{"timestamp_utc":"2026-10-27T13:31:39.065Z","host":"wsdev14","user":"alice","agent":"claude-code","session_id":"e9022006-4f4a-5a45-a27b-6623a05d1ab4","project_path":"/srv/proj","git_branch":"main","turn_type":"system","model":"","tool_name":"","tool_use_id":"toolu_01MEroK0ktBdtn9G7jU7wt3e","text":"permission decision: accept source=config reason=rule {\"decision\":\"accept\",\"reasonType\":\"rule\",\"ruleSource\":\"userSettings\",\"source\":\"config\"}","source_file":"/home/alice/.claude/projects/-srv-proj/e9022006-4f4a-5a45-a27b-6623a05d1ab4.jsonl","source_line":6,"recovered":""}
{"timestamp_utc":"2026-10-27T13:31:47.000Z","host":"wsdev14","user":"alice","agent":"claude-code","session_id":"e9022006-4f4a-5a45-a27b-6623a05d1ab4","project_path":"","git_branch":"","turn_type":"system","model":"","tool_name":"","tool_use_id":"","text":"session synced: remote event 4e425611-e14e-462e-b7e0-8b3ee2ffefa6 at 2026-10-27T13:31:47.918Z (from .claude/projects/-srv-proj/e9022006-4f4a-5a45-a27b-6623a05d1ab4/ccr-tip.json, file mtime)","source_file":"/home/alice/.claude/projects/-srv-proj/e9022006-4f4a-5a45-a27b-6623a05d1ab4/ccr-tip.json","source_line":1,"recovered":""}
{"timestamp_utc":"2026-10-27T13:31:47.392Z","host":"wsdev14","user":"alice","agent":"claude-code","session_id":"e9022006-4f4a-5a45-a27b-6623a05d1ab4","project_path":"/srv/proj","git_branch":"main","turn_type":"assistant","model":"claude-fable-5-1","tool_name":"","tool_use_id":"","text":"One package outside invoice fails: internal/export, where TestExportCSV_Amounts finds a trailing space in \"12.50 \". invoice still fails as it did on main this morning, and every other package passes.","source_file":"/home/alice/.claude/projects/-srv-proj/e9022006-4f4a-5a45-a27b-6623a05d1ab4.jsonl","source_line":7,"recovered":""}
{"timestamp_utc":"2026-10-27T13:58:40.000Z","host":"wsdev14","user":"bob","agent":"codex-cli","session_id":"","project_path":"","git_branch":"","turn_type":"system","model":"","tool_name":"","tool_use_id":"","text":"permission setting: approval_policy=on-request (from .codex/config.toml, file mtime)","source_file":"/home/bob/.codex/config.toml","source_line":0,"recovered":""}
{"timestamp_utc":"2026-10-27T13:58:40.000Z","host":"wsdev14","user":"bob","agent":"codex-cli","session_id":"","project_path":"","git_branch":"","turn_type":"system","model":"","tool_name":"","tool_use_id":"","text":"permission setting: sandbox_mode=workspace-write (from .codex/config.toml, file mtime)","source_file":"/home/bob/.codex/config.toml","source_line":0,"recovered":""}
{"timestamp_utc":"2026-10-27T13:58:40.000Z","host":"wsdev14","user":"bob","agent":"codex-cli","session_id":"","project_path":"/srv/proj","git_branch":"","turn_type":"system","model":"","tool_name":"","tool_use_id":"","text":"permission setting: projects./srv/proj.trust_level=trusted (from .codex/config.toml, file mtime)","source_file":"/home/bob/.codex/config.toml","source_line":0,"recovered":""}
{"timestamp_utc":"2026-10-27T14:02:11.204Z","host":"wsdev14","user":"bob","agent":"codex-cli","session_id":"01a17dab-2784-70e1-8cd3-70e4412754b8","project_path":"/srv/proj","git_branch":"main","turn_type":"system","model":"","tool_name":"","tool_use_id":"","text":"session start: codex-tui 0.162.1 provider=openai cwd=/srv/proj account=32420289-a417-4e0c-be42-02da2bd8ecd9 user=user-XNddso5bC4XCeUmoPz38FuQm","source_file":"/home/bob/.codex/sessions/2026/10/27/rollout-2026-10-27T14-02-11-01a17dab-2784-70e1-8cd3-70e4412754b8.jsonl","source_line":1,"recovered":""}
{"timestamp_utc":"2026-10-27T14:02:11.204Z","host":"wsdev14","user":"bob","agent":"codex-cli","session_id":"01a17dab-2784-70e1-8cd3-70e4412754b8","project_path":"/srv/proj","git_branch":"main","turn_type":"system","model":"","tool_name":"","tool_use_id":"","text":"git remote: git@github.com:acme--ledger/ledger.git","source_file":"/home/bob/.codex/sessions/2026/10/27/rollout-2026-10-27T14-02-11-01a17dab-2784-70e1-8cd3-70e4412754b8.jsonl","source_line":1,"recovered":""}
{"timestamp_utc":"2026-10-27T14:02:11.204Z","host":"wsdev14","user":"bob","agent":"codex-cli","session_id":"01a17dab-2784-70e1-8cd3-70e4412754b8","project_path":"/srv/proj","git_branch":"main","turn_type":"system","model":"","tool_name":"","tool_use_id":"","text":"thread record: 01a17dab-2784-70e1-8cd3-70e4412754b8 title=TestExportCSV_Amounts fails because every amount ends with a space. Find where the CSV writer adds it and fix it. rollout=/home/bob/.codex/sessions/2026/10/27/rollout-2026-10-27T14-02-11-01a17dab-2784-70e1-8cd3-70e4412754b8.jsonl branch=main model=gpt-6.1-sol","source_file":"/home/bob/.codex/state_5.sqlite","source_line":1,"recovered":""}
{"timestamp_utc":"2026-10-27T14:02:19.880Z","host":"wsdev14","user":"bob","agent":"codex-cli","session_id":"01a17dab-2784-70e1-8cd3-70e4412754b8","project_path":"/srv/proj","git_branch":"main","turn_type":"system","model":"","tool_name":"","tool_use_id":"","text":"task_started turn=01a17dab-4968-7fa8-b1b2-2d507731075d","source_file":"/home/bob/.codex/sessions/2026/10/27/rollout-2026-10-27T14-02-11-01a17dab-2784-70e1-8cd3-70e4412754b8.jsonl","source_line":2,"recovered":""}
{"timestamp_utc":"2026-10-27T14:02:19.881Z","host":"wsdev14","user":"bob","agent":"codex-cli","session_id":"01a17dab-2784-70e1-8cd3-70e4412754b8","project_path":"/srv/proj","git_branch":"main","turn_type":"system","model":"gpt-6.1-sol","tool_name":"","tool_use_id":"","text":"model: gpt-6.1-sol","source_file":"/home/bob/.codex/sessions/2026/10/27/rollout-2026-10-27T14-02-11-01a17dab-2784-70e1-8cd3-70e4412754b8.jsonl","source_line":3,"recovered":""}
{"timestamp_utc":"2026-10-27T14:02:19.881Z","host":"wsdev14","user":"bob","agent":"codex-cli","session_id":"01a17dab-2784-70e1-8cd3-70e4412754b8","project_path":"/srv/proj","git_branch":"main","turn_type":"system","model":"gpt-6.1-sol","tool_name":"","tool_use_id":"","text":"permission mode: approval=on-request sandbox={\"type\":\"workspace-write\"} profile=- network=- reviewer=- profile_id=-","source_file":"/home/bob/.codex/sessions/2026/10/27/rollout-2026-10-27T14-02-11-01a17dab-2784-70e1-8cd3-70e4412754b8.jsonl","source_line":3,"recovered":""}
{"timestamp_utc":"2026-10-27T14:02:19.902Z","host":"wsdev14","user":"bob","agent":"codex-cli","session_id":"01a17dab-2784-70e1-8cd3-70e4412754b8","project_path":"/srv/proj","git_branch":"main","turn_type":"system","model":"gpt-6.1-sol","tool_name":"","tool_use_id":"","text":"developer: <permissions instructions>\nApprovals are on request. The sandbox lets you write inside the workspace only; ask before anything else.\n</permissions instructions>","source_file":"/home/bob/.codex/sessions/2026/10/27/rollout-2026-10-27T14-02-11-01a17dab-2784-70e1-8cd3-70e4412754b8.jsonl","source_line":4,"recovered":""}
{"timestamp_utc":"2026-10-27T14:02:19.903Z","host":"wsdev14","user":"bob","agent":"codex-cli","session_id":"01a17dab-2784-70e1-8cd3-70e4412754b8","project_path":"/srv/proj","git_branch":"main","turn_type":"system","model":"gpt-6.1-sol","tool_name":"","tool_use_id":"","text":"context: environments.environment_context: <environment_context>\n <cwd>/srv/proj</cwd>\n <approval_policy>on-request</approval_policy>\n <sandbox_mode>workspace-write</sandbox_mode>\n <network_access>restricted</network_access>\n <shell>bash</shell>\n</environment_context>","source_file":"/home/bob/.codex/sessions/2026/10/27/rollout-2026-10-27T14-02-11-01a17dab-2784-70e1-8cd3-70e4412754b8.jsonl","source_line":5,"recovered":""}
{"timestamp_utc":"2026-10-27T14:02:19.903Z","host":"wsdev14","user":"bob","agent":"codex-cli","session_id":"01a17dab-2784-70e1-8cd3-70e4412754b8","project_path":"/srv/proj","git_branch":"main","turn_type":"user","model":"gpt-6.1-sol","tool_name":"","tool_use_id":"","text":"TestExportCSV_Amounts fails because every amount ends with a space. Find where the CSV writer adds it and fix it.","source_file":"/home/bob/.codex/sessions/2026/10/27/rollout-2026-10-27T14-02-11-01a17dab-2784-70e1-8cd3-70e4412754b8.jsonl","source_line":6,"recovered":""}
{"timestamp_utc":"2026-10-27T14:02:24.315Z","host":"wsdev14","user":"bob","agent":"codex-cli","session_id":"01a17dab-2784-70e1-8cd3-70e4412754b8","project_path":"/srv/proj","git_branch":"main","turn_type":"tool_use","model":"gpt-6.1-sol","tool_name":"exec_command","tool_use_id":"call_WDWjD05lkBrmhyYL86B7VP4B","text":"{\"cmd\":\"go test ./internal/export/\"}","source_file":"/home/bob/.codex/sessions/2026/10/27/rollout-2026-10-27T14-02-11-01a17dab-2784-70e1-8cd3-70e4412754b8.jsonl","source_line":8,"recovered":""}
{"timestamp_utc":"2026-10-27T14:02:26.019Z","host":"wsdev14","user":"bob","agent":"codex-cli","session_id":"01a17dab-2784-70e1-8cd3-70e4412754b8","project_path":"/srv/proj","git_branch":"main","turn_type":"tool_result","model":"gpt-6.1-sol","tool_name":"","tool_use_id":"call_WDWjD05lkBrmhyYL86B7VP4B","text":"[exit 1] --- FAIL: TestExportCSV_Amounts (0.00s)\n csv_test.go:42: row 2 amount = \"12.50 \", want \"12.50\"\nFAIL\nFAIL\tgithub.com/acme--ledger/ledger/internal/export\t0.009s\nFAIL","source_file":"/home/bob/.codex/sessions/2026/10/27/rollout-2026-10-27T14-02-11-01a17dab-2784-70e1-8cd3-70e4412754b8.jsonl","source_line":9,"recovered":""}
{"timestamp_utc":"2026-10-27T14:02:31.442Z","host":"wsdev14","user":"bob","agent":"codex-cli","session_id":"01a17dab-2784-70e1-8cd3-70e4412754b8","project_path":"/srv/proj","git_branch":"main","turn_type":"tool_use","model":"gpt-6.1-sol","tool_name":"exec_command","tool_use_id":"call_sXYp6mAX457CEaMFZQO0eex4","text":"{\"cmd\":\"grep -n '%.2f ' internal/export/csv.go\"}","source_file":"/home/bob/.codex/sessions/2026/10/27/rollout-2026-10-27T14-02-11-01a17dab-2784-70e1-8cd3-70e4412754b8.jsonl","source_line":11,"recovered":""}
{"timestamp_utc":"2026-10-27T14:02:31.497Z","host":"wsdev14","user":"bob","agent":"codex-cli","session_id":"01a17dab-2784-70e1-8cd3-70e4412754b8","project_path":"/srv/proj","git_branch":"main","turn_type":"tool_result","model":"gpt-6.1-sol","tool_name":"","tool_use_id":"call_sXYp6mAX457CEaMFZQO0eex4","text":"57:\t\tfmt.Fprintf(w, \"%s,%.2f ,%s\\n\", row.Date, row.Amount, row.Currency)","source_file":"/home/bob/.codex/sessions/2026/10/27/rollout-2026-10-27T14-02-11-01a17dab-2784-70e1-8cd3-70e4412754b8.jsonl","source_line":12,"recovered":""}
{"timestamp_utc":"2026-10-27T14:02:38.760Z","host":"wsdev14","user":"bob","agent":"codex-cli","session_id":"01a17dab-2784-70e1-8cd3-70e4412754b8","project_path":"/srv/proj","git_branch":"main","turn_type":"tool_use","model":"gpt-6.1-sol","tool_name":"apply_patch","tool_use_id":"call_cc0jc1nYnX6N0XgjsVSHZTSh","text":"*** Begin Patch\n*** Update File: internal/export/csv.go\n@@ func writeRows(w io.Writer, rows []Row) error {\n-\t\tfmt.Fprintf(w, \"%s,%.2f ,%s\\n\", row.Date, row.Amount, row.Currency)\n+\t\tfmt.Fprintf(w, \"%s,%.2f,%s\\n\", row.Date, row.Amount, row.Currency)\n*** End Patch","source_file":"/home/bob/.codex/sessions/2026/10/27/rollout-2026-10-27T14-02-11-01a17dab-2784-70e1-8cd3-70e4412754b8.jsonl","source_line":14,"recovered":""}
{"timestamp_utc":"2026-10-27T14:02:38.812Z","host":"wsdev14","user":"bob","agent":"codex-cli","session_id":"01a17dab-2784-70e1-8cd3-70e4412754b8","project_path":"/srv/proj","git_branch":"main","turn_type":"tool_result","model":"gpt-6.1-sol","tool_name":"","tool_use_id":"call_cc0jc1nYnX6N0XgjsVSHZTSh","text":"Success. Updated the following files:\nM internal/export/csv.go","source_file":"/home/bob/.codex/sessions/2026/10/27/rollout-2026-10-27T14-02-11-01a17dab-2784-70e1-8cd3-70e4412754b8.jsonl","source_line":15,"recovered":""}
{"timestamp_utc":"2026-10-27T14:02:41.106Z","host":"wsdev14","user":"bob","agent":"codex-cli","session_id":"01a17dab-2784-70e1-8cd3-70e4412754b8","project_path":"/srv/proj","git_branch":"main","turn_type":"tool_use","model":"gpt-6.1-sol","tool_name":"exec_command","tool_use_id":"call_vHYKQRDN6tj2iHBo3o6NuhJw","text":"{\"cmd\":\"go test ./internal/export/\"}","source_file":"/home/bob/.codex/sessions/2026/10/27/rollout-2026-10-27T14-02-11-01a17dab-2784-70e1-8cd3-70e4412754b8.jsonl","source_line":17,"recovered":""}
{"timestamp_utc":"2026-10-27T14:02:42.731Z","host":"wsdev14","user":"bob","agent":"codex-cli","session_id":"01a17dab-2784-70e1-8cd3-70e4412754b8","project_path":"/srv/proj","git_branch":"main","turn_type":"tool_result","model":"gpt-6.1-sol","tool_name":"","tool_use_id":"call_vHYKQRDN6tj2iHBo3o6NuhJw","text":"ok \tgithub.com/acme--ledger/ledger/internal/export\t0.008s","source_file":"/home/bob/.codex/sessions/2026/10/27/rollout-2026-10-27T14-02-11-01a17dab-2784-70e1-8cd3-70e4412754b8.jsonl","source_line":18,"recovered":""}
{"timestamp_utc":"2026-10-27T14:02:44.512Z","host":"wsdev14","user":"bob","agent":"codex-cli","session_id":"01a17dab-2784-70e1-8cd3-70e4412754b8","project_path":"/srv/proj","git_branch":"main","turn_type":"tool_use","model":"gpt-6.1-sol","tool_name":"exec_command","tool_use_id":"call_oATZgmFjw0MwWSeqAcPT9Uo3","text":"{\"cmd\":\"git commit -am 'Drop the trailing space from CSV amounts'\"}","source_file":"/home/bob/.codex/sessions/2026/10/27/rollout-2026-10-27T14-02-11-01a17dab-2784-70e1-8cd3-70e4412754b8.jsonl","source_line":20,"recovered":""}
{"timestamp_utc":"2026-10-27T14:02:44.690Z","host":"wsdev14","user":"bob","agent":"codex-cli","session_id":"01a17dab-2784-70e1-8cd3-70e4412754b8","project_path":"/srv/proj","git_branch":"main","turn_type":"tool_result","model":"gpt-6.1-sol","tool_name":"","tool_use_id":"call_oATZgmFjw0MwWSeqAcPT9Uo3","text":"[main 8953e04] Drop the trailing space from CSV amounts\n 1 file changed, 1 insertion(+), 1 deletion(-)","source_file":"/home/bob/.codex/sessions/2026/10/27/rollout-2026-10-27T14-02-11-01a17dab-2784-70e1-8cd3-70e4412754b8.jsonl","source_line":21,"recovered":""}
{"timestamp_utc":"2026-10-27T14:02:47.255Z","host":"wsdev14","user":"bob","agent":"codex-cli","session_id":"01a17dab-2784-70e1-8cd3-70e4412754b8","project_path":"/srv/proj","git_branch":"main","turn_type":"assistant","model":"gpt-6.1-sol","tool_name":"","tool_use_id":"","text":"The CSV writer's format string had a space after `%.2f`, so every amount ended with one. I removed it in internal/export/csv.go, committed that on main, and the export tests pass.","source_file":"/home/bob/.codex/sessions/2026/10/27/rollout-2026-10-27T14-02-11-01a17dab-2784-70e1-8cd3-70e4412754b8.jsonl","source_line":23,"recovered":""}
{"timestamp_utc":"2026-10-27T14:02:47.301Z","host":"wsdev14","user":"bob","agent":"codex-cli","session_id":"01a17dab-2784-70e1-8cd3-70e4412754b8","project_path":"/srv/proj","git_branch":"main","turn_type":"system","model":"gpt-6.1-sol","tool_name":"","tool_use_id":"","text":"task_complete turn=01a17dab-4968-7fa8-b1b2-2d507731075d duration_ms=27421","source_file":"/home/bob/.codex/sessions/2026/10/27/rollout-2026-10-27T14-02-11-01a17dab-2784-70e1-8cd3-70e4412754b8.jsonl","source_line":24,"recovered":""}
{"timestamp_utc":"2026-10-27T16:40:02.118Z","host":"wsdev14","user":"alice","agent":"claude-code","session_id":"99b95ab5-5229-41e2-bc47-dcba1398c74b","project_path":"/srv/proj","git_branch":"main","turn_type":"system","model":"","tool_name":"","tool_use_id":"","text":"session surface: cli","source_file":"/home/alice/.claude/projects/-srv-proj/99b95ab5-5229-41e2-bc47-dcba1398c74b.jsonl","source_line":1,"recovered":""}
{"timestamp_utc":"2026-10-27T16:40:02.118Z","host":"wsdev14","user":"alice","agent":"claude-code","session_id":"99b95ab5-5229-41e2-bc47-dcba1398c74b","project_path":"/srv/proj","git_branch":"main","turn_type":"system","model":"","tool_name":"","tool_use_id":"","text":"session origin: remote (teleport, cloud scratchpad /tmp/claude-0/-home-user-repo/8c75ab24-50a1-4181-8625-11f05dca893b/scratchpad)","source_file":"/home/alice/.claude/projects/-srv-proj/99b95ab5-5229-41e2-bc47-dcba1398c74b.jsonl","source_line":1,"recovered":""}
{"timestamp_utc":"2026-10-27T16:40:02.250Z","host":"wsdev14","user":"alice","agent":"claude-code","session_id":"99b95ab5-5229-41e2-bc47-dcba1398c74b","project_path":"/srv/proj","git_branch":"main","turn_type":"system","model":"","tool_name":"","tool_use_id":"","text":"session origin: remote (teleport, remoteSourced)","source_file":"/home/alice/.claude/projects/-srv-proj/99b95ab5-5229-41e2-bc47-dcba1398c74b.jsonl","source_line":2,"recovered":""}
{"timestamp_utc":"2026-10-27T16:40:02.250Z","host":"wsdev14","user":"alice","agent":"claude-code","session_id":"99b95ab5-5229-41e2-bc47-dcba1398c74b","project_path":"/srv/proj","git_branch":"main","turn_type":"system","model":"","tool_name":"","tool_use_id":"","text":"session synced: cloud session (teleported: remoteSourced records replayed from the vendor's copy)","source_file":"/home/alice/.claude/projects/-srv-proj/99b95ab5-5229-41e2-bc47-dcba1398c74b.jsonl","source_line":2,"recovered":""}
{"timestamp_utc":"2026-10-27T16:40:02.250Z","host":"wsdev14","user":"alice","agent":"claude-code","session_id":"99b95ab5-5229-41e2-bc47-dcba1398c74b","project_path":"/srv/proj","git_branch":"main","turn_type":"system","model":"","tool_name":"","tool_use_id":"","text":"permission mode: default","source_file":"/home/alice/.claude/projects/-srv-proj/99b95ab5-5229-41e2-bc47-dcba1398c74b.jsonl","source_line":2,"recovered":""}
{"timestamp_utc":"2026-10-27T16:40:02.250Z","host":"wsdev14","user":"alice","agent":"claude-code","session_id":"99b95ab5-5229-41e2-bc47-dcba1398c74b","project_path":"/srv/proj","git_branch":"main","turn_type":"user","model":"","tool_name":"","tool_use_id":"","text":"Read the README and list the make targets it documents, one line each.","source_file":"/home/alice/.claude/projects/-srv-proj/99b95ab5-5229-41e2-bc47-dcba1398c74b.jsonl","source_line":2,"recovered":""}
{"timestamp_utc":"2026-10-27T16:40:02.250Z","host":"wsdev14","user":"alice","agent":"claude-code","session_id":"99b95ab5-5229-41e2-bc47-dcba1398c74b","project_path":"/srv/proj","git_branch":"main","turn_type":"system","model":"","tool_name":"","tool_use_id":"","text":"prompt trigger: promptSource=sdk turnOrigin=human","source_file":"/home/alice/.claude/projects/-srv-proj/99b95ab5-5229-41e2-bc47-dcba1398c74b.jsonl","source_line":2,"recovered":""}
{"timestamp_utc":"2026-10-27T16:40:06.913Z","host":"wsdev14","user":"alice","agent":"claude-code","session_id":"99b95ab5-5229-41e2-bc47-dcba1398c74b","project_path":"/srv/proj","git_branch":"main","turn_type":"system","model":"claude-opus-5-5","tool_name":"","tool_use_id":"","text":"model: claude-opus-5-5","source_file":"/home/alice/.claude/projects/-srv-proj/99b95ab5-5229-41e2-bc47-dcba1398c74b.jsonl","source_line":3,"recovered":""}
{"timestamp_utc":"2026-10-27T16:40:06.913Z","host":"wsdev14","user":"alice","agent":"claude-code","session_id":"99b95ab5-5229-41e2-bc47-dcba1398c74b","project_path":"/srv/proj","git_branch":"main","turn_type":"tool_use","model":"claude-opus-5-5","tool_name":"Read","tool_use_id":"toolu_01nkG02qOBC3oBDdZhc4Ullu","text":"/home/user/repo/README.md","source_file":"/home/alice/.claude/projects/-srv-proj/99b95ab5-5229-41e2-bc47-dcba1398c74b.jsonl","source_line":3,"recovered":""}
{"timestamp_utc":"2026-10-27T16:40:07.020Z","host":"wsdev14","user":"alice","agent":"claude-code","session_id":"99b95ab5-5229-41e2-bc47-dcba1398c74b","project_path":"/srv/proj","git_branch":"main","turn_type":"tool_result","model":"","tool_name":"","tool_use_id":"toolu_01nkG02qOBC3oBDdZhc4Ullu","text":"1\t# ledger\n 2\t\n 3\tThe billing team's invoice service: it records invoices and exports them as CSV.\n 4\t\n 5\t## Make targets\n 6\t\n 7\t- `make build` compiles the service into `bin/ledger`.\n 8\t- `make test` runs `go vet` and the unit tests.\n 9\t- `make lint` runs gofmt and the linter.\n 10\t- `make migrate` applies the migrations in `migrations/`.","source_file":"/home/alice/.claude/projects/-srv-proj/99b95ab5-5229-41e2-bc47-dcba1398c74b.jsonl","source_line":4,"recovered":""}
{"timestamp_utc":"2026-10-27T16:40:07.020Z","host":"wsdev14","user":"alice","agent":"claude-code","session_id":"99b95ab5-5229-41e2-bc47-dcba1398c74b","project_path":"/srv/proj","git_branch":"main","turn_type":"system","model":"","tool_name":"","tool_use_id":"toolu_01nkG02qOBC3oBDdZhc4Ullu","text":"permission decision: accept source=config reason=rule {\"decision\":\"accept\",\"reasonType\":\"rule\",\"ruleSource\":\"cliArg\",\"source\":\"config\"}","source_file":"/home/alice/.claude/projects/-srv-proj/99b95ab5-5229-41e2-bc47-dcba1398c74b.jsonl","source_line":4,"recovered":""}
{"timestamp_utc":"2026-10-27T16:40:13.587Z","host":"wsdev14","user":"alice","agent":"claude-code","session_id":"99b95ab5-5229-41e2-bc47-dcba1398c74b","project_path":"/srv/proj","git_branch":"main","turn_type":"assistant","model":"claude-opus-5-5","tool_name":"","tool_use_id":"","text":"The README documents four targets: `make build` compiles bin/ledger, `make test` runs go vet and the unit tests, `make lint` runs gofmt and the linter, and `make migrate` applies the migrations.","source_file":"/home/alice/.claude/projects/-srv-proj/99b95ab5-5229-41e2-bc47-dcba1398c74b.jsonl","source_line":5,"recovered":""}
{"timestamp_utc":"2026-10-27T16:40:13.640Z","host":"wsdev14","user":"alice","agent":"claude-code","session_id":"99b95ab5-5229-41e2-bc47-dcba1398c74b","project_path":"/srv/proj","git_branch":"main","turn_type":"system","model":"","tool_name":"","tool_use_id":"","text":"stop_hook_summary: {\"hasOutput\":false,\"hookAdditionalContext\":[],\"hookCount\":1,\"hookErrors\":[],\"hookInfos\":[],\"level\":\"info\",\"preventedContinuation\":false,\"stopReason\":\"\",\"toolUseID\":\"\"}","source_file":"/home/alice/.claude/projects/-srv-proj/99b95ab5-5229-41e2-bc47-dcba1398c74b.jsonl","source_line":6,"recovered":""}
{"timestamp_utc":"2026-10-27T16:55:40.871Z","host":"wsdev14","user":"alice","agent":"claude-code","session_id":"99b95ab5-5229-41e2-bc47-dcba1398c74b","project_path":"/srv/proj","git_branch":"","turn_type":"system","model":"","tool_name":"","tool_use_id":"","text":"claude-code process 52904 interactive status=idle entrypoint=cli bridge=none host_session=none","source_file":"/home/alice/.claude/sessions/52904.json","source_line":1,"recovered":""}
{"timestamp_utc":"2026-10-27T16:55:41.006Z","host":"wsdev14","user":"alice","agent":"claude-code","session_id":"99b95ab5-5229-41e2-bc47-dcba1398c74b","project_path":"/srv/proj","git_branch":"main","turn_type":"system","model":"","tool_name":"","tool_use_id":"","text":"informational: Session resumed","source_file":"/home/alice/.claude/projects/-srv-proj/99b95ab5-5229-41e2-bc47-dcba1398c74b.jsonl","source_line":7,"recovered":""}
{"timestamp_utc":"2026-10-27T16:55:41.212Z","host":"wsdev14","user":"alice","agent":"claude-code","session_id":"99b95ab5-5229-41e2-bc47-dcba1398c74b","project_path":"/tmp/claude-1000/-srv-proj/99b95ab5-5229-41e2-bc47-dcba1398c74b/scratchpad","git_branch":"main","turn_type":"system","model":"","tool_name":"","tool_use_id":"","text":"scratch directory: /tmp/claude-1000/-srv-proj/99b95ab5-5229-41e2-bc47-dcba1398c74b/scratchpad (session scratchpad, from the environment attachment)","source_file":"/home/alice/.claude/projects/-srv-proj/99b95ab5-5229-41e2-bc47-dcba1398c74b.jsonl","source_line":8,"recovered":""}
{"timestamp_utc":"2026-10-27T16:56:02.000Z","host":"wsdev14","user":"alice","agent":"claude-code","session_id":"","project_path":"/srv/proj","git_branch":"","turn_type":"system","model":"","tool_name":"","tool_use_id":"","text":"permission setting: allowedTools=Bash(go vet:*) (from .claude.json, file mtime)","source_file":"/home/alice/.claude.json","source_line":1,"recovered":""}
{"timestamp_utc":"2026-10-27T16:56:02.000Z","host":"wsdev14","user":"alice","agent":"claude-code","session_id":"","project_path":"/srv/proj","git_branch":"","turn_type":"system","model":"","tool_name":"","tool_use_id":"","text":"permission setting: hasTrustDialogAccepted=true (from .claude.json, file mtime)","source_file":"/home/alice/.claude.json","source_line":1,"recovered":""}
{"timestamp_utc":"2026-10-27T16:56:02.000Z","host":"wsdev14","user":"alice","agent":"claude-code","session_id":"","project_path":"/srv/proj","git_branch":"","turn_type":"system","model":"","tool_name":"","tool_use_id":"","text":"remote control: spawn mode same-dir for /srv/proj (from .claude.json, file mtime)","source_file":"/home/alice/.claude.json","source_line":1,"recovered":""}
{"timestamp_utc":"2026-10-27T16:56:02.000Z","host":"wsdev14","user":"alice","agent":"claude-code","session_id":"","project_path":"/srv/invoicing-old","git_branch":"","turn_type":"system","model":"","tool_name":"","tool_use_id":"","text":"permission setting: hasTrustDialogAccepted=true (from .claude.json, file mtime)","source_file":"/home/alice/.claude.json","source_line":1,"recovered":""}
{"timestamp_utc":"2026-10-27T16:56:02.000Z","host":"wsdev14","user":"alice","agent":"claude-code","session_id":"","project_path":"","git_branch":"","turn_type":"system","model":"","tool_name":"","tool_use_id":"","text":"source host: github status=connected (from .claude.json, file mtime)","source_file":"/home/alice/.claude.json","source_line":1,"recovered":""}
{"timestamp_utc":"2026-10-27T16:56:02.000Z","host":"wsdev14","user":"alice","agent":"claude-code","session_id":"","project_path":"","git_branch":"","turn_type":"system","model":"","tool_name":"","tool_use_id":"","text":"remote control: machine id set (from .claude.json, file mtime)","source_file":"/home/alice/.claude.json","source_line":1,"recovered":""}
{"timestamp_utc":"2026-10-27T16:56:02.000Z","host":"wsdev14","user":"alice","agent":"claude-code","session_id":"","project_path":"","git_branch":"","turn_type":"system","model":"","tool_name":"","tool_use_id":"","text":"cloud: hasRemoteEnvironment=true (from .claude.json, file mtime)","source_file":"/home/alice/.claude.json","source_line":1,"recovered":""}
{"timestamp_utc":"2026-10-27T16:56:02.377Z","host":"wsdev14","user":"alice","agent":"claude-code","session_id":"99b95ab5-5229-41e2-bc47-dcba1398c74b","project_path":"/srv/proj","git_branch":"main","turn_type":"system","model":"","tool_name":"","tool_use_id":"","text":"permission mode: default -> acceptEdits","source_file":"/home/alice/.claude/projects/-srv-proj/99b95ab5-5229-41e2-bc47-dcba1398c74b.jsonl","source_line":9,"recovered":""}
{"timestamp_utc":"2026-10-27T16:56:02.377Z","host":"wsdev14","user":"alice","agent":"claude-code","session_id":"99b95ab5-5229-41e2-bc47-dcba1398c74b","project_path":"/srv/proj","git_branch":"main","turn_type":"user","model":"","tool_name":"","tool_use_id":"","text":"Which of those should I run before I push?","source_file":"/home/alice/.claude/projects/-srv-proj/99b95ab5-5229-41e2-bc47-dcba1398c74b.jsonl","source_line":9,"recovered":""}
{"timestamp_utc":"2026-10-27T16:56:08.940Z","host":"wsdev14","user":"alice","agent":"claude-code","session_id":"99b95ab5-5229-41e2-bc47-dcba1398c74b","project_path":"/srv/proj","git_branch":"main","turn_type":"system","model":"claude-fable-5-1","tool_name":"","tool_use_id":"","text":"model: claude-opus-5-5 -> claude-fable-5-1","source_file":"/home/alice/.claude/projects/-srv-proj/99b95ab5-5229-41e2-bc47-dcba1398c74b.jsonl","source_line":10,"recovered":""}
{"timestamp_utc":"2026-10-27T16:56:08.940Z","host":"wsdev14","user":"alice","agent":"claude-code","session_id":"99b95ab5-5229-41e2-bc47-dcba1398c74b","project_path":"/srv/proj","git_branch":"main","turn_type":"assistant","model":"claude-fable-5-1","tool_name":"","tool_use_id":"","text":"Run `make test`, which AGENTS.md asks for before every commit, and `make lint`, which it wants clean. `make migrate` is only needed when you add a migration.","source_file":"/home/alice/.claude/projects/-srv-proj/99b95ab5-5229-41e2-bc47-dcba1398c74b.jsonl","source_line":10,"recovered":""}
{"timestamp_utc":"","host":"wsdev14","user":"alice","agent":"claude-code","session_id":"99b95ab5-5229-41e2-bc47-dcba1398c74b","project_path":"","git_branch":"","turn_type":"system","model":"","tool_name":"","tool_use_id":"","text":"parser: unparseable line (Unterminated string starting at: line 1 column 188 (char 187)): {\"parentUuid\":\"1d1bd4e2-9917-4fd6-9ae6-2f66c67c4187\",\"isSidechain\":false,\"type\":\"system\",\"subtype\":\"turn_duration\",\"durationMs\":6563,\"messageCount\":3,\"isMeta\":false,\"level\":\"info\",\"uuid\":\"","source_file":"/home/alice/.claude/projects/-srv-proj/99b95ab5-5229-41e2-bc47-dcba1398c74b.jsonl","source_line":11,"recovered":""}
{"timestamp_utc":"","host":"wsdev14","user":"alice","agent":"claude-code","session_id":"d4ccb1ca-a86b-4f26-9501-752de29ee24d","project_path":"/srv/proj","git_branch":"fix/invoice-rounding","turn_type":"system","model":"","tool_name":"","tool_use_id":"","text":"permission mode: acceptEdits","source_file":"/home/alice/.claude/projects/-srv-proj/d4ccb1ca-a86b-4f26-9501-752de29ee24d.jsonl","source_line":2,"recovered":""}
{"timestamp_utc":"","host":"wsdev14","user":"bob","agent":"ollama","session_id":"","project_path":"","git_branch":"","turn_type":"user","model":"","tool_name":"","tool_use_id":"","text":"how do I read a CSV file line by line in Go?","source_file":"/home/bob/.ollama/history","source_line":1,"recovered":""}
{"timestamp_utc":"","host":"wsdev14","user":"bob","agent":"ollama","session_id":"","project_path":"","git_branch":"","turn_type":"user","model":"","tool_name":"","tool_use_id":"","text":"what does %.2f do in fmt.Printf, and can it add a trailing space?","source_file":"/home/bob/.ollama/history","source_line":2,"recovered":""}
{"timestamp_utc":"","host":"wsdev14","user":"bob","agent":"ollama","session_id":"","project_path":"","git_branch":"","turn_type":"system","model":"","tool_name":"","tool_use_id":"","text":"slash command: /bye","source_file":"/home/bob/.ollama/history","source_line":3,"recovered":""}
Row order and system rows¶
Rows are sorted by timestamp, then by source file and line. Rows without a
timestamp sort last. Injected context is typed system, not user, so
the user rows are what the person typed. In Claude Code that is
isMeta records, task notifications, slash commands and their output, a
subagent's task, compaction summaries and messages from other agents. In
Codex it is developer messages and user-role blocks whose content kind
names harness context rather than user.* input, or, in older rollouts
without content kinds, a block that is wholly a harness wrapper such as
<environment_context>. These system rows start with a lowercase label
naming the source, such as subagent task:, task notification:,
slash command:, command output: or context: <kind>:, followed by the
full text. Each line a parser cannot decode, such as a transcript's last
line cut off mid-write, is reported as its own system row whose
source_line is that line's number and whose text is
parser: unparseable line (<reason>): <line>: <reason> is the
decoder's message and <line> is the whole line as read (decoded as
UTF-8 with undecodable bytes replaced, less its line terminator), with
nothing repaired, extracted or shortened. The rest of the file is still
read. A line that is valid JSON but not an object, in a format whose
lines are objects, is reported the same way as parser: unparseable line
(not a JSON object): <line> (since 0.19.0). A database row or an array
element that does not decode is parser: unparseable record <N>
(<reason>): <record>: <N> is the row's rowid (or the id the table
keys it by), the element's position or its index, <reason> names the
table and column when the parser knows them ((<table>.<column>:
<reason>), as in goose's messages.content_json or opencode's
part.data) and otherwise the decoder's message or not a JSON object,
and <record> is the stored value in full as text (a binary blob as
hex, as zed writes a zstd thread blob). Readers of JSON arrays use
the same form, with the record's text from its first character to the
end of the file, and a line that decodes but cannot be applied names what
failed, as VS Code's parser: mutation not applied (<reason>): <line>
does. An error that concerns the whole file rather than one line (the
file cannot be read, is not a JSON array, ends inside an array, is not a
SQLite database, or lacks the table the format keeps its records in) is
one system row parser: <message>, such as parser: unterminated JSON
array, parser: not a SQLite database, parser: SQLite error:
<error> or parser: no <table> table in the session database, with
the line number when the error has one and 0 otherwise.
Antigravity's permission state is also typed system. A grant recorded on
a step is permission grant: action=<action> target=<target>
allow=<true|false> scope=<scope>, or for a file access entry
permission grant: path=<path> is_directory=<true|false>
allow=<true|false> scope=<scope>; it follows the step's own rows and
carries the step's tool_name and tool_use_id, so it joins to the call
it governed. A standing grant from a settings file is
permission setting: <key>=<value> (from <file>, file mtime): its
timestamp_utc is the file's modification time, not the time of the
grant, which no file records, and its session_id is empty, so it
appears in timeline.jsonl but in no sessions.jsonl record. The
antigravity subsection of the agents page lists the
files.
Permission state and the model in use are system rows too. The
claude-code parser (and claude-desktop for the transcripts it reads
with the same reader) writes permission mode: <mode> at a session's
first permission mode and permission mode: <old> -> <new> on each
change, and model: <id> at the first assistant reply and
model: <old> -> <new> on each change, with requested=<id> when the
model asked for differs from the one that answered; the model column of
a model row is the new model. Each subagent transcript tracks its own.
Standing permission settings are one row per entry, permission setting:
<key>=<value> (from <file>, file mtime): permissions.allow, deny,
ask, additionalDirectories, defaultMode and
disableBypassPermissionsMode from .claude/settings.json and
.claude/settings.local.json (the user's, or a project's with
project_path the project), and allowedTools, hasTrustDialogAccepted
and hasClaudeMdExternalIncludesApproved per project from .claude.json
(with project_path the project key). Their timestamp_utc is the
file's modification time (the manifest mtime when there is a
manifest), not the time the entry was added, and their session_id is
empty. Two more rows come from the same files, timestamped the same way:
retention setting: cleanupPeriodDays=<n> (from <file>, file mtime)
after a settings file's permission rows, the number of days after which
Claude Code's retention sweep deletes a transcript, and source host:
github status=<status> (from .claude.json, file mtime) from the
githubWebConnectionStatusCache object of .claude.json (written by
Claude Code 2.1.310 once the GitHub app is connected; connected is the
value seen), with an empty project_path. The account and organization
UUIDs beside that status are not printed. A settings file's string model
gives model setting: model=<alias> (from <file>, file mtime) after its
retention row: the model alias (fable, opus) /model saves as the
default for new sessions, which the VS Code extension's start can also
rewrite, so it is the file's value when collected, not a session's model.
.claude.json adds, timestamped the same way, remote control: spawn mode
<mode> for <path> (from .claude.json, file mtime) for a project with a
saved Remote Control spawn mode (same-dir or worktree), with
project_path the project, then remote control: machine id set (from
.claude.json, file mtime) when Remote Control has been enabled on the
machine (the id is not printed) and cloud: hasRemoteEnvironment=<value>
(from .claude.json, file mtime) once a cloud session has been created
from the CLI, both with an empty project_path. A Claude Desktop session record adds permission_mode=<mode> effort=<effort> to its row and one permission grant: <key> <entry> or
permission grant: <key>=<value> row per session grant, timestamped at
the record's lastActivityAt (else createdAt), which the text names as
(as of <field>). A per-call decision stays permission decision:
(Claude Code) or permission request:, permission response:,
permission auto-approved: and permission auto-denied: (the Claude
Desktop audit log). On Claude Code 2.1.309 the decision object also has
grantScope (once on a one-time approval) and reasonCode (other,
unknown, subcommandResults, and on 2.1.310 bashMiss:no-rule-match
for a shell command that matched no allow rule), which the row carries in the compact
JSON after reason=, as permission decision: accept
source=user_temporary reason=-
{"decision":"accept","grantScope":"once","reasonCode":"other","source":"user_temporary"}.
Since 0.21.0 the claude-code parser (and claude-desktop for the
transcripts it reads with the same reader) also writes these system
rows from a session transcript:
| Row text | From | When |
|---|---|---|
session surface: <entrypoint>, then session surface: <old> -> <new> |
the entrypoint key of the file's records: cli (the terminal), claude-vscode (the VS Code extension), claude-desktop (the desktop app's Code tab) |
at the first record of each transcript file that carries it, and at each change |
prompt trigger: promptSource=<promptSource> turnOrigin=<turnOrigin> |
a prompt's promptSource and turnOrigin, - for a missing key |
after a user record's rows when promptSource is set and not typed or turnOrigin is set and not human: queued is a prompt sent from a Remote Control client such as the phone or one typed while a turn ran, sdk one from the VS Code extension, from claude.ai to a Remote Control spawn, or the cloud prompt of a teleported session, system a task notification, a background shell's completion notice or a /loop firing, suggestion_accepted a prompt Claude suggested, turnOrigin=scheduled a /loop firing, task_notification a background task's completion |
scratch directory: <path> (session scratchpad, from the environment attachment) |
snapshot.scratchpadDirectory of an environment attachment |
once per session and path, with project_path the scratchpad, so inventory.json lists it under scratch_directories |
session continued in: <continuedInSessionId> |
a continued-in record |
when the session daemon hands a console session to a worker session, whose own file holds the later turns |
remote control: bridge session <bridgeSessionId or none> lastSequenceNum=<n> |
a bridge-session record (Remote Control) |
when the bridge id or sequence number changes; none is the detach |
session synced: remote control bridge <bridgeSessionId> url <url> |
the first bridge-session record of a session that names a bridge id, and the url of the bridge_status system record that belongs to that bridge (among the status records between the previous and the next bridge-session record of another id, the first at or after it, else the nearest before, else -); also a .claude/sessions/<pid>.json registry file that names a bridge id, as ... url - (from <path>, file mtime) at the file's modification time |
once per session and bridge id: the bridge puts the session on claude.ai, so sessions.jsonl synced is yes for it and no for a claude-code session without one |
session synced: remote event <eventId> at <updatedAt> (from <path>, file mtime) |
a .claude/projects/<slug>/<session>/ccr-tip.json file (Claude Code Remote), the last claude.ai event the local side of a Remote Control session processed |
one row per file, at the file's modification time, with session_id the session directory's name: the session is synced yes |
session synced: cloud session (teleported: remoteSourced records replayed from the vendor's copy) |
the first remoteSourced record of a session, a turn of a cloud session replayed by claude --teleport |
beside the session's session origin: remote (teleport, remoteSourced) row; the conversation is held on Anthropic's servers, so the session reads synced yes, like a Remote Control spawn |
remote control: environment <environmentId> session <sessionId> source=<source> (from <path>, file mtime) |
a .claude/projects/<slug>/bridge-pointer.json file, the Remote Control environment enabled for the project, which stays after the bridge stops |
one row per file, at the file's modification time; sessionId is the bridge's, so session_id is empty |
session origin: remote (Remote Control, entrypoint sdk-cli) |
a transcript whose records have entrypoint sdk-cli and a prompt with promptSource sdk: a session started from claude.ai through claude remote-control |
at the session's first sdk-cli record: sessions.jsonl origin is remote |
session origin: remote (teleport, remoteSourced) |
a record with remoteSourced true: a turn of a cloud session replayed by claude --teleport |
at the session's first such record |
session origin: remote (teleport, cloud scratchpad <path>) |
an environment attachment on or before the file's last remoteSourced record, in the block a teleport replayed before the local process wrote its own records: the cloud machine's scratchpad, whatever its project directory name |
once per session and path, in place of a scratch directory: row, since the path is not on this host |
session origin: local (VS Code extension) |
a record with entrypoint claude-vscode |
once per session; a claude-code session without a session origin: remote row reads origin local |
scheduled task fired: <taskId> cron=<cron> |
a scheduled_task_fire system record (a /loop firing; - for a missing key) |
in place of the scheduled_task_fire: <content> row; the prompt it fires follows as a user row with prompt trigger: promptSource=system turnOrigin=scheduled |
Each takes its record's timestamp, or the latest one seen in the file for
a record with none (bridge-session). The owner account and organization
UUIDs of a bridge-session record are not printed. A /clear is not a
deletion row: it starts a new transcript file whose first records are the
/clear command and its output (slash command: and command output:
rows, or a local_command: row), and leaves the old file as it was,
ending with its cost-state line.
A remote URL an agent recorded for the repository it worked in is a
system row too, git remote: <url>, with the URL exactly as the agent
recorded it after the prefix; inventory.json lists the URLs of these rows
under each project's recorded_remotes. A parser writes it once per
session and URL, at the time of the record that names it (since 0.21.0):
codex-cliandopen-interpreter: the rollout'ssession_metagit.repository_url, right after thesession start:row, and thestate_5.sqlitethreads.git_origin_urlat the thread's creation time, right after itsthread record:row. The second is a secondary source, so the timeline keeps one of the two when both survive.zed: thegit_state.remote_urlof the thread'sinitial_project_snapshotworktree whoseworktree_pathis the thread's project (else of the first worktree, as for the branch), at the thread-start time, right after thethread start:row.openclaw: eachprojectsrow ofstate/openclaw.sqlitethat has anorigin_url, at itscreated_at_ms, withproject_pathitsrepo_rootand nosession_id, since the table names a repository the Gateway registered or cloned, not a session.
The URL can carry credentials in its user part; redact them before
sharing the timeline. That holds for every source, the rollout's
git.repository_url included: Codex has removed every password and
every username but the ssh git user before storing the value since
late August 2026
(session_meta in research/codex-cli.md),
but a rollout an earlier build wrote, or one written by a fork such as
Open Interpreter, may hold the URL as the remote had it, and the parser
writes the stored string as it is.
The codex-cli and open-interpreter parsers (one shared reader) write
model: <id> at the first record of a rollout that names a model and
model: <old> -> <new> at each later one whose model differs, and one
composite permission mode: approval=<approval_policy>
sandbox=<sandbox_policy> profile=<permission_profile> network=<network>
reviewer=<approvals_reviewer> profile_id=<active_permission_profile.id>
row at the first record with any of those fields and permission mode:
<old> -> <new> whenever the composite changes. The records are each
turn_context and each thread_settings_applied event, which the
ChatGPT desktop app writes the moment the user changes the model, the
reviewer or the access level, before any turn, so the row is dated when
the setting changed rather than at the next turn. A settings event
supplies only the thread-level fields, approval, reviewer and
profile_id; sandbox and profile are the turn's, so a row dated at
a settings event shows them as the last turn_context had them, or as
- when the access level (profile_id) changed, until the next
turn_context fills them in, and a settings event before the thread's
first turn_context is folded into that turn's row. A
missing field is - and an object field is compact JSON, so a value
reads sandbox={"type":"workspace-write"}. reviewer is user when
the user approves each escalated call, and auto_review in the desktop
app's "Approve for me" mode, where an automatic reviewer (a guardian
review thread) decides instead; profile_id is the desktop app's
access level, :workspace or :danger-full-access ("Full access", no
sandbox and no approvals), on Codex Desktop 0.163.0-alpha.10.2 inside
ChatGPT 26.1013.40934 (macOS, 2026-10-18). Both rows take the record's
timestamp, and the model column of both is the new model. The
reviewer and profile_id fields were added at the end of the
composite in analyzer 0.16.0, so every permission mode: row of these
parsers ends reviewer=- profile_id=- when the rollout records
neither.
Each turn of a rollout whose task_started record carries
turn_attribution.turn_trigger gets a row turn trigger: <turn_trigger>
turn=<turn_id> after its task_started turn=<turn_id> row. The values
seen with Codex Desktop 0.163.0-alpha.10.2 inside ChatGPT 26.1013.40934
(macOS, 2026-10-18 and 2026-10-19):
turn_trigger |
What started the turn |
|---|---|
composer |
A prompt typed in the desktop app. |
remote_ios |
A prompt sent from the ChatGPT iPhone app over remote control (Settings > Connections > Control this Mac or PC): the phone drives the desktop's thread, which stays local. |
remote_android |
The same from the ChatGPT Android app. |
guardian_review |
The automatic reviewer's turn, in a guardian review thread. |
scheduled_task_setup |
The first turn of a scheduled task's thread, when the task is created. |
automation_heartbeat_run_now |
A scheduled task run with Run now. |
automation_heartbeat_scheduled |
A scheduled task run at its scheduled time. |
Codex records no per-call approval in its rollouts. The desktop app's
are written to logs_<N>.sqlite: each user decision is one row
permission decision: <decision> exec <item id> turn=<turn_id> (from
logs_<N>.sqlite logs, ts) at the log row's time, with the thread as
session_id and the governed item's id as tool_use_id, which is the
tool_use_id of the call's tool_use row; <decision> is the log's
own word, denied or approved on this row. An app-server response to
an approval request (Decline or Accept) with no such row within two
seconds of it is permission decision: <decision> request <request id>
(from logs_<N>.sqlite logs, ts) with no session; on the install
observed those response rows had left the database before it was
collected, while the decision rows above remained. In the "Approve for me" mode the app spawns a guardian review
thread (a rollout with thread_source guardian_review and the user's
thread as parent_thread_id) whose final answer is the verdict: it is
one row permission decision: <outcome> (auto_review
risk_level=<risk_level> user_authorization=<user_authorization>) exec
<item id> reviewer <review thread id>: <rationale> at the review's
task_complete time, carrying the user's thread as session_id, an
empty model (the reviewer's model is not the thread's), and
the governed item's id as tool_use_id when the log names exactly one
call under review in that window (else exec - and an empty
tool_use_id); allow is the outcome seen. A log row of an approval
whose text the parser cannot read is a parser: unreadable approval log
row (<target>): <text> row.
From Codex source, with no real install yet to confirm them, and for
Open Interpreter's home as well where the fork keeps the code: an
apply_patch approval in logs_<N>.sqlite is permission decision:
<decision> patch <item id> (from logs_<N>.sqlite logs, ts), with the
thread as session_id and the FileChange item's id as tool_use_id;
each record of the guardian_review_feedback table of state_5.sqlite,
an automatic review that did not allow the action, is a row at the
record's capture time with the reviewed thread as session_id and the
governed item as tool_use_id. A denied record whose decision is the
reviewer's JSON verdict reads exactly as the guardian review thread's
row, so the timeline keeps one of the two; any other reads
permission decision: <status> (auto_review feedback model=<model>) exec
<item id> turn=<turn id> reviewer <review thread id>: <decision> action:
<reviewed command>, <status> being denied, invalid_decision,
missing_decision, failed, timed_out or aborted. The standing
grants are one row each, permission setting: <key>=<value> (from
.codex/config.toml, file mtime) for approval_policy,
approvals_reviewer, sandbox_mode, default_permissions,
projects.<path>.trust_level (with project_path the path) and each MCP
or app tool's approval_mode, and permission setting: <statement> (from
.codex/rules/<file>, file mtime) for each statement of a rules file
(prefix_rule(...) for an "approve and remember" answer,
network_rule(...) for a network answer), at the file's modification
time with an empty session_id; a project's own .codex/config.toml and
.codex/rules/ give the same rows with the project as project_path.
No other key of config.toml is shown. A thread_goal_updated event is
thread goal: status=<status> turn=<turn id> objective: <objective>, and
each .codex/memories/rollout_summaries/<file>.md is one row
memory summary: thread <thread id> rollout=<path> (from <file>,
updated_at): <summary> at the summary's updated_at, with the thread as
session_id; it repeats no event, so it is never dropped as a copy.
Each /feedback report recorded in logs_<N>.sqlite
is one row session synced: feedback report uploaded to Sentry,
attachments <uploaded_attachments> failed <attachments_failed> (from
logs_<N>.sqlite logs, ts) at the log row's time, with - for a field
the log line lacks and session_id the reported thread, empty when no
thread was open. Each row of the remote_control_enrollments table of
state_5.sqlite, the host's enrollment for remote control, is one row
remote control: enabled=<0|1> server=<server_id>
environment=<environment_id> host=<server_name>
client=<app_server_client_name> account=<account_id> at its
updated_at, with an empty session_id: remote control lets a paired
phone or desktop drive this host's threads, and turning it off keeps the
row with enabled=0. It is not a sync: the threads stay on the host.
The codex-cli parser also reads the desktop app's
.codex/.codex-global-state.json, a secondary source that keeps traces
of a thread the app deleted. Its prompt history gives user rows with
an empty timestamp_utc and the thread as session_id, which pair with
the rollout's prompts on text and are kept, and marked recovered, when
the rollout is gone. Its system rows are at the file's modification
time and end (from .codex/.codex-global-state.json <key>, file mtime)
or (from .codex/.codex-global-state.json, file mtime):
session deleted: <thread id> for each thread the app deleted, thread
description: <description> last_activity=<time> per thread, permission
mode (standing): approval=<approvalPolicy> sandbox=<sandboxPolicy>
reviewer=<approvalsReviewer> profile_id=<id> per thread, permission
setting: <key>.<host>=<value> or permission setting: <key>=<value>
for the app's agent mode (agent-mode-by-host-id,
preferred-non-full-access-agent-mode-by-host-id,
permission-selection-by-host-id:<host>; guardian-approvals is the
"Approve for me" mode), and remote control: mobile device
connected=<true|false> environment=<id> when a phone was ever paired.
Since 0.18.0, each entry of the top-level
thread-projectless-output-directories map, the directory the app made
for a thread with no project, is also a scratch directory: <path>
(projectless thread <thread id>) row whose project_path is that
directory (see inventory.json).
A value the file lacks is -.
The gemini-cli and qwen-code parsers write model: <id> at a
session's first record that names a model and model: <old> -> <new>
on each change, tracked per session id: for Gemini CLI from each gemini
message's model, for Qwen Code from the session_model record, which
reads model: <id> auth=<authType>, and from each assistant record's
model; a Qwen Code logs.json model_switch entry reads
model: <from> -> <to> reason=<reason>. A tool call whose status is
cancelled or awaiting_approval keeps its [<status>] prefix on the
tool_result row and is followed by permission decision: <status>
with the call's tool_name and tool_use_id (after the tool_use row
when there is no result). Each folder in .gemini/trustedFolders.json
or .qwen/trustedFolders.json is one row
permission setting: trustedFolders.<path>=<value> (from <file>, file mtime),
where <value> is the recorded trust level (TRUST_FOLDER,
TRUST_PARENT or DO_NOT_TRUST), with project_path the folder, the
file's modification time and an empty session_id. Each entry of Qwen
Code's .qwen/channels/sessions.json or
.qwen/channels/daemon/<hash>/routes.json is one row session relayed:
channel <channelName> chat <chatId> sender <senderId> thread <threadId>
(from channels/sessions.json, startedAt), with - for an absent value,
the entry's sessionId as session_id and its cwd as
project_path, at startedAt, or at the file's modification time with
the text ending file mtime); the daemon form names its routes.json.
Since 0.19.0 both parsers read a trustedFolders.json with a byte-order
mark, comments or trailing commas, as Qwen Code does. The gemini-cli
parser also reads Gemini CLI's other standing permission files, one
system row per entry at the file's modification time with an empty
session_id: from .gemini/settings.json in the home or a project,
permission setting: <key>=<value> (from <file>, file mtime) for each
entry of tools.sandboxAllowedPaths, tools.core, tools.allowed,
tools.confirmationRequired, tools.exclude, mcp.allowed,
mcp.excluded and security.allowedExtensions and each set value of
general.defaultApprovalMode, tools.sandbox,
tools.sandboxNetworkAccess, security.toolSandboxing,
security.disableYoloMode, security.disableAlwaysAllow,
security.enablePermanentToolApproval,
security.autoAddToPolicyByDefault, security.blockGitExtensions,
security.folderTrust.enabled and security.enableConseca, and
session retention: general.sessionRetention.<key>=<value> (from <file>,
file mtime) for each of enabled, maxAge, maxCount and
minRetention that is set; from each .gemini/policies/*.toml (and
auto-saved.toml.bak) in the home or a project, permission setting:
policy <decision> <toolName> <key>=<value> ... (from <file>, file
mtime) per rule, its other keys in file order; from
.gemini/trusted_hooks.json, permission setting:
trustedHooks=<name>:<command> (from <file>, file mtime); and from
.gemini/acknowledgments/agents.json, permission setting:
acknowledgedAgents.<agent>=<hash> (from <file>, file mtime). A
project's file carries the project as project_path, and the last two
files the project each entry names. Policy files are read with Python's
tomllib, so under Python 3.10 each is one row parser: <file> not
read: its [[rule]] tables need Python 3.11 or later (tomllib). No
session retention: row means the defaults: Gemini CLI deletes a
project's sessions older than 30 days each time it starts there, so a
session missing from chats/ may have expired rather than been deleted,
and its prompts remain in logs.json. Gemini CLI's shell_history
yields one user row per shell-mode command, and a /chat save
checkpoint or a /bug history export yields a system row naming it
(chat checkpoint: tag <tag> authType=<authType> (from <file>, file
mtime) at the file's modification time, or bug report history:
exported (from <file>, time in the file name) at the time in its name)
and then, for each saved turn, its text as one user or assistant row
and a thinking (when asked for), tool_use or tool_result row per
thought, call or response part, with no timestamp; all three are
secondary sources (see Secondary sources). A Qwen Code user
record the user did not type is a system row <subtype>
(provenance=<provenance> agent=<agentName> agentId=<agentId>): <text>,
the parts the record lacks left out: a cron or notification record
(cron (provenance=system): ...), an agent_message or agent_mention
another workspace agent wrote (provenance=external_agent), and a
record of goal_runtime provenance; the user's own agent_mention stays a user row.
A Qwen Code record's executionContext (on user prompts) feeds the same
model tracking, as model: <id> auth=<authType> or model: <old> ->
<new> auth=<authType> (the auth= part left out when not recorded),
and an approval-mode tracking per session, permission mode: <mode> the
first time and permission mode: <old> -> <new> on a change, before the
record's own rows; a session_approval_mode record reads permission
mode: <mode> or permission mode: <old> -> <mode>, followed by
prePlanMode=<mode> and planExecutionMode=<mode> when recorded. A
ui_telemetry record of a tool call the user was asked about keeps its
ui_telemetry: <JSON> row and is followed by permission decision:
<decision> (tool_call ui_telemetry), <decision> one of accept,
reject, modify or auto_accept (an "allow always" answer), with the
call's tool_name and tool_use_id. Each entry of permissions.allow,
permissions.ask and permissions.deny in .qwen/settings.json, in
the home or a project, is one row permission setting:
permissions.<list>=<rule> (from .qwen/settings.json, file mtime), and
tools.approvalMode one row permission setting:
tools.approvalMode=<mode> (from .qwen/settings.json, file mtime), at the
file's modification time with an empty session_id (project_path the
project for a project's file). Each line of .qwen/usage_record.jsonl
is one row session usage: project <project> duration <ms>ms models
<id>,... tools <tool>=<count>,... (from usage_record.jsonl, startTime),
with - for an absent part, the record's sessionId and project, at
its startTime; the line is written when a session ends, at /clear
and before a session is deleted, so it can be all that is left of one.
The antigravity parser writes model: <id> before the rows of a
conversation's first step that the gen_metadata table names a model for
(chat_model.response_model, else model_display_name) and
model: <old> -> <new> before the rows of each later step whose model
differs, at the step's created_at, with the new model in the model
column; the format has no switch record, so a change is seen only as a
different model on a later step. A run_command or mcp_tool call the
user rejected is followed, after its tool_result row (which keeps its
[rejected by user] marker), by permission decision: user_rejected with
the call's tool_name, tool_use_id and result time, and a
run_command result that ran in Antigravity's sandbox ends with
sandbox=true.
The opencode and kilo-code parsers write model: <modelID>
provider=<providerID> before the first message of a session that names a
model and model: <old> -> <new> provider=<providerID> before each later
message, or V2 model-switched record, whose model differs; the model
column of the row is the new <providerID>/<modelID>. They write
permission mode: agent=<name> and permission mode: agent=<old> ->
agent=<new> for the message's agent and for a V2 agent-switched
record, because an OpenCode or Kilo Code agent (build, plan, a custom
one) is a mode with its own permission ruleset. Both rows take the
message's timestamp and source_line and precede its other rows. Since
0.19.0 they read an OpenCode 2.0 database: session-start rows come from
session_v2 and end with fork=<id> for a forked session, and the
session_message records of 2.0 add these rows: a shell record's
tool_use row (tool shell) and its tool_result row, whose text starts
[timeout], [killed] or [exit <code>] when the command did not exit
0; location switched: <old directory> -> <new directory> with
workspace=<id> and subpath=<path> when recorded (the session's
project_path stays its directory); skill <name>: <text>; idle:
<outcome> with the error after it when there is one; and compaction
<reason> <status> <summary>, all system but the shell rows. A session
that a 2.0 database holds only in the 1.x session table has a
session-start row ending (only in the 1.x session table), and its rows
come from the 1.x message and part tables (see
Secondary sources). The
crush parser writes model: <model> provider=<provider> and model:
<old> -> <new> provider=<provider> the same way, from each message's
model and provider, where <model> is the model that served the
turn: Crush's prism_model_id for a Hyper Prism turn, else model.
Since 0.19.0 the row ends with requested=<model> when the requested
model differs from the served one, and the model column reads
<provider>/<served model>, or the served model alone when no provider
is recorded. After a tool_result row it writes permission decision:
denied by user when the result is Crush's User denied permission
error, permission decision: <allow|deny|halt> by hook <names>
reason=<reason> from the result's hook metadata ( reason= only when
the hook gave one), and permission decision: <deny|halt> by hook for a
hook's block or halt message with no metadata, each with the call's
tool_name and tool_use_id. Standing entries are permission setting:
<key>=<value> (from <file>, file mtime), one per entry of
permissions.allowed_tools, options.disabled_tools,
mcp.<name>.disabled_tools, mcp.<name>.enabled_tools and
hooks.<event> in each crush.json, of permissions allow (as
permissions.allowed_tools) and permissions deny (as
options.disabled_tools) lines in a crushrc, where the file reads
<file> line <n>, and of the mcp_enabled_servers and
mcp_disabled_servers tables of crush.db, at the file's modification
time with an empty session_id. A session bound to an MCP channel gets
session relayed: channel <server> (from crush.db sessions.channel, as
of updated_at), and the first user message a channel pushed for each
source, sender and group session relayed: channel <source> sender
<sender> group <group> (from crush.db message <id>), sender and
group only when present; the parser records relay state. A sub-agent
session whose parent is gone gives session deleted: <parent>
(parent_session_id of sub-agent session <id> names no session in
crush.db; time is the sub-agent's updated_at) with session_id the
parent. Each files snapshot is file version: <path> version=<n>
bytes=<length> (from crush.db files) at its created_at, and each
read_files entry file read: <path> (from crush.db read_files,
read_at) at its read_at.
The opencode and kilo-code parsers write, after a database's
session-start rows, session synced: share <id> url <url> (from
<database> session_share, time_created) for each session published to
the share service, at the share's creation time; for a session with no
session_share row whose session.updated.1 events record a share URL
(one shared and then unshared), session synced: share url <url> (from
<database> event session.updated.1, info.time.updated) at the first
event that names it and session unshared (from <database> event
session.updated.1, info.time.updated) at the first later event without
it, <database> being the file name (opencode.db, kilo.db). The
kilo-code parser also writes session synced: kilo session <id> ingest
<ingestPath> share <url> (from storage/session_share/<sessionID>.json,
file mtime) for each Kilo cloud session marker, at the file's
modification time, - for an absent value.
The opencode and kilo-code parsers write a system row
permission decision: rejected, permission decision: rejected with
feedback, permission decision: denied by rule or permission
decision: rejected (permission.rejected) after the tool_result row of
a tool call the user or a deny rule refused, with the call's tool_name,
tool_use_id and result time; an approval leaves no record. After a
database's session-start and sync rows they write session archived
(from <database> <table>, time_archived) at the time a session was
archived, permission setting: session <permission> <pattern>=<effect>
(from <database> <table>.permission, time_updated) for each rule of a
session's rule list, scratch directory: <directory> (git worktree of
<project> strategy=<strategy>) (from <database> worktree,
time_created) (or project_directory) for each worktree the agent
made, with that directory as project_path and no session, and
permission setting: <action> <resource>=allow (from <database>
permission, time_created) for each saved "always" grant, with the
project as project_path. From the terminal UI's state directory they
read prompt-history.jsonl, a secondary source whose lines are user
rows, or tool_use rows with tool shell for a shell command, with an
empty timestamp_utc; prompt-stash.jsonl, one prompt stashed
(unsent): <text> (from <file>, timestamp) row per prompt set aside; and,
for OpenCode, session.json, one session pinned: <id> (from <file>
pinned, file mtime) row per pinned session, which outlives a deleted
session. The kilo-code parser also writes permission setting:
permission.<permission>.<pattern>=<action> (from <file>, file mtime)
(or permission.<permission>=<action>) for each rule of the
permission key of a global config file in .config/kilo, where an
"always" approval writes it; permission setting: sandbox.<key>=<value>
(from <file>, file mtime) for each key of a sandbox policy file and
permission setting: sandbox.enabled=<value> for a sandbox preference;
sessions deleted by retention: deleted=<n> scanned=<n>
skippedActive=<n> failed=<n> durationMs=<n> (from retention/state.json,
at) for the last automatic deletion pass, with no session; and, from
log/<time>.log, remote control: connection enabled connected=<v>,
remote control: connection disabled, remote control: session started
model=<model> directory=<directory> and remote control: session ended
reason=<reason> durationMs=<n> startedAt=<time>, each ending (from
log/<file>, <time basis>), the session rows with the session's id.
Remote control lets Kilo Cloud drive this host's sessions; it is not a
sync, since the transcript stays in kilo.db. These rows are
source-derived; no real install has shown them.
The pi session reader (the pi parser, the little-coder sessions in
pi's directory, which stay agent pi, and Letta's local-backend
messages.jsonl) and the openclaw parser write model: <id>
provider=<provider> at the first entry of a session that names a model
and model: <old> -> <new> provider=<provider> on each change, with
requested=<model> when an assistant message's responseModel (the
model the provider reported) differs from its model (the one asked
for); a model_change entry is one of those entries, and one that
names the model already in force, being the user's own switch, still
gets the plain model: <id> provider=<provider> row; for openclaw
the session_windows model and model_provider start the session at
its start time. openclaw writes session synced: public share <id>
generation <sessionId> (from openclaw-agent.sqlite
session_nodes.entry_json, publicShare.createdAt) after the start row of
a session published through the Gateway's public link, at the time
access was turned on, and one backup uploaded: <kind> to <location or
archive path> key <object key or commit> namespace <namespace> (from
state/openclaw.sqlite backup_runs, created_at) row, with an empty
session_id, per successful openclaw backup run that uploaded the
state directory to a storage location or committed it to a Git backup
repository whose push did not fail. The letta parser adds the same row
after the first
.letta/sessions.jsonl line of each CLI session and a change row when a
later line of the session names another model. The pi parser reads
.pi/agent/trust.json, the project trust decisions pi and little-coder
share, as one permission setting: <path>=<true|false> (from
.pi/agent/trust.json, file mtime) row per project, with project_path
the project, the file's modification time and an empty session_id.
The openclaw parser also writes, each text ending in
(from <file> <table>, <time column>), which names the source and the time the row
carries. After the start row of a session's current generation, from the
session's session_nodes.entry_json at updated_at:
model: <modelOverride> provider=<providerOverride>
source=<modelOverrideSource> (with fallback_from=<provider>/<model>
when a fallback replaced the user's choice; the entry keeps no previous
model), one permission setting: <key>=<value> per elevatedLevel,
execHost, execNode, execCwd, toolOverrides and sendPolicy, and
session archived: <archiveReason> by <type>:<id> at archivedAt. One
session relayed: <channel> account <account> <kind> peer <peer>
thread <thread> role <role> per chat-platform conversation linked to a
generation, else session relayed: <channel>
account <account> <chat_type> from its window, else session relayed: <channel>
account <account> <chatType> from <from> to <to> from the session's external
delivery route, and session relayed: gmail hook for a session a Gmail
hook started; the Control UI (webchat) and the internal wake channels
give none, and sessions.jsonl relayed reads no for a session
without the row. session deleted: <session_id> archive <archive_name>
follows each deleted archive's rows, and session forgotten from
memory: <session_id> agent <agent_id> reason=<reason> marks each memory
tombstone. A prompt in session_pending_inputs that no transcript entry
consumed gives a user row and pending input: <state>, not consumed by
the transcript (input <input_id> run <run_id>; ...); one consumed by an
entry the database no longer holds gives its user row, and a full-text
index entry the transcript no longer holds its user, assistant or
system row, both marked recovered. channel
delivery: <kind> <status> channel <channel> conversation <id> platform message <id> and
channel delivery reply: <text> (reply message <id>; ...) record the
sends to a platform conversation and the platform's replies. From
state/openclaw.sqlite: permission decision: <decision>
status=<status> kind=<kind> resolver=<resolver_kind>
reason=<terminal_reason> approval <id> per approval, at its resolution
(its request while pending), with the governed call's session_id,
tool_name and tool_use_id; one permission
setting: <scope>.<setting> per default and per-agent exec policy value,
allowlist entry and MCP tool grant (the approvals socket token is never
read), per allow-always standing grant (permission setting: standing
grant <id> ...) and per allowed channel sender (permission setting:
channel <channel> account <account> allowFrom=<sender>); channel
ingress: <channel> account <account> status <status>: <payload> per
inbound channel message, the payload whole and with no session; and
source host: github account <login> id <id> identity <source>;
publication <status> repository <repo> branch <branch> pull
request <url> per push or pull request the Gateway made on GitHub, with the
session's id. An agent or state database that is only a PostgreSQL
engine anchor is one row, parser: agent database is a PostgreSQL anchor
(store <store_id>, schema <schema>): transcripts are on the database
server (state database ... the Gateway state is on the database
server).
For a /bug report recorded in a session, the pi parser writes
session synced: bug report <id> uploaded to radius.pi.dev
transcript=<true|false> summary=<true|false> (from session entry
pi.bug-report, entry timestamp) when it was uploaded and bug report
exported: <id> path=<path> (from session entry pi.bug-report, entry
timestamp) when it was written to a zip, at the entry's time.
The pi parser also writes permission setting:
defaultProjectTrust=<ask|always|never> (from .pi/agent/settings.json,
file mtime) when .pi/agent/settings.json sets that key, at the file's
modification time with an empty session_id; permission decision:
denied (<reason>) after the tool_result row of a call little-coder's
permission gate refused (an error result reading edit cancelled by
user, command cancelled by user or starting shell whitelist:), with
the call's tool_name and tool_use_id; one crash: <kind> <message>
version=<version> sessionFile=<file> (from .pi/agent/crashes.json,
record timestamp) row per .pi/agent/crashes.json record, the stack on
the lines after it, at the record's time, with the record's cwd as
project_path, the id in its session file name as session_id and -
for an absent value; and, for .pi/agent/pi-debug.log, one debug
snapshot: /debug wrote <N> message(s) of the open session, which the log
does not name (from .pi/agent/pi-debug.log, Debug output at line) row
at the time the log was written, before the log's messages, which have
no session_id or project_path. The little-coder parser reads the
v0.0.x releases' ~/.little-coder: each saved session starts with
session saved: little-coder v0.0.x session <id> turn_count=<n>
input_tokens=<n> output_tokens=<n> (saved_at <saved_at>, host local
time), and every row of the session carries saved_at, host local
time written as if it were UTC, and an empty project_path; a refused
call is followed by permission decision: denied (Denied: user rejected
this operation); each checkpoint snapshot is little-coder v0.0.x
checkpoint <id>: turn_count=<n> prompt=<prompt> files=<path> (backup
<name>|did not exist); ... (created_at host local time); and
.little-coder/config.json gives session synced: gist <id>, session
relayed: telegram chat <id> and permission setting:
permission_mode=<mode>, each ending (from .little-coder/config.json,
file mtime), at the file's modification time with an empty
session_id.
The letta parser writes session synced: server conversation
<conversation id> agent <agent id> server <server> (from
.letta/transcripts path, first captured_at) before the first row of
each transcript whose agent id does not start agent-local-, at that
line's time, <server> the agent's baseUrl in .letta/settings.json,
api.letta.com when it has none and - when the file has no entry
for the agent; and session synced: last session on <server> agent
<agentId> conversation <conversationId> (from <file>, file mtime) for
each sessionsByServer entry of .letta/settings.json and of a
project's .letta/settings.local.json that is not the local backend's,
at the file's modification time, with the conversation as session_id.
Each .letta/sessions.jsonl line, which Letta writes when a session
ends with /exit or /logout, is letta session end: agent=<agent id>
exit_reason=<reason> messages=<n> tool_calls=<n> wall_ms=<ms> at the
time of exit. Since the review of Letta Code 0.34.17 the letta parser
also writes, from source: permission setting:
permissions.<key>=<value> (from <file>, file mtime) per entry of the
permissions object (mode, allow, deny, ask, alwaysAsk,
additionalDirectories) of .letta/settings.json,
.config/letta/settings.json and a project's .letta/settings.json and
.letta/settings.local.json, at the file's modification time with an
empty session_id; session relayed: <channel> chat <chatId> type
<chatType> thread <threadId> account <accountId> agent <agentId>
enabled=<enabled> outbound=<outboundEnabled> detached=<detached>
updated=<updatedAt> (from .letta/channels/<channel>/routing.json,
createdAt) per route, at createdAt, with the conversation as
session_id; scheduled task: <id> name=<name> status=<status>
cron=<cron> timezone=<timezone> recurring=<true|false>
scheduled_for=<time> agent=<agent id> conversation=<conversation id>
fire_count=<n> last_fired=<time> last_run=<time> outcome=<outcome>
reason=<reason> error=<error> cancel_reason=<reason> (from
.letta/crons.json, created_at) description: <description> prompt:
<prompt> per task of .letta/crons.json, at its creation time;
scheduled task run: <task id> action=<action> status=<status>
outcome=<outcome> reason=<reason> agent=<agent id>
conversation=<conversation id> run=<run id> scheduled_for=<time>
fired=<time> missed=<n> summary=<summary> error=<error> (from
.letta/runs/<task id>.jsonl, ts) per run log line, at its time; and,
for a turn the remote listener did not finish,
.letta/listener-state/<hash>/<agent>_<conversation>.json, interrupted
turn: agent <agent id> conversation <conversation id> run <run id>
tool_calls=<ids> unstarted=<ids> results=<n> working_directory=<dir>
acting_user=<id> (from <file>, file mtime) followed by permission
decision: <approve|deny> reason=<reason> (from <file>, interrupted
turn, file mtime) per recorded decision and a tool_result row per
recorded tool result, each with the call's id as tool_use_id, at the
file's modification time, with the conversation as session_id and
the working directory as project_path. A record without a time of its
own takes the file's modification time and its text ends file mtime);
- stands for an absent value.
The cline, continue and pearai parsers write the same model:
rows, with the model column the new model. Cline's SDK sessions write
model: <id> provider=<provider> after the session start (or
sessions.db index) row, from the manifest's model and provider, and
model: <old> -> <new> provider=<provider> before a message whose
modelInfo.id differs; its legacy tasks write model: <model_id>
model_provider_id=<id> mode=<mode> and model: <old> -> <new> ... from
the model_usage entries of task_metadata.json at each entry's time,
and permission mode: <mode> and permission mode: <old> -> <new> from
the same entries' mode. Continue writes model: <chatModelTitle> after
the session row and model: <old> -> <new> before a history item whose
promptLogs[].modelTitle differs; PearAI's chat sessions write
model: <id> and model: <old> -> <new> from each item's
promptLogs[].completionOptions.model. These rows inherit the session's
time like every other row of a Continue or PearAI session. Continue
also writes per-call decisions with the call's tool_use_id and
tool_name: permission decision: accepted=<accepted> after each
dev_data toolUsage.jsonl record's tool_result row, at the record's
time and with an empty session_id (the record names none), and
permission decision: canceled reason=user (the CLI's Permission
denied by user), permission decision: canceled reason=policy
(Command blocked by security policy) or permission decision:
canceled after a session tool call whose toolCallStates[] status is
canceled, and permission decision: errored reason=policy after an
errored one whose output is the extension's Security Policy
Violation, at the session time. A Continue session with a mode
writes permission mode: <mode> after its model: row, and one whose
workspaceDirectory ends in cn-review-<digits>-<digits> writes
scratch directory: <workspaceDirectory> (cn review worktree), both
at the session time. Each rule of the CLI's .continue/permissions.yaml
is permission setting: <list>=<rule> (from .continue/permissions.yaml,
file mtime), <list> being allow, ask or exclude, at the file's
modification time; a file in any other YAML layout is one parser:
unsupported YAML at line <N> (only allow, ask and exclude lists of
strings are read) row. Each dev_data editOutcome.jsonl line is
permission decision: edit accepted=<accepted> file=<filepath> lines
<previousCodeLines> -> <newCodeLines>, each editInteraction.jsonl
line edit interaction: file=<filepath> before its prompt (user) and
edited file (assistant), and each chatFeedback.jsonl line chat
feedback: feedback=<true|false|-> model=<modelTitle> after its prompt
and reply, all at the line's time and with - for an absent value. A
Cline
SDK session whose metadata has the enterprise.blobUpload key gets
session synced: enterprise prompt upload to <adapterType> bucket
<bucket> object sessions/<userDistinctId or unknown>/<session_id>/messages.json
endpoint=<endpoint> region=<region> account=<accountId> (from
<session_id>.json metadata enterprise.blobUpload, started_at) after its
model: row, at the session's start and with - for an absent value,
or the same row ending (from sessions.db sessions.metadata_json
enterprise.blobUpload, started_at) for a session known only from
sessions.db; each item of the VS Code extension's upload queue
cache/sync-queue.json is enterprise prompt upload <status> key
<key> retries <retryCount> (from
saoudrizwan.claude-dev/cache/sync-queue.json, item timestamp), prefixed
session synced: only when <status> is synced (a pending or
failed item has not reached the bucket), with the item's taskId as
session_id, at the item's time. A Cline SDK tool result for a call
the runtime did not run is followed by permission decision: rejected
reason=<reason> when the reason ends with the runtime's -- NOT a tool
or system failure. Clarify with user before proceeding. (written
without it), or permission decision: blocked reason=<reason> for
Tool "<name>" is disabled by policy and Tool <name> was blocked by a
runtime hook, with the result's tool_use_id, tool_name and time.
Cline's standing settings are one permission setting: <key>=<value>
(from <file>, file mtime) row each, at the file's modification time:
autoApprovalSettings.enabled, .maxRequests, .favorites (per entry)
and .actions.<name> and mode from .cline/data/globalState.json;
toolAutoApprove, planActMode, disabledTools, disabledPlugins and
disabledAgentPlugins (per entry) and tools.<name>.enabled from
.cline/data/settings/global-settings.json; and
mcpServers.<name>.autoApprove (per entry) and
mcpServers.<name>.disabled from settings/cline_mcp_settings.json.
Each Cline Cloud session in globalState.json cloudSessionStatuses
gets session origin: remote cline-cloud session <id> status <status>
(from .cline/data/globalState.json cloudSessionStatuses, observedAt)
and session synced: cline-cloud holds session <id>, no local transcript
(from ...) at its observedAt, with the id as session_id, and each
record of a .cline/data/cloud-pending-starts/*.json file the same pair,
the origin row reading sandbox start pending account=<account>
endpoint=<endpoint>, at the file's modification time, so such a session
has origin remote and synced yes; every other Cline session has
origin local. The cloudTaskTarget is cloud task target:
target=<target> repository_id=<id> branch=<branch> (from
.cline/data/globalState.json, file mtime). A Cline session imported
from another agent names it on its start row: imported_from=<tool>
imported_session=<id> imported_path=<path> imported_at=<time>
imported_provider=<provider> imported_model=<model>. The hook audit log
.cline/data/logs/hooks.jsonl gives a hook audit: <hookName> ... row
for each agent_start, agent_abort, agent_error and
session_shutdown line, with cline_version=, user=, agent=,
parent_agent=, reason=, pid=, source= and error=<name>:
<message> when present. Continue's .continue/logs/cn.log, and the cn<N>.log
files it is rotated to, give agent metadata posted: agent <agentId>
keys <metadataKeys> <status> (from logs/cn.log, log line time,
host-local) for each metadata post of cn serve --id, <status>
being Success, Unexpected response, Auth required, Failed,
Error or - when no result line follows, and storage upload
requested, skipped (from logs/cn.log, log line time, host-local), and
for each rule a "don't ask again" answer added to permissions.yaml
permission decision: allow always rule=<rule> (from logs/cn.log, log
line time, host-local), all with an empty session_id and the log's
host-local time written as if it were UTC. roo-code records no
model and writes no such row.
The aider parser reads the announcement lines Aider writes to
.aider.chat.history.md at startup and after each /model,
/weak-model, /editor-model or /chat-mode switch, and after the
tool_result row of the blockquote run that holds them writes
model: <name> at a session's first announcement and model: <old> ->
<new> on each later one that names another main model, the same text
followed by (editor model) or (weak model) for those two roles,
with the new model in the model column; a Model: line, which means
no separate weak model, sets the weak model to the main one without a
row unless it replaces a separate weak model. Each confirmation line
is permission decision: <decision> (<question>) answer=<letter>
default=<default> after the same kind of tool_result row, with
<decision> yes, no, all, skip all or no, don't ask again;
a y or n answer ends ; may be automatic (--yes-always), because
Aider writes the same line when --yes-always answers for the user.
Both take the run's time, a session header's or the last prompt's.
The shellgpt chat files record no model, permission or time. From
.config/shell_gpt/.sgptrc the parser reads four keys and no other,
since the file also holds the API key: DEFAULT_MODEL is model:
<value> (configured default, from .config/shell_gpt/.sgptrc, file
mtime), with the value in the model column, and
OPENAI_USE_FUNCTIONS, DEFAULT_EXECUTE_SHELL_CMD and
SHELL_INTERACTION are each permission setting: <key>=<value> (from
.config/shell_gpt/.sgptrc, file mtime), in file order. A line of that
file with no = is parser: unparseable line <n> (no "=" separator; not
shown, the file holds the API key), without the line. Each file in the
function store is permission setting: function <file name> (from
.config/shell_gpt/functions, file mtime); the function calls in the
chat files ran with no confirmation. All of these rows carry the file's
modification time and an empty session_id.
The tabby parser records no model per message and writes no model:
row. From the server's .tabby/config.toml it writes one row per
configured model, model configured: <group> <model_name or model_id>
via <http|local>[ kind=<kind>][ endpoint=<api_endpoint>][
supported_models=<a,b>] (from config.toml [model.<group>.<http|local>],
file mtime), <group> being completion, chat or embedding, at the
file's modification time; under Python 3.10, which has no tomllib, the
one row parser: config.toml model tables not read: Python 3.11 or later
needed instead. From the database it writes page start | title <title>
| author <email (name)> | code_source <id> for each AI-written page,
with session_id page:<id>, followed by the page content and its
sections as assistant rows; source host: <host> integration
<display_name> kind=<kind> api_base=<api_base> (account not recorded;
from integrations table, created_at) for each GitHub or GitLab
integration, <host> being github.com, gitlab.com or the host of a
self-hosted api_base; and source host repository: <git_url>
type=<kind> name=<name> active=<yes|no> (integration <display_name>;
from provided_repositories table, updated_at) for each repository
cached from one, both at their own times. The editor plugins'
inline-edit command histories, from the IntelliJ plugin's
intellij-tabby-command-history.xml and the edit.recentlyCommand key
of the TabbyML.vscode-tabby row of a VS Code or fork state.vscdb,
give inline edit command history: <N> commands, newest first (from
<source>, no times) followed by one user row per command, newest
first, with an empty timestamp_utc.
The pearai parser writes per-call decisions on the agent surface (the
Roo Code-based agent pane, as opposed to the chat), from
the Roo Code 3.15.3 result texts of api_conversation_history.json: a
tool result that starts The user denied this operation. is followed by
permission decision: denied, one that starts The user denied this
operation and provided the following feedback: by permission decision:
denied with feedback, and one that starts The user approved this
operation and provided the following context: by permission decision:
approved with feedback, each with the call's tool_name and
tool_use_id at the API message's time; beside a ui_messages.json the
API history yields these rows only, with the tool_use_id of the last
tool ask of ui_messages.json before the message. An approval with no
note leaves no mark. The pearai.pearai-roo-cline row of the editor's
state.vscdb gives one task: <task> | tokensIn=<n> tokensOut=<n>
totalCost=<n> number=<n> row per taskHistory entry at the entry's
ts, with the task id as session_id, ending | creator mode: newProjectType=<type> newProjectPath=<path> for a task PearAI Creator
started, and one permission setting: <key>=<value> (from state.vscdb
pearai.pearai-roo-cline, file mtime) per standing auto-approval key
(autoApprovalEnabled, alwaysAllowReadOnly, alwaysAllowExecute,
allowedCommands and the other alwaysAllow keys) at the file's
modification time. On the chat surface, the PearAI output channel log
gives model: <id> and model: <old> -> <new> from each request's
Settings: block, tracked over the file; a request whose prompt is not in
the <user> and <assistant> block form (an autocomplete request) is
model request: <prompt> and its reply model completion: <reply>.
dev_data/chat.jsonl gives user feedback: <true|false> (from
dev_data/chat.jsonl) after each rated reply, with the line's
sessionId, and dev_data/tokens_generated.jsonl one tokens
generated: model=<model> provider=<provider> promptTokens=<n>
generatedTokens=<n> row per request; all of these rows have an empty
timestamp_utc, since neither file records a time.
The roo-code parser writes Roo Code's permission state as system
rows. In a task's ui_messages.json, a tool approval ask marked
isAnswered gives permission decision: approved isAnswered=true (user
or auto-approval) after the ask's tool_use row, with its tool_use_id
and tool_name: the record does not say whether the user clicked or the
auto-approval settings approved it. In the API history, a tool_result
that opens with Roo Code's denial or approval JSON gives permission
decision: denied or permission decision: approved, with feedback=<text>
when the user typed a note, joined on the call's tool_use_id and
tool_name; it follows the tool_result row when the API history is
the task's only transcript, and is the API history's only row beside a
ui_messages.json. Each task row from history_item.json or
tasks/_index.json is followed by permission mode: <mode>, the task's
mode slug (code, architect, ask, debug or a custom mode). The
standing settings are permission setting: <key>=<value> (from <file>,
file mtime), one row per scalar or list entry, at the file's modification
time and with an empty session_id: the auto-approval switches, command
allow and deny lists and the current mode from the extension's global
state (the RooVeterinaryInc.roo-cline row of an editor's
state.vscdb, named <file> ItemTable RooVeterinaryInc.roo-cline) and
the CLI's .vscode-mock/global-storage/global-state.json;
requireApproval, dangerouslySkipPermissions and mode from
.roo/cli-settings.json; and mcpServers.<name>.alwaysAllow=<tool> from
settings/mcp_settings.json and a project's .roo/mcp.json, with the
project as project_path. Releases up to 3.53.0 also signed in to Roo
Code Cloud, the vendor's hosted service, and uploaded each task message
while task sync was on; no file records which tasks were uploaded, so the
parser writes no session synced: row. It writes the settings cache that
decided it, sync setting: user-settings.settings.taskSyncEnabled=<value>
(unset (defaults to true) when the key is absent) and sync setting:
organization-settings.cloudSettings.<key>=<value> for recordTaskMessages,
enableTaskSharing, allowPublicTaskSharing, taskShareExpirationDays,
allowMembersViewAllTasks and workspaceTaskVisibility, each ending
(from <file>, Roo Code Cloud settings cache, file mtime), and, for each
upload that failed and was queued for retry in the workspace state,
cloud upload queued: <event> task=<task id> type=<type>
retries=<retryCount> url=<url> lastError=<error> (Roo Code Cloud retry
queue, from <file> roo.retryQueue, request timestamp) at the time it was
queued, with the task as session_id, lastError=<error> only when one
is recorded, and - for an absent value. The queued request's headers and
body are never written.
The vscode, cody and twinny parsers write the same model: <id>
and model: <old> -> <new> rows, from the request modelId (VS Code),
the assistant message model (Cody, per chat) and meta.model (Twinny,
per conversation), before the rows of the request or message that
names the model. VS Code also writes permission mode: <mode> and
permission mode: <old> -> <new> from the request's chat mode (ask,
agent, edit, custom), then permission level: <level> and
permission level: <old> -> <new> from the request's permission level
(default, assisted, autoApprove, autopilot); when the request
records the model's options, they follow the model id on the model row
as sorted key=value pairs (model: <id> <key>=<value>), and a change
of options alone is a model change. Per-call decisions are permission
decision: rows. In VS Code, a tool call's confirmation follows its
tool_result row with the call's tool_use_id: permission decision:
confirmed=true or confirmed=false (before VS Code 1.104), else
confirmed=<kind>(<number>), one of Denied(0),
ConfirmationNotNeeded(1), Setting(2), LmServicePerTool(3),
UserAction(4) and Skipped(5), followed by that record's source,
id, scope, selectedButton or selectedButtonKind, the tool's
origin as tool_source=<type> and any reason last. A question put to
the user in the chat is permission decision: confirmation
isUsed=<true|false> buttons=<list> title=<title> with its message on
the next line, and the request that answering it sent is followed by
permission decision: confirmation=<button label>. Cody's confirmation
steps are permission decision: <decision> confirmation state=<state>
error=<message> title=<title>, the parts the step lacks left out, with
the step's description and content on the next lines. Since 0.19.0
<decision> is rejected for a step the user refused (saved as state
error with the message Command rejected) and approved or
unanswered for a pending step, because Cody saves an approval and an
unanswered request the same way; any other step reads permission
decision: confirmation state=<state> ... with no separate decision
word. Cody's standing settings in the editor's User/settings.json are
permission setting: <key>=<value> (from <file>, file mtime) rows, one
per leaf of cody.agentic.context.experimentalOptions (such as
cody.agentic.context.experimentalOptions.shell.allow=["git"]), for
cody.agentic.context.experimentalShell and cody.chat.agenticContext,
and for disabled and disabledTools of each cody.mcpServers.<server>;
its model preferences are model setting: <selected|defaults>.<usage>=<id>
endpoint=<endpoint> (from <file>, file mtime), from
JetBrains-globalState/cody-model-preferences or, ending (from <file>
cody-model-preferences, file mtime), from the state.vscdb row, with
the model column the id. Both kinds have an empty session_id and the
file's modification time, not the time the setting was made. A JetBrains
project's legacy .idea/cody_history.xml gives, per chat, chat:
<internalId> | account=<accountId> | messages=<n> (legacy JetBrains
history), model: <id> before the first assistant message, and the user
and assistant rows, with the chat's updatedAt as session_id and time
and the project as project_path. VS Code's session-start row ends with workspace=<path> when the
chat's workspace is a .code-workspace file (since 0.19.0); the
project_path is the session's workingDirectory, else the
workspace's folder, never that file. Twinny's tool steps saved while waiting for the user's Run or
Skip, or skipped, are permission decision: waiting and permission
decision: skipped, each with approval=<command|change> when the
step records what was asked, after the step's tool_result row with
its tool_use_id. Twinny's standing grants are system rows with an
empty session_id at the modification time of the editor's
globalStorage/state.vscdb, each ending (from state.vscdb rjmacarthy.twinny, file mtime): permission mode: agentMode=<true|false>,
permission setting: autoRunCommands=<true|false>, one permission
setting: alwaysRunCommands=<command> per command the user chose Always
run for, one permission setting: alwaysRunMcpTools=<server/tool> per
MCP tool, and one permission setting: twinny.teamPolicy.<dotted
key>=<value> per leaf of the team gateway policy the user accepted (a
list gives one row per entry). Each file a reply changed is file
changed: <status> <path> +<added> -<removed> reply=<id>, with
undone=true and noUndo=true when set: in the conversation, after
the reply's tool steps, and again from the reply's agent-changes/
record with the record's root as project_path, its createdAt as the
time and an empty session_id; reply=<id> joins the two, which
deduplication cannot pair because the conversation records neither the
project nor the reply's time. Each entry of the Twinny log channel is
log <level>: <message> (from <file name>, log line time, host-local),
its continuation lines (a Debug block's prompt or reply) on the
following lines, with an empty session_id, source_line the entry's
first line and the log's host-local time written as if it were UTC.
The rjmacarthy.twinny row of a workspace's
workspaceStorage/<id>/state.vscdb gives source host repository:
https://github.com/<owner>/<repo> type=review (from workspaceStorage
state.vscdb rjmacarthy.twinny, file mtime) for the repository the pull
request review tab last listed, at that file's modification time.
The vscode parser writes session synced: remote session
<cloudSessionId> task <cloudTaskId> (from
User/globalStorage/github.copilot-chat/cloudSessions.json, file mtime)
for each chat session Copilot Chat's session sync uploaded to the
user's GitHub account, with the chat session id as session_id and the
file's modification time, and session synced: cloud agent task
<taskId> application=<application> (from state.vscdb
GitHub.copilot-chat github.copilot.cloudAgent.ownedTasks, recordedAt)
for each cloud agent task started from the editor, with session_id
copilot-cloud-agent:/task/<taskId>, at the time it was recorded; -
stands for an absent value. The task ran on GitHub's servers from the
start, but GitHub holds its copy as it does an uploaded session's, so
its session's synced in sessions.jsonl is yes. A second row at
the same time and session, session origin: remote GitHub cloud agent
(from state.vscdb GitHub.copilot-chat
github.copilot.cloudAgent.ownedTasks, recordedAt), records where it
began, so its origin is remote.
VS Code's standing tool approvals are one row per entry, permission
setting: <key>.<id>=<value> (from User/<path>/state.vscdb, file
mtime), for the keys chat/autoconfirm, chat/autoconfirm-post,
chat/servers/autoconfirm, chat/servers/autoconfirm-post and
chat/autoconfirm-combination of a profile's or a workspace's
state.vscdb, <value> being true, the tool's label or
{"arguments":...,"label":...}; permission setting:
chat.tools.global.autoApprove.optIn=<value> and one permission
setting: chat.externalPath.workspaceAllowlist=<path> per allowed
folder end the same way. Each auto-approve setting of a profile's
settings.json (the keys matching chat.tools.*autoApprov*) and
chat.permissions.default is permission setting: <key>=<value>, or
one permission setting: <key>.<entry>=<value> per entry of an object,
and chat.defaultModel is model setting: chat.defaultModel=<value>,
each ending (from User/<path>/settings.json, file mtime). All of them
carry the file's modification time and an empty session_id; a
workspace's rows carry the workspace's folder as project_path. The
GitHub account VS Code signed in with is source host: <host> account
<login> extension=<id> scopes=<scopes> (from state.vscdb
github-<login>-usages, lastUsed), or mcp_server=<id> from
github-<login>-mcpserver-usages, one per extension or MCP server that
used the account, at its last use, and each repository picked for a
cloud agent task in the last week is source host repository: <owner/repo>
type=cloud agent selection (from state.vscdb GitHub.copilot-chat
userSelectedRepositories, timestamp), both with an empty session_id.
The chat editing state gives edit state: <Modified|Accepted|Rejected>
<path> request=<requestId> model=<modelId> (from chatEditingSessions
state.json, file mtime), with deleted=true before the parenthesis
for a file the edit deleted, one per file of the session's most recent
snapshot, with the chat session id and the workspace's folder, at the
file's modification time.
The chatgpt-desktop parser reads the ChatGPT desktop app's list of
the account's ChatGPT conversations, the chatgpt rows of
.codex/sqlite/codex-dev.db local_thread_catalog, which the app
writes at sign-in; a conversation's text stays on OpenAI's servers and
is not on disk. Per conversation it writes three system rows with the
conversation id as session_id, at the row's source_updated_at:
account conversation: <title> origin=<conversation_origin>
created=<UTC> updated=<UTC> host=<host_id> (from
.codex/sqlite/codex-dev.db local_thread_catalog), where host_id is
chatgpt:<workspace uuid>:<user id> and - stands for an
absent value; session origin: remote chatgpt.com conversation <id>
(listed at sign-in; its transcript is server-side); and session
synced: chatgpt.com holds conversation <id>, no local transcript. Such
a session's origin is remote, its synced yes, and its turn
counts 0. The update time is the conversation's as of sign-in: a reply
from inside the app did not change it.
The cursor parser writes session origin: remote cursor cloud agent
for a Cursor cloud agent, which runs on Cursor's servers, with the
agent's bc-<id> as session_id and its repository
(github.com/<owner>/<repo>) as project_path: at the cached
conversation's updated_at when conversation-search.db holds one for
the agent, else at the agent's createdAt from the
cloudAgentRepository.agents.<account> list in state.vscdb. Beside it,
at the agent's createdAt, session synced: cursor cloud agent <bcId>
(vendor copy; from state.vscdb cloudAgentRepository.agents) records
that Cursor holds the conversation. The
session also has model: <model> (with requested=<id> when the
requested model id differs), source host: github.com <owner>/<repo>
(cloud agent), cloud agent: <name> repo=<repoUrl> status=<status>
workflowStatus=<workflowStatus> archived=<isArchived> killed=<isKilled>
vmStarted=<hasStartedVm> (from state.vscdb cloudAgentRepository.agents,
rowUpdatedAtMs) and, when the search index holds its text, cached
conversation text (conversation-search.db, source=cloud-cache): <text>,
one system row because the index runs the prompt and the reply
together; when the user answered the agent's request to authorise an
MCP server, the session also has permission decision: <userDecision>
mcp auth for cloud agent <bcId> tool_call=<toolCallId> (from state.vscdb
cloudAgentRepository.mcpAuthResolution, file mtime), with the tool call
id as tool_use_id. Its origin is remote and its synced yes; every other
Cursor session's synced is no, since the editor records no sync of a
local conversation.
The zed, goose, kiro, hermes and ollama parsers write model rows
in the same form. zed writes model: <model> provider=<provider> once
per thread, after the thread-start row, since a thread blob keeps one
model. goose writes model: <model_name> provider=<provider_name> after
each session's start row and model: <old> -> <new> provider=<provider>
before a message whose recorded model differs, with
requested=<requestedModel> when the resolved model is another id.
kiro writes model: <id> and model: <old> -> <new> before the rows of
the history entry whose request_metadata.model_id names it, ending
source=model_info or source=model when the entry had none and the
conversation-level field stood in; the entries it recovers from a
checkpoint snapshot or the tangent-mode main history track their own
model sequence. hermes writes them from the
session_model_usage table at each model's first use, with
provider=<billing_provider>, or one model: <id> from the session row
at its start. Since 0.19.0 a switch made in the Hermes TUI or desktop
app, stored as a message with display_kind model_switch, is also a
row model: <old> -> <new> provider=<provider> at the switch time,
<old> being the previous switch's model, else the last model the
session_model_usage table first saw before it; model: <new> when
neither is known, and model_switch: <text> when the stored message
does not parse. Such a switch can therefore appear twice, once from the
stored message and once from the usage table at the model's first use.
ollama writes them before the assistant message whose
model_name names the model; it also writes one permission setting:
integrations.<name>.automode=<true|false> (from .ollama/config.json, file
mtime) row per integration whose launcher configuration holds the key,
and permission setting: autoModeEnabled=<true|false> (set by Ollama's
Claude Desktop launcher, from <file>, file mtime) from the Claude Desktop
profile configLibrary/00000000-0000-4000-8000-000000000114.json under a
Claude-3p base, a file the catalog attributes to claude-desktop; both
at the file's modification time with an empty session_id. goose also writes
permission mode: <goose_mode> after each session's start row, and,
for a session the gateway relayed through its Telegram bot, session
relayed: gateway <platform> user <user_id> display <display name> (from
sessions.db session_type gateway, created_at). A tool confirmation in
sessions.db is permission request: <toolName> | <prompt> |
<arguments> (an empty prompt left out) and its answer permission
decision: <permission> (toolConfirmationResponse), <permission> one
of allow_once, always_allow, deny_once, always_deny and
cancel, both with the tool call's tool_use_id and tool_name; a
tool call the permission check refused or held, marked
goose.executable false and never answered, is followed by
permission decision: not executable (goose.executable=false). Its
standing lists are permission setting: <category>.<level>=<tool> (from
<file>, file mtime) per tool in permission.yaml (user or
smart_approve, then always_allow, ask_before or never_allow)
and permission setting: roaming_trust.<allowed|revoked_keys>=<key>
(from <file>, file mtime) per peer key in roaming_trust.json, and
each job in schedule.json is scheduled task: <id> <cron>
paused=<true|false> running=<true|false> source=<recipe file>
last_run=<time> current_session=<session id> recipe_base_dir=<dir>
(from <file>, file mtime), with parameters=<compact JSON> before
(from when the job has parameters and - for an absent value; these
rows carry the file's modification time and an empty session_id. zed
writes one permission setting: <key>=<value> (thread blob, as of
updated_at) row per grant the user gave for the rest of a thread
(profile, each sandbox_grants.write_paths and network_hosts entry,
and each sandbox_grants escape flag that is true): unlike the
settings-file rows these live in the thread blob, so their session_id is
the thread id and their timestamp_utc is the thread's updated_at, the
blob's last write rather than the time of the grant. It also writes, after
a failed tool result whose text is Zed's denial, permission decision:
denied (the user denied the call) or permission decision: auto_denied
reason=<text> (a rule denied it), with the call's tool_name and
tool_use_id; an approval leaves no record. From the user settings files
(settings.json, global_settings.json) it writes one permission
setting: <key>=<value> (from <file>, file mtime) row per entry of
agent.tool_permissions and agent.sandbox_permissions, per
agent_servers.<id>.default_mode and per session.trust_all_worktrees,
the key prefixed with the override block or profile it sits in
(stable.agent.tool_permissions.default), and from db.sqlite one
permission setting: trusted_worktrees.<path>=true[ host=<user>@<host>]
(from <file> trusted_worktrees, file mtime) row per trusted project, with
the path as project_path. Its other db.sqlite rows are sidebar thread:
title=<title> archived=<true|false> interacted_at=<time|->
main_worktree=<path|-> per native thread, at the sidebar row's
updated_at; unsent draft: <text> per draft prompt, at the draft's
updated_at, or ending (from <file> agent_draft_prompts, file mtime)
when the draft's sidebar row is gone, and unsent draft: <text> (thread
blob, as of updated_at) for a thread blob's own draft; archived worktree:
<worktree> of <main repository> branch=<branch> staged=<hash>
unstaged=<hash> original=<hash> ref=refs/archived-worktrees/<id> (from
<file> archived_git_worktrees, file mtime) per archived worktree and
linked thread; and sidebar terminal: <title> cwd=<directory> per
sidebar terminal at its created_at. Its scratch directory: rows are
scratch directory: <path> (sandboxed terminal temp directory of thread
<id>) (thread blob, as of updated_at) for a macOS thread's sandbox temp
directory and scratch directory: <worktree> (Zed-created git worktree
of <main repository>) (from <file> created_git_worktrees, created_at) for each
linked worktree Zed made, at its creation time, with host=<host> after
the main repository for a remote one; both have an empty session_id. These rows rest on source
alone. hermes also
writes one debug report uploaded: <url> (from pastes/pending.json,
expire_at minus 6 hours) row per paste.rs link that hermes debug
share uploaded and that is still due for deletion, at the upload time,
with an empty session_id: the uploaded logs hold recent messages but
name no session. Since 0.20.1 hermes also writes, at a session's
started_at after its model rows, session relayed: <source> chat
<chat_id> type <chat_type> thread <thread_id> user <user_id> name
<user_name> display <display_name> (from state.db sessions, started_at)
for a session carried through a messaging platform, so its relayed is
yes and every other Hermes session's no; permission setting:
yolo_mode=<true|false> (from state.db sessions.model_config,
started_at) when the session's /yolo bypass (every approval off) is
recorded; and scratch directory: <cwd> (git worktree the agent created
in <repo>, branch <git_branch>) for a session run in a worktree Hermes
made under <repo>/.worktrees/. The session-start row ends
| archived=1 for an archived session and | imported from <tool> <path> session <id> for a session Hermes copied from Claude Code or
Codex CLI. After a terminal tool result that records the approval
outcome, a permission decision: approved (<approval note>),
permission decision: blocked <error> (with user_summary=<summary>
when a person was asked) or permission decision: pending
pattern=<pattern_key> smart_denied=<true|false> row carries the call's
tool_use_id. Standing approvals are permission setting:
command_allowlist=<pattern> (from config.yaml, file mtime) per pattern
an "always" answer allowed, and permission setting:
pairing.<platform>.<user id> user_name=<name> (from
pairing/<platform>-approved.json, approved_at) (or
platforms/pairing/...) per messaging user the gateway's owner
approved, at the approval time. session deleted: <id>
(from state.db state_meta gateway.retired_session.v1, retired_at), with
sweep after the id when the idle sweep rather than a delete removed
the session, is the deletion record Hermes keeps after the rows are
gone, at the deletion time and with the deleted session's id. relay
peer: <source> <session_key> generation <n> (from state.db
conversation_generations), with no time or session, names a messaging
chat that ever had a conversation; bot mode room <name> (<room_id>)
seq <seq> <kind> actor <actor kind>:<actor id> (<display_name>): <text>
is one event of a desktop Bot Mode group chat, at its time; and
remote backend: <kind> <label> <url or user@host:port> (from
connections.json, file mtime), with org=, name= and profile=
when recorded, is a remote, SSH or Hermes Cloud backend the desktop app
was connected to, with no session. In the secondary sources, a prompt
history entry that starts with / is slash command: <entry>, each
cron run file starts with cron run: job <job id> (from cron/output,
file name time, host-local) and a run document with no prompt section
is one row cron run output: <document>, each response_store.db
response is api server response <id> status=<status> model=<model>
conversation=<name> (from response_store.db responses, created_at), and
each trajectory line is trajectory: model=<model> completed=<true|false>
(from <file>, timestamp host-local) before its turns, whose system
prompt is trajectory system prompt: <text>. - stands for an absent
value.
The kiro parser follows each cancelled tool_result of an Amazon Q
Developer CLI conversation with a row carrying its tool_use_id and
tool_name: permission decision: denied by=n when the user answered
the approval prompt with a bare n, permission decision: denied
by=deny-rule tool=<name> when a deny rule of the agent configuration
rejected the call, permission decision: cancelled by=interrupt for an
interrupt during tool execution, permission decision: denied
by=user-text when the user typed something else instead of approving,
and permission decision: cancelled when the message has no prompt; a
deny-rule rejection stored as a plain prompt gives the same deny-rule row
with the tool name and no id. Each agent configuration
(.aws/amazonq/cli-agents/*.json, or a project's
.amazonq/cli-agents/*.json with project_path the project) gives
permission setting: allowedTools=<entry> (from <file>, file mtime) per
entry and permission setting: toolsSettings.<tool>.<key>=<value> (from
<file>, file mtime) per tool setting, with an empty session_id. The
turns it recovers from a conversation's checkpoint snapshots or the main
history held aside in tangent mode follow the conversation's own rows,
each group behind checkpoint snapshot: tag=<tag>[ tool=<tool_name>]
recovered=<n> entries[: <description>] at the checkpoint's time or
tangent mode: main conversation held aside, recovered=<n> entries. A
project's .amazonq/.subagents/<agent>.json gives delegate task
launched: agent=<agent> pid=<pid>[ trust_all_tools=true]: <task> at the
launch, trust_all_tools=true marking the default agent, which the
delegate tool runs with --trust-all-tools, and, once the task ended,
delegate task <status>: agent=<agent> exit_code=<code> followed by
summary: and output: lines at its completion, with project_path the
task's working directory and no session_id.
OpenHands, Agent Zero, nanobot and Muse Code write the same kinds of row.
openhands writes model: <id> after each conversation's header row, at
the header's time: the model of the first response (token_usages[] in
base_state.json, joined on llm_response_id), else agent.llm.model.
It writes model: <old> -> <new> before the first event of a response
whose model changed, model: <old> -> <new> profile=<profile>[
class=<chosen_class>] after the agent's own switch
(SwitchLLMObservation, ClassifyAndSwitchLLMObservation), and, on
base_state.json at its modification time, model: <old> -> <new> (from
base_state.json agent.llm, file mtime) when the file names a model the
events do not reach (a switch over the Agent Server's REST endpoints). A
rejected call's tool_result is followed by permission decision:
rejected rejection_source=<source> <reason> with the call's tool_name
and tool_use_id, and an action's tool_use text ends with
security_risk=<value> when the event has one. Each entry of a security
analyzer's verdict is permission decision: analyzed risk=<risk>
analyzer=<analyzer> policy=<policy>[ details=<JSON>] with the analysed
action's tool_name and tool_use_id ( action_id=<id> when the action
is not on disk). At base_state.json's modification time it writes
permission setting: confirmation_policy=<kind>[ threshold=<threshold>][
confirm_unknown=<true|false>] (from base_state.json, file mtime),
permission setting: security_analyzer=<kind> (from base_state.json,
file mtime) (none when no analyzer is set) and permission setting:
blocked_actions.<action id>=<reason> (from base_state.json, file mtime)
per hook-blocked action. A conversation whose working directory is under
the Agent Server's default worktree root gets scratch directory:
<working_dir> (conversation worktree under the Agent Server's default
conversation_worktree_root /tmp/conversation-worktrees) after its model
row. agent-zero writes
model: <id> before an agent's first reply and model: <old> -> <new>
on a change, tracked per chat and agent number, with agent=<n> for a
subordinate agent. After the chat start row, at the file's modification
time, it also writes session synced: provider response <response_id>
previous <previous_response_id> model <provider_model_key> (<n> response
ids) agent=<n> (from chat.json agents[].data.responses_state, file
mtime) for each agent that chained a turn on a response the provider
stored, and for a chat carried by a messaging plugin session relayed:
telegram bot <bot> chat <chat id> user <user id> @<username>,
session relayed: whatsapp chat <chat id> number <number> or session
relayed: email thread <thread id> from <sender>, each ending (from
chat.json data, file mtime). After those, at the file's modification
time and holding the latest value only, an editor (ACP) session gives
permission mode: <acp_mode> (acp, as of file mtime), model: requested
<acp_model_id> (acp, as of file mtime) and scratch directory:
<workdir_path> (acp container transport, from chat.json
data.workdir_path, as of file mtime), whose project_path is that
directory, while the chat's other rows carry the editor's workspace
acp_cwd as their project_path; a model chosen for the chat gives
model: requested preset <name> (chat override, as of file mtime), or
model: requested chat=<provider>/<name> utility=<provider>/<name> (chat
override, as of file mtime) for a custom choice, never its API key. A
warning log item that records a permission decision is followed, at its
time, by permission decision: blocked tool=<name> profile=<profile>
(with tool_name), permission decision: denied automatically (provider
MCP approval request) or permission decision: <clarify, passed or
terminated> (infection check). Standing settings are permission
setting: tool_access.<key>=<value>, permission setting:
orchestrator.<cli>.<key>=<value> and permission setting:
<infection_check|orchestrator>=<enabled|disabled>, each followed by
profile=<profile> and project=<name> when the file is scoped and
ending (from <file>, file mtime), with an empty session_id. The
scheduler's tasks.json gives scheduled task: <name> type=<type>
state=<state> at the task's created_at and, for a failed run,
scheduled task failed: <error> at last_run; the Telegram chat map
gives session deleted: telegram bot <bot> chat <chat id> user <user id>
ctxid <ctxid> (from state.json, no chat.json in the input, file mtime)
for a mapped chat whose chat.json is not in the input; and the A0 CLI
connector's .agent-zero/.env gives session origin: remote agent-zero
instance <host> context <id> (from .agent-zero/.env, file mtime) for
the last chat it used, so agent-zero sessions read origin remote
or local. nanobot writes model: <preset> (preset) after
the session start row: the value is a model preset of nanobot's
configuration, not a model id. A /model <name> switch gives
model: <old> -> <new> (preset) requested=<name> resolved=<model id>
after the reply that confirms it; the session records only its last
preset, so in a switched session the first switch reads model: <new>
(preset) requested=<name> resolved=<model id> and the start row's
model: row is not written. After the session start row, at its time,
nanobot also writes session relayed: <channel> chat <chat id> (from
session key) for a session keyed to a messaging channel (Telegram,
Discord, Slack, WhatsApp, email and the other chat services it answers
through), session relayed: <channel> chat <chat id> (from
metadata.last_channel) when the session's last delivery route is such a
channel and differs from its key, and permission mode:
access_mode=<restricted|full> project=<path> (from session metadata
workspace_scope) for the last workspace scope a WebUI chat set. Its
config.json gives permission setting: <key>=<value> (from <path>,
file mtime) per standing setting (tools.restrictToWorkspace,
tools.ssrfWhitelist, tools.webuiAllowLocalServiceAccess,
tools.webuiAllowRemotePackageInstall, tools.exec.enable,
tools.exec.sandbox, each tools.exec.allowPatterns, denyPatterns,
sandboxRoBinds, sandboxRwBinds and allowedEnvKeys entry, and each
channels.<name>.allowFrom entry), <path> relative to the home and the
key spelled as the file spells it; its pairing.json gives permission
setting: pairing.approved.<channel>=<sender id> (from <path>, file
mtime) per approved chat sender and permission setting:
pairing.pending.<channel>=<sender id> expires_at=<time>
delivery_status=<status> (from <path>, created_at) per pending request,
at its created_at. A cron run record gives cron run: job <job name>
(<job id>) run <run id> status=<status> and a trigger run record
trigger run: trigger <trigger name> (<trigger id>) delivery <delivery
id> status=<status> attempts=<n>, each with error=<error> when the
run failed, beside the run's prompt and reply; the WebUI sidebar cache
gives webui sidebar entry: key=<key> title=<title> preview=<preview>
model_preset=<preset> updated_at=<time> (from .webui_session_index.json
version <n>) per cached chat, absent fields left out. muse-code writes, per session log,
permission mode: approval=<approval> reviewer=<reviewer>
profile=<profile id> followed by cause=<cause> and
format=<format> from the log's permission frame,
permission mode: security_mode=<value>, and permission mode: <old> ->
<new> on a change of either; model: <id> provider=<provider> at the
first model a log names and model: <old> -> <new> on a change or a
/model command; permission decision: <outcome>
kind=automated_review_completed risk_level=<r> user_authorization=<u>
status=<s> <rationale> for the automated reviewer's verdict and
permission decision: <decision> source=<source kind> policy_result=<p>
amendment=<a> for the decision applied (- for a missing value), both
at the record's place in the log, before the call's tool_result, and
carrying the call's tool_name and tool_use_id; and one
permission setting: projects.<path>.decision=<decision> (from
.config/muse/trust.json, file mtime) row per trusted project, with
project_path the project, the file's modification time and an empty
session_id.
The claude-code decision row also ends with
toolDenialKind=<value> userFeedback=<text> when the same line records
either key, each printed as recorded and userFeedback last because it
is free text. A line with toolDenialKind and no permissionDecision is
a denial that interrupted the turn (Claude Code 2.1.310 wrote it on macOS
for No at the permission dialog and for a declined plan, the reason
being the next prompt) and yields permission decision: reject
(interrupted) toolDenialKind=<value>; a line with only userFeedback
yields permission decision: - source=- reason=- userFeedback=<text>. The
claude-desktop parser writes model: <id> from each audit log's
system init line and model: <old> -> <new> when an assistant
line's model changes, from every audit log whether or not the session's
transcript exists; a model_refusal_fallback line, whose fields are not
recorded, is model_refusal_fallback: <compact JSON of its fields>. A
Cowork session record adds one permission grant: <key> <entry>,
permission grant: <key>.<member>=<value> or permission grant:
<key>=<value> row, with the same (as of <field>) time, per entry of
approvedToolNames, webFetchAllowedUrls, egressAllowedDomains,
userApprovedFileAccessPaths, enabledMcpTools and
fileDeleteApprovedMounts. A scheduled task's row ends with
permission_mode=<permissionMode> model=<model> (- when absent) and
carries the task's model in the model column. Each folder in
remote-session-spaces.json is permission setting:
entries.<sessionId>.folders=<folder> (from <file>, file mtime), with
project_path the folder and session_id empty, because the
<sessionId> there is the server-side id of a remote Cowork session.
Secondary sources¶
Many agents keep a second copy of some of their events beside their transcript store: a per-prompt history file, a mirror or projection of the transcript, a full-text index, a backup or snapshot of the store, a copy kept by a reset, a compaction or a repair. The table below names them for each agent. The analyzer parses every source on every run, even when the matching transcript exists, because a copy can survive the transcript's deletion, and keeps each event once: a row from a secondary source is dropped when a row from the agent's primary store has
- the same
host,user,agent,turn_typeandtext; twotool_useor twotool_resultrows that both carry atool_use_idmatch on the sametool_use_idinstead, and never on text when the ids differ, because a mirror can record a call in another form than the transcript does (Codex CLI'sthread_history_1.sqliteholds the parsed command, the rollout the model's arguments) and a call id is unique within a session; a tool row without an id matches on text. Claude Code's rows compare a slash command and a prompt in one form on both sides: itshistory.jsonlkeeps a command as typed (/rename lab-round) where the transcript has the command in tags (aslash command:orlocal_command:row), and a paste as the expanded text the parser builds from the placeholder, which the transcript's prompt wraps in<pasted_content id="...">...</pasted_content>; so the command matches as theusertext/<name>, plus<args>when its<command-args>is not empty, and a prompt with the wrapper removed and each run of whitespace collapsed to one space (the whitespace itself stays, so prompts that differ by a space stay distinct). A command line's window is 600 seconds, because a command that opens a picker or an overlay (/model,/help) is recorded in the transcript when it completes; the window extends forward only (a record never precedes its line by more than the source's own window), it applies against transcript records only (two history files keep their own window with each other), and within it a command line may not take a record written at or after the next line of the same command that could take that record (a line of the record's session, a session-less line of its project, or a line with neither), so a line whose own record is missing stays, under its own time, rather than taking a later line's record. A/clear(its history line carries the ended session, its record the new one), a/logoutand an/exittyped while a turn ran have no record to pair with and stay; - the same
session_id, when the secondary row has one; when it has none, the sameproject_path(whatever the primary row'ssession_id), when it has one; when it has neither, as a text-only prompt history does, any primary row of that host, user and agent; - when the secondary row has a
timestamp_utc, atimestamp_utcon the primary row too, at most the source's tolerance apart, the bound included; when the secondary row has none, no time constraint, so it matches on text and scope alone. A secondary row with a timestamp never matches a primary row without one.
The tolerance is per source: 2 seconds, except for the sources that stamp an event later than the primary store does, whose wider window the table below gives.
Matching is one-to-one. The secondary rows with a timestamp go first, in
timestamp order, each matched to the nearest primary row within its
tolerance that no earlier secondary row took, so a transcript cut after
three of five identical prompts keeps two of the five history lines.
Then the secondary rows with no timestamp go, those with a session_id
first, then those with only a project_path, then those with neither,
each group in input order, each matched to the earliest-timestamped
primary row still untaken (primary rows with no timestamp last, in input
order); taking them second keeps an undated row from claiming the
primary row a timestamped one pairs with, and taking the most specific
first lets a session's copy, not a text-only history line, take the
primary row. The primary row always survives, with its own source_file
and source_line.
Last, the secondary rows that no primary row took are matched against
the other secondary rows, one-to-one, under the same rule (the tolerance
of the row being tested), and a row that matches is dropped and counted
with the others. Its candidates are every secondary row a primary row
took, so a third copy of a primary event collapses onto the copy that
took it and only the primary row is left, and every secondary row tested
before it: the rows with a timestamp in timestamp order, then those with
none in input order. The copy kept of several is therefore the earliest
one with a timestamp, and a copy with no timestamp matches one with a
timestamp, with no time constraint, rather than the reverse. In this pass
only, a row that finds no candidate under its own scope tries the
broader ones, its project_path when it has one and then any scope, so
Antigravity's history line (a project, no session) and its brain mirror
line (a session, no project) of the same prompt meet; under a broader
scope a candidate whose session_id or project_path differs from one
the row records is never taken, so the same prompt typed in two sessions
stays two events. The copies of one event that several backups,
snapshots, archives or mirrors keep are thus shown once, even after the
primary store is gone. A row matches only a row from a different
source_file, so two identical prompts in one history file, with a
timestamp or without, are both kept as the two events the file records.
A system row whose text starts with parser: is never dropped and
never takes a copy, since it describes its own source_file (unreadable,
cut off, or holding an unparseable line), so a history file and a
transcript that fail with the same message are two problems, not one
event.
A secondary row with no match is kept, which recovers a prompt whose
transcript was deleted, excluded by size or truncated, and a prompt the
transcript no longer holds (a Gemini CLI rewind). The rule runs after
--agent and before --since, --until and --match, and the rows it
drops are counted per agent on the dropped: line of stdout and under
deduplicated in inventory.json. Before analyzer
0.13.0 both rows were written. Since analyzer 0.15.0 every secondary
event row kept is marked recovered yes in timeline.jsonl; a
system row of a secondary source is not, since it describes the
source or the session rather than a lost turn. A parser whose primary
and secondary copies live in one file pairs them itself and marks the
event rows it took from the secondary copy recovered yes in the same
way: since 0.19.0 the opencode parser reads the 1.x
message and part tables of a database upgraded to OpenCode 2.0 only
for a session its session_message table does not hold, matched exactly
on the session id, the kiro parser reads a conversation's checkpoint
snapshots and tangent-mode main history only for the turns the
conversation's history no longer holds, matched on the user message and
its time and the assistant message, and the openclaw parser reads the
full-text index (session_transcript_fts_content) and the consumed
pending inputs of an agent database only for an entry whose session and
entry id the database's transcript does not hold.
sessions.jsonl
recovered says whether a session was rebuilt from such rows alone
(yes) or in part (partial):
jq -c 'select(.recovered == "yes")' out/sessions.jsonl lists the
sessions whose primary transcript is gone. Two cases read partial or
yes without anything lost. Deduplication matches on text, so a
secondary source that records an event in another form than the primary
store (an index that compresses a tool call into one line) keeps that
row, and its session reads partial: partial marks a session to
compare against its secondary sources, not proof that the transcript
was cut. And Letta's server backend keeps the store of record on the
server, so the local transcript is its mirror, a secondary source, and
a server-backend Letta session reads yes or partial by design,
beside its session synced: server conversation row.
| Agent | Secondary source | Primary store |
|---|---|---|
agent-zero |
usr/scheduler/tasks.json of the install directory or volume (each task's prompt, with no timestamp, matches on text within the task's chat; its last result is stamped when the run ends: a 60-second window); the Telegram plugin's usr/plugins/_telegram_integration/state.json (only system rows, for a mapped chat that is gone) |
usr/chats/<ctxid>/chat.json |
aider |
.aider.input.history and .aider.llm.history, in a home or a repository |
.aider.chat.history.md beside them |
antigravity |
.gemini/antigravity-cli/history.jsonl; the brain mirror .gemini/<dir>/brain/<uuid>/.system_generated/logs/transcript_full.jsonl, else its chunks, else transcript.jsonl, else its chunks, <dir> each of antigravity-cli, antigravity, antigravity-ide and antigravity-backup (600 seconds: the mirror records a step's start, the database row its completion) |
.gemini/<dir>/conversations/<uuid>.db |
claude-code |
.claude/history.jsonl (and .claude-<name>/history.jsonl; a [Pasted text #<n> ...] placeholder is expanded from the line's pastedContents or paste-cache/<contentHash>.txt when either holds the paste; /exit and other commands the transcript does not record never pair) |
.claude/projects/<slug>/*.jsonl and its subagent files |
claude-desktop |
each Cowork session's local-agent-mode-sessions/<acct>/<org>/[agent/]<session dir>/audit.jsonl under the app-data directory; the session records local_<uuid>.json beside the session dirs and under claude-code-sessions/ (a Cowork record's first prompt has no timestamp: matches on text within the session) |
the session dir's .claude/projects/<slug>/*.jsonl and its subagent files |
cline |
.cline/data/db/session-search.db (no timestamp per message: matches on text and session; a message of content blocks keeps their type names and tool ids, so only plain-string prompts match); .cline/data/cache/user_input_history.jsonl (only the last line timestamped: the others match on text alone); .cline/data/logs/hooks.jsonl (tolerance one hour; tool rows match on the call id) |
.cline/data/sessions/<id>/<id>.messages.json |
codex-cli |
.codex/history.jsonl; .codex/state_5.sqlite (first prompt with no timestamp: matches on text alone); .codex/thread_history_1.sqlite; .codex/.codex-global-state.json (the desktop app's prompt history, no timestamp: matches on text within the thread); .codex/db-backups/<folder>/state_5.sqlite (a copy Codex set aside as corrupt, read as state_5.sqlite) |
the rollouts under .codex/sessions/ and .codex/archived_sessions/ |
cody |
a JetBrains project's .idea/cody_history.xml, the pre-agent plugin's history (the migration's chat id updatedAt as session and time; complete pairs match the migrated chat, an unpaired message is recovered) |
the sourcegraph.cody-ai row of the editor's state.vscdb and Cody-nodejs/[Data/]JetBrains-globalState/cody-local-chatHistory-v2 |
continue |
.continue/input_history.json (the CLI's prompt history, no session or project: matches on text at any distance in time); .continue/dev_data/<schema>/chatFeedback.jsonl (a rated reply's prompt and completion, stamped at the click: matches within the session at any distance in time) |
.continue/sessions/<id>.json and the dev_data chatInteraction.jsonl lines |
copilot-cli |
.copilot/session-store.db (turns, the prompt and the final reply of each turn, stamped when the turn ends: a one-hour window; its sessions name pairs with the workspace.yaml row; session_files, session_refs, checkpoints and forge_trajectory_events rows the log has no copy of are always kept); .copilot/session-state/<id>/rewind-file-snapshots/index.json (each turn's prompt, a one-hour window); .copilot/command-history-state.json (no timestamp, session or project: matches on text alone); each also under the pre-XDG .local/state/.copilot |
.copilot/session-state/<id>/events.jsonl and workspace.yaml |
cursor |
.cursor/projects/<project>/agent-transcripts/<composerId>/<composerId>.jsonl, the agent worker's copy of each conversation (a prompt is stamped to the minute in local time with its UTC offset: a 60-second window; replies and tool calls have no timestamp and match on text within the conversation; the tool calls name the tools differently from the database, so they never pair) |
the editor's User/globalStorage/state.vscdb cursorDiskKV composer and bubble rows |
gemini-cli |
.gemini/tmp/<slug>/logs.json; shell_history (no timestamp or session: matches on text and project, and pairs with the logs.json copy of the command), checkpoint-<tag>.json from /chat save and bug-report-history-<ms>.json from /bug (no timestamp or session: match on text and project, tool rows on tool_use_id) in the same directory; each also under .cache/.gemini |
.gemini/tmp/<slug>/chats/*.jsonl |
goose |
.local/state/goose/history.txt and .config/goose/history.txt (AppData/Roaming/Block/goose/data/history.txt on Windows; no timestamp, session or project: matches on text alone); .local/state/goose/logs/llm_request.<N>.jsonl (AppData/Roaming/Block/goose/data/logs/ on Windows; the last 10 provider calls, with no session or project: a 600-second window; streamed chunks of one message are assembled first; the llm request row has no twin and is always kept) |
.local/share/goose/sessions/sessions.db |
hermes |
.hermes/state-snapshots/<id>/state.db (copies of state.db; several snapshots collapse to one copy; collected only with --full before collector 1.18.0) and .hermes/sessions/<id>.jsonl (no timestamp on some lines: those match on text within the session); since 0.20.1 also .hermes/.hermes_history (no session or project: matches on text and time), .hermes/cron/output/<job>/<time>.md (no session: matches on text and time), .hermes/response_store.db (no timestamp per message: matches on text within the session) and the trajectory_samples.jsonl and failed_trajectories.jsonl files of a project (no timestamp per turn: matches on text within the project); all but the trajectory files also under .hermes<suffix>, AppData/Local/hermes, AppData/Local/hermes<suffix>, profiles/<name>/ and, since 0.19.0, the root of a Docker or Podman volume |
.hermes/state.db |
kilo-code |
.local/state/kilo/prompt-history.jsonl (no timestamp, session or project: matches on text alone) |
.local/share/kilo/kilo.db |
kiro |
.aws/amazonq/.cli_bash_history, the chat prompt history (no timestamp, session or project: matches on text alone); in each conversation, the checkpoint snapshots and the tangent-mode main history, read only for the turns the conversation no longer holds and paired by the parser itself |
data.sqlite3 conversations under amazon-q/ or kiro-cli/, and /save exports |
letta |
.letta/transcripts/<agent>/<conversation>/transcript.jsonl (stamped at the end of the turn: a one-hour window) |
.letta/lc-local-backend/conversations/<key>/messages.jsonl |
little-coder |
the v0.0.x .little-coder/sessions/history.json and sessions/mr_sessions/session_latest.json (the same sessions under the same id and saved_at); .little-coder/input_history.txt (no timestamp, session or project: matches on text alone) |
the v0.0.x .little-coder/sessions/daily/<date>/session_*.json and the /save files under sessions/ |
muse-code |
.local/share/muse/tui-history.jsonl (no timestamp: matches on text alone) |
.local/share/muse/sessions/.../session.jsonl |
nanobot |
.nanobot*/webui/<key>.jsonl and <key>.segments/NNNNNN.jsonl (tolerance 15 hours: true UTC against the session file's local time, at most 14 hours apart, plus an hour of turn); .nanobot*/history/cli_history (no session or project: matches on text and time); when a sessions.sqlite3 sits in the namespace directory, the namespace's <key>.jsonl files and their jsonl-backup/ copies, which the database imported (since 0.19.0); a workspace's cron/runs/*.json and triggers/runs/*.json run records (tolerance 15 hours, as for the WebUI transcript: true UTC); .nanobot*/sessions/<workspace-id>/.webui_session_index.json (system rows only, which never pair) |
.nanobot*/sessions/<workspace-id>/sessions.sqlite3, else .nanobot*/sessions/<workspace-id>/<key>.jsonl |
open-interpreter |
.openinterpreter/history.jsonl; .openinterpreter/state_5.sqlite (first prompt with no timestamp); .openinterpreter/thread_history_1.sqlite; .openinterpreter/db-backups/<folder>/state_5.sqlite |
the rollouts under .openinterpreter/sessions/ and .openinterpreter/archived_sessions/ |
openclaw |
.openclaw/agents/<id>/sessions/ copies of a transcript: the published *.jsonl.reset.* / *.jsonl.deleted.* archive files, *.checkpoint.<uuid>.jsonl and *.jsonl.pre-doctor-*-repair-*.bak (each copy's session-start row names it and is kept) |
.openclaw/agents/<id>/agent/openclaw-agent.sqlite (transcript_events and the session_transcript_archives blobs) and the legacy sessions/<id>.jsonl |
opencode |
.local/share/opencode/opencode-next.db, the pre-release 2.0 database that 2.0 imports and leaves in place; in a database upgraded to 2.0, the 1.x message and part tables, read only for a session session_message does not hold and paired by the parser itself (since 0.19.0); .local/state/opencode/prompt-history.jsonl (no timestamp, session or project: matches on text alone); .local/state/opencode/session.json (its session pinned: rows are system rows and never pair) |
.local/share/opencode/opencode.db (or opencode-<channel>.db): session_v2 and session_message |
openhands |
.openhands/projects/<sha256 of cwd>/prompt_history.json, the CLI's last 100 prompts per working directory (no conversation id: matches on text and time; project_path is the working directory of a conversation whose SHA-256 is the directory name) |
.openhands/conversations/<hex>/events/event-*.json |
pearai |
the PearAI output channel log <editor>/logs/<session>/window<N>/exthost/output_logging_<time>/<n>-PearAI.log (no timestamp, session or project: matches on text alone; a prompt sent with context items carries them and never pairs); .pearai/dev_data/chat.jsonl (the rated replies, no timestamp: matches on text within the session) |
.pearai/sessions/<sessionId>.json |
pi |
.pi/agent/pi-debug.log, the last /debug snapshot of the open session's messages (no session or project; one hour: a message's own time is its start, the session entry's its end) |
.pi/agent/sessions/<cwd>/<time>_<id>.jsonl |
qwen-code |
.qwen/tmp/<hash>/logs.json; .qwen/usage_record.jsonl (per-session summaries, system rows that no transcript row repeats) |
.qwen/projects/<slug>/chats/*.jsonl |
roo-code |
.roo/cli-history.json (no timestamp, task or project: matches on text alone) |
the task files under .vscode-mock/global-storage/tasks/ and the editor's globalStorage/rooveterinaryinc.roo-cline/tasks/ |
shellgpt |
the response cache AppData/Local/Temp/shell_gpt/cache/<md5>, one answer per file (no timestamp, session or project: matches on text alone; the answer after a function call is cached without the chat file's > @FunctionCall marker, so it never pairs and is kept) |
the chat files chat_cache/<id> |
tabby |
.tabby/ee/db.backup-*.sqlite and dev-db.backup-*.sqlite (copies of the live threads, pages, events and integrations); .tabby/events/YYYY-MM-DD.json (its user and assistant rows pair with user_events; its system rows name the owner by encoded id, not email, and never pair); the IntelliJ intellij-tabby-command-history.xml and the VS Code state.vscdb edit.recentlyCommand (no timestamp; the server keeps no copy of an inline-edit instruction, so they never pair) |
.tabby/ee/db.sqlite or dev-db.sqlite |
twinny |
the editor's User/workspaceStorage/<id>/rjmacarthy.twinny/agent-changes/<id>.json (or under globalStorage/rjmacarthy.twinny/; system rows with the project root and no session, which never pair); the log channel logs/.../rjmacarthy.twinny/Twinny.log and Twinny.log_<n> (system rows with no session, which never pair) |
the rjmacarthy.twinny row of the editor's User/globalStorage/state.vscdb |
vscode |
User/workspaceStorage/<hash>/chatEditingSessions/<id>/state.json, the chat editing state (only edit state: system rows, which never pair and are never marked recovered; it outlives a deleted chat) |
User/workspaceStorage/<hash>/chatSessions/<id>.jsonl or .json |
The 2 seconds cover Codex's history.jsonl, which records whole seconds
beside the rollouts' milliseconds, and Antigravity's history line, under
a second from the database step. Qwen Code's logs.json rows have no
project path and match by sessionId only. A known miss: a prompt the
agent queued and sent later (Claude Code queues input typed while it
works) can be minutes from its transcript record, and is then shown
twice, the history row at the time it was typed. Collectors before
1.18.0 excluded Hermes's quick snapshots (state-snapshots/) unless run
with --full, so in their collections the snapshots appear only in a
full collection or loose input.
Some sources have a wider window, because they stamp an event later
than the primary store does or more coarsely. Cursor's agent transcript
stamps a prompt to the minute, so its rows match within 60 seconds. Antigravity's brain mirror records only
when a step started, while the database rows of a response or a tool
result carry its completion, so its rows match within 600 seconds.
Letta's transcript.jsonl stamps every line of a turn when the turn
ends, so its window is one hour; a turn longer than that leaves its
earlier lines shown twice. Copilot CLI's session store writes a turn
when it ends, so its turns rows and its rewind snapshots match within
one hour, and only against an identical event of the same session.
Cline's hook audit log logs/hooks.jsonl stamps a tool call when it
starts or ends and the transcript stamps the message when the turn is
written, so its rows match within one hour; its call rows match on the
call id.
Goose's request logs stamp a message with the time of its first
streamed chunk, while sessions.db stores the later of that time and
the session's newest stored message, so a database row can follow its
log copy by as long as the tool calls before it ran; their window is
600 seconds.
nanobot's WebUI transcript stamps events in
true UTC while its session files record host local time, emitted as if
UTC, so its window is 15 hours (the largest UTC offset, 14 hours, plus
an hour of turn); a WebUI row kept because no session row matched
carries true UTC, unlike every other nanobot row. Continue's two
secondary sources have a window of ten years, in effect none: its
session rows carry the session's creation time rather than the prompt's,
so a prompt history entry is later by as long as the session ran, and a
chatFeedback line is stamped when the user rated the reply, any time
after its chatInteraction line; a history entry still never matches a
session row with no time, as a session the index lost has. Hermes's
.hermes_history (the classic CLI's entries) and cron run files record
host local time, emitted as if UTC, against the true UTC of state.db,
so their window is 14 hours plus 2 seconds; the TUI's history entries
are true UTC and fall within it as well. nanobot's cron and trigger run
records stamp in true UTC as well and use the same window; their prompt
row carries the text the session stores for the turn, rebuilt from the
record, and a trigger's content or reply longer than the 4,000
characters nanobot keeps in its record is shown twice.
Several sources record no time for an event, and their rows match on
text and scope alone: Muse Code's tui-history.jsonl, Goose's
history.txt, Kiro's .cli_bash_history and Roo Code's
cli-history.json record none at all;
Claude Desktop's Cowork session record keeps its first prompt with no
time of its own; Cline's session-search.db records none per message,
and its CLI user_input_history.jsonl only on its newest line, since
every rewrite stamps every line with the time of that rewrite; Codex
CLI's and Open Interpreter's state_5.sqlite hold a thread's first
prompt beside the thread's creation time, which can precede the prompt,
so the prompt row has none. A row with neither a session nor a project,
such as a Goose history.txt line, can match a primary row of any
session of the same user, so a prompt typed in two sessions is dropped
once, against the earliest database row.
Some secondary rows never match, because the primary store has no twin
for them or renders the event differently, and are always kept: Tabby's
event-log system rows, which name the server user by an encoded id
where the database gives an email, and its editor command histories,
whose inline-edit instructions the server does not store; Cline's session-search.db messages
stored as content blocks, which the index keeps with their block type
names and tool ids; a nanobot WebUI prompt whose session copy has
runtime context appended (a quoted reply, an automation template);
Claude Desktop's permission requests, responses and auto-decisions,
result lines and init: cwd=... model=... rows, which the audit log
alone records (its model: <id> row does match the transcript's), and
the session records' system rows (the session, its grants, its error);
and the session-start row each OpenClaw copy begins with, which names
the copy.
Two sources that look like second copies are primary. Ollama's
.ollama/history is the ollama run REPL's readline history, while
its db.sqlite holds the desktop app's chats, a different surface, so
both are parsed as primary records. little-coder's prompt history rows
carry agent little-coder, while its transcripts are pi sessions under
agent pi; the rule pairs rows of one agent only, so it cannot pair
them, and both stay. Continue's toolUsage.jsonl is not a secondary
source either: its event rows carry their own time
but no session or project, and the session rows they repeat carry only
the session's creation time, so a match within 2 seconds would be
chance, and both are kept.
One fallback remains: Cline's api_conversation_history.json is read only
when the task's ui_messages.json is absent, because the two record different
texts for the same turn (the API history wraps the first prompt in <task>
tags, and its tool calls are content blocks where the UI messages have one
JSON record per tool), so the rule could not pair them and the task's turns
would be shown twice; see Parser plan in
research/cline.md.
sessions.jsonl¶
sessions.jsonl has one record per distinct host, user, agent and
session_id, built from the records of timeline.jsonl that have a
non-empty session_id; records without one belong to no session. It is
built after filtering, so it summarises the rows written. It is sorted by
first_timestamp_utc, then agent, then session_id, so sessions with
no timestamp come first. Its fields, in order:
| Field | Type | Meaning |
|---|---|---|
host |
string | The host of the session's timeline records. |
user |
string | The user of the session's timeline records. |
agent |
string | The agent of the session's timeline records. |
session_id |
string | The session_id of the session's timeline records, never empty. |
project_path |
string | The first non-empty project_path among the session's records in timeline order, so a Windows path is in the canonical form described under timeline.jsonl; "" when none has one. A scratch directory: row, which names the agent's own directory rather than the session's project, counts only when no other record has a path (since 0.21.0). inventory.json counts the session under this path. |
first_timestamp_utc |
string | The earliest non-empty timestamp_utc among the session's records, "" when none has one. |
last_timestamp_utc |
string | The latest non-empty timestamp_utc among the session's records, "" when none has one. |
models |
array of strings | Each distinct non-empty model of the session's records, in order of first use in the timeline; [] when none was recorded. |
user_turns |
integer | The number of the session's records with turn_type user. |
assistant_turns |
integer | The number with turn_type assistant. |
tool_calls |
integer | The number with turn_type tool_use. |
source_file |
string | The source_file that contributed the most of the session's records, the first met on a tie, so a transcript wins over a one-line-per-prompt history file; "" when no record names one. |
synced |
string | Whether the session is synced to a remote account, from the agent's own record of it: yes when one of the session's records is a system row whose text starts with session synced: (written by the agent-zero, chatgpt-desktop, claude-code (since 0.21.0), cline, codex-cli, copilot-cli, cursor, kilo-code, letta, little-coder, open-interpreter, openclaw, opencode, pi and vscode parsers); no when the agent's parser records sync state and no record carries that row; "" when the agent's parser records no sync state, so nothing is known. A string rather than a boolean, because false would say the agent can sync and did not. Since analyzer 0.14.0. |
relayed |
string | Whether the session was carried through a messaging platform (Telegram, WhatsApp, email, a GitHub thread), which keeps its own copy of the conversation, from the agent's own record of it: yes when one of the session's records is a system row whose text starts with session relayed: (written by the agent-zero, goose and qwen-code parsers, and since analyzer 0.20.1 by the crush, hermes, letta, little-coder, nanobot and openclaw parsers); no when the agent's parser records relay state and no record carries that row; "" when the agent's parser records no relay state, so nothing is known. A relay is not a sync: the copy is on the platform, not in the agent vendor's account, so a session can read yes in both fields (an Agent Zero Telegram chat chained on a stored provider response). The little-coder rows in both fields come from the v0.0.x .little-coder/config.json and name no session, so every little-coder session reads no in both. Since analyzer 0.15.0. |
origin |
string | Where the session began, from the agent's own record of it: remote when one of the session's records is a system row whose text starts with session origin: remote (written by the vscode parser, for a Copilot cloud agent task that ran on GitHub's servers and is only listed in the editor, and by the chatgpt-desktop parser, for a ChatGPT conversation the app lists from the account; since analyzer 0.20.0 also by the cursor parser, for a conversation whose header names an agent location other than local, and since analyzer 0.20.1 for a Cursor cloud agent, session origin: remote cursor cloud agent, by the cline parser, for a Cline Cloud session, and by the agent-zero parser, for the chat the A0 CLI connector last used on an Agent Zero instance; since analyzer 0.21.0 by the claude-code parser, for a session started from claude.ai through Remote Control or a cloud session brought local with claude --teleport); local when the agent's parser records origin and no record carries that row; "" when the agent's parser records no origin, so nothing is known. A remote vscode, chatgpt-desktop or cursor session is also synced yes, since the vendor holds its copy; since analyzer 0.20.1 every other Cursor session's synced is no. Since analyzer 0.15.0. |
recovered |
string | Whether the session was rebuilt from secondary sources (see Secondary sources): yes when every one of its records in timeline.jsonl has recovered yes, so its primary transcript is gone and only secondary sources hold it; partial when some do, so the primary was cut or rewound and a secondary source kept turns it lost; no when none does and the agent's parser reads at least one secondary source; "" when the agent's parser reads no secondary source, so nothing is known. Computed after the filters, over the session's event records only (system rows left out), so a secondary source's header, record or parser: row never makes a complete session partial, and a session with no event record reads no or "". Because deduplication matches on text, partial also results when a secondary source records an event in another form than the primary store does (an index that compresses a tool call into one line), so it marks a session to compare against its secondary sources, not proof of a cut; and a Letta session on the server backend, whose local transcript mirrors the server's store of record, reads yes or partial by design. Since analyzer 0.15.0. |
This is the sessions.jsonl that timeline -o out writes for
the example collection, the run shown under
timeline: one record per session, alice's Claude Code
sessions and bob's Codex CLI thread. The Remote Control and teleported
sessions read synced yes and origin remote; the terminal
session, which turned on Remote Control, reads synced yes but
origin local, since it began on this host; the sessions known only
from alice's prompt history read recovered yes. The Aider and
Ollama rows and the settings rows have no session_id, so they belong
to no session:
{"host":"wsdev14","user":"alice","agent":"claude-code","session_id":"18386daa-971d-40ca-828a-12b746180948","project_path":"/srv/invoicing-old","first_timestamp_utc":"2026-10-27T08:20:14.903Z","last_timestamp_utc":"2026-10-27T08:20:14.903Z","models":[],"user_turns":1,"assistant_turns":0,"tool_calls":0,"source_file":"/home/alice/.claude/history.jsonl","synced":"no","relayed":"","origin":"local","recovered":"yes"}
{"host":"wsdev14","user":"alice","agent":"claude-code","session_id":"083d4578-89db-41c5-8894-271b3f00cb9b","project_path":"/srv/proj","first_timestamp_utc":"2026-10-27T08:41:37.226Z","last_timestamp_utc":"2026-10-27T08:41:37.226Z","models":[],"user_turns":1,"assistant_turns":0,"tool_calls":0,"source_file":"/home/alice/.claude/history.jsonl","synced":"no","relayed":"","origin":"local","recovered":"yes"}
{"host":"wsdev14","user":"alice","agent":"claude-code","session_id":"d4ccb1ca-a86b-4f26-9501-752de29ee24d","project_path":"/srv/proj","first_timestamp_utc":"2026-10-27T09:11:58.402Z","last_timestamp_utc":"2026-10-27T11:02:57.130Z","models":["claude-opus-5-5","claude-fable-5-1"],"user_turns":3,"assistant_turns":7,"tool_calls":10,"source_file":"/home/alice/.claude/projects/-srv-proj/d4ccb1ca-a86b-4f26-9501-752de29ee24d.jsonl","synced":"yes","relayed":"","origin":"local","recovered":"no"}
{"host":"wsdev14","user":"alice","agent":"claude-code","session_id":"e9022006-4f4a-5a45-a27b-6623a05d1ab4","project_path":"/srv/proj","first_timestamp_utc":"2026-10-27T13:31:05.512Z","last_timestamp_utc":"2026-10-27T13:31:47.392Z","models":["claude-fable-5-1"],"user_turns":1,"assistant_turns":2,"tool_calls":1,"source_file":"/home/alice/.claude/projects/-srv-proj/e9022006-4f4a-5a45-a27b-6623a05d1ab4.jsonl","synced":"yes","relayed":"","origin":"remote","recovered":"no"}
{"host":"wsdev14","user":"bob","agent":"codex-cli","session_id":"01a17dab-2784-70e1-8cd3-70e4412754b8","project_path":"/srv/proj","first_timestamp_utc":"2026-10-27T14:02:11.204Z","last_timestamp_utc":"2026-10-27T14:02:47.301Z","models":["gpt-6.1-sol"],"user_turns":1,"assistant_turns":1,"tool_calls":5,"source_file":"/home/bob/.codex/sessions/2026/10/27/rollout-2026-10-27T14-02-11-01a17dab-2784-70e1-8cd3-70e4412754b8.jsonl","synced":"no","relayed":"","origin":"","recovered":"no"}
{"host":"wsdev14","user":"alice","agent":"claude-code","session_id":"99b95ab5-5229-41e2-bc47-dcba1398c74b","project_path":"/srv/proj","first_timestamp_utc":"2026-10-27T16:40:02.118Z","last_timestamp_utc":"2026-10-27T16:56:08.940Z","models":["claude-opus-5-5","claude-fable-5-1"],"user_turns":2,"assistant_turns":2,"tool_calls":1,"source_file":"/home/alice/.claude/projects/-srv-proj/99b95ab5-5229-41e2-bc47-dcba1398c74b.jsonl","synced":"yes","relayed":"","origin":"remote","recovered":"no"}
Compatibility¶
The two JSONL layouts are a compatibility contract with the analysts and tooling that consume them. A future version may append new fields after the existing ones, but existing fields keep their names, order, type and meaning.