Skip to content

Releases: agentstrack/collector

v0.5.0

Choose a tag to compare

@github-actions github-actions released this 29 Sep 10:30

v0.4.6

Choose a tag to compare

@github-actions github-actions released this 25 Sep 10:32

v0.4.5

Choose a tag to compare

@github-actions github-actions released this 23 Sep 23:50

v0.4.4

Choose a tag to compare

@github-actions github-actions released this 23 Sep 23:44

v0.4.3

Choose a tag to compare

@github-actions github-actions released this 18 Sep 11:00

v0.4.2

Choose a tag to compare

@github-actions github-actions released this 18 Sep 10:41

v0.4.1 — Redact before truncating, and report which secret pattern fired

Choose a tag to compare

@ddcodepl ddcodepl released this 08 Sep 12:39

This tag carries both 0.4.0 and 0.4.1; 0.4.0 has no separate commit in this repository and was never published to npm.

0.4.1

Fixed

  • A session title could ship a piece of a secret (security). derived_title is the first
    meaningful line of your prompt, cut to 120 characters. Until now that cut was made on the raw
    prompt and the secret scan ran afterwards, on the already-shortened title. If a key happened to
    straddle the 120-character boundary, the cut split it in two, the leftover head no longer looked
    like a key to any pattern, and it was uploaded as ordinary title text. A prompt of a hundred
    characters followed by an sk-ant-… key produced a title ending in sk-ant-api03-AAAAA… while
    the event dutifully reported secrets_redacted: [{ anthropic_key, 1 }] — the tally was right and
    the title still carried a fragment.

    The prompt is now redacted before the title is taken from it, in all three adapters (Claude
    Code, Codex, OpenCode), so a truncation can only ever cut through a [REDACTED:…] marker. The
    privacy pipeline still redacts derived_title afterwards; that pass is now a no-op and stays in
    place as defence in depth.

    The same cut-then-scan mistake applied to a Codex error event's message, truncated to 1000
    characters; it is redacted before truncation now too.

    What to do: only a partial value could escape, never a whole one, and only when a secret sat
    across the cut. But a fragment is enough to identify which key was pasted, and the rest of it may
    be guessable from context. If you have used analytics or full mode, look through your existing
    session titles for key-shaped fragments, and rotate anything you find. Titles produced from 0.4.1
    on are safe.

    Fragments already uploaded stay in the product until you delete those sessions — upgrading the
    collector does not rewrite history.

Changed

  • Documented the ordering (redact, then truncate) in the README and docs/EVENT_SCHEMA.md, and
    corrected two stale lines there that still said privacy.prompts: never leaves derived_title
    in place under mode: full. It has not since 0.4.0 — never drops it in every mode.

0.4.0

Added

  • Secret exposure is reported as metadata. When local redaction fires, the event now carries
    secrets_redacted: [{ kind, count }] — which pattern matched and how many times, sorted by kind
    and omitted entirely when nothing fired. The matched value never travels: not the text, not a
    prefix of it, not a hash, not the surrounding context. The tally is computed before the mode
    strip, so it survives metadata mode — the mode where a team most wants to know a credential was
    typed into an agent and least wants the credential itself. An org-supplied rule reports as the
    single generic kind org_rule, because a rule name can itself describe the shape of that
    organization's secrets.

v0.3.0 — Skills, sub-agents, workflows and machine info

Choose a tag to compare

@ddcodepl ddcodepl released this 08 Sep 12:39

Added

  • 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 buffe...
Read more

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

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.

v0.1.0

Choose a tag to compare

@github-actions github-actions released this 27 Aug 17:47