Activity
Yardsort keeps a small, local record of what it itself saw happen in a workspace: when an agent, a shell or the project's run command was started, whether that was a fresh conversation or one being resumed or forked, and how the process ended. It is called activity, it lives in your Yardsort database, and it never leaves your machine.
This is the first piece of a longer plan — see the design note on agent events — and it is deliberately modest. Nothing here reads what an agent prints or does. The terminal is still the truth; activity is the bookkeeping around it.
What is recorded
One run per process Yardsort starts in a workspace, and a few events for each:
| Event | When |
|---|---|
process.started | The process is up. Says which agent (or shell, or the run command's program), the model if one was chosen, whether it was fresh, resumed or forked, and whether the app or ys started it. |
process.exited | The process ended: the exit code and, where the platform reports one, the signal. Says how Yardsort learned of it — as it happened, from the background process's spool, or because the process was simply gone when Yardsort next looked. |
process.spawn_failed | The program could not be started at all — usually "not found on PATH". |
session.resumed | A recorded conversation was continued in a new process. |
session.forked | A copy of a conversation was started, and which one it came from. |
What is not recorded here, on purpose: your first message, the command line, anything the
agent printed, the files it touched, tool calls, tokens. Those need the agent's own cooperation,
which Yardsort asks for per agent and only when you turn it on — today for
Claude Code, Codex,
OpenCode and Grok. Every event names its source
(yardsort/lifecycle, claude/hook, codex/session_file, opencode/plugin,
grok/session_file) so you can tell "Yardsort saw the process end" from "the agent says it
wrote a file".
Activity is on by default, because it is only what Yardsort already knew; switch it off in Settings → General and nothing new is written. What is already recorded stays until you clear it.
The timeline (experimental)
Turn on Show the activity timeline in Settings → General and every workspace gains an Activity button at the left of its footer. It opens a list, newest first, of that workspace's events, with Show earlier to page back, Refresh, Clear for that workspace, and a note on what the list covers.
It is experimental: the words and the layout will change as later stages add more to show. While an agent is reporting, the list moves as it works; you do not have to press Refresh.
A row that names a file which is on the Changes list — a reported write, a Write done — has a
Show diff button that opens that file's diff in the right panel. The diff is everything that
changed since the last commit, whoever changed it; the row says the agent reported writing the
file, not which lines are its. The Changes list shows the same join from the other side: a badge
on each file an agent reported writing. See Who wrote it.
What Claude Code reports
Switch on Capture what Claude Code reports in Settings → General (it needs Record when
agents start and exit, and is off by default) and every Claude Code that Yardsort starts —
from the composer, the tab bar, Resume, Fork or ys — reports what it does, in its own words:
| Timeline row | What Claude Code said |
|---|---|
| agent session started, agent resumed its session, agent forked its session | Its session began, and how. On a resume, how many tokens of context it picked up. |
| prompt submitted · 147 characters | You sent a message. The length, never the text. |
| Edit started, Edit done · src/app.rs · 12 ms | A tool ran: its name, the file's path relative to the workspace (or a file outside the workspace), and how long it took. |
| Bash started, Bash done | A command ran. The command itself is not recorded. |
| Agent (Explore) started | It started a subagent, of that type. |
| Read failed, Bash denied, permission asked for Bash | A tool failed, was refused, or is waiting for your yes. No error text, no command. |
| agent raised a notification · permission_prompt | It wants you: a permission prompt, or it has been idle waiting for input. |
| agent finished its turn · 154,003 tokens of context / agent's turn failed · rate_limit | The turn ended, and how much context its last request carried — read from the turn's last message in Claude Code's transcript, its usage only, after Claude Code has written it (the hook waits up to a second for that) — or it ended in an error of that kind. |
| agent compacted its context, agent switched model | Housekeeping it did. |
| agent session ended | It is shutting down, and why. |
The context counts are what the context bar reads, for Claude Code and Codex alike.
The run's own claude started row says reporting through hooks when this was on for it, so a quiet timeline means the agent had nothing to say, not that nobody was listening.
How it works, and what it touches. Claude Code has hooks: commands it runs at points in its
own life, handing them a description of the moment. Yardsort gives its own launches of Claude
Code one extra settings file (claude --settings <file>, kept under activity/hooks/ in your
data directory) whose hooks run the Yardsort executable itself, as an argument list, never
through a shell. Claude Code merges that file with your settings, so your own hooks and
settings are untouched — nothing is written to ~/.claude, and a launch made while the switch
is off has none of ours. The hook keeps the moment's metadata, drops the rest before anything
reaches disk, leaves one small file in activity/inbox/, and exits — with a success code
whatever happened, because a hook that fails can make Claude Code stop and ask, and Yardsort
only watches. The app takes the inbox in as files land, on start, and on every exit; ys takes
it in before it lists anything. It works while the window is closed, exactly like the exit
spool.
What is kept, and what is not. Names, ids, relative paths, durations, counts, kinds. Not the
prompt, not a command, not a tool's input or output, not what Claude wrote back, not a path
outside the workspace. The recorded shapes come from Claude Code 2.1.280; a newer version
that renames a field loses that detail, not the event. Claude Code started by any other means —
from your own terminal, say — reports nothing to Yardsort. A Claude Code harness whose arguments
already carry --settings or --bare is left alone, and the timeline's settings show a
hooks_not_armed counter so you know.
What Codex reports
Switch on Capture what Codex reports in Settings → General (also needs Record when agents
start and exit, also off by default) and every Codex that Yardsort starts reports each of its
turns. Codex works differently from Claude Code, so the mechanism does too: Codex is given, for
that launch only, a notify program — the Yardsort executable — that it runs at the end of every
turn with the turn's ids. Yardsort then reads what the turn did from Codex's own session file,
the one Codex keeps under ~/.codex/sessions/ and that codex resume reads. Rows appear when a
turn ends, not while it runs:
| Timeline row | What it comes from |
|---|---|
| agent session started | The session file's header: which Codex version, and that it was started from the CLI. |
| prompt submitted · 214 characters | Your message. The length, never the text. |
| shell done · exit 0 · 3 ms, shell failed · exit 1 | Each command Codex ran, with its exit code and how long it took. The command itself is not recorded. |
| file added · hello.txt, file changed · … | Each file Codex wrote through its patch tool: the path relative to the workspace, and whether it was added, changed or deleted. |
| mcp:server/tool done | A tool call to one of your MCP servers. |
| tokens used · 29,842 total · 138 out | What the turn cost. Codex records this; Claude Code's hooks do not. |
| agent finished its turn · 11.0 s · 14,992 of 258,400 tokens of context | The turn ended, how long it took, and how much of its window the turn's last request filled, from the session file's own count. |
Why not Codex's hooks, which look just like Claude Code's? Because Codex, rightly, refuses to run
a hook until you have reviewed it in its own /hooks screen, and the only way past that is a
flag that also waives review for every other hook, including a cloned repository's. Yardsort will
not pass that on your behalf. notify needs no review, is run as an argument list rather than
through a shell, and touches nothing in ~/.codex. If your config.toml has a notify of your
own, Yardsort's runs first and then yours, with the same argument, so nothing you set up stops
working.
What is kept, and what is not. Ids, exit codes, durations, counts, relative paths, kinds of
change. Not a command, not a file's contents, not your message, not Codex's answer, not its
reasoning, not a path outside the workspace. A turn that notify reports before Codex has finished
writing it to the session file is picked up on a later look — the app looks again every few
seconds while a turn is waiting, ys on its next command — for up to five minutes; after that,
or if the file cannot be read at all, the timeline still shows agent finished its turn, marked
as coming from notify alone, and Settings → General counts what was missed
(codex_turn_incomplete, codex_session_file). If Codex's automatic reviewer is on, its review runs as a second thread, and its turns show as
agent finished its turn from notify alone. Recorded against Codex 0.156.1; the session
file is Codex's own and undocumented, so a newer version may change a detail, and a detail this
build does not recognise is skipped rather than guessed. A Codex harness whose arguments already
set notify is left alone.
What OpenCode reports
Switch on Capture what OpenCode reports in Settings → General (also needs Record when
agents start and exit, also off by default) and every OpenCode that Yardsort starts reports as
it works. OpenCode has plugins: small programs it loads and calls at points in its own life.
Yardsort gives its launches one, for that launch alone, through OpenCode's environment
(OPENCODE_CONFIG_CONTENT, which OpenCode merges with your own configuration), kept under
activity/hooks/ in your data directory. Your opencode.json is not edited, and your own
plugins keep running beside it.
| Timeline row | What OpenCode reported |
|---|---|
| agent session started | A session began, and which OpenCode version. |
| prompt submitted · 216 characters | Your message. The length, never the text. |
| write started, write done · hello.txt | A tool ran: its name and, for a file tool, the path relative to the workspace. A bash done row carries the exit code. |
| read failed · missing.txt · 5 ms | A tool failed, and how long it took. No error text. |
| file changed · hello.txt | OpenCode edited a file. |
| permission asked for bash | It is waiting for your yes. |
| tokens used · 10,814 total · 2 out | What each of its replies cost, with the model. OpenCode reports this per step, so a turn has several. |
| agent finished its turn | It went idle: your turn. |
What is kept, and what is not. Names, ids, relative paths, exit codes, counts, the failed
tool's duration. Not your message, not a command, not a tool's output, not a file's contents,
not an error message, not OpenCode's reply, not a path outside the workspace. The plugin keeps
only those fields before anything leaves OpenCode's process, and hands them to the Yardsort
executable, which writes one small file to the inbox. Recorded against OpenCode 1.18.31. An
OpenCode harness whose arguments carry --pure — OpenCode's own "no external plugins" — is left
alone, and the timeline's settings count it under hooks_not_armed.
What Grok records
Switch on Read what Grok records in Settings → General (also needs Record when agents start
and exit, also off by default) and every Grok that Yardsort starts has its turns read from the
log Grok keeps of its own accord. Grok writes, for each session, a directory under
~/.grok/sessions/ with an event log that holds no commands, paths, prompts or replies — only
which tool ran, how long it took, how it came out, which permissions you were asked for, and
when each turn began and ended — plus a file of tokens and cost per turn. Yardsort chooses
Grok's session id when it starts it, so it knows which directory is which conversation's, and
reads it as it grows: every few seconds while the agent is running, once more after it exits.
Nothing is given to Grok, and nothing in ~/.grok is written.
| Timeline row | What Grok recorded |
|---|---|
| agent session started · grok-4.7 | The session, its model and reasoning effort. |
| agent started a turn · turn 0 · grok-4.7 | You sent a message. Grok's log does not say how long it was. |
| read_file started, read_file done · 8 ms | A tool ran, and how long it took. Grok's own tool names: run_terminal_command, read_file, search_replace… |
| read_file failed · 8 ms | A tool failed. |
| run_terminal_command allow · you took 4.2 s | A permission you were actually asked for, and how long you took; or deny. The ones Grok granted itself at once are not listed. |
| tokens used · 57,107 total · 464 out | What the turn cost. costUsdTicks is kept as Grok writes it; its unit is not documented. |
| agent finished its turn / agent's turn was interrupted | The turn ended, and how. |
What is kept, and what is not. Tool names, durations, outcomes, decisions, waits, turn numbers, models, token counts. Not a command, not a path, not your message, not Grok's reply — the log never had them. Recorded against Grok 1.0.41; the event log is Grok's own and not in its documented file list, so a newer version may change it, and a line this build does not recognise is skipped rather than guessed.
What OMP and pi report
Switch on Capture what OMP reports or Capture what pi reports in Settings → General
(both also need Record when agents start and exit) and every OMP or pi that Yardsort starts is
given a small extension, on its command line for that launch alone, that reports what it does.
OMP is a fork of pi and the two share one extension API, so it is the same file for both. Your
own extensions keep running; nothing under ~/.pi or ~/.omp is edited. Passing
--trusted-extension yourself, in the harness's arguments, means that launch gets no extension.
| Timeline row | What the agent reported |
|---|---|
| agent session started · openai-codex/gpt-5.3-codex | The session and its model. On pi, the session id is one Yardsort chose; OMP picks its own. |
| prompt submitted · 184 characters | You sent a message, and how long it was. Never the message. |
| agent started a turn · turn 0 · openai-codex/… | A turn began. |
| write started, write done · src/a.rs · 12 ms | A tool ran, on which file, how long it took. Tool names are the agent's own: write, read, bash, edit, grep… |
| read failed · missing.txt | A tool failed. Not why. |
| permission asked for bash, bash allow | OMP asked you for permission, and what you answered (allow / deny). pi has no such events. |
| agent finished its turn · 6.1 s · 19,963 tokens | The turn ended, how long it took and what it cost. The cost in the agent's currency is in the payload (cost). |
| agent switched model · anthropic/opus | pi's model was changed mid-session. OMP has no such event. |
| agent session ended | The agent shut down. |
What is kept, and what is not. Tool names, file paths relative to the workspace (a path
outside it is marked as such, not shown), durations, outcomes, decisions, turn numbers, models,
token counts and cost, the length of each prompt. Not the prompt, not a command (a bash call's
command line stays in the agent), not a file's contents, not the reply — the extension copies
only the fields listed before anything leaves the agent's process. Recorded against OMP
18.2.11 and pi 0.87.1. The opening message Yardsort passes on the command line is not an
input event to OMP, so its length is not reported there; pi does report it. The messages you
type afterwards are reported by both. On pi, a write is named by its write started row and
not by write done, which carries no path.
What Cursor reports
Switch on Capture what Cursor reports in Settings → General (also needs Record when agents
start and exit) and every Cursor agent that Yardsort starts is given a small plugin, on its
command line for that launch alone, whose hooks report what it does. Cursor runs the plugin's
hooks beside your own from ~/.cursor/hooks.json and the project's; nothing of yours is edited.
Only hooks Cursor does not wait on for a decision are used, so nothing is ever blocked.
| Timeline row | What Cursor reported |
|---|---|
| agent session started · composer-2 | The session and its model. |
| Read done · src/a.rs · 12 ms, Read failed | A tool ran or failed, on which file, how long it took. Cursor's own tool names. |
| shell done · 30 ms, mcp:search done | A shell command ran, or an MCP tool. Never the command line. |
| file changed · src/b.rs | Cursor edited a file. How many edits is in the payload, never their text. |
| subagent stopped · explore | A subagent finished. |
| agent finished its turn · 1,500 tokens | The turn ended and what it cost — when Cursor fires that hook for plugins, which this version was reported not to. |
What is kept, and what is not. Tool names, file paths relative to the workspace, durations,
outcomes, edit counts, token counts. Not the prompt, not a command, not a tool's output, not an
edit's text, not the reply. Nor the prompt's length: the one hook that carries the prompt can
also block it, and Yardsort never answers a hook that decides. Recorded against Cursor agent
2026.09.23-86fc751. In that build, Cursor does not deliver stop, afterAgentResponse or
subagentStop to a plugin's hooks, so a Cursor turn's end, its tokens and a subagent's end are
not on the timeline; the rows appear when a build sends them. A field named differently in
your version is left blank on the timeline rather than guessed.
Every built-in agent now reports natively when asked; a custom harness stays at what Yardsort itself sees. The design notes say exactly what each one can and cannot report.
Exits while Yardsort is closed
Agents keep working when you close the window. Before activity existed, an agent that then finished had nobody to tell: the next time Yardsort started, the conversation was listed as interrupted even though it had ended cleanly.
Now the background process keeps every exit in a small spool — one file per exit, under
activity/spool/ in your data directory — and whichever client connects next, the app or ys,
takes them into the database and empties it. The conversation is then shown as ended with its
real exit code and the time it actually ended, on the background process's clock — not the time
you next opened Yardsort — and the timeline says the exit came via spool. The spool is capped at 2 000
entries; past that, exits are dropped and counted, never allowed to fill a disk.
What the program is told
A process started in a workspace is given three environment variables, so a script — or, later, an agent hook — can say which run it belongs to:
| Variable | Value |
|---|---|
YARDSORT_RUN_ID | The run's id, when activity is being recorded. |
YARDSORT_WORKSPACE_ID | The workspace's id. |
YARDSORT_SESSION_RECORD_ID | The conversation's id, for an agent. Absent for shells. |
Ids only; nothing else is added and nothing is taken out of your environment. (Project setup
scripts get a different pair, YARDSORT_PROJECT and YARDSORT_WORKSPACE, which are paths — see
Project automation.)
Keeping it small
Yardsort keeps the newest 20 000 events and nothing older than 90 days, pruning when it starts. The agents' inbox holds at most 10 000 reports waiting to be taken in; past that, reports are dropped and counted. Clear all recorded activity in Settings → General forgets everything at once; the timeline's Clear forgets one workspace. Deleting a workspace, or forgetting one without keeping its history, takes its activity with it, exactly as it takes the conversations. Runs still going are never cleared, so their exit can still be matched when it comes.
Settings → General also shows how much is recorded, where the spool and the inbox are and whether
anything is waiting in them, where the Claude Code hooks file is, and counters for anything that
went wrong while recording — a write that failed, a spool file that could not be read, a report
that named a run this database does not have (inbox_unlinked: it is dropped rather than
guessed at). Recording is never allowed to get in the way: if the database cannot take a row,
the agent starts anyway and the failure is counted here.
From the command line
ys activity list prints the newest events (--workspace for one workspace, --limit N,
--json), and ys activity export writes every event as NDJSON — one JSON object per line, the
same fields the app sees — for whatever you want to do with it. ys doctor reports the counts,
the spool and the inbox. See The ys command line.