@threadcrumbs/common 0.1.8 → 0.1.9
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/harnesses/claude-code.mjs +578 -578
- package/harnesses/hermes.mjs +2 -1
- package/lib/claude-transcript.mjs +16 -0
- package/lib/opencode-store.mjs +734 -734
- package/package.json +1 -1
- package/telemetry/README.md +290 -290
- package/telemetry/claude-code.mjs +3 -2
- package/telemetry/codex.mjs +700 -700
- package/telemetry/hermes.mjs +441 -441
- package/ui/rows.css +5 -0
package/telemetry/README.md
CHANGED
|
@@ -1,290 +1,290 @@
|
|
|
1
|
-
# Harness telemetry readers
|
|
2
|
-
|
|
3
|
-
A **harness** is whatever runs the agent sessions: Claude Code, Codex, OpenCode, and so on. Each
|
|
4
|
-
file here reads one harness's own session files and reports live telemetry for each session:
|
|
5
|
-
what it's doing now, its context, tokens, spend, and plan. Threadgauge shows them.
|
|
6
|
-
|
|
7
|
-
These are the telemetry half of each harness. The other half, finding every thread and opening
|
|
8
|
-
it, is in [`../harnesses/`](../harnesses/README.md), which Threadnauts uses. They're imported
|
|
9
|
-
separately (`@threadcrumbs/common/telemetry` and `@threadcrumbs/common/harnesses`), so an app
|
|
10
|
-
that only needs one half never loads the other.
|
|
11
|
-
|
|
12
|
-
## The shape of it
|
|
13
|
-
|
|
14
|
-
```js
|
|
15
|
-
export default {
|
|
16
|
-
id: 'my-harness',
|
|
17
|
-
name: 'My Harness',
|
|
18
|
-
detect,
|
|
19
|
-
watchRoots,
|
|
20
|
-
scanSessions,
|
|
21
|
-
sessionDetail,
|
|
22
|
-
}
|
|
23
|
-
```
|
|
24
|
-
|
|
25
|
-
| Member | What it does |
|
|
26
|
-
| --- | --- |
|
|
27
|
-
| `id` | Stable and kebab-case. It's part of every session id, so it must never change |
|
|
28
|
-
| `name` | What a person sees |
|
|
29
|
-
| `detect()` | `Promise<boolean>`: is this harness on this machine at all? It runs on every scan, so keep it cheap |
|
|
30
|
-
| `watchRoots()` | The folders to watch recursively. A change anywhere under them triggers a scan |
|
|
31
|
-
| `scanSessions()` | `Promise<Session[]>`: the sessions worth showing now. Cache against file size and mtime, because it runs often |
|
|
32
|
-
| `sessionDetail(id, options)` | Optional. `Promise<Detail \| null>`: one session's details, for Threadgauge's details panel. `null` for an id the last scan didn't list. It runs about once a second while that panel is open, so build it from state the scan already keeps, or one read-only query. Claude Code also takes `{ subagent }`, a subagent's id from `subagents`, and returns that subagent's own Detail, with `agentId` set
|
|
33
|
-
| `readSession(id, options)` | Optional, Claude Code only so far. `Promise<{ session, detail } \| null>`: one session read on demand, even one older than the last day that `scanSessions` leaves out, so Threadgauge Pro can open an older thread. Takes `{ subagent }` like `sessionDetail`. It keeps the last few files it read, so asking again reads only what was added |
|
|
34
|
-
|
|
35
|
-
Throwing from `scanSessions` is survivable. Threadgauge's monitor reports the error under the
|
|
36
|
-
harness's name and carries on with the other harnesses.
|
|
37
|
-
|
|
38
|
-
Register a reader by adding it to `TELEMETRY` in `index.mjs`. Use the same `id` as that harness's
|
|
39
|
-
adapter in `../harnesses/`, so a session has the same id in both apps. Add the harness to
|
|
40
|
-
`test/shared-ids.test.mjs` too, which fails until both readers are compared on one fake install.
|
|
41
|
-
|
|
42
|
-
## The `Session`
|
|
43
|
-
|
|
44
|
-
| Field | Type | What it means |
|
|
45
|
-
| --- | --- | --- |
|
|
46
|
-
| `id` | string | `<harness id>:<session id>`, unique across every harness |
|
|
47
|
-
| `harness` | string | The reader's `id` |
|
|
48
|
-
| `title` | string | The harness's own title, or the first line of the first prompt |
|
|
49
|
-
| `project` / `projectPath` | string | The repo folder's name and absolute path |
|
|
50
|
-
| `worktree` | string | Worktree name, or `''` |
|
|
51
|
-
| `cwd`, `gitBranch` | string | Where it's working |
|
|
52
|
-
| `model`, `effort` | string | The harness's own ids, as recorded |
|
|
53
|
-
| `status` | string | `asking`, `working`, `delegating`, `done`, `error`, or `idle`. See [Done, needing you, and archived](#done-needing-you-and-archived) |
|
|
54
|
-
| `statusReason` | string | What the reader saw that decided `status`, one code from a fixed list. See [Why this status](#why-this-status) |
|
|
55
|
-
| `ask` | object or `null` | What the thread needs from you, while it's `asking` or `error`; `null` otherwise. See [What a thread needs](#what-a-thread-needs) |
|
|
56
|
-
| `usageLimits` | `{ at, subscription, windows: [{ id, usedPercent, windowMinutes, resetsAt }] }` or `null` | The plan's usage limits as the harness last recorded them in this session, at `at`. They cover the whole account, so an app shows the newest across a harness's sessions. `subscription` is the plan's name, such as `pro`, or `null`. `resetsAt` is epoch ms. Codex only, from `token_count`'s `rate_limits`, whose `primary` and `secondary` become the window ids; `null` elsewhere |
|
|
57
|
-
| `live` | boolean | A process for it is running now. A harness that keeps no record of its processes reports `true` while a turn is open |
|
|
58
|
-
| `startedAt`, `lastActivityAt` | number | Epoch ms |
|
|
59
|
-
| `tool` | `{ name, startedAt, target, kind }` or `null` | The tool running right now, only while working or delegating. `target` is its short file path, command, or search, as in `Detail.activity`, or `''` when the harness doesn't record one. `kind` is one of the [tool kinds](#tool-kinds) |
|
|
60
|
-
| `tokens` | `{ input, output, cacheRead, cacheWrite }` or `null` | Totals for the session's own thread. `input` leaves out cache reads and writes |
|
|
61
|
-
| `context` | `{ used, limit }` or `null` | Tokens in the window after the last reply; `limit` is `0` when unknown |
|
|
62
|
-
| `contextHistory` | number[] or `null` | The context after each reply, oldest first, for a sparkline: at most 24 whole numbers, evenly spaced across the session and always ending at `context.used`. A reply written twice counts once. Each reader keeps a running sample as it reads, never the whole history; OpenCode 2.x reads only the replies newer than the last one it saw, once the session changes, leaves the newest for the next read while it has no `time.completed`, and starts over if replies were removed. `null` for Cursor, Antigravity, and Hermes, whose session scan sees no per-reply counts, and before the first reply |
|
|
63
|
-
| `costUsd` | number or `null` | Spend as the harness records it; `null` when it doesn't |
|
|
64
|
-
| `lines` | `{ added, removed }` or `null` | Lines changed, as the harness records it |
|
|
65
|
-
| `plan` | `{ total, done, current }` or `null` | Plan progress, if the agent keeps one |
|
|
66
|
-
| `subagents` | number | Subagents still working for it |
|
|
67
|
-
| `workingSubagents` | `[{ id, name }]` or `null` | Those subagents, oldest first, named as `Detail.subagents` names them. `null` for Antigravity, which records none |
|
|
68
|
-
| `awaiting` | `[{ id, title }]` | Claude Code only: other sessions it asked to hear from that are still busy, by their own `id`. Either this or `subagents` makes an idle session `delegating`. Other harnesses leave it out |
|
|
69
|
-
| `prompts` | number or `null` | Prompts typed in this session |
|
|
70
|
-
| `turnStartedAt` | number or `null` | Epoch ms when the open turn began, for a turn timer. `null` when no turn is open, which includes a session waiting only on its subagents after handing its turn back. See [When a turn began](#when-a-turn-began) |
|
|
71
|
-
|
|
72
|
-
`null` means "this harness doesn't record it". Threadgauge shows "—" instead of guessing.
|
|
73
|
-
|
|
74
|
-
### When a turn began
|
|
75
|
-
|
|
76
|
-
| Harness | `turnStartedAt` comes from |
|
|
77
|
-
| --- | --- |
|
|
78
|
-
| Claude Code | The first record after a reply that handed the turn back: a prompt, a message from another session, or anything else that reopens it |
|
|
79
|
-
| Codex | `task_started` or `turn_started`, whichever opened the turn |
|
|
80
|
-
| Cursor | The `<timestamp>` on the prompt that opened the turn, which is to the minute |
|
|
81
|
-
| Antigravity CLI | The newest prompt in `history.jsonl`, the one that shows the turn underway |
|
|
82
|
-
| Hermes | The newest user message across the compression chain |
|
|
83
|
-
| OpenCode, Kilo Code | 1.x: the newest user message. 2.x: `time_busy`, or the newest user message without it |
|
|
84
|
-
|
|
85
|
-
### Why this status
|
|
86
|
-
|
|
87
|
-
| `status` | `statusReason` | Meaning | Harnesses |
|
|
88
|
-
| --- | --- | --- | --- |
|
|
89
|
-
| `asking` | `question` | A question tool is open | Claude Code, Codex, Hermes (clarify), OpenCode, Kilo Code |
|
|
90
|
-
| | `approval` | A tool waits for your permission | Claude Code, Codex, Hermes |
|
|
91
|
-
| | `waiting` | The live process says it waits on you, with no open tool to say why | Claude Code |
|
|
92
|
-
| | `marked-blocked` | The Claude app marked the turn as waiting on you | Claude Code |
|
|
93
|
-
| `working` | `turn-open` | The transcript or database shows a turn underway | All but Antigravity |
|
|
94
|
-
| | `process-busy` | The live process says it's busy | Claude Code |
|
|
95
|
-
| | `new-prompt` | A prompt is newer than the transcript | Antigravity |
|
|
96
|
-
| `delegating` | `subagents` | Its subagents are working, or its only open calls start them | All but Antigravity |
|
|
97
|
-
| | `awaiting` | It asked to hear from other sessions that are still busy (`awaiting`) | Claude Code |
|
|
98
|
-
| `error` | `turn-failed` | The last turn ended in an error | All but Hermes |
|
|
99
|
-
| `done` | `unread` | The harness records that you haven't read the reply | Claude Code (the app's record), Codex, Hermes, OpenCode 2.x |
|
|
100
|
-
| | `recent` | No record of reading, and the turn ended in the last 30 minutes | All |
|
|
101
|
-
| | `process-idle` | The live process says it's idle, with no record of reading, and the turn ended in the last 30 minutes | Claude Code |
|
|
102
|
-
| `idle` | `read` | The harness records that you've read the reply | Claude Code (the app's record), Codex, Hermes, OpenCode 2.x |
|
|
103
|
-
| | `quiet` | No record of reading, and nothing for 30 minutes | All |
|
|
104
|
-
| | `process-idle` | The live process says it's idle, with no record of reading, and nothing for 30 minutes | Claude Code |
|
|
105
|
-
|
|
106
|
-
### What a thread needs
|
|
107
|
-
|
|
108
|
-
`ask` is one of these. Free text is cut like an activity target: headers and labels to 160
|
|
109
|
-
characters, questions, descriptions, and error text to 500, with "…" at the cut. At most four
|
|
110
|
-
questions with eight options each are kept.
|
|
111
|
-
- `{ type: 'question', questions: [{ header, text, options: [{ label, description }], multiple }] }`
|
|
112
|
-
comes from the open question tool's input: Claude Code's AskUserQuestion, Codex's
|
|
113
|
-
`request_user_input`, and OpenCode's and Kilo Code's `question`.
|
|
114
|
-
- `{ type: 'approval', tool, target, kind }` is the open tool waiting for permission: Claude
|
|
115
|
-
Code's newest open call while its process waits on you, and Codex's `request_permissions`.
|
|
116
|
-
- `{ type: 'error', text }` is the newest error's text while the status is `error`. It can be
|
|
117
|
-
`''` when the harness records the failure but not its text.
|
|
118
|
-
- `{ type: 'unknown' }` means the thread needs you but the harness doesn't say why: the Claude
|
|
119
|
-
app's mark, or Hermes, whose activity label is all it records.
|
|
120
|
-
|
|
121
|
-
### Done, needing you, and archived
|
|
122
|
-
|
|
123
|
-
These follow the session readers in `../harnesses/`, so Threadgauge and Threadnauts count a
|
|
124
|
-
thread the same way.
|
|
125
|
-
- **Done** means the turn ended and you haven't read the reply. After that it's `idle`.
|
|
126
|
-
- **Where the harness records reading, that decides:** Codex's read receipts, Hermes's
|
|
127
|
-
`last_read_at`, and OpenCode 2.x's `time_viewed`.
|
|
128
|
-
- **Claude Code** counts a reply as unread when the Claude app's record of the thread changed
|
|
129
|
-
after you last focused it. A live session that handed its turn back in the last 30 minutes
|
|
130
|
-
is done either way, as the session reader's is.
|
|
131
|
-
- **Everywhere else,** a terminal-only Claude Code session included, a turn is done for 30
|
|
132
|
-
minutes after it ends. Not knowing isn't the same as read.
|
|
133
|
-
- **Needing you** also covers a Claude Code turn the Claude app marked `blocked`, unless
|
|
134
|
-
subagents or other sessions are still out. `delegating` comes first.
|
|
135
|
-
- **Archived** sessions are left out, and so are Claude Code sessions deleted in the app. Cursor
|
|
136
|
-
and Antigravity keep no archive record, so theirs always show.
|
|
137
|
-
|
|
138
|
-
## The `Detail`
|
|
139
|
-
|
|
140
|
-
| Field | Type | What it means |
|
|
141
|
-
| --- | --- | --- |
|
|
142
|
-
| `id`, `sessionId` | string | The session's id, and the harness's own id for it |
|
|
143
|
-
| `source` | string | The transcript or database it was read from |
|
|
144
|
-
| `models` | `[{ model, tokens, costUsd }]` | Tokens, and spend where recorded, per model |
|
|
145
|
-
| `contextParts` | `{ input, cacheRead, cacheWrite, output }` | What filled the context in the latest request: new input, cache reads, cache writes, and its output. They add up to the session's `context.used`. `null` for Cursor, Antigravity, and Hermes |
|
|
146
|
-
| `history` | `[{ at, context, output }]` | Context and output after each reply, the newest 600 |
|
|
147
|
-
| `compactions` | `[{ at, before, after }]` | When the context was compacted, and its size before and after |
|
|
148
|
-
| `activity` | `[{ id, name, kind, server, target, input, path, line, startedAt, endedAt, running, error, errorText }]` | The newest 100 tool calls. `kind` and `server` are described under [Tool kinds](#tool-kinds). `target` is a short file path, command, or search, never the output. `input` is the full command, or the call's input as JSON without file contents, cut to 2,000 characters. `path` and `line` are the file a call names, relative to the session's folder, and its line where known. `errorText` is the start of a failed call's own error output, cut to 500 characters. Any of these is `null` when the harness doesn't record it |
|
|
149
|
-
| `tools` | `[{ name, kind, server, calls, errors, ms }]` | Every call in the session, per tool. A tool's `kind` comes from its name alone, so a shell tool is `execute` here even when some of its calls are `search` |
|
|
150
|
-
| `plan` | `[{ content, status }]` | Every plan step; `[]` when the agent kept none |
|
|
151
|
-
| `subagents` | `[{ id, name, type, status, model, tokens, tool, startedAt, lastActivityAt }]` | Every subagent it started, finished or not. `tool` has the Session's `tool` shape |
|
|
152
|
-
| `files` | `[{ path, added, removed, edits, line, hunks }]` | Lines changed per file, with paths relative to the session's folder. `line` is where its latest change starts. `hunks` (`[{ line, oldLine, lines, cut }]`) are its newest 5 changes as diff lines, each with at most 60 lines of 240 characters, and `cut` counts the lines left out. Only the 25 most recently edited files keep hunks; older ones have `[]`. `line` and `hunks` are `null` when the harness doesn't record them, and a hunk's `line` is `null` when the patch has no line numbers |
|
|
153
|
-
| `prompts` | `[{ at, text }]` | The newest 50 prompts, each cut to 2,000 characters, with line breaks kept |
|
|
154
|
-
| `thoughts` | `[{ at, text }]` | The agent's reasoning, where the harness writes it as text: the newest 20 messages that had some, each cut to 2,000 characters. Encrypted, redacted, and signature-only reasoning is skipped |
|
|
155
|
-
| `replies` | `[{ at, text }]` | The agent's text to you, one entry per message with its text blocks joined: the newest 50, each cut to 2,000 characters. Tool calls and API errors are left out |
|
|
156
|
-
| `errors` | `[{ at, message }]` | The newest 20 API errors and failed turns, each cut to 500 characters |
|
|
157
|
-
| `contextBreakdown` | `{ at, total, limit, reserved, parts }` | What fills the context, like Claude Code's `/context`. Only `total` is measured; every part is an estimate. See [Context breakdown](#context-breakdown). `null` for Cursor, Antigravity, Hermes, OpenCode, and Kilo Code |
|
|
158
|
-
|
|
159
|
-
Again, a `null` field means the harness doesn't record it. The shared helpers for building one
|
|
160
|
-
(`startCall`, `endCall`, `toolTarget`, `keepNewest`, `fileList`, and the limits) are in
|
|
161
|
-
`../lib/telemetry-fields.mjs`.
|
|
162
|
-
|
|
163
|
-
`sessionDetail(id, { filesOnly: true })` returns only `{ id, sessionId, files }`, with each file
|
|
164
|
-
as `{ path }`, from what the session scan already holds. It's for finding other threads that
|
|
165
|
-
changed the same files, so it never opens the readers that serve an open thread.
|
|
166
|
-
|
|
167
|
-
The readers that serve an open thread (thoughts and replies for Claude Code, Codex, Cursor, and
|
|
168
|
-
Antigravity) are kept for the last three sessions opened. Claude Code's and Codex's read the
|
|
169
|
-
transcript from its end first, in growing steps, until they have the newest 50 replies, and then
|
|
170
|
-
follow it forwards.
|
|
171
|
-
|
|
172
|
-
### Context breakdown
|
|
173
|
-
|
|
174
|
-
`contextBreakdown` splits the context into parts, so an app can show what fills it.
|
|
175
|
-
- **Only `total` is measured.** It's the harness's own count of the latest request, the same
|
|
176
|
-
number as the Session's `context.used`, and `at` is that request's time. Every part is an
|
|
177
|
-
estimate scaled to it, and the parts always add up to it. An app should say so.
|
|
178
|
-
- **`limit`** is the context limit, and **`reserved`** is what the harness keeps free for
|
|
179
|
-
compacting by default: 33,000 tokens for Claude Code, and for Codex the gap between the
|
|
180
|
-
window it reports and the 90% of its full window where it compacts. The settings that change
|
|
181
|
-
them aren't read, so an app should call it the default. Free space is `limit - total -
|
|
182
|
-
reserved`.
|
|
183
|
-
- **`parts`** are `[{ id, tokens, items }]` in this order: `messages`, `system`,
|
|
184
|
-
`builtInTools`, `mcpTools`, `skills`, `memory`. A part the harness records is sent even at 0
|
|
185
|
-
tokens; one it doesn't record is left out, and its tokens stay in `system`.
|
|
186
|
-
- `messages` items: `{ id: 'prompts' }`, `{ id: 'replies' }`, `{ id: 'summary' }` (from the
|
|
187
|
-
last compaction), and `{ id: 'tool', name }` for each tool's calls and results.
|
|
188
|
-
- `mcpTools` items: `{ name, tools }` per server, `tools` a count or `null`.
|
|
189
|
-
- `skills` items: `{ name }`. `memory` items: `{ path }`, the file's full path.
|
|
190
|
-
- Every item also has `tokens`.
|
|
191
|
-
- **How it's worked out:**
|
|
192
|
-
- What was there before the first message is the first request's input count minus the
|
|
193
|
-
first prompt (at 4 characters a token).
|
|
194
|
-
- That gives a characters-per-token ratio for the text of each fixed part (system prompt,
|
|
195
|
-
tool definitions, MCP servers, skills, memory files), so each part is its text length at
|
|
196
|
-
that ratio. A part added later counts at the same ratio.
|
|
197
|
-
- The ratio is used only when the system prompt and tool definitions were recorded, as in
|
|
198
|
-
Claude Code's `api-request-shape`. Otherwise it would spread their unrecorded text over the
|
|
199
|
-
parts that were, so each recorded part is its text at 4 characters a token, and `system` is
|
|
200
|
-
the rest of the fixed count. That's always the case for Codex, whose built-in tool
|
|
201
|
-
definitions aren't written.
|
|
202
|
-
- `messages` is the rest of the total. Each request's growth over the one before is split
|
|
203
|
-
among the prompts, replies, and tool calls and results since then, by text length. After a
|
|
204
|
-
compaction, the next request's growth over the fixed parts is the summary.
|
|
205
|
-
- **When it falls back:** if the transcript began mid-session (no first prompt before the first
|
|
206
|
-
request, or a compaction summary first), or the fixed count comes out at or below zero or
|
|
207
|
-
above the total, there are only two parts: `system` is the fixed text at 4 characters a
|
|
208
|
-
token, and `messages` the rest. With no fixed text known either, it's `null`. A transcript
|
|
209
|
-
with no record of what was sent has `messages` and `system` only, `system` being the whole
|
|
210
|
-
fixed count.
|
|
211
|
-
- **Where the fixed text comes from:** only the files the readers already open; no config file
|
|
212
|
-
is read.
|
|
213
|
-
- Claude Code: its transcript's own records of each request (`api-request-shape` and the
|
|
214
|
-
`instructions`, `skill_listing`, `deferred_tools_delta`, `mcp_instructions_delta`, and
|
|
215
|
-
`agent_listing_delta` attachments; see [HARNESS-NOTES.md](HARNESS-NOTES.md)).
|
|
216
|
-
- Codex: the rollout's base instructions and developer messages, its AGENTS.md messages, and
|
|
217
|
-
its `additional_tools` items. Its built-in tool definitions aren't written, so they're in
|
|
218
|
-
`system`.
|
|
219
|
-
- **Its sizes are kept by the session scan,** which already reads every record, as numbers and
|
|
220
|
-
names only, never the text, so opening a thread doesn't read its transcript again.
|
|
221
|
-
|
|
222
|
-
### Tool kinds
|
|
223
|
-
|
|
224
|
-
Every call and every tool total has a `kind`, so an app can draw one icon per kind instead of
|
|
225
|
-
one per tool. The kinds are ACP's tool kinds plus Gemini CLI's `agent`, `plan`, and
|
|
226
|
-
`communicate`: `read`, `search`, `edit`, `delete`, `move`, `execute`, `think`, `fetch`, `agent`,
|
|
227
|
-
`plan`, `communicate`, `switch_mode`, and `other`.
|
|
228
|
-
- **By name:** `../lib/tool-kinds.mjs` keeps one table of every harness's tool names, compared
|
|
229
|
-
without case, spaces, or dashes. A name it doesn't know is `other`.
|
|
230
|
-
- **MCP tools** (`mcp__<server>__<tool>`) are `other`, with `server` set to the server's name.
|
|
231
|
-
`server` is `null` for every other tool.
|
|
232
|
-
- **Shell calls:** a call whose tool is `execute` takes `read`, `search`, `delete`, or `move`
|
|
233
|
-
when every step of its command does only that. Anything else stays `execute`: a step the
|
|
234
|
-
table doesn't know, mixed kinds, a redirect into a file, command substitution, a background
|
|
235
|
-
`&`, or a pipe into anything but a filter such as `head` or `sort`.
|
|
236
|
-
- A known command with an option that writes a file or runs one stays `execute` too:
|
|
237
|
-
`find -exec`, `-delete`, or `-fprint`; `fd -x`; `rg --pre`; `tree -o`; `less -o`;
|
|
238
|
-
`sort -o`; and `uniq` with an output file.
|
|
239
|
-
- `sed` is `read` only when its script just prints, such as `10,20p`.
|
|
240
|
-
- `git` skips `-C <dir>` and `--no-pager` before its subcommand.
|
|
241
|
-
- A command cut short to fit `input` is `execute`, because the part that was cut could
|
|
242
|
-
write. An open call reads its command whole.
|
|
243
|
-
- `startCall` sets both fields, so a reader gets them by passing the call's name and input.
|
|
244
|
-
|
|
245
|
-
| Harness | Where its details come from | `null` |
|
|
246
|
-
| --- | --- | --- |
|
|
247
|
-
| Claude Code | The transcript the scan already reads, collected as it goes (`transcriptReader({ detail: true })`), plus every subagent transcript and its `.meta.json`. Hunks come from each edit's `structuredPatch`, and a failed call's error text from its tool result. Thoughts and replies come from a second reader of the transcript, kept for the last three sessions opened | — |
|
|
248
|
-
| Codex | The rollout state: calls and their exit codes, per-file lines and hunks from patches, and each `token_count`. Subagents are the child rollouts. A failed command's error text is its output. Thoughts are reasoning summaries and replies are assistant messages, from a second reader kept for the last three sessions opened | `compactions`. Hunks have no line numbers |
|
|
249
|
-
| Cursor | The transcript state. A call's start is when it was first read. Replies come from a second reader kept for the last three sessions opened; Cursor writes no time on them, so each takes its prompt's time, and it writes the agent's thinking into the same text | `thoughts`, `history`, `models`, `files`, `compactions`. A call has no end time or error |
|
|
250
|
-
| Antigravity CLI | Transcript steps, each ended by the next, with prompts from `history.jsonl`. Thoughts and replies are planner steps' `thinking` and `content`, from a second reader kept for the last three sessions opened | `history`, `models`, `files`, `subagents`, `compactions`. A call has no input or file |
|
|
251
|
-
| Hermes | One query for the compression chain: tool calls joined to their results, user and assistant messages, and child sessions. Thoughts are the `reasoning` column, or `reasoning_content` | `history`, `files`, `compactions`; `thoughts` when the database has neither column. A call has no error |
|
|
252
|
-
| OpenCode, Kilo Code | One query for the session's messages and tool parts. Files are the 1.x per-turn diffs; a failed call's error text is its `state.error`. Thoughts and replies are the reasoning and text parts of each reply. On 2.x a reply's parts (calls, plan, thoughts, and text) are its part rows when OpenCode writes them (`experimental.part_rows`, off by default), and otherwise the message's own content | `compactions`; `files` on 2.x and when Kilo kept no diffs; `hunks` always |
|
|
253
|
-
|
|
254
|
-
## What each harness records
|
|
255
|
-
|
|
256
|
-
[HARNESS-NOTES.md](HARNESS-NOTES.md) has what each harness saves of its reasoning and replies,
|
|
257
|
-
the hooks it offers, and how much of its context it keeps free for compacting, with the version
|
|
258
|
-
and date each was checked.
|
|
259
|
-
|
|
260
|
-
Only Claude Code was checked against a real install. The others were written from each
|
|
261
|
-
harness's source code, or, for the closed ones, from what other open-source tools parse, and
|
|
262
|
-
tested against fake installs. Recheck them when a harness changes its format.
|
|
263
|
-
|
|
264
|
-
| Harness | What the reader takes, and from where | Not on disk, so `null` |
|
|
265
|
-
| --- | --- | --- |
|
|
266
|
-
| Claude Code | Transcripts, the live session records, the model catalog cache, and the Claude app's own record of each thread, for whether you've read it, whether its turn is waiting on you, and whether it's archived (`../lib/claude-desktop.mjs`). Spend is every reply's usage priced at Anthropic's list prices (`../lib/anthropic-prices.mjs`), the way Claude Code's own `/usage` total is computed, subagents included. Lines are counted from Edit and Write results and the files shell commands changed. The `cost-state` records Claude Code writes are a fallback only, for a model with no list price: they're written now and then, and they cover a whole process, which can span several sessions | — |
|
|
267
|
-
| Codex | Rollouts, plus the writer lock each loaded thread holds. Tokens, context, and usage limits come from `token_count`. Lines are counted from patches that applied. Renames come from `session_index.jsonl` and the state database, which also says what's archived and what you've read | Spend. An open approval prompt can't be told apart from a running command; only a question to you (`request_user_input`) shows as needing you |
|
|
268
|
-
| Cursor | Agent transcripts: status from `turn_ended`, the tool last called, the todo list, and subagents | Tokens, context, model, spend, lines, and tool start times (the start is when the record appeared) |
|
|
269
|
-
| Antigravity CLI | The transcript, written after each turn ends; the prompt history, whose newer prompt shows a turn underway; and the `task.md` checklist | Tokens, context, model, spend, lines, the current tool, and subagents. They're only in the protobuf conversation database, which isn't read |
|
|
270
|
-
| Hermes | `state.db`: token and spend columns added up across compaction segments, turn leases, the newest todo list, and the tool calls with no result yet. Context limits come from `context_length_cache.yaml` | Lines. Needing you shows only through the activity label, which Hermes updates at most once a minute |
|
|
271
|
-
| OpenCode | `opencode.db`, both the 1.x tables and 2.x's `session_v2`; a session in both keeps the newer copy. Totals come from the session row's usage columns, context from the last reply that produced output, and limits from `models.json` (1.x) or the `kv` catalog (2.x). Lines are the per-turn diff summaries (1.x). Needing you is a running `question` tool | Lines on 2.x. A pending permission prompt looks like any running tool |
|
|
272
|
-
| Kilo Code | `kilo.db`, the same 1.x tables as OpenCode. Its `summary_*` columns are live, so they give lines | As OpenCode 1.x |
|
|
273
|
-
|
|
274
|
-
## Ground rules
|
|
275
|
-
|
|
276
|
-
- **Read-only.** A reader opens files for reading and nothing else. `test/read-only.test.mjs`
|
|
277
|
-
checks this folder along with `harnesses/` and `lib/`.
|
|
278
|
-
- **Only the user's own home directory.** Never read from or run anything inside another app's
|
|
279
|
-
bundle.
|
|
280
|
-
- **Read incrementally.** Transcripts grow to tens of megabytes, so keep a byte offset per file
|
|
281
|
-
and parse only what was added. `jsonlFollower` in `../lib/jsonl-follower.mjs` does that for any
|
|
282
|
-
JSONL transcript, and Claude Code's go through `../lib/claude-transcript.mjs`. A transcript that
|
|
283
|
-
shrank was rewritten: start it over.
|
|
284
|
-
- **Open databases read-only, and check columns first.** `withReadOnly` in `../lib/sqlite.mjs`
|
|
285
|
-
opens one and closes it again. Check a column with `columnsOf` before a query names it, so a
|
|
286
|
-
column a harness renames costs one field instead of the whole harness.
|
|
287
|
-
- **Expect half-written lines.** A session being written right now is normal. Hold a partial
|
|
288
|
-
last line until its newline arrives, and skip a record that won't parse.
|
|
289
|
-
- **Never read secrets.** Claude Code keeps `*.key` files beside its session records. Read only
|
|
290
|
-
the `.json` files.
|
|
1
|
+
# Harness telemetry readers
|
|
2
|
+
|
|
3
|
+
A **harness** is whatever runs the agent sessions: Claude Code, Codex, OpenCode, and so on. Each
|
|
4
|
+
file here reads one harness's own session files and reports live telemetry for each session:
|
|
5
|
+
what it's doing now, its context, tokens, spend, and plan. Threadgauge shows them.
|
|
6
|
+
|
|
7
|
+
These are the telemetry half of each harness. The other half, finding every thread and opening
|
|
8
|
+
it, is in [`../harnesses/`](../harnesses/README.md), which Threadnauts uses. They're imported
|
|
9
|
+
separately (`@threadcrumbs/common/telemetry` and `@threadcrumbs/common/harnesses`), so an app
|
|
10
|
+
that only needs one half never loads the other.
|
|
11
|
+
|
|
12
|
+
## The shape of it
|
|
13
|
+
|
|
14
|
+
```js
|
|
15
|
+
export default {
|
|
16
|
+
id: 'my-harness',
|
|
17
|
+
name: 'My Harness',
|
|
18
|
+
detect,
|
|
19
|
+
watchRoots,
|
|
20
|
+
scanSessions,
|
|
21
|
+
sessionDetail,
|
|
22
|
+
}
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
| Member | What it does |
|
|
26
|
+
| --- | --- |
|
|
27
|
+
| `id` | Stable and kebab-case. It's part of every session id, so it must never change |
|
|
28
|
+
| `name` | What a person sees |
|
|
29
|
+
| `detect()` | `Promise<boolean>`: is this harness on this machine at all? It runs on every scan, so keep it cheap |
|
|
30
|
+
| `watchRoots()` | The folders to watch recursively. A change anywhere under them triggers a scan |
|
|
31
|
+
| `scanSessions()` | `Promise<Session[]>`: the sessions worth showing now. Cache against file size and mtime, because it runs often |
|
|
32
|
+
| `sessionDetail(id, options)` | Optional. `Promise<Detail \| null>`: one session's details, for Threadgauge's details panel. `null` for an id the last scan didn't list. It runs about once a second while that panel is open, so build it from state the scan already keeps, or one read-only query. Claude Code also takes `{ subagent }`, a subagent's id from `subagents`, and returns that subagent's own Detail, with `agentId` set, `subagents: null`, and `contextLimit`, its own model's context window in tokens (or `null` when unknown) |
|
|
33
|
+
| `readSession(id, options)` | Optional, Claude Code only so far. `Promise<{ session, detail } \| null>`: one session read on demand, even one older than the last day that `scanSessions` leaves out, so Threadgauge Pro can open an older thread. Takes `{ subagent }` like `sessionDetail`. It keeps the last few files it read, so asking again reads only what was added |
|
|
34
|
+
|
|
35
|
+
Throwing from `scanSessions` is survivable. Threadgauge's monitor reports the error under the
|
|
36
|
+
harness's name and carries on with the other harnesses.
|
|
37
|
+
|
|
38
|
+
Register a reader by adding it to `TELEMETRY` in `index.mjs`. Use the same `id` as that harness's
|
|
39
|
+
adapter in `../harnesses/`, so a session has the same id in both apps. Add the harness to
|
|
40
|
+
`test/shared-ids.test.mjs` too, which fails until both readers are compared on one fake install.
|
|
41
|
+
|
|
42
|
+
## The `Session`
|
|
43
|
+
|
|
44
|
+
| Field | Type | What it means |
|
|
45
|
+
| --- | --- | --- |
|
|
46
|
+
| `id` | string | `<harness id>:<session id>`, unique across every harness |
|
|
47
|
+
| `harness` | string | The reader's `id` |
|
|
48
|
+
| `title` | string | The harness's own title, or the first line of the first prompt |
|
|
49
|
+
| `project` / `projectPath` | string | The repo folder's name and absolute path |
|
|
50
|
+
| `worktree` | string | Worktree name, or `''` |
|
|
51
|
+
| `cwd`, `gitBranch` | string | Where it's working |
|
|
52
|
+
| `model`, `effort` | string | The harness's own ids, as recorded |
|
|
53
|
+
| `status` | string | `asking`, `working`, `delegating`, `done`, `error`, or `idle`. See [Done, needing you, and archived](#done-needing-you-and-archived) |
|
|
54
|
+
| `statusReason` | string | What the reader saw that decided `status`, one code from a fixed list. See [Why this status](#why-this-status) |
|
|
55
|
+
| `ask` | object or `null` | What the thread needs from you, while it's `asking` or `error`; `null` otherwise. See [What a thread needs](#what-a-thread-needs) |
|
|
56
|
+
| `usageLimits` | `{ at, subscription, windows: [{ id, usedPercent, windowMinutes, resetsAt }] }` or `null` | The plan's usage limits as the harness last recorded them in this session, at `at`. They cover the whole account, so an app shows the newest across a harness's sessions. `subscription` is the plan's name, such as `pro`, or `null`. `resetsAt` is epoch ms. Codex only, from `token_count`'s `rate_limits`, whose `primary` and `secondary` become the window ids; `null` elsewhere |
|
|
57
|
+
| `live` | boolean | A process for it is running now. A harness that keeps no record of its processes reports `true` while a turn is open |
|
|
58
|
+
| `startedAt`, `lastActivityAt` | number | Epoch ms |
|
|
59
|
+
| `tool` | `{ name, startedAt, target, kind }` or `null` | The tool running right now, only while working or delegating. `target` is its short file path, command, or search, as in `Detail.activity`, or `''` when the harness doesn't record one. `kind` is one of the [tool kinds](#tool-kinds) |
|
|
60
|
+
| `tokens` | `{ input, output, cacheRead, cacheWrite }` or `null` | Totals for the session's own thread. `input` leaves out cache reads and writes |
|
|
61
|
+
| `context` | `{ used, limit }` or `null` | Tokens in the window after the last reply; `limit` is `0` when unknown |
|
|
62
|
+
| `contextHistory` | number[] or `null` | The context after each reply, oldest first, for a sparkline: at most 24 whole numbers, evenly spaced across the session and always ending at `context.used`. A reply written twice counts once. Each reader keeps a running sample as it reads, never the whole history; OpenCode 2.x reads only the replies newer than the last one it saw, once the session changes, leaves the newest for the next read while it has no `time.completed`, and starts over if replies were removed. `null` for Cursor, Antigravity, and Hermes, whose session scan sees no per-reply counts, and before the first reply |
|
|
63
|
+
| `costUsd` | number or `null` | Spend as the harness records it; `null` when it doesn't |
|
|
64
|
+
| `lines` | `{ added, removed }` or `null` | Lines changed, as the harness records it |
|
|
65
|
+
| `plan` | `{ total, done, current }` or `null` | Plan progress, if the agent keeps one |
|
|
66
|
+
| `subagents` | number | Subagents still working for it |
|
|
67
|
+
| `workingSubagents` | `[{ id, name }]` or `null` | Those subagents, oldest first, named as `Detail.subagents` names them. `null` for Antigravity, which records none |
|
|
68
|
+
| `awaiting` | `[{ id, title }]` | Claude Code only: other sessions it asked to hear from that are still busy, by their own `id`. Either this or `subagents` makes an idle session `delegating`. Other harnesses leave it out |
|
|
69
|
+
| `prompts` | number or `null` | Prompts typed in this session |
|
|
70
|
+
| `turnStartedAt` | number or `null` | Epoch ms when the open turn began, for a turn timer. `null` when no turn is open, which includes a session waiting only on its subagents after handing its turn back. See [When a turn began](#when-a-turn-began) |
|
|
71
|
+
|
|
72
|
+
`null` means "this harness doesn't record it". Threadgauge shows "—" instead of guessing.
|
|
73
|
+
|
|
74
|
+
### When a turn began
|
|
75
|
+
|
|
76
|
+
| Harness | `turnStartedAt` comes from |
|
|
77
|
+
| --- | --- |
|
|
78
|
+
| Claude Code | The first record after a reply that handed the turn back: a prompt, a message from another session, or anything else that reopens it |
|
|
79
|
+
| Codex | `task_started` or `turn_started`, whichever opened the turn |
|
|
80
|
+
| Cursor | The `<timestamp>` on the prompt that opened the turn, which is to the minute |
|
|
81
|
+
| Antigravity CLI | The newest prompt in `history.jsonl`, the one that shows the turn underway |
|
|
82
|
+
| Hermes | The newest user message across the compression chain |
|
|
83
|
+
| OpenCode, Kilo Code | 1.x: the newest user message. 2.x: `time_busy`, or the newest user message without it |
|
|
84
|
+
|
|
85
|
+
### Why this status
|
|
86
|
+
|
|
87
|
+
| `status` | `statusReason` | Meaning | Harnesses |
|
|
88
|
+
| --- | --- | --- | --- |
|
|
89
|
+
| `asking` | `question` | A question tool is open | Claude Code, Codex, Hermes (clarify), OpenCode, Kilo Code |
|
|
90
|
+
| | `approval` | A tool waits for your permission | Claude Code, Codex, Hermes |
|
|
91
|
+
| | `waiting` | The live process says it waits on you, with no open tool to say why | Claude Code |
|
|
92
|
+
| | `marked-blocked` | The Claude app marked the turn as waiting on you | Claude Code |
|
|
93
|
+
| `working` | `turn-open` | The transcript or database shows a turn underway | All but Antigravity |
|
|
94
|
+
| | `process-busy` | The live process says it's busy | Claude Code |
|
|
95
|
+
| | `new-prompt` | A prompt is newer than the transcript | Antigravity |
|
|
96
|
+
| `delegating` | `subagents` | Its subagents are working, or its only open calls start them | All but Antigravity |
|
|
97
|
+
| | `awaiting` | It asked to hear from other sessions that are still busy (`awaiting`) | Claude Code |
|
|
98
|
+
| `error` | `turn-failed` | The last turn ended in an error | All but Hermes |
|
|
99
|
+
| `done` | `unread` | The harness records that you haven't read the reply | Claude Code (the app's record), Codex, Hermes, OpenCode 2.x |
|
|
100
|
+
| | `recent` | No record of reading, and the turn ended in the last 30 minutes | All |
|
|
101
|
+
| | `process-idle` | The live process says it's idle, with no record of reading, and the turn ended in the last 30 minutes | Claude Code |
|
|
102
|
+
| `idle` | `read` | The harness records that you've read the reply | Claude Code (the app's record), Codex, Hermes, OpenCode 2.x |
|
|
103
|
+
| | `quiet` | No record of reading, and nothing for 30 minutes | All |
|
|
104
|
+
| | `process-idle` | The live process says it's idle, with no record of reading, and nothing for 30 minutes | Claude Code |
|
|
105
|
+
|
|
106
|
+
### What a thread needs
|
|
107
|
+
|
|
108
|
+
`ask` is one of these. Free text is cut like an activity target: headers and labels to 160
|
|
109
|
+
characters, questions, descriptions, and error text to 500, with "…" at the cut. At most four
|
|
110
|
+
questions with eight options each are kept.
|
|
111
|
+
- `{ type: 'question', questions: [{ header, text, options: [{ label, description }], multiple }] }`
|
|
112
|
+
comes from the open question tool's input: Claude Code's AskUserQuestion, Codex's
|
|
113
|
+
`request_user_input`, and OpenCode's and Kilo Code's `question`.
|
|
114
|
+
- `{ type: 'approval', tool, target, kind }` is the open tool waiting for permission: Claude
|
|
115
|
+
Code's newest open call while its process waits on you, and Codex's `request_permissions`.
|
|
116
|
+
- `{ type: 'error', text }` is the newest error's text while the status is `error`. It can be
|
|
117
|
+
`''` when the harness records the failure but not its text.
|
|
118
|
+
- `{ type: 'unknown' }` means the thread needs you but the harness doesn't say why: the Claude
|
|
119
|
+
app's mark, or Hermes, whose activity label is all it records.
|
|
120
|
+
|
|
121
|
+
### Done, needing you, and archived
|
|
122
|
+
|
|
123
|
+
These follow the session readers in `../harnesses/`, so Threadgauge and Threadnauts count a
|
|
124
|
+
thread the same way.
|
|
125
|
+
- **Done** means the turn ended and you haven't read the reply. After that it's `idle`.
|
|
126
|
+
- **Where the harness records reading, that decides:** Codex's read receipts, Hermes's
|
|
127
|
+
`last_read_at`, and OpenCode 2.x's `time_viewed`.
|
|
128
|
+
- **Claude Code** counts a reply as unread when the Claude app's record of the thread changed
|
|
129
|
+
after you last focused it. A live session that handed its turn back in the last 30 minutes
|
|
130
|
+
is done either way, as the session reader's is.
|
|
131
|
+
- **Everywhere else,** a terminal-only Claude Code session included, a turn is done for 30
|
|
132
|
+
minutes after it ends. Not knowing isn't the same as read.
|
|
133
|
+
- **Needing you** also covers a Claude Code turn the Claude app marked `blocked`, unless
|
|
134
|
+
subagents or other sessions are still out. `delegating` comes first.
|
|
135
|
+
- **Archived** sessions are left out, and so are Claude Code sessions deleted in the app. Cursor
|
|
136
|
+
and Antigravity keep no archive record, so theirs always show.
|
|
137
|
+
|
|
138
|
+
## The `Detail`
|
|
139
|
+
|
|
140
|
+
| Field | Type | What it means |
|
|
141
|
+
| --- | --- | --- |
|
|
142
|
+
| `id`, `sessionId` | string | The session's id, and the harness's own id for it |
|
|
143
|
+
| `source` | string | The transcript or database it was read from |
|
|
144
|
+
| `models` | `[{ model, tokens, costUsd }]` | Tokens, and spend where recorded, per model |
|
|
145
|
+
| `contextParts` | `{ input, cacheRead, cacheWrite, output }` | What filled the context in the latest request: new input, cache reads, cache writes, and its output. They add up to the session's `context.used`. `null` for Cursor, Antigravity, and Hermes |
|
|
146
|
+
| `history` | `[{ at, context, output }]` | Context and output after each reply, the newest 600 |
|
|
147
|
+
| `compactions` | `[{ at, before, after }]` | When the context was compacted, and its size before and after |
|
|
148
|
+
| `activity` | `[{ id, name, kind, server, target, input, path, line, startedAt, endedAt, running, error, errorText }]` | The newest 100 tool calls. `kind` and `server` are described under [Tool kinds](#tool-kinds). `target` is a short file path, command, or search, never the output. `input` is the full command, or the call's input as JSON without file contents, cut to 2,000 characters. `path` and `line` are the file a call names, relative to the session's folder, and its line where known. `errorText` is the start of a failed call's own error output, cut to 500 characters. Any of these is `null` when the harness doesn't record it |
|
|
149
|
+
| `tools` | `[{ name, kind, server, calls, errors, ms }]` | Every call in the session, per tool. A tool's `kind` comes from its name alone, so a shell tool is `execute` here even when some of its calls are `search` |
|
|
150
|
+
| `plan` | `[{ content, status }]` | Every plan step; `[]` when the agent kept none |
|
|
151
|
+
| `subagents` | `[{ id, name, type, status, model, tokens, tool, startedAt, lastActivityAt }]` | Every subagent it started, finished or not. `tool` has the Session's `tool` shape |
|
|
152
|
+
| `files` | `[{ path, added, removed, edits, line, hunks }]` | Lines changed per file, with paths relative to the session's folder. `line` is where its latest change starts. `hunks` (`[{ line, oldLine, lines, cut }]`) are its newest 5 changes as diff lines, each with at most 60 lines of 240 characters, and `cut` counts the lines left out. Only the 25 most recently edited files keep hunks; older ones have `[]`. `line` and `hunks` are `null` when the harness doesn't record them, and a hunk's `line` is `null` when the patch has no line numbers |
|
|
153
|
+
| `prompts` | `[{ at, text, via? }]` | The newest 50 prompts, each cut to 2,000 characters, with line breaks kept. A message the user sent through Threadgauge, delivered when a turn ended, has `via: 'threadgauge'` (Claude Code) |
|
|
154
|
+
| `thoughts` | `[{ at, text }]` | The agent's reasoning, where the harness writes it as text: the newest 20 messages that had some, each cut to 2,000 characters. Encrypted, redacted, and signature-only reasoning is skipped |
|
|
155
|
+
| `replies` | `[{ at, text }]` | The agent's text to you, one entry per message with its text blocks joined: the newest 50, each cut to 2,000 characters. Tool calls and API errors are left out |
|
|
156
|
+
| `errors` | `[{ at, message }]` | The newest 20 API errors and failed turns, each cut to 500 characters |
|
|
157
|
+
| `contextBreakdown` | `{ at, total, limit, reserved, parts }` | What fills the context, like Claude Code's `/context`. Only `total` is measured; every part is an estimate. See [Context breakdown](#context-breakdown). `null` for Cursor, Antigravity, Hermes, OpenCode, and Kilo Code |
|
|
158
|
+
|
|
159
|
+
Again, a `null` field means the harness doesn't record it. The shared helpers for building one
|
|
160
|
+
(`startCall`, `endCall`, `toolTarget`, `keepNewest`, `fileList`, and the limits) are in
|
|
161
|
+
`../lib/telemetry-fields.mjs`.
|
|
162
|
+
|
|
163
|
+
`sessionDetail(id, { filesOnly: true })` returns only `{ id, sessionId, files }`, with each file
|
|
164
|
+
as `{ path }`, from what the session scan already holds. It's for finding other threads that
|
|
165
|
+
changed the same files, so it never opens the readers that serve an open thread.
|
|
166
|
+
|
|
167
|
+
The readers that serve an open thread (thoughts and replies for Claude Code, Codex, Cursor, and
|
|
168
|
+
Antigravity) are kept for the last three sessions opened. Claude Code's and Codex's read the
|
|
169
|
+
transcript from its end first, in growing steps, until they have the newest 50 replies, and then
|
|
170
|
+
follow it forwards.
|
|
171
|
+
|
|
172
|
+
### Context breakdown
|
|
173
|
+
|
|
174
|
+
`contextBreakdown` splits the context into parts, so an app can show what fills it.
|
|
175
|
+
- **Only `total` is measured.** It's the harness's own count of the latest request, the same
|
|
176
|
+
number as the Session's `context.used`, and `at` is that request's time. Every part is an
|
|
177
|
+
estimate scaled to it, and the parts always add up to it. An app should say so.
|
|
178
|
+
- **`limit`** is the context limit, and **`reserved`** is what the harness keeps free for
|
|
179
|
+
compacting by default: 33,000 tokens for Claude Code, and for Codex the gap between the
|
|
180
|
+
window it reports and the 90% of its full window where it compacts. The settings that change
|
|
181
|
+
them aren't read, so an app should call it the default. Free space is `limit - total -
|
|
182
|
+
reserved`.
|
|
183
|
+
- **`parts`** are `[{ id, tokens, items }]` in this order: `messages`, `system`,
|
|
184
|
+
`builtInTools`, `mcpTools`, `skills`, `memory`. A part the harness records is sent even at 0
|
|
185
|
+
tokens; one it doesn't record is left out, and its tokens stay in `system`.
|
|
186
|
+
- `messages` items: `{ id: 'prompts' }`, `{ id: 'replies' }`, `{ id: 'summary' }` (from the
|
|
187
|
+
last compaction), and `{ id: 'tool', name }` for each tool's calls and results.
|
|
188
|
+
- `mcpTools` items: `{ name, tools }` per server, `tools` a count or `null`.
|
|
189
|
+
- `skills` items: `{ name }`. `memory` items: `{ path }`, the file's full path.
|
|
190
|
+
- Every item also has `tokens`.
|
|
191
|
+
- **How it's worked out:**
|
|
192
|
+
- What was there before the first message is the first request's input count minus the
|
|
193
|
+
first prompt (at 4 characters a token).
|
|
194
|
+
- That gives a characters-per-token ratio for the text of each fixed part (system prompt,
|
|
195
|
+
tool definitions, MCP servers, skills, memory files), so each part is its text length at
|
|
196
|
+
that ratio. A part added later counts at the same ratio.
|
|
197
|
+
- The ratio is used only when the system prompt and tool definitions were recorded, as in
|
|
198
|
+
Claude Code's `api-request-shape`. Otherwise it would spread their unrecorded text over the
|
|
199
|
+
parts that were, so each recorded part is its text at 4 characters a token, and `system` is
|
|
200
|
+
the rest of the fixed count. That's always the case for Codex, whose built-in tool
|
|
201
|
+
definitions aren't written.
|
|
202
|
+
- `messages` is the rest of the total. Each request's growth over the one before is split
|
|
203
|
+
among the prompts, replies, and tool calls and results since then, by text length. After a
|
|
204
|
+
compaction, the next request's growth over the fixed parts is the summary.
|
|
205
|
+
- **When it falls back:** if the transcript began mid-session (no first prompt before the first
|
|
206
|
+
request, or a compaction summary first), or the fixed count comes out at or below zero or
|
|
207
|
+
above the total, there are only two parts: `system` is the fixed text at 4 characters a
|
|
208
|
+
token, and `messages` the rest. With no fixed text known either, it's `null`. A transcript
|
|
209
|
+
with no record of what was sent has `messages` and `system` only, `system` being the whole
|
|
210
|
+
fixed count.
|
|
211
|
+
- **Where the fixed text comes from:** only the files the readers already open; no config file
|
|
212
|
+
is read.
|
|
213
|
+
- Claude Code: its transcript's own records of each request (`api-request-shape` and the
|
|
214
|
+
`instructions`, `skill_listing`, `deferred_tools_delta`, `mcp_instructions_delta`, and
|
|
215
|
+
`agent_listing_delta` attachments; see [HARNESS-NOTES.md](HARNESS-NOTES.md)).
|
|
216
|
+
- Codex: the rollout's base instructions and developer messages, its AGENTS.md messages, and
|
|
217
|
+
its `additional_tools` items. Its built-in tool definitions aren't written, so they're in
|
|
218
|
+
`system`.
|
|
219
|
+
- **Its sizes are kept by the session scan,** which already reads every record, as numbers and
|
|
220
|
+
names only, never the text, so opening a thread doesn't read its transcript again.
|
|
221
|
+
|
|
222
|
+
### Tool kinds
|
|
223
|
+
|
|
224
|
+
Every call and every tool total has a `kind`, so an app can draw one icon per kind instead of
|
|
225
|
+
one per tool. The kinds are ACP's tool kinds plus Gemini CLI's `agent`, `plan`, and
|
|
226
|
+
`communicate`: `read`, `search`, `edit`, `delete`, `move`, `execute`, `think`, `fetch`, `agent`,
|
|
227
|
+
`plan`, `communicate`, `switch_mode`, and `other`.
|
|
228
|
+
- **By name:** `../lib/tool-kinds.mjs` keeps one table of every harness's tool names, compared
|
|
229
|
+
without case, spaces, or dashes. A name it doesn't know is `other`.
|
|
230
|
+
- **MCP tools** (`mcp__<server>__<tool>`) are `other`, with `server` set to the server's name.
|
|
231
|
+
`server` is `null` for every other tool.
|
|
232
|
+
- **Shell calls:** a call whose tool is `execute` takes `read`, `search`, `delete`, or `move`
|
|
233
|
+
when every step of its command does only that. Anything else stays `execute`: a step the
|
|
234
|
+
table doesn't know, mixed kinds, a redirect into a file, command substitution, a background
|
|
235
|
+
`&`, or a pipe into anything but a filter such as `head` or `sort`.
|
|
236
|
+
- A known command with an option that writes a file or runs one stays `execute` too:
|
|
237
|
+
`find -exec`, `-delete`, or `-fprint`; `fd -x`; `rg --pre`; `tree -o`; `less -o`;
|
|
238
|
+
`sort -o`; and `uniq` with an output file.
|
|
239
|
+
- `sed` is `read` only when its script just prints, such as `10,20p`.
|
|
240
|
+
- `git` skips `-C <dir>` and `--no-pager` before its subcommand.
|
|
241
|
+
- A command cut short to fit `input` is `execute`, because the part that was cut could
|
|
242
|
+
write. An open call reads its command whole.
|
|
243
|
+
- `startCall` sets both fields, so a reader gets them by passing the call's name and input.
|
|
244
|
+
|
|
245
|
+
| Harness | Where its details come from | `null` |
|
|
246
|
+
| --- | --- | --- |
|
|
247
|
+
| Claude Code | The transcript the scan already reads, collected as it goes (`transcriptReader({ detail: true })`), plus every subagent transcript and its `.meta.json`. Hunks come from each edit's `structuredPatch`, and a failed call's error text from its tool result. Thoughts and replies come from a second reader of the transcript, kept for the last three sessions opened | — |
|
|
248
|
+
| Codex | The rollout state: calls and their exit codes, per-file lines and hunks from patches, and each `token_count`. Subagents are the child rollouts. A failed command's error text is its output. Thoughts are reasoning summaries and replies are assistant messages, from a second reader kept for the last three sessions opened | `compactions`. Hunks have no line numbers |
|
|
249
|
+
| Cursor | The transcript state. A call's start is when it was first read. Replies come from a second reader kept for the last three sessions opened; Cursor writes no time on them, so each takes its prompt's time, and it writes the agent's thinking into the same text | `thoughts`, `history`, `models`, `files`, `compactions`. A call has no end time or error |
|
|
250
|
+
| Antigravity CLI | Transcript steps, each ended by the next, with prompts from `history.jsonl`. Thoughts and replies are planner steps' `thinking` and `content`, from a second reader kept for the last three sessions opened | `history`, `models`, `files`, `subagents`, `compactions`. A call has no input or file |
|
|
251
|
+
| Hermes | One query for the compression chain: tool calls joined to their results, user and assistant messages, and child sessions. Thoughts are the `reasoning` column, or `reasoning_content` | `history`, `files`, `compactions`; `thoughts` when the database has neither column. A call has no error |
|
|
252
|
+
| OpenCode, Kilo Code | One query for the session's messages and tool parts. Files are the 1.x per-turn diffs; a failed call's error text is its `state.error`. Thoughts and replies are the reasoning and text parts of each reply. On 2.x a reply's parts (calls, plan, thoughts, and text) are its part rows when OpenCode writes them (`experimental.part_rows`, off by default), and otherwise the message's own content | `compactions`; `files` on 2.x and when Kilo kept no diffs; `hunks` always |
|
|
253
|
+
|
|
254
|
+
## What each harness records
|
|
255
|
+
|
|
256
|
+
[HARNESS-NOTES.md](HARNESS-NOTES.md) has what each harness saves of its reasoning and replies,
|
|
257
|
+
the hooks it offers, and how much of its context it keeps free for compacting, with the version
|
|
258
|
+
and date each was checked.
|
|
259
|
+
|
|
260
|
+
Only Claude Code was checked against a real install. The others were written from each
|
|
261
|
+
harness's source code, or, for the closed ones, from what other open-source tools parse, and
|
|
262
|
+
tested against fake installs. Recheck them when a harness changes its format.
|
|
263
|
+
|
|
264
|
+
| Harness | What the reader takes, and from where | Not on disk, so `null` |
|
|
265
|
+
| --- | --- | --- |
|
|
266
|
+
| Claude Code | Transcripts, the live session records, the model catalog cache, and the Claude app's own record of each thread, for whether you've read it, whether its turn is waiting on you, and whether it's archived (`../lib/claude-desktop.mjs`). Spend is every reply's usage priced at Anthropic's list prices (`../lib/anthropic-prices.mjs`), the way Claude Code's own `/usage` total is computed, subagents included. Lines are counted from Edit and Write results and the files shell commands changed. The `cost-state` records Claude Code writes are a fallback only, for a model with no list price: they're written now and then, and they cover a whole process, which can span several sessions | — |
|
|
267
|
+
| Codex | Rollouts, plus the writer lock each loaded thread holds. Tokens, context, and usage limits come from `token_count`. Lines are counted from patches that applied. Renames come from `session_index.jsonl` and the state database, which also says what's archived and what you've read | Spend. An open approval prompt can't be told apart from a running command; only a question to you (`request_user_input`) shows as needing you |
|
|
268
|
+
| Cursor | Agent transcripts: status from `turn_ended`, the tool last called, the todo list, and subagents | Tokens, context, model, spend, lines, and tool start times (the start is when the record appeared) |
|
|
269
|
+
| Antigravity CLI | The transcript, written after each turn ends; the prompt history, whose newer prompt shows a turn underway; and the `task.md` checklist | Tokens, context, model, spend, lines, the current tool, and subagents. They're only in the protobuf conversation database, which isn't read |
|
|
270
|
+
| Hermes | `state.db`: token and spend columns added up across compaction segments, turn leases, the newest todo list, and the tool calls with no result yet. Context limits come from `context_length_cache.yaml` | Lines. Needing you shows only through the activity label, which Hermes updates at most once a minute |
|
|
271
|
+
| OpenCode | `opencode.db`, both the 1.x tables and 2.x's `session_v2`; a session in both keeps the newer copy. Totals come from the session row's usage columns, context from the last reply that produced output, and limits from `models.json` (1.x) or the `kv` catalog (2.x). Lines are the per-turn diff summaries (1.x). Needing you is a running `question` tool | Lines on 2.x. A pending permission prompt looks like any running tool |
|
|
272
|
+
| Kilo Code | `kilo.db`, the same 1.x tables as OpenCode. Its `summary_*` columns are live, so they give lines | As OpenCode 1.x |
|
|
273
|
+
|
|
274
|
+
## Ground rules
|
|
275
|
+
|
|
276
|
+
- **Read-only.** A reader opens files for reading and nothing else. `test/read-only.test.mjs`
|
|
277
|
+
checks this folder along with `harnesses/` and `lib/`.
|
|
278
|
+
- **Only the user's own home directory.** Never read from or run anything inside another app's
|
|
279
|
+
bundle.
|
|
280
|
+
- **Read incrementally.** Transcripts grow to tens of megabytes, so keep a byte offset per file
|
|
281
|
+
and parse only what was added. `jsonlFollower` in `../lib/jsonl-follower.mjs` does that for any
|
|
282
|
+
JSONL transcript, and Claude Code's go through `../lib/claude-transcript.mjs`. A transcript that
|
|
283
|
+
shrank was rewritten: start it over.
|
|
284
|
+
- **Open databases read-only, and check columns first.** `withReadOnly` in `../lib/sqlite.mjs`
|
|
285
|
+
opens one and closes it again. Check a column with `columnsOf` before a query names it, so a
|
|
286
|
+
column a harness renames costs one field instead of the whole harness.
|
|
287
|
+
- **Expect half-written lines.** A session being written right now is normal. Hold a partial
|
|
288
|
+
last line until its newline arrives, and skip a record that won't parse.
|
|
289
|
+
- **Never read secrets.** Claude Code keeps `*.key` files beside its session records. Read only
|
|
290
|
+
the `.json` files.
|