You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
v0.3.0 — Skills, sub-agents, workflows and machine info
Skills, sub-agents and workflows are named. A Claude Code tool_use named Skill carries skill; Agent carries subagent_type and description; Workflow carries workflow_name
(the name from the script's meta header, when it is that simple). The Agent call's prompt and the
script body are never copied out of the call; a sub-agent transcript's own opening prompt is a
prompt like any other and follows privacy.prompts. The extras ride on tool.started and on the matching tool.completed / tool.failed.
Sub-agent transcripts are attributed. Claude Code writes each sub-agent to <session-uuid>/subagents/[workflows/<wf>/]agent-<id>.jsonl with the parent's session id; the
tailer already walked them, but nothing said which lines were the sub-agent's. Every event from
such a file (or any line with isSidechain: true) now carries sidechain: true, agent_id, agent_kind (subagent | workflow) and, from the sibling agent-<id>.meta.json, agent_type
— so the sub-agent's own model.response usage can be attributed to it server-side.
user.prompted.ultracode, true when the prompt contains the whole word ultracode
(case-insensitive). Computed locally before redaction, so it survives metadata mode; the prompt
does not travel to be inspected.
Machine info on register and health. Alongside hostname, os and arch the collector now
sends os_release and machine_kind — ci (a CI env var), container
(/.dockerenv or a docker/containerd/kubepods cgroup), workstation (macOS, Windows, or Linux
with a display), server (headless Linux), else unknown. Health repeats it, so a device that
changes shape is updated. See the README privacy section for what the server does with it.
Changed
description (the Agent tool's one-line label) is treated like a title: secret-redacted, and
dropped in metadata mode.
Security
api_url must be https. The bearer API key rides on every request, so http is now rejected
for any host except localhost/127.0.0.1/[::1]. login prints the URL it is about to use.
The API key can stay off the command line.agentstrack login now takes the key as an optional
argument and otherwise reads AGENTSTRACK_API_KEY, an echo-off terminal prompt, or stdin
(agentstrack login < key.txt), keeping it out of shell history and ps.
config.yaml is written atomically at mode 600 — a temp file created 0600 then renamed over the
target, so a crash can no longer leave a truncated config or a brief world-readable window.
Org redaction rules are bounded. A server-supplied pattern that is malformed, over 256
characters, or using a backreference is skipped (as a malformed rule already was), the common
catastrophic nested-quantifier shapes ((a+)+, (a|aa)+, ((a+)b)+) are rejected, and org rules
match only the first 64 KB of a value — a pathological pattern is much less likely to hang the
single-threaded daemon.
Fixed
Claude Code tokens and cost were inflated ~1.8x. Claude Code writes one assistant line per
content block of a single response, each repeating the same message.id and usage; every line
became a model.response. Usage is now emitted once per message.id.
Claude Code tool outcomes were all named unknown.tool_result blocks carry only a tool_use_id; the name is now resolved from the tool_use that started the call.
Claude Code prompts pasted with an image (or as text blocks) were never counted, and slash
command echoes (<command-name>, <command-message>, <local-command-stdout>, <task-notification>) were counted as human prompts. Both fixed.
Codex sessions went dark after a collector restart. The session id lived only in memory from session_meta; it is now seeded from the rollout file name, which carries the same uuid.
Codex cached tokens were billed twice.cached_input_tokens is a subset of input_tokens
in Codex; the collector now subtracts it so the two are exclusive, as the schema documents.
Codex shell commands landed as shell with duration_ms: 0 and never failed. exec_command_end carries an argv list, a {secs,nanos} duration and a numeric exit code; function_call_output.output is a string. Both are read as they really are, and tool.failed
is emitted on a non-zero exit.
OpenCode session.ended was rejected by the server on every emit (missing external_session_id, reason outside the enum). It now sends reason: normal with the
archived/compacted distinction in end_kind.
OpenCode resumed sessions never got a session.started. Sessions are fetched by time_updated but starts were gated on a time_created cursor; a per-session marker in the
spool's meta store replaces it.
logout now tears the service down first. It previously left the launchd/systemd unit
installed, so the supervisor kept restarting an unauthenticated collector. Both units now restart
only on a crash (launchd KeepAlive/SuccessfulExit, systemd Restart=on-failure with a
5-in-5-minutes start limit), and the unauthenticated foreground path exits cleanly so the
supervisor idles.
Service units handle paths with spaces and non-ASCII characters. The CLI path is resolved with fileURLToPath instead of a percent-encoded URL.pathname, plist strings are XML-escaped, and
systemd ExecStart arguments are quoted. AGENTSTRACK_HOME is written into the unit when set.
A second foreground start refuses to run when one is already collecting (checked via the pid
file, created with wx), preventing two collectors from racing on one spool.
stop verifies the pid still belongs to a collector (via ps) before signalling it, so a
recycled pid in a stale pid file is not killed.
doctor reports the real scan window. It now prints modified in the last N day(s) using tracking.max_age_days instead of a hard-coded "7 days".
git.commit polling stops re-diffing history every tick. Each repo's git log --since now
starts from its last poll, SHAs are listed before any diffstat so git show --numstat runs only for
commits not yet emitted, and the emitted-SHA guard is pruned by age instead of cleared wholesale, so
a commit inside the lookback window is never re-emitted.
Changed
detect() reads only the first 16 KB of the newest transcript (by mtime) for the agent version,
cached on mtime, instead of the whole file every 5 s. OpenCode keeps one read-only database
handle with prepared statements rather than opening and closing one per query, and caches the
version for 60 s.
The contract test now runs every adapter fixture through the server's own payload schemas when
the server checkout is present, and fails (rather than skips) when AGENTSTRACK_SERVER_REPO is
set but missing.
Node floor is >=22 (was >=24), matching .nvmrc and the runtime the code actually needs;
CI now tests Node 22 and 24.
status shows an upload-paused reason when one is present.
Fixed — daemon, queue and transport
The upload failure policy ran once per concurrent batch, not once per wave. With upload.concurrency: 4 a dead API escalated the backoff counter by four per wave (5-minute waits
after two waves), slept inside the wave so tailing froze for the duration, and a sibling's success
reset the counter or undid a 413 halving. sendBatch now returns a pure outcome and flush()
applies the policy once on the wave: halve once, one backoff step, strikes only for poison
batches, counter reset only when the whole wave succeeded. Backoff sets a next-upload time instead
of sleeping, honours Retry-After / Retry-After-ingest as the minimum, and a daemon tick sends
at most five waves before scanning again. Covered by src/queue/flush.test.ts.
401/403 no longer count strikes against telemetry. They pause uploads (status shows the
reason), as does an over-quota 200 — previously acked and dropped locally with the quota block
ignored — and a 200 that rejects every event as malformed, which now logs a version-mismatch
error instead of deleting the batch.
Checkpoints were written before the events were spooled. A throw between the two lost those
lines for good. The tailer now streams in 4 MB chunks and commits each chunk's checkpoint in the
same transaction as its events; a line over 8 MB is skipped to the next newline and counted, at
most 64 MB per file is read per scan, short reads are looped, and one unreadable file no longer
aborts the scan for every file after it.
Re-reading a transcript double-counted on the server.event_id was a fresh randomUUID per
spool write; it is now derived from the adapter, file, byte offset and line content (a UUID v8
shape), so a rotated inode, a purged spool or a second collector on the same files dedupes.
VERSION was hard-coded 0.1.0. It is read from package.json; the CLI, register and health
report the published version, and every request carries User-Agent: agentstrack-collector/<v>.
A Claude Code response spanning a restart was billed twice.model.response is now keyed on message.id (the daemon's deterministic id, seeded by the adapter), not on the line, so the
server's dedupe absorbs the second line's copy after a restart.
One rejected event paused every upload as a "schema mismatch". A single-event batch the
server rejects is now struck like any other poison event; the pause is reserved for a whole
batch rejected without a quota reason.
A failed commit left the checkpoint cache ahead of disk.setCheckpoint() updated the
in-memory Map before COMMIT; the cache is now reloaded from the table when a transaction throws.
OpenCode cursors and started-markers were written before the events they covered. They are
now buffered and committed in the same transaction as the enqueue, so a full disk cannot mark a
session started that was never spooled.
systemd WorkingDirectory= was quoted. Path-typed settings are not unquoted by systemd; the
unit now writes the bare path (ExecStart= keeps its quoting).
Claude Code and Codex sessions never ended. The daemon emits session.ended
(reason: timeout) for a session quiet longer than tracking.idle_timeout_seconds, stamped at
the moment the timeout elapsed, and reason: unknown for anything still open on shutdown.
The batch response is validated with zod at the trust boundary; an unreadable body is retried,
never acked. Rejected allSettled outcomes are logged instead of swallowed. Local batch_size is
clamped to the server's max_batch_events.
Changed — daemon, queue and transport
The spool is VACUUMed at open when the freelist is both over 2048 pages and more than half the
file (a 206 MB spool holding 78 events was observed), the WAL is capped at 8 MB
(journal_size_limit), statements are prepared once, checkpoints are cached in memory so an
unchanged file costs no SQL, and WAL/SHM are chmod 600 after they exist.
describeRepo is cached per scan pass; flush(), reportHealth() and depth() are inside the
loop's try/catch; the log rotates once at 5 MB to collector.log.1 and a scan pass writes one Queued N events across M files line instead of one per file.
Org redaction rules are compiled once per server-config refresh and handed to the privacy
pipeline pre-compiled, instead of being recompiled for every event.