@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.
- package/CHANGELOG.md +177 -1
- package/README.md +78 -28
- package/dist/adapters/claude.d.ts +18 -0
- package/dist/adapters/claude.js +153 -45
- package/dist/adapters/claude.js.map +1 -1
- package/dist/adapters/codex.d.ts +15 -1
- package/dist/adapters/codex.js +81 -33
- package/dist/adapters/codex.js.map +1 -1
- package/dist/adapters/opencode.d.ts +19 -7
- package/dist/adapters/opencode.js +88 -29
- package/dist/adapters/opencode.js.map +1 -1
- package/dist/adapters/types.d.ts +16 -0
- package/dist/adapters/types.js +61 -0
- package/dist/adapters/types.js.map +1 -1
- package/dist/cli.js +165 -34
- package/dist/cli.js.map +1 -1
- package/dist/commands/service.js +40 -10
- package/dist/commands/service.js.map +1 -1
- package/dist/config.d.ts +2 -1
- package/dist/config.js +31 -5
- package/dist/config.js.map +1 -1
- package/dist/daemon.d.ts +73 -21
- package/dist/daemon.js +358 -121
- package/dist/daemon.js.map +1 -1
- package/dist/git/commits.d.ts +7 -1
- package/dist/git/commits.js +36 -17
- package/dist/git/commits.js.map +1 -1
- package/dist/git/repo.d.ts +13 -4
- package/dist/git/repo.js +34 -20
- package/dist/git/repo.js.map +1 -1
- package/dist/machine.d.ts +27 -0
- package/dist/machine.js +46 -0
- package/dist/machine.js.map +1 -0
- package/dist/privacy/pipeline.d.ts +3 -0
- package/dist/privacy/pipeline.js +4 -2
- package/dist/privacy/pipeline.js.map +1 -1
- package/dist/privacy/redact.d.ts +10 -1
- package/dist/privacy/redact.js +29 -3
- package/dist/privacy/redact.js.map +1 -1
- package/dist/queue/event-id.d.ts +9 -0
- package/dist/queue/event-id.js +15 -0
- package/dist/queue/event-id.js.map +1 -0
- package/dist/queue/spool.d.ts +24 -5
- package/dist/queue/spool.js +89 -33
- package/dist/queue/spool.js.map +1 -1
- package/dist/queue/tailer.d.ts +27 -4
- package/dist/queue/tailer.js +89 -28
- package/dist/queue/tailer.js.map +1 -1
- package/dist/transport/client.d.ts +37 -13
- package/dist/transport/client.js +50 -3
- package/dist/transport/client.js.map +1 -1
- 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.
|
|
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
|
[](https://www.npmjs.com/package/@agentstrack/collector)
|
|
6
6
|
[](https://github.com/agentstrack/collector/actions/workflows/ci.yml)
|
|
7
7
|
[](./LICENSE)
|
|
8
|
-
[](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 >=
|
|
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
|
-
|
|
|
90
|
-
| Your hostname, OS, arch and each detected agent's version —
|
|
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)
|
|
202
|
-
|
|
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
|
|
213
|
-
|
|
214
|
-
|
|
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;
|
|
295
|
-
the
|
|
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
|
|
349
|
-
|
|
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`,
|
|
355
|
-
|
|
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.
|
|
376
|
-
|
|
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
|
-
| `
|
|
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.
|
|
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
|
|
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`
|
|
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
|
-
- **`
|
|
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
|
|
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.
|