Skip to content

Tracing

hmz trace collect turns everything a run's agents left behind into one timeline. Reach for it after a long run, when you want to see what each agent did and where the time went. It works on any session the backends logged, whether or not a flow drove it.

Try it

In the project you have been running in, run:

sh
hmz trace collect
console
~/.humanize/cycles/-home-you-code-myproject/20260809T014455.212Z-9f21ab/traces/20260809T014455Z.trace.json of 20260809T014455.212Z-9f21ab: 3 sessions, 412 slices

The line prints the file, then the run it is a trace of, then what went into it. The file lands in traces/ inside that run's own directory, next to the run's record and the links to its sessions.

hmz trace collect writing into the last run's own directory: the path, the run it is of, and
1 session, 10 slices, 3 programs

Open it. Go to ui.perfetto.dev and drag the file in. Nothing is uploaded; Perfetto opens it in the browser. chrome://tracing works too, as does anything that reads a Chrome JSON trace.

You see one row per agent, and one track per session:

process   agent          actor · claude-opus-5 · max
  track     session ──▶ ▓▓▓ ▓ ▓▓▓▓▓▓ ▓▓  ▓▓▓▓▓  ▓ ▓▓▓▓▓▓▓▓▓▓
  track     sub-agent ─▶      ▓▓▓▓▓▓▓▓▓▓▓
process   agent          reviewer · gpt-5.6-sol · high
  track     session ──▶            ▓▓▓▓        ▓▓▓▓        ▓▓▓▓

Click a slice. Its arguments are there: the prompt, the reasoning, the tool input, the tool output.

That is the whole of it: one command gathers the sessions, Perfetto draws them as a timeline, and each slice holds the details.

What you get

process   agent          builder · 4 sessions
  track     main ──────────────▶ ▓▓▓ ▓ ▓▓▓▓▓▓ ▓▓  ▓▓▓▓▓  ▓ ▓▓▓▓
  track     subagent · explore ▶      ▓▓▓▓▓▓▓▓▓▓▓
process   agent          reviewer · 2 sessions
  track     main ──────────────▶            ▓▓▓▓        ▓▓▓▓

A process is an agent and everything it drove. A track is one row of that agent's sessions. For a profiled run the same two words carry over to the programs the agents ran: a process is a program, and a track is one of its threads.

In the traceIs
a processone agent, called <agent> · <n> sessions — or, for a profiled run, one program it ran, called <program> · <pid>
a trackone row of that agent's sessions: main for the ones somebody started, subagent for what a turn reached for, named after the kind where a row is all of one kind. Sessions of one agent that never run at the same time share a track; root sessions and sub-agents stay apart.
a sliceone action — a tool call, a message, or waiting for reasoning

Click a slice and its arguments are there: the prompt, the reasoning, the tool input, the tool output. As much as the backend wrote down.

On your first trace, look for:

  • A wide gap on every track. Nobody was working. That is the flow sleeping, committing, or reading what the last turn wrote.
  • One very long slice. A single tool call that took minutes — usually a test suite, sometimes a find over the whole disk.
  • A reviewer whose tracks all start after the actor's stop. That is the loop working as designed. If they overlap, it is not.
  • Two hundred short tracks on one process. A Ralph loop, one session per turn.

The first two are guesses until the run is profiled.

Why two agents do not read as one

The backends log a session under an id and never say whose it was. By default an agent in a trace is one configuration: a backend at a model at an effort, plus every sub-agent it started. A Ralph loop of a hundred one-shot sessions reads as one agent, which is right. An actor and a reviewer at the same model and effort would read as one agent, which is not.

That is what a cycle is for. hmz trace collect reads the run it is tracing, so official/rlar traces as actor and reviewer without being told anything.

Driving agents by hand, say so yourself:

python
collect(agents={a.id: a.opened for a in (actor, reviewer)})

Sessions nobody claims are read as the configuration they ran at.

What a run writes down

Every run of a flow is one cycle, which is a directory:

~/.humanize/cycles/<workspace>/<datetime>-<hex>/
    cycle.jsonl                     what happened, a line at a time
    state.json                      what a flow that can be picked up again left behind
    profile.jsonl                   the programs it ran, for a run that was profiled
    sessions/<session>/…            a link per file the backend logged that session to
    traces/<datetime>.trace.json    what was gathered of it afterwards

Not all of it every time: state.json is there for a flow that can be picked up, profile.jsonl for a directory that asked to be profiled, and traces/ from the first time a trace is collected.

Find the run that just finished and list it:

sh
run=$(ls -dt ~/.humanize/cycles/*/*/ | head -1)   # the one that just finished
ls "$run"
console
cycle.jsonl  sessions  traces

ls of one run's directory: cycle.jsonl, profile.jsonl, sessions and state.json, and no traces
yet

cycle.jsonl is JSON lines, appended and flushed as it goes. A run that died is a run whose cycle still says what it got to:

sh
head -3 "$run"cycle.jsonl
console
{"event":"began","at":"...","flow":"official/rlar","task":"...","workspace":"...","resumable":false,"agents":[{"agent":"actor",...}]}
{"event":"opened","at":"...","agent":"actor","backend":"claude","provider":"local","session":"0a1b2c3d-...","name":"actor-claude@local-0a1b2c3d-...","where":"sessions/actor-claude@local-0a1b2c3d-..."}
{"event":"ended","at":"...","how":"done"}
eventWrittenCarries
beganwhen the flow startsflow, task, workspace, whether the flow can be picked up again and which run this one was picked up from, and one entry per agent with its id, backend, model, effort, account, what it may do, whether it could use goals and whether it was the person at the prompt
openedeach time an agent opens a sessionagent, backend, provider, session, the name the run gives it and where inside the cycle its links are
endedwhen the flow stopshow: done, failed, or stopped

Each session's own logs are pointed at from sessions/<name>/, under a name that says whose session it was, what took its turns, which account they ran as and what the backend called it: builder-claude@work-0a1b2c3d, and @local where the turns ran as the account this machine is already signed into. Links for reading: humanize reads and writes every log where the backend keeps it. They are made again when the run ends, because a sub-agent's transcript is written whenever that sub-agent ran, and a filesystem that will not make one is a run without links rather than a run that stops.

one run's sessions/ directory, its name saying agent, CLI and account, holding a symlink to
Claude Code's own log

/cycles is the same list at the prompt: every run of this directory, newest first, with a mark on the ones whose flow says it can be picked up. Enter opens what there is to do with the run under the cursor: carry on from here, collect a trace, where it is. The mark and that first row are one question, and it is asked of the flow as it stands rather than of the run. A flow marked resumable=True after a run of it has that older run marked and offered too, and one that has since dropped the mark has neither, whatever the run wrote down at the time. Carrying one on is picking a run up; collecting a trace is offered for every run, whatever its flow says.

It is not a transcript. The backend's own log is the turn-by-turn record. A cycle is the shape of the run: enough to gather a trace afterwards out of the ids alone.

A cycle covers one run and is never reopened. Carrying a flow on is another run, with sessions of its own, written into a cycle that says which run it was picked up from.

An agent stopped by hand makes the run stopped rather than failed, whatever the turn under way made of it. A run you ended with esc is written down as stopped too.

python
from hmz.cycle import cycles, opened

for cycle in cycles():                 # this workspace, oldest first
    print(cycle, opened(cycle))        # {"actor": ["0a1b…"], "reviewer": [...]}

Which run, and what else there is to trace

sh
hmz trace collect                                    # the last run of this workspace
hmz trace collect ~/code/other                       # the last run of another workspace
hmz trace collect --cycle 20260809T0144              # that run of it, by name
hmz trace collect --start "3 days ago"               # and only what it did since
hmz trace collect --end "yesterday 18:00" --output /tmp/before.json

A trace is of a run. It holds the sessions that run opened and no others, by the ids the run wrote down as it went. A directory run in fifty times has fifty traces to collect, and none of them holds another's work. A run that opened nothing is a trace of nothing. It goes by id rather than by directory, so a flow that ran on a machine of its own is in its own trace too, though the backend logged it under a mirror this directory has never heard of.

0 sessions, 0 slices

Three usual reasons. You are in a different directory from the one the run happened in. The backend was opencode or mimocode, which keep sessions in a database and have nothing to gather. Or the run being traced never opened a session. It died first, and a run that opened nothing is a trace of nothing. See Troubleshooting.

A directory also holds sessions no run of a flow ever opened: your own afternoon at a coding agent. Ask for those outright:

sh
hmz trace collect --all                              # every session of this workspace
hmz trace collect --session 0a1b2c3d,5f6e            # two sessions, wherever they ran
hmz trace collect ~/code/other --session 0a1b2c3d    # that session, only if it ran there

Neither is a trace of any run, so neither is filed inside one. They go beside that workspace's runs, in ~/.humanize/cycles/<workspace>/. Asking for --cycle with --all or --session is a usage error rather than one of them quietly winning. Neither is offered in the interface, because /cycles is a list of runs with nothing to hang them on.

A session is named by its whole id, by the key the trace shows it under, or by a leading part of either. The sub-agents it started come with it. --start and --end take anything dateparser understands. --output wins over where any of these would otherwise land; a trace is also a thing to attach to an issue.

The default output is named after the UTC moment it was collected, so collecting twice keeps both.

hmz trace collect three times: the last run of this directory, one named with --cycle, and
one sent elsewhere with --output

From Python the same choices are one call:

python
from hmz.tracing import collect

document = collect(
    "~/code/myproject",
    sessions=["0a1b2c3d"],
    agents={"actor": actor.opened, "reviewer": reviewer.opened},
    output="trace.json",       # omit and nothing is written
    start="3 days ago",
)

It returns the document. Writing a file only when output is given is the one thing the library does that the command line does not let you skip.

Profiling a run

An agent's turn is mostly other programs: the tests, the build, the greps. None of them is in a backend's log, which records the tool call rather than the process. So a directory may ask for its runs to be profiled as well as traced. The switch is the profile row on the second page of /settings, which is the page for this directory.

the /settings page for this directory, with the profile row switched on beside workspace,
flow and forget

While a flow runs there, every process underneath it is sampled as each is seen: the agent CLIs themselves, and the tests, the builds and the greps their turns start. Each sample says what it was, what started it, and how long it took, into profile.jsonl in that run's cycle. Collecting the run draws them in the same document as its sessions, at the same scale, so what was this run doing at 09:41 has one answer. A trace of a profiled run counts them: 3 sessions, 412 slices, 61 programs.

process   agent          builder · 4 sessions
  track     main ──────▶ ▓▓▓ ▓ ▓▓▓▓▓▓ ▓▓  ▓▓▓▓▓  ▓ ▓▓▓▓▓▓▓▓▓▓
process   program        pytest · 41207
  track     main ──────▶       ▓▓▓▓▓▓▓▓▓▓

Off until a directory asks for it. It is a sampler running for as long as the flow does, and what a run costs in processes is a question about the project rather than about the machine: a repository whose tests take an hour is a different question from one whose tests take a minute. What it costs is a thread reading the process tree twenty times a second, and two lines of JSON per program — one when it is first seen, one when it has gone.

Sampled rather than intercepted: nothing goes between an agent and what it runs, a program that lived for thirty milliseconds may be missed, and a machine whose processes cannot be read is a run with no profile rather than a run that stops.

The switch is read where a run starts. Turning it on holds from the next run rather than the one under way. A run hmz exec starts in that directory is profiled too: the switch says nothing about what runs, only about whether what runs is watched. From Python it is one property and one call:

python
from hmz.settings import Settings

Settings().profiling            # whether a run in this directory is profiled
Settings().profiles(on=True)    # written down for it, from now on

Where it reads from

The backends' own home directories, which humanize only ever reads:

BackendVariableDefault
Claude CodeCLAUDE_CONFIG_DIR~/.claude
CodexCODEX_HOME~/.codex
DeepSeek HarnessDSH_HOME~/.dsh
Kimi CodeKIMI_CODE_HOME~/.kimi-code

Those four, and no others. opencode, mimocode and Antigravity keep a session in a database rather than in a log file, and nothing here reads pi's, Grok Build's or Qwen Code's own logs yet. So there is nothing to gather for those: a run of theirs is watched as it happens rather than collected after.

A home that does not exist is skipped rather than being an error. So is a backend humanize has no reader for; its home being there changes nothing.

A flow that ran on a machine of its own worked in a mirror rather than in this directory. Find its trajectories with --session rather than by workspace.

Watching instead

A trace is for after. While a run is going, /status shows the same shape live. It is read off the turns going past, never by asking the flow.

See also

Released under the Apache-2.0 licence.