Skip to content

v0.2.1 — Parallel uploads, the OpenCode adapter, and a full history import

Choose a tag to compare

@ddcodepl ddcodepl released this 08 Sep 12:39
· 14 commits to master since this release

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. listTranscripts defaulted to maxAgeDays = 7 and
    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 now tracking.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 to 3650, 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_days therefore backfilled Claude Code and Codex completely and left OpenCode at 4 of 19
    sessions. The symptom was a lopsided event mix: 19 user.prompted but only 4 session.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 from min(recorded start, earliest stored event) and re-runs
    reconstruction after every batch, so a later batch landing first is corrected once the rest arrive.
    One peek covers the whole wave — peeking per batch would hand identical rows to every request and
    upload the same events concurrency times. A failing batch no longer abandons its siblings; the
    413 / poison-batch / backoff policy is unchanged.

    Set upload.concurrency: 1 in ~/.agentstrack/config.yaml to restore serial uploads.

  • OpenCode adapter. Reads ~/.local/share/opencode/opencode.db (honouring $XDG_DATA_HOME,
    $OPENCODE_DATA_DIR and $OPENCODE_DB) and emits session.started, session.ended, cumulative
    usage.reported, user.prompted, tool.completed / tool.failed, command.executed,
    file.changed and file.read. OpenCode records the provider's real settled cost, so it is the
    first agent whose sessions carry cost_basis: REPORTED rather than an estimate.

    This is a live database the OpenCode app writes to, so it is opened readonly + fileMustExist
    with PRAGMA query_only and a busy_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 same tracking.agents gating, the same
    excluded_projects filter 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.started and user.prompted now carry an optional account
    object: a stable opaque key (Claude Code's oauthAccount.accountUuid; OpenCode's
    <serviceID>:<accountId>), plus label, org and provider. Sessions from a personal login and
    a work login no longer merge into one identity and one bill.

    label and org are PII and are stripped by the privacy pipeline in metadata mode; key and
    provider always travel, so sessions still split correctly there. Credentials are never read:
    OpenCode's account.json holds a live API key beside the account id and only id/serviceID are
    touched, while auth.json is 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.cwd is no longer sent as an absolute path regardless of
    privacy.file_paths. cwd is the project root, and it leaked the username, the client name and
    the directory tree even in metadata mode — where repo.project_path was already being dropped.
    It now follows the same rule: sent only under file_paths: absolute. Affected the Codex adapter
    since 0.1.0.

Changed

  • tracking.agents defaults to [claude_code, codex, opencode].
  • privacy.prompts: never now strips the locally derived derived_title in analytics mode, so
    it is possible to keep token, tool and cost analytics while sending nothing derived from a prompt.
    Previously the setting was consulted only in full mode, which made never and
    local_summary_only behave identically everywhere else — a privacy control that silently did
    nothing. never now applies in every mode, full included.
  • Agent versions in the registration payload are real. ClaudeCodeAdapter.detect() reads the
    version field 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, whose detect() does not open a rollout to read
    session_meta.cli_version.
  • ~/.agentstrack/ is created mode 700, and spool.db with its -wal / -shm sidecars is
    chmoded 600. Previously only config.yaml was hardened, so on a shared host another user could
    read spooled telemetry.
  • The schema contract test resolves the server repository from a sibling ../agentstrack.ai checkout
    or AGENTSTRACK_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: 0 no longer empties the spool. The value is clamped to a minimum of 1.
    attempts >= 0 matched every row, including never-attempted ones, so a config of 0 wiped the
    whole queue on the first upload failure. The config still accepts 0–20; 0 behaves as 1.