Repository navigation
v0.2.1 — Parallel uploads, the OpenCode adapter, and a full history import
Published from a squashed history: this tag carries both 0.2.0 and 0.2.1, which have no separate commit in this repository. Both sets of notes are below.
0.2.1
Fixed
-
A first import silently stopped at 7 days.
listTranscriptsdefaulted tomaxAgeDays = 7and
both call sites took the default, so only transcripts touched in the last week were ever opened —
258 of 690 files on the machine this was found on. Everything older stayed on disk with nothing in
the output indicating it had been skipped. The window is nowtracking.max_age_days, still
defaulting to 7 so a running collector keeps its cheap steady-state scan and a fresh install does
not unexpectedly upload years of history. Set it to3650, restart, then set it back to import
what is already on disk. -
OpenCode ignored that window and kept its own. It reads a live SQLite database rather than
tailing files, so it has an independent history floor — also hardcoded to 7 days. Widening
max_age_daystherefore backfilled Claude Code and Codex completely and left OpenCode at 4 of 19
sessions. The symptom was a lopsided event mix: 19user.promptedbut only 4session.started,
because the part cursor and the session-created cursor fell back to that floor differently. The
adapter now takes the same setting.Together these two are why a full import of one machine went from 63 sessions to 196.
Added
tracking.max_age_days(default 7, max 3650) — see above.
0.2.0
Added
-
Parallel uploads.
upload.concurrency(default 4, max 8) sends that many batches at once.
Uploading is round-trip bound rather than bandwidth bound. Measured on an 80k-event backfill:
upload throughput ~330 -> ~627 events/s, wall clock 240s -> 175s. The end-to-end gain is smaller
than the upload gain because reading and parsing transcripts is single-threaded and becomes the
co-bottleneck once uploading stops being one.Arrival order is deliberately not preserved across in-flight batches and does not need to be: the
server derives a session's start frommin(recorded start, earliest stored event)and re-runs
reconstruction after every batch, so a later batch landing first is corrected once the rest arrive.
Onepeekcovers the whole wave — peeking per batch would hand identical rows to every request and
upload the same eventsconcurrencytimes. A failing batch no longer abandons its siblings; the
413 / poison-batch / backoff policy is unchanged.Set
upload.concurrency: 1in~/.agentstrack/config.yamlto restore serial uploads. -
OpenCode adapter. Reads
~/.local/share/opencode/opencode.db(honouring$XDG_DATA_HOME,
$OPENCODE_DATA_DIRand$OPENCODE_DB) and emitssession.started,session.ended, cumulative
usage.reported,user.prompted,tool.completed/tool.failed,command.executed,
file.changedandfile.read. OpenCode records the provider's real settled cost, so it is the
first agent whose sessions carrycost_basis: REPORTEDrather than an estimate.This is a live database the OpenCode app writes to, so it is opened
readonly+fileMustExist
withPRAGMA query_onlyand abusy_timeout, one short query at a time. The collector never
writes to, migrates or locks a user's data. -
AgentAdapter.poll(), an optional pull-based source for database-backed agents.normalize()
is line-oriented and a SQLite file has no lines; rather than pretend otherwise, the daemon drives
poll()on the same scan cycle, under the sametracking.agentsgating, the same
excluded_projectsfilter and the same privacy pipeline. Resumption uses the spool's meta store —
the tailer's(path, inode, offset)checkpoint means nothing to a database. -
Per-account attribution.
session.startedanduser.promptednow carry an optionalaccount
object: a stable opaquekey(Claude Code'soauthAccount.accountUuid; OpenCode's
<serviceID>:<accountId>), pluslabel,organdprovider. Sessions from a personal login and
a work login no longer merge into one identity and one bill.labelandorgare PII and are stripped by the privacy pipeline inmetadatamode;keyand
provideralways travel, so sessions still split correctly there. Credentials are never read:
OpenCode'saccount.jsonholds a live API key beside the account id and onlyid/serviceIDare
touched, whileauth.jsonis never opened.Events that predate the collector's start carry no account. These files record only who is
signed in now, so a historical transcript cannot be attributed without guessing, and a guess that
looks like a fact is worse than a blank. -
SUPPORT.md,ROADMAP.md,GOVERNANCE.md,CODEOWNERS,.editorconfig,.gitattributes,
.nvmrc, Dependabot config and a CodeQL workflow.
Fixed
session.started.cwdis no longer sent as an absolute path regardless of
privacy.file_paths.cwdis the project root, and it leaked the username, the client name and
the directory tree even inmetadatamode — whererepo.project_pathwas already being dropped.
It now follows the same rule: sent only underfile_paths: absolute. Affected the Codex adapter
since 0.1.0.
Changed
tracking.agentsdefaults to[claude_code, codex, opencode].privacy.prompts: nevernow strips the locally derivedderived_titleinanalyticsmode, so
it is possible to keep token, tool and cost analytics while sending nothing derived from a prompt.
Previously the setting was consulted only infullmode, which madeneverand
local_summary_onlybehave identically everywhere else — a privacy control that silently did
nothing.nevernow applies in every mode,fullincluded.- Agent versions in the registration payload are real.
ClaudeCodeAdapter.detect()reads the
versionfield out of a transcript (e.g.2.1.247) instead of reporting the literal string
'detected'. An adapter that cannot cheaply establish a version reports none, rather than a
placeholder — which today is the case for Codex, whosedetect()does not open a rollout to read
session_meta.cli_version. ~/.agentstrack/is created mode700, andspool.dbwith its-wal/-shmsidecars is
chmoded600. Previously onlyconfig.yamlwas hardened, so on a shared host another user could
read spooled telemetry.- The schema contract test resolves the server repository from a sibling
../agentstrack.aicheckout
orAGENTSTRACK_SERVER_REPO, instead of an absolute path to one maintainer's machine. It still
skips cleanly when the server repo is absent.
Fixed
- A partial trailing line is no longer consumed. The tailer advances its checkpoint only to the
last complete newline and re-reads an unterminated line whole on the next pass. Previously a line
the agent was mid-way through writing was emitted broken and its remainder arrived orphaned, so
both halves failed to parse and the event was lost — which happened on every live session. - A failed batch can no longer delete unrelated queued events.
Spool.fail()now scopes its
retry-exhaustion delete to the event ids in the failing batch; it previously deleted table-wide on
attempts, so one batch exhausting its retries could destroy events it had never touched. upload.max_retries: 0no longer empties the spool. The value is clamped to a minimum of 1.
attempts >= 0matched every row, including never-attempted ones, so a config of0wiped the
whole queue on the first upload failure. The config still accepts0–20;0behaves as1.