@agentstrack/collector 0.2.0 → 0.3.0

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.
Files changed (52) hide show
  1. package/CHANGELOG.md +177 -1
  2. package/README.md +78 -28
  3. package/dist/adapters/claude.d.ts +18 -0
  4. package/dist/adapters/claude.js +153 -45
  5. package/dist/adapters/claude.js.map +1 -1
  6. package/dist/adapters/codex.d.ts +15 -1
  7. package/dist/adapters/codex.js +81 -33
  8. package/dist/adapters/codex.js.map +1 -1
  9. package/dist/adapters/opencode.d.ts +19 -7
  10. package/dist/adapters/opencode.js +88 -29
  11. package/dist/adapters/opencode.js.map +1 -1
  12. package/dist/adapters/types.d.ts +16 -0
  13. package/dist/adapters/types.js +61 -0
  14. package/dist/adapters/types.js.map +1 -1
  15. package/dist/cli.js +165 -34
  16. package/dist/cli.js.map +1 -1
  17. package/dist/commands/service.js +40 -10
  18. package/dist/commands/service.js.map +1 -1
  19. package/dist/config.d.ts +2 -1
  20. package/dist/config.js +31 -5
  21. package/dist/config.js.map +1 -1
  22. package/dist/daemon.d.ts +73 -21
  23. package/dist/daemon.js +358 -121
  24. package/dist/daemon.js.map +1 -1
  25. package/dist/git/commits.d.ts +7 -1
  26. package/dist/git/commits.js +36 -17
  27. package/dist/git/commits.js.map +1 -1
  28. package/dist/git/repo.d.ts +13 -4
  29. package/dist/git/repo.js +34 -20
  30. package/dist/git/repo.js.map +1 -1
  31. package/dist/machine.d.ts +27 -0
  32. package/dist/machine.js +46 -0
  33. package/dist/machine.js.map +1 -0
  34. package/dist/privacy/pipeline.d.ts +3 -0
  35. package/dist/privacy/pipeline.js +4 -2
  36. package/dist/privacy/pipeline.js.map +1 -1
  37. package/dist/privacy/redact.d.ts +10 -1
  38. package/dist/privacy/redact.js +29 -3
  39. package/dist/privacy/redact.js.map +1 -1
  40. package/dist/queue/event-id.d.ts +9 -0
  41. package/dist/queue/event-id.js +15 -0
  42. package/dist/queue/event-id.js.map +1 -0
  43. package/dist/queue/spool.d.ts +24 -5
  44. package/dist/queue/spool.js +89 -33
  45. package/dist/queue/spool.js.map +1 -1
  46. package/dist/queue/tailer.d.ts +27 -4
  47. package/dist/queue/tailer.js +89 -28
  48. package/dist/queue/tailer.js.map +1 -1
  49. package/dist/transport/client.d.ts +37 -13
  50. package/dist/transport/client.js +50 -3
  51. package/dist/transport/client.js.map +1 -1
  52. package/package.json +2 -2
package/CHANGELOG.md CHANGED
@@ -10,6 +10,179 @@ released as a major version, with a migration note in this file.
10
10
 
11
11
  ## [Unreleased]
12
12
 
13
+ ## [0.3.0] — 2026-09-05
14
+
15
+ ### Added
16
+ - **Skills, sub-agents and workflows are named.** A Claude Code `tool_use` named `Skill` carries
17
+ `skill`; `Agent` carries `subagent_type` and `description`; `Workflow` carries `workflow_name`
18
+ (the `name` from the script's `meta` header, when it is that simple). The `Agent` call's prompt and the
19
+ script body are never copied out of the call; a sub-agent transcript's own opening prompt is a
20
+ prompt like any other and follows `privacy.prompts`. The extras ride on `tool.started` and on the matching
21
+ `tool.completed` / `tool.failed`.
22
+ - **Sub-agent transcripts are attributed.** Claude Code writes each sub-agent to
23
+ `<session-uuid>/subagents/[workflows/<wf>/]agent-<id>.jsonl` with the parent's session id; the
24
+ tailer already walked them, but nothing said which lines were the sub-agent's. Every event from
25
+ such a file (or any line with `isSidechain: true`) now carries `sidechain: true`, `agent_id`,
26
+ `agent_kind` (`subagent` | `workflow`) and, from the sibling `agent-<id>.meta.json`, `agent_type`
27
+ — so the sub-agent's own `model.response` usage can be attributed to it server-side.
28
+ - **`user.prompted.ultracode`**, `true` when the prompt contains the whole word `ultracode`
29
+ (case-insensitive). Computed locally before redaction, so it survives `metadata` mode; the prompt
30
+ does not travel to be inspected.
31
+ - **Machine info on register and health.** Alongside `hostname`, `os` and `arch` the collector now
32
+ sends `os_release` and `machine_kind` — `ci` (a CI env var), `container`
33
+ (`/.dockerenv` or a docker/containerd/kubepods cgroup), `workstation` (macOS, Windows, or Linux
34
+ with a display), `server` (headless Linux), else `unknown`. Health repeats it, so a device that
35
+ changes shape is updated. See the README privacy section for what the server does with it.
36
+
37
+ ### Changed
38
+ - `description` (the Agent tool's one-line label) is treated like a title: secret-redacted, and
39
+ dropped in `metadata` mode.
40
+
41
+ ### Security
42
+ - **`api_url` must be `https`.** The bearer API key rides on every request, so `http` is now rejected
43
+ for any host except `localhost`/`127.0.0.1`/`[::1]`. `login` prints the URL it is about to use.
44
+ - **The API key can stay off the command line.** `agentstrack login` now takes the key as an optional
45
+ argument and otherwise reads `AGENTSTRACK_API_KEY`, an echo-off terminal prompt, or stdin
46
+ (`agentstrack login < key.txt`), keeping it out of shell history and `ps`.
47
+ - **`config.yaml` is written atomically at mode 600** — a temp file created 0600 then renamed over the
48
+ target, so a crash can no longer leave a truncated config or a brief world-readable window.
49
+ - **Org redaction rules are bounded.** A server-supplied pattern that is malformed, over 256
50
+ characters, or using a backreference is skipped (as a malformed rule already was), the common
51
+ catastrophic nested-quantifier shapes (`(a+)+`, `(a|aa)+`, `((a+)b)+`) are rejected, and org rules
52
+ match only the first 64 KB of a value — a pathological pattern is much less likely to hang the
53
+ single-threaded daemon.
54
+
55
+ ### Fixed
56
+ - **Claude Code tokens and cost were inflated ~1.8x.** Claude Code writes one `assistant` line per
57
+ content block of a single response, each repeating the same `message.id` and usage; every line
58
+ became a `model.response`. Usage is now emitted once per `message.id`.
59
+ - **Claude Code tool outcomes were all named `unknown`.** `tool_result` blocks carry only a
60
+ `tool_use_id`; the name is now resolved from the `tool_use` that started the call.
61
+ - **Claude Code prompts pasted with an image (or as text blocks) were never counted**, and slash
62
+ command echoes (`<command-name>`, `<command-message>`, `<local-command-stdout>`,
63
+ `<task-notification>`) were counted as human prompts. Both fixed.
64
+ - **Codex sessions went dark after a collector restart.** The session id lived only in memory from
65
+ `session_meta`; it is now seeded from the rollout file name, which carries the same uuid.
66
+ - **Codex cached tokens were billed twice.** `cached_input_tokens` is a subset of `input_tokens`
67
+ in Codex; the collector now subtracts it so the two are exclusive, as the schema documents.
68
+ - **Codex shell commands landed as `shell` with `duration_ms: 0` and never failed.**
69
+ `exec_command_end` carries an argv list, a `{secs,nanos}` duration and a numeric exit code;
70
+ `function_call_output.output` is a string. Both are read as they really are, and `tool.failed`
71
+ is emitted on a non-zero exit.
72
+ - **OpenCode `session.ended` was rejected by the server on every emit** (missing
73
+ `external_session_id`, `reason` outside the enum). It now sends `reason: normal` with the
74
+ archived/compacted distinction in `end_kind`.
75
+ - **OpenCode resumed sessions never got a `session.started`.** Sessions are fetched by
76
+ `time_updated` but starts were gated on a `time_created` cursor; a per-session marker in the
77
+ spool's meta store replaces it.
78
+ - **`logout` now tears the service down first.** It previously left the launchd/systemd unit
79
+ installed, so the supervisor kept restarting an unauthenticated collector. Both units now restart
80
+ only on a crash (launchd `KeepAlive`/`SuccessfulExit`, systemd `Restart=on-failure` with a
81
+ 5-in-5-minutes start limit), and the unauthenticated foreground path exits cleanly so the
82
+ supervisor idles.
83
+ - **Service units handle paths with spaces and non-ASCII characters.** The CLI path is resolved with
84
+ `fileURLToPath` instead of a percent-encoded `URL.pathname`, plist strings are XML-escaped, and
85
+ systemd `ExecStart` arguments are quoted. `AGENTSTRACK_HOME` is written into the unit when set.
86
+ - **A second foreground `start` refuses to run** when one is already collecting (checked via the pid
87
+ file, created with `wx`), preventing two collectors from racing on one spool.
88
+ - **`stop` verifies the pid still belongs to a collector** (via `ps`) before signalling it, so a
89
+ recycled pid in a stale pid file is not killed.
90
+ - **`doctor` reports the real scan window.** It now prints `modified in the last N day(s)` using
91
+ `tracking.max_age_days` instead of a hard-coded "7 days".
92
+ - **`git.commit` polling stops re-diffing history every tick.** Each repo's `git log --since` now
93
+ starts from its last poll, SHAs are listed before any diffstat so `git show --numstat` runs only for
94
+ commits not yet emitted, and the emitted-SHA guard is pruned by age instead of cleared wholesale, so
95
+ a commit inside the lookback window is never re-emitted.
96
+
97
+ ### Changed
98
+ - `detect()` reads only the first 16 KB of the newest transcript (by mtime) for the agent version,
99
+ cached on mtime, instead of the whole file every 5 s. OpenCode keeps one read-only database
100
+ handle with prepared statements rather than opening and closing one per query, and caches the
101
+ version for 60 s.
102
+ - The contract test now runs every adapter fixture through the server's own payload schemas when
103
+ the server checkout is present, and fails (rather than skips) when `AGENTSTRACK_SERVER_REPO` is
104
+ set but missing.
105
+ - **Node floor is `>=22`** (was `>=24`), matching `.nvmrc` and the runtime the code actually needs;
106
+ CI now tests Node 22 and 24.
107
+ - `status` shows an upload-paused reason when one is present.
108
+
109
+ ### Fixed — daemon, queue and transport
110
+ - **The upload failure policy ran once per concurrent batch, not once per wave.** With
111
+ `upload.concurrency: 4` a dead API escalated the backoff counter by four per wave (5-minute waits
112
+ after two waves), slept inside the wave so tailing froze for the duration, and a sibling's success
113
+ reset the counter or undid a `413` halving. `sendBatch` now returns a pure outcome and `flush()`
114
+ applies the policy once on the wave: halve once, one backoff step, strikes only for poison
115
+ batches, counter reset only when the whole wave succeeded. Backoff sets a next-upload time instead
116
+ of sleeping, honours `Retry-After` / `Retry-After-ingest` as the minimum, and a daemon tick sends
117
+ at most five waves before scanning again. Covered by `src/queue/flush.test.ts`.
118
+ - **`401`/`403` no longer count strikes against telemetry.** They pause uploads (`status` shows the
119
+ reason), as does an over-quota `200` — previously acked and dropped locally with the `quota` block
120
+ ignored — and a `200` that rejects every event as malformed, which now logs a version-mismatch
121
+ error instead of deleting the batch.
122
+ - **Checkpoints were written before the events were spooled.** A throw between the two lost those
123
+ lines for good. The tailer now streams in 4 MB chunks and commits each chunk's checkpoint in the
124
+ same transaction as its events; a line over 8 MB is skipped to the next newline and counted, at
125
+ most 64 MB per file is read per scan, short reads are looped, and one unreadable file no longer
126
+ aborts the scan for every file after it.
127
+ - **Re-reading a transcript double-counted on the server.** `event_id` was a fresh `randomUUID` per
128
+ spool write; it is now derived from the adapter, file, byte offset and line content (a UUID v8
129
+ shape), so a rotated inode, a purged spool or a second collector on the same files dedupes.
130
+ - **`VERSION` was hard-coded `0.1.0`.** It is read from `package.json`; the CLI, register and health
131
+ report the published version, and every request carries `User-Agent: agentstrack-collector/<v>`.
132
+ - **A Claude Code response spanning a restart was billed twice.** `model.response` is now keyed on
133
+ `message.id` (the daemon's deterministic id, seeded by the adapter), not on the line, so the
134
+ server's dedupe absorbs the second line's copy after a restart.
135
+ - **One rejected event paused every upload as a "schema mismatch".** A single-event batch the
136
+ server rejects is now struck like any other poison event; the pause is reserved for a whole
137
+ batch rejected without a quota reason.
138
+ - **A failed commit left the checkpoint cache ahead of disk.** `setCheckpoint()` updated the
139
+ in-memory Map before COMMIT; the cache is now reloaded from the table when a transaction throws.
140
+ - **OpenCode cursors and started-markers were written before the events they covered.** They are
141
+ now buffered and committed in the same transaction as the enqueue, so a full disk cannot mark a
142
+ session started that was never spooled.
143
+ - **systemd `WorkingDirectory=` was quoted.** Path-typed settings are not unquoted by systemd; the
144
+ unit now writes the bare path (`ExecStart=` keeps its quoting).
145
+ - **Claude Code and Codex sessions never ended.** The daemon emits `session.ended`
146
+ (`reason: timeout`) for a session quiet longer than `tracking.idle_timeout_seconds`, stamped at
147
+ the moment the timeout elapsed, and `reason: unknown` for anything still open on shutdown.
148
+ - The batch response is validated with zod at the trust boundary; an unreadable body is retried,
149
+ never acked. Rejected `allSettled` outcomes are logged instead of swallowed. Local `batch_size` is
150
+ clamped to the server's `max_batch_events`.
151
+
152
+ ### Changed — daemon, queue and transport
153
+ - The spool is `VACUUM`ed at open when the freelist is both over 2048 pages and more than half the
154
+ file (a 206 MB spool holding 78 events was observed), the WAL is capped at 8 MB
155
+ (`journal_size_limit`), statements are prepared once, checkpoints are cached in memory so an
156
+ unchanged file costs no SQL, and WAL/SHM are chmod 600 after they exist.
157
+ - `describeRepo` is cached per scan pass; `flush()`, `reportHealth()` and `depth()` are inside the
158
+ loop's try/catch; the log rotates once at 5 MB to `collector.log.1` and a scan pass writes one
159
+ `Queued N events across M files` line instead of one per file.
160
+ - Org redaction rules are compiled once per server-config refresh and handed to the privacy
161
+ pipeline pre-compiled, instead of being recompiled for every event.
162
+
163
+ ## [0.2.1] — 2026-08-31
164
+
165
+ ### Fixed
166
+ - **A first import silently stopped at 7 days.** `listTranscripts` defaulted to `maxAgeDays = 7` and
167
+ both call sites took the default, so only transcripts touched in the last week were ever opened —
168
+ 258 of 690 files on the machine this was found on. Everything older stayed on disk with nothing in
169
+ the output indicating it had been skipped. The window is now `tracking.max_age_days`, still
170
+ defaulting to 7 so a running collector keeps its cheap steady-state scan and a fresh install does
171
+ not unexpectedly upload years of history. Set it to `3650`, restart, then set it back to import
172
+ what is already on disk.
173
+
174
+ - **OpenCode ignored that window and kept its own.** It reads a live SQLite database rather than
175
+ tailing files, so it has an independent history floor — also hardcoded to 7 days. Widening
176
+ `max_age_days` therefore backfilled Claude Code and Codex completely and left OpenCode at 4 of 19
177
+ sessions. The symptom was a lopsided event mix: 19 `user.prompted` but only 4 `session.started`,
178
+ because the part cursor and the session-created cursor fell back to that floor differently. The
179
+ adapter now takes the same setting.
180
+
181
+ Together these two are why a full import of one machine went from 63 sessions to 196.
182
+
183
+ ### Added
184
+ - `tracking.max_age_days` (default 7, max 3650) — see above.
185
+
13
186
  ## [0.2.0] — 2026-08-31
14
187
 
15
188
  ### Added
@@ -180,5 +353,8 @@ As released. Several of these have since been fixed — see `## Unreleased` abov
180
353
  - The service installer supports launchd and systemd only. `agentstrack start --foreground` works
181
354
  anywhere Node 20+ does.
182
355
 
183
- [Unreleased]: https://github.com/agentstrack/collector/compare/v0.1.0...HEAD
356
+ [Unreleased]: https://github.com/agentstrack/collector/compare/v0.3.0...HEAD
357
+ [0.3.0]: https://github.com/agentstrack/collector/compare/v0.2.1...v0.3.0
358
+ [0.2.1]: https://github.com/agentstrack/collector/compare/v0.2.0...v0.2.1
359
+ [0.2.0]: https://github.com/agentstrack/collector/compare/v0.1.0...v0.2.0
184
360
  [0.1.0]: https://github.com/agentstrack/collector/releases/tag/v0.1.0
package/README.md CHANGED
@@ -5,7 +5,7 @@
5
5
  [![npm version](https://img.shields.io/npm/v/@agentstrack/collector.svg)](https://www.npmjs.com/package/@agentstrack/collector)
6
6
  [![CI](https://github.com/agentstrack/collector/actions/workflows/ci.yml/badge.svg)](https://github.com/agentstrack/collector/actions/workflows/ci.yml)
7
7
  [![License: Apache 2.0](https://img.shields.io/badge/license-Apache--2.0-blue.svg)](./LICENSE)
8
- [![Node](https://img.shields.io/badge/node-%3E%3D24-brightgreen.svg)](https://nodejs.org)
8
+ [![Node](https://img.shields.io/badge/node-%3E%3D22-brightgreen.svg)](https://nodejs.org)
9
9
 
10
10
  `@agentstrack/collector` turns the records Claude Code, Codex and OpenCode already keep on your
11
11
  machine into a normalized event stream:
@@ -30,7 +30,7 @@ before anything is: [Verify it yourself](#verify-it-yourself).
30
30
  ## Quick start
31
31
 
32
32
  ```bash
33
- npm install -g @agentstrack/collector # requires Node >= 24
33
+ npm install -g @agentstrack/collector # requires Node >= 22
34
34
 
35
35
  agentstrack login at_live_xxxxxxxx_xxxxxxxx # key from Settings → API keys
36
36
  agentstrack start # installs a login service and starts collecting
@@ -86,8 +86,9 @@ Nothing showing up? Run `agentstrack doctor`.
86
86
  | File paths, **relative to the project root by default** | Send absolute paths — which leak your username and your clients' names — unless you opt in |
87
87
  | Lines added/removed per edit, computed locally from the tool input | Send the lines themselves |
88
88
  | Git branch, commit SHA, additions/deletions/files changed | Send your git remote URL (only a SHA-256 of it) or commit messages and diffs |
89
- | Locally generated session titles (`analytics` mode and above) | Send the prompt those titles were derived from |
90
- | Your hostname, OS, arch and each detected agent's version — **once, at registration** | Store the raw hostname server-side (it is kept only as a SHA-256) |
89
+ | A session title — the first line of the prompt, capped at 120 chars and secret-redacted (`analytics` mode and above) | Send the rest of the prompt those titles were taken from |
90
+ | Your hostname, OS and release, arch, Node version, a coarse machine kind (`workstation` / `server` / `container` / `ci`) and each detected agent's version — at registration and with each health report | Show your hostname or IP address in the product see [Machine info](#machine-info) |
91
+ | Which skill, sub-agent type or workflow a Claude Code session invoked, and which events came from a sub-agent | Copy the `Agent` call's prompt or a workflow script's body out of the call (a sub-agent transcript's own opening prompt follows `privacy.prompts` like any other prompt) |
91
92
  | | Install hooks or modify `~/.claude/settings.json` / `~/.codex/hooks.json` |
92
93
  | | Send `organization_id` or `user_id` — they are not in the wire format at all |
93
94
  | | Watch your keyboard, your screen, or any process on your machine |
@@ -198,8 +199,20 @@ collector keeps parsing and keeps spooling; when the network returns it drains o
198
199
  over 1 KB are gzipped.
199
200
 
200
201
  **Restart is safe.** File read offsets live in the same SQLite database as the queue, keyed by
201
- `(path, inode)`. A restart resumes mid-file. If a file is replaced (new inode) or truncated (offset
202
- past the end), it is re-read from the start rather than silently skipped.
202
+ `(path, inode)`, and a chunk's offset is committed in the **same transaction** as the events parsed
203
+ from it a crash or a full disk between "read" and "queued" re-reads those lines rather than losing
204
+ them. A restart resumes mid-file. If a file is replaced (new inode) or truncated (offset past the
205
+ end), it is re-read from the start rather than silently skipped. Re-reading never double-counts:
206
+ every event's `event_id` is derived from the file, the line's byte offset and the line's content, so
207
+ the server's dedupe absorbs a replay. A Claude Code `model.response` is keyed on its `message.id`
208
+ instead, since one response spans several lines; an idle `session.ended` on the agent, session and
209
+ last-activity time. Only events with no source line (`git.commit`, database-backed agents) get a
210
+ random id.
211
+
212
+ **Big files are streamed, not slurped.** Transcripts are read in 4 MB chunks with the partial line
213
+ carried across the boundary, at most 64 MB per file per scan so a first import keeps yielding to the
214
+ upload loop. A single line over 8 MB is skipped to the next newline and counted in the log — never
215
+ buffered, never logged. One unreadable file is logged and skipped; it cannot stall the other files.
203
216
 
204
217
  **A line the agent is still writing is never consumed.** The tailer advances its checkpoint only as
205
218
  far as the **last complete newline**; a partial trailing line is left unread and picked up whole on
@@ -209,9 +222,18 @@ so both halves would fail to parse and that event would be lost. Byte offsets ar
209
222
  raw buffer, not from decoded text, so a multi-byte character cannot desynchronise the position
210
223
  either.
211
224
 
212
- **Backpressure is handled.** A `413` halves the batch size and the collector recovers it on the next
213
- success. A `5xx`, a timeout, a `408` or a `429` is retried with jittered exponential backoff (1s
214
- base, capped at 5 minutes). A `4xx` that is none of those means the server will never accept the
225
+ **Backpressure is handled, once per wave.** Batches go out `upload.concurrency` at a time, and the
226
+ failure policy runs on the wave's collected outcomes rather than inside each request so four
227
+ failing siblings cost one backoff step, not four, and a sibling's success cannot undo a `413`
228
+ shrink. A `413` halves the batch size once (never above the server's `max_batch_events`) and it
229
+ creeps back up on success. A `5xx`, a timeout, a `408` or a `429` sets the next upload time with
230
+ jittered exponential backoff (1s base, capped at 5 minutes, or the server's `Retry-After` if longer)
231
+ — the daemon never sleeps on it, so tailing continues meanwhile. A daemon tick sends at most five
232
+ waves before scanning again. Three responses **pause** uploads instead: a `401`/`403` (the key, not
233
+ the events, is the problem), a `200` whose `quota.exceeded` says the org is over its monthly cap,
234
+ and a `200` that rejects every event as malformed (the collector is probably older than the server).
235
+ Nothing is acked or dropped while paused; `agentstrack status` shows the reason, and the pause lifts
236
+ by itself once a wave succeeds. A `4xx` that is none of those means the server will never accept the
215
237
  batch: **the attempt counter is incremented for the events in that batch only, and one of them is
216
238
  deleted once it reaches `upload.max_retries` (default 8)**. It is not parked and it does not come
217
239
  back — a poison event must not be able to block the queue forever.
@@ -291,8 +313,10 @@ A match is replaced in place, and most rules substitute `[REDACTED:rule_name]`.
291
313
  `bearer_header` → `Bearer [REDACTED]` and `basic_auth_url` → `scheme://[REDACTED]@host` keep the
292
314
  surrounding syntax so the shape of the command survives, `env_assignment` → `NAME=[REDACTED]` keeps
293
315
  the variable name, and `generic_hex_secret` substitutes the shorter `[REDACTED:hex]`. Your
294
- organization can add patterns server-side; a malformed org pattern is skipped rather than breaking
295
- the collector.
316
+ organization can add patterns server-side; an org pattern that is malformed, longer than 256
317
+ characters, or using a backreference is skipped, the common catastrophic nested-quantifier shapes
318
+ (`(a+)+`, `(a|aa)+`, `((a+)b)+`) are rejected — a heuristic, not a proof — and org patterns are
319
+ matched against at most the first 64 KB of any value.
296
320
 
297
321
  Redaction is defence in depth, not the primary control. The primary control is that in `metadata`
298
322
  and `analytics` modes the content is **deleted locally** and never enters the pipeline at all.
@@ -311,6 +335,22 @@ The project root itself (`repo.project_path`) is **dropped entirely** in `never`
311
335
  modes — it is only transmitted if you opt into `file_paths: absolute`. Repositories are correlated
312
336
  by `remote_hash`, a SHA-256 of the normalized remote URL, not by path.
313
337
 
338
+ ### Machine info
339
+
340
+ `login` (registration) and the daemon's health report, once a minute, send: `hostname`, `os`
341
+ (`darwin` / `linux` / `win32`), `os_release` (`os.release()`), `arch`, and
342
+ `machine_kind` — `ci` when `CI`, `GITHUB_ACTIONS` or `GITLAB_CI` is set; `container` when
343
+ `/.dockerenv` exists or `/proc/1/cgroup` mentions docker, containerd or kubepods; `workstation` on
344
+ macOS and Windows, or Linux with `DISPLAY` / `WAYLAND_DISPLAY` / a graphical `XDG_SESSION_TYPE`;
345
+ `server` for headless Linux; `unknown` otherwise. It is derived from those probes only.
346
+
347
+ The server keeps the hostname (raw, plus a SHA-256 that keys registration) and the IP address it
348
+ saw the register, health and upload requests come from, **for operations and abuse prevention
349
+ only** — telling one machine's collector from another, and shutting off a key that is being abused.
350
+ Neither is returned by any user-facing endpoint or shown anywhere in the product; the dashboard
351
+ identifies a device by its label and machine kind. Everything else in the list (OS, arch, kind,
352
+ versions) is what the device page shows.
353
+
314
354
  ### Excluding a project entirely
315
355
 
316
356
  ```yaml
@@ -345,14 +385,18 @@ events at all** — not even counts. Edit the YAML and restart the collector.
345
385
  agentstrack login <api-key> [--api-url <url>] [--label <name>]
346
386
  ```
347
387
 
348
- The key is a **positional argument** there is no interactive prompt and no environment variable.
349
- `--api-url` points at a self-hosted instance; `--label` names this machine in the dashboard.
388
+ The key argument is **optional**. Passing it on the command line leaves it in your shell history and
389
+ in `ps`, so `login` also reads `AGENTSTRACK_API_KEY`, or prompts on the terminal (echo off), or takes
390
+ the key on stdin — `agentstrack login < key.txt`. `--api-url` points at a self-hosted instance and
391
+ **must be `https`** (plain `http` is accepted only for `localhost`); `login` prints the URL it is
392
+ about to use. `--label` names this machine in the dashboard.
350
393
  Registration is idempotent on (user, hostname hash), so re-running `login` on the same machine reuses
351
394
  the existing collector instead of fragmenting its history. The config file is written mode `600`, in
352
395
  a directory created mode `700`.
353
396
 
354
- The registration payload is `hostname`, `label`, `os`, `arch`, the collector's own version, and one
355
- entry per configured agent: `{ agent, version }`. The **agent version is the real one**, read out of
397
+ The registration payload is `hostname`, `label`, `os`, `os_release`, `arch`, `machine_kind`,
398
+ (see [Machine info](#machine-info)), the collector's own version, and one entry per
399
+ configured agent: `{ agent, version }`. The **agent version is the real one**, read out of
356
400
  a transcript the agent already wrote (`2.1.247`, say, from Claude Code's `version` field). Where an
357
401
  adapter cannot cheaply establish a version at detection time the field is simply **omitted** rather
358
402
  than filled with a placeholder, so a missing version in the dashboard means "not reported", never
@@ -372,8 +416,11 @@ If your local privacy mode is stricter than the org's, login says so and keeps y
372
416
 
373
417
  `agentstrack start` writes a **launchd** agent on macOS (`~/Library/LaunchAgents/ai.agentstrack.collector.plist`)
374
418
  or a **systemd user unit** on Linux (`~/.config/systemd/user/agentstrack.service`), loads it, and
375
- returns. Neither needs root. `-f` / `--foreground` runs in the terminal instead best for a first
376
- run, and the only mode where `status` reports `Running: yes`.
419
+ returns. Neither needs root. The unit restarts the collector only on a **crash**, not after a clean
420
+ exit — after `logout` the collector exits cleanly and the supervisor leaves it stopped instead of
421
+ respawning it every few seconds (launchd `KeepAlive`/`SuccessfulExit`, systemd `Restart=on-failure`
422
+ with a 5-in-5-minutes start limit). `-f` / `--foreground` runs in the terminal instead — best for a
423
+ first run, and the only mode where `status` reports `Running: yes`.
377
424
 
378
425
  `agentstrack stop` removes the service unit *and* signals a foreground collector. There is no
379
426
  "stop but keep the unit"; use `agentstrack service install` to put it back.
@@ -389,9 +436,9 @@ Configuration
389
436
 
390
437
  Agents
391
438
  ✓ claude_code transcripts found
392
- 225 file(s) modified in the last 7 days
439
+ 225 file(s) modified in the last 7 days # window is tracking.max_age_days (default 7)
393
440
  ✓ codex transcripts found
394
- 3 file(s) modified in the last 7 days
441
+ 3 file(s) modified in the last 7 days # window is tracking.max_age_days (default 7)
395
442
 
396
443
  Connectivity
397
444
  ✗ API reachable at https://api.agentstrack.ai
@@ -569,7 +616,7 @@ database filename or an absolute path — the same override OpenCode itself hono
569
616
 
570
617
  | Agent | Status | Reads |
571
618
  |---|---|---|
572
- | **Claude Code** | ✅ Stable | `~/.claude/projects/<slug>/<session-uuid>.jsonl` |
619
+ | **Claude Code** | ✅ Stable | `~/.claude/projects/<slug>/<session-uuid>.jsonl`, plus `<session-uuid>/subagents/**/agent-<id>.jsonl` |
573
620
  | **Codex** | ✅ Stable | `~/.codex/sessions/YYYY/MM/DD/rollout-<ts>-<uuid>.jsonl` |
574
621
  | **OpenCode** | ✅ Stable | `~/.local/share/opencode/opencode.db` — SQLite, opened **read-only** |
575
622
  | Gemini CLI · Cursor · Cline · Copilot CLI | 🗓 Planned | ids reserved in the schema, no adapter yet |
@@ -593,6 +640,7 @@ identical things:
593
640
  | Commits | ✅ (from `git log`, not the transcript) | ✅ (same) | ✅ (same) |
594
641
  | Plan / subscription type | ➖ | ✅ `plan_type` | ➖ |
595
642
  | Account attribution | ✅ from `~/.claude.json` | ➖ no account file | ✅ from `account.json`, per provider |
643
+ | Skills / sub-agents / workflows | ✅ `Skill`, `Agent`, `Workflow` tool calls named; sub-agent transcripts stamped `sidechain` | ➖ Codex's `spawn_agent` collaboration tools are defined in its prompt but no rollout on hand shows one invoked, so nothing is parsed yet | ➖ `parent_session_id` only |
596
644
 
597
645
  Every adapter is read-only. Adapter formats drift between agent releases: an unparseable line is
598
646
  skipped, never fatal to the file.
@@ -678,8 +726,10 @@ gzipped (`content-encoding: gzip`).
678
726
 
679
727
  | Symptom | Cause | Fix |
680
728
  |---|---|---|
681
- | `401` in the log | Key revoked or wrong | `agentstrack login <new-key>` |
682
- | `403` | Key lacks ingest permission | Issue a new key |
729
+ | `Uploads paused (auth)` `401` | Key revoked or wrong | `agentstrack login <new-key>`; nothing was dropped |
730
+ | `Uploads paused (auth)` — `403` | Key lacks ingest permission | Issue a new key; nothing was dropped |
731
+ | `Uploads paused (quota)` | Org over its monthly event cap | Events stay spooled and resume when the cap resets or the plan changes |
732
+ | `Uploads paused (schema)` | Server rejects every event — collector older than the API | Upgrade the collector; nothing was dropped |
683
733
  | `fetch failed`, `ETIMEDOUT` | Network, VPN or proxy | Set `HTTPS_PROXY`; events keep spooling meanwhile |
684
734
  | `Server rejected the batch as too large` | Batch above the server's limit | Automatic — batch size halves and recovers |
685
735
  | `Batch permanently rejected: … (dropped N)` | Non-retryable `4xx` | N events **in that batch** hit `max_retries` and were deleted. Nothing outside the batch is touched. Check the API version matches the collector's schema. |
@@ -720,7 +770,8 @@ grep -i "error\|failed\|rejected" ~/.agentstrack/collector.log | tail -20
720
770
  ```
721
771
 
722
772
  The log records counts, queue depths and `event_id`s — never payloads, prompts, code or keys. That is
723
- what makes it safe to attach to an issue. Please attach `agentstrack doctor --json` too.
773
+ what makes it safe to attach to an issue. It rotates once at 5 MB to `collector.log.1`; a scan pass
774
+ writes one `Queued N events across M files` line, not one per file. Please attach `agentstrack doctor --json` too.
724
775
 
725
776
  ### Complete reset
726
777
 
@@ -739,15 +790,14 @@ Honest list of things that are **not** in 0.1.0, so you do not go looking for th
739
790
  [ROADMAP.md](./ROADMAP.md) has the same list with the design constraints and what "help wanted"
740
791
  means for each.
741
792
 
742
- - **Backfill window control** (`sync --since 30d`). Today the daemon reads whatever was modified in the last 7 days, and that window is not configurable.
793
+ - **Backfill window control** (`sync --since 30d`). Today the window is `tracking.max_age_days` (default 7) for every run; there is no per-invocation override.
743
794
  - **`config get` / `config set` / `config edit`** — edit the YAML by hand for now.
744
795
  - **`--verbose` logging** and per-run agent selection (`start --agent codex`); use `tracking.agents`.
745
- - **Local time accounting.** Human-active / agent-active / idle windows are derived server-side from the event stream; `tracking.idle_timeout_seconds` is parsed by the collector but not used by it.
796
+ - **Local time accounting.** Human-active / agent-active / idle windows are derived server-side from the event stream; the collector uses `tracking.idle_timeout_seconds` only to decide when a quiet session has ended.
746
797
  - **Process metrics.** `tracking.process_metrics` is accepted and ignored.
747
- - **`session.ended`, `heartbeat`, `model.request` and `git.branch_changed`** are in the schema but no adapter emits them yet.
798
+ - **`heartbeat`, `model.request` and `git.branch_changed`** are in the schema but no adapter emits them yet. `session.ended` is not read from any Claude Code or Codex transcript either — the daemon emits it after `tracking.idle_timeout_seconds` of quiet (`reason: timeout`) or on shutdown (`reason: unknown`).
748
799
  - **Local task classification** (`task_category`) — the field exists in the schema; the collector only derives a title.
749
- - **Windows.** The service installer covers launchd and systemd only; `--foreground` works anywhere Node 24+ does.
750
- - **Content-derived `event_id`.** Ids are random per enqueue, so retrying a batch is safe but re-reading a truncated transcript would create duplicates.
800
+ - **Windows.** The service installer covers launchd and systemd only; `--foreground` works anywhere Node 22+ does.
751
801
  - **`MultiEdit`.** The Claude Code adapter derives file changes from `Edit`, `Write`, `NotebookEdit` and `Read`; a `MultiEdit` call is still recorded as `tool.started`/`tool.completed`, but produces no `file.changed` events and no line counts.
752
802
 
753
803
  ---
@@ -14,10 +14,22 @@ import { type TokenUsage } from '../schema.js';
14
14
  * "output_tokens_details":{"thinking_tokens":257}}},"timestamp":"…"}
15
15
  *
16
16
  * Tool calls appear as tool_use / tool_result blocks inside message.content.
17
+ * One API response is written as SEVERAL assistant lines — one per content
18
+ * block (thinking, text, tool_use) — each repeating the same message.id and
19
+ * the same usage. Usage is therefore billed once per message.id, not per line.
20
+ *
21
+ * Sub-agents (the Agent tool, and Workflow scripts) write their own transcript
22
+ * under <session-uuid>/subagents/[workflows/<wf>/]agent-<id>.jsonl, with the
23
+ * parent's sessionId on every line, isSidechain: true and an agentId. A sibling
24
+ * agent-<id>.meta.json carries {"agentType","description","toolUseId",…}. Every
25
+ * event from such a line is stamped sidechain/agent_id/agent_kind/agent_type
26
+ * so the server can attribute the sub-agent's own usage to it.
17
27
  */
18
28
  export declare const CLAUDE_DIR: string;
19
29
  export declare class ClaudeCodeAdapter implements AgentAdapter {
20
30
  readonly id = "claude_code";
31
+ /** Cross-line facts within one transcript: a message's lines are contiguous. */
32
+ private readonly fileState;
21
33
  detect(): Promise<DetectionResult>;
22
34
  /**
23
35
  * The account Claude Code is signed in as right now.
@@ -34,6 +46,12 @@ export declare class ClaudeCodeAdapter implements AgentAdapter {
34
46
  private normalizeUser;
35
47
  private normalizeAssistant;
36
48
  }
49
+ /**
50
+ * Maps Claude Code's usage block onto the normalized shape.
51
+ *
52
+ * thinking_tokens sits under output_tokens_details and is a SUBSET of
53
+ * output_tokens — it must not be added on top, or thinking gets billed twice.
54
+ */
37
55
  export declare function readUsage(raw: unknown): TokenUsage;
38
56
  /**
39
57
  * Line deltas for one edit, derived from the tool input.