Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
36 commits
Select commit Hold shift + click to select a range
f9ddbd6
telemetry: the package behind anonymous usage counting, per the wire …
santoshkumarradha Sep 17, 2026
8ee3758
telemetry: tests, doc, and contract fixes for the usage-count package
santoshkumarradha Sep 17, 2026
9c403c8
telemetry: fix review defects 1-8 with tests
santoshkumarradha Sep 17, 2026
7d2afb1
installer: record the install locally and print the telemetry notice
santoshkumarradha Sep 17, 2026
d0c1a28
installer: four review corrections to the telemetry commit
santoshkumarradha Sep 17, 2026
2bfe267
release-downloads: daily PostHog report of public GitHub release down…
santoshkumarradha Sep 17, 2026
18fdab2
release-downloads: tests cover the workflow, docs and pagination
santoshkumarradha Sep 17, 2026
8ae5647
release-downloads: four review corrections to the script
santoshkumarradha Sep 17, 2026
520e03b
release-downloads: test runs on Python 3.9 too
santoshkumarradha Sep 17, 2026
8116eb0
installer telemetry: the notice is read from docs/TELEMETRY.md
santoshkumarradha Sep 17, 2026
de6e36a
release-downloads: the download-counts paragraph lives in docs/TELEME…
santoshkumarradha Sep 17, 2026
402df2c
installer telemetry: the mode check asks GNU stat first
santoshkumarradha Sep 17, 2026
cd11bab
telemetry: refuse to send to the production relay from a test binary
santoshkumarradha Sep 17, 2026
b5b59dd
telemetry: testHome points the endpoint at a dead loopback
santoshkumarradha Sep 17, 2026
611f7dc
telemetry: the orphan-adoption test names the dead relay it now gets
santoshkumarradha Sep 17, 2026
c5fb2c0
telemetry: a test pins that no test binary reaches the production relay
santoshkumarradha Sep 17, 2026
8b8c3ee
telemetry: three tests pin the test-binary relay refusal
santoshkumarradha Sep 17, 2026
8857a46
telemetry: allocation-free session counters
santoshkumarradha Sep 17, 2026
b77ac09
telemetry: gofmt the counters test
santoshkumarradha Sep 17, 2026
81b6d09
config: telemetry setting row (CODEAF_TELEMETRY)
santoshkumarradha Sep 17, 2026
da2c807
cmd/codeaf: the telemetry command, and the setting it writes
santoshkumarradha Sep 17, 2026
8635404
cmd/codeaf: session lifecycle events at execute (work in progress)
agentfield-bot Sep 17, 2026
55c49e2
provider: count answered model calls and their spend at the log door
santoshkumarradha Sep 17, 2026
8eb935b
session: count sealed turns and executed tool calls
santoshkumarradha Sep 17, 2026
f7460ad
cmd/codeaf: the session-ended event carries the run's counters
santoshkumarradha Sep 17, 2026
15c8561
cmd/codeaf: the counters test says the tally is process-wide
agentfield-bot Sep 17, 2026
4cfb273
guard: a fault hook Recover calls, for a reporter above this package
santoshkumarradha Sep 17, 2026
857a3ff
cmd/codeaf: the fault event at both crash sites, off the current session
santoshkumarradha Sep 17, 2026
e327fa2
cmd/codeaf: a test that the crash site spools a fault and not the pan…
santoshkumarradha Sep 17, 2026
6750145
docs: finish the anonymous usage counts contract and add the 1095 cha…
santoshkumarradha Sep 17, 2026
714db92
guard: the fault hook sits behind an atomic pointer
santoshkumarradha Sep 17, 2026
6eb6d42
cmd/codeaf: a task's interrupt word comes from the signal, not the ca…
santoshkumarradha Sep 17, 2026
6430d50
tui3, installer: the telemetry row has a tab, and a custom install wr…
santoshkumarradha Sep 17, 2026
b5cdb1c
cmd/codeaf: the counters test zeroes the process tally first
santoshkumarradha Sep 17, 2026
c51e08b
cmd/codeaf: the version test drives the telemetry lifecycle, not execute
santoshkumarradha Sep 17, 2026
c828d0c
cmd/codeaf: gofmt the lifecycle test
santoshkumarradha Sep 17, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 33 additions & 0 deletions .github/workflows/release-downloads.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
name: Release downloads

# Reports release download counts to PostHog once a day. The counts come from
# public GitHub release data — the API publishes a cumulative download_count
# per asset — so this needs no code in the binary. One event per binary asset
# (codeaf-<os>-<arch>[.exe]); sidecars like checksums.txt are not reported.
on:
schedule:
# 06:00 UTC every day.
- cron: '0 6 * * *'
workflow_dispatch:

permissions:
contents: read

concurrency:
group: release-downloads
cancel-in-progress: false

jobs:
report:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Report release download counts
env:
GH_TOKEN: ${{ github.token }}
GITHUB_REPOSITORY: ${{ github.repository }}
CODEAF_POSTHOG_PROJECT_KEY: ${{ secrets.CODEAF_POSTHOG_PROJECT_KEY }}
# Repository variable; unset falls back to https://us.i.posthog.com.
POSTHOG_HOST: ${{ vars.POSTHOG_HOST }}
run: |
python3 scripts/release_downloads.py
23 changes: 23 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,29 @@ pinning are on the [releases page](https://github.com/Agent-Field/codeaf/release
On first start it asks for a key: OpenRouter, DeepSeek, GLM, Kimi, MiniMax or
Qwen. Ollama needs none.

## Telemetry

```text
codeaf sends anonymous usage counts to AgentField.
Sent: version, OS, mode (chat or task), how many sessions, how many errors.
Never: anything about you or your work. No prompts, code, file names,
paths, repo names, keys, email, IP, or machine name.
See exactly what leaves: codeaf telemetry show
Turn off: CODEAF_TELEMETRY=off
```

The events are counts and buckets only — no prompts, code, or file names ever
leave the machine. `docs/TELEMETRY.md` lists every field that is sent and every
way to turn the counts off. The installer records how and when this copy was
installed (`telemetry/install.json` under the state root) so the counts can
bucket by channel; it sends nothing.

`codeaf telemetry show` prints exactly what is waiting to leave the machine;
`status`, `on` and `off` are the rest. Turn it off with `CODEAF_TELEMETRY=off`
(also `0` or `false`), `DO_NOT_TRACK=1`, `codeaf telemetry off`, `telemetry =
off` in a project's `.codeaf/config.json`, or an empty
`CODEAF_TELEMETRY_ENDPOINT`.

## One window for every project

An agent that lives in one folder means a terminal per repository, and a tmux
Expand Down
15 changes: 15 additions & 0 deletions cmd/codeaf/do.go
Original file line number Diff line number Diff line change
Expand Up @@ -589,6 +589,21 @@ func errandRun(request doRequest, seats config.Seats, started time.Time) (outcom
// nothing deletes the store either.
signalled, stopSignals := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM)
defer stopSignals()
// The person's own ending of the run is the one fact the usage counts
// keep about how it ended: the interrupt word, not a failure word.
// signalled cannot answer it — stopSignals cancels that context on the
// way out too, so its Err reads the same for a clean end and a ctrl-c.
// A channel only a signal fills can.
interrupts := make(chan os.Signal, 1)
signal.Notify(interrupts, os.Interrupt, syscall.SIGTERM)
defer signal.Stop(interrupts)
defer func() {
select {
case <-interrupts:
telemetryInterrupted = true
default:
}
}()
// A PERSON TYPED THIS, so its calls are made for somebody who is reading
// them (exec.go's [typedDoorContext]): the talk pin rides them and a
// refused pin is said to the one who is waiting.
Expand Down
37 changes: 36 additions & 1 deletion cmd/codeaf/fault.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package main

import (
"context"
"fmt"
"io"
"os"
Expand All @@ -9,6 +10,7 @@ import (
"time"

"github.com/Agent-Field/codeaf/internal/config"
"github.com/Agent-Field/codeaf/internal/telemetry"
)

// faultMessage is what the user reads when codeaf could not keep going. It is
Expand All @@ -17,7 +19,10 @@ import (
const faultMessage = "codeaf hit an internal fault and had to stop. Nothing is lost — the graph is durable, and restarting resumes where it left off. Details: %s\n"

// reportFault writes the stack where it is useful and the sentence where it is
// read, and answers with the process exit code.
// read, and answers with the process exit code. The anonymous fault count is
// the last thing it does, once the sentence is out: a flush may spend a second
// this process no longer has, and the person should not wait on it to read
// what happened.
func reportFault(stderr io.Writer, detail string, stack []byte) int {
// A fault has no settings to read — it is what is left when the launch did
// not get that far — but the profile is an environment pin and is still
Expand All @@ -29,9 +34,39 @@ func reportFault(stderr io.Writer, detail string, stack []byte) int {
path := chatLogPath(config.ProfileDir())
writeFaultLog(path, detail, stack)
fmt.Fprintf(stderr, faultMessage, displayPath(path))
telemetryFault(stack)
return 1
}

// telemetryFault spools the contract's fault event for a dying process and
// sends what the spool holds. It is given the stack and nothing else. The
// library hashes the fingerprint out of the stack's own function names, and
// `detail` — the panic's text — never crosses this line: a panic value is the
// person's words, a path on their machine or a fragment of the file they were
// working in, and the fault event has no property for any of it.
//
// SpoolSync and then Flush under a one-second context, the same budget
// telemetryEnd gives a run's last event, because this is the last thing the
// process does: there is no later flush to carry the line, and an append that
// has not landed when the process exits is an event that never happened. The
// gate is the opt-out ladder's own answer, so a person who turned the counts
// off gets the same crash report they always got and nothing on the wire.
func telemetryFault(stack []byte) {
if !telemetry.Enabled() {
return
}
session := currentTelemetrySession
event := telemetry.FaultEvent(telemetry.Fault{
Mode: string(session.mode),
Scope: telemetry.ScopeMain,
Stack: stack,
}, session.sessionID, time.Now())
_ = telemetry.SpoolSync(event)
ctx, cancel := context.WithTimeout(context.Background(), time.Second)
defer cancel()
_ = telemetry.Flush(ctx)
}

// chatLogPath is the ONE name of the file this binary parks the standard logger
// in: EVERY door that opens the v3 surface does it for the surface's whole
// lifetime so a log line cannot tear through the frame (chatv3_surface.go's
Expand Down
51 changes: 40 additions & 11 deletions cmd/codeaf/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ import (
lanes "github.com/Agent-Field/codeaf/internal/lane"
"github.com/Agent-Field/codeaf/internal/plan"
"github.com/Agent-Field/codeaf/internal/router"
"github.com/Agent-Field/codeaf/internal/telemetry"
"github.com/Agent-Field/codeaf/internal/trace"
)

Expand Down Expand Up @@ -71,6 +72,21 @@ func execute() (code int) {
code = reportFault(os.Stderr, fmt.Sprint(recovered), debug.Stack())
}
}()
// The anonymous usage counts get their one configured answer before any
// command is dispatched, and their one session event at the exit every
// command shares. Both are here, in execute, because nothing else in this
// file is reached by all of chat, do, exec, run and `plan run` — and a
// counter that missed a door would miscount the runs it was built to count.
//
// The CONFIG read must never become a failure of its own: a machine with
// no profile yet, or a broken project file, is a machine `codeaf version`
// still owes an answer to. Any error reads as "on" — the default — and
// the run carries on.
telemetry.Configure(telemetryConfiguredOff())
telemetrySession := telemetryBegin()
defer func() {
telemetryEnd(telemetrySession, code)
}()
// The model-call log's file descriptor goes back at the one exit every
// command shares (internal/calllog). Nothing depends on it — every record is
// written and flushed as it happens — but a process that closes what it
Expand Down Expand Up @@ -225,6 +241,10 @@ func run() error {
return runRebuild(os.Args[2:])
case "why":
return runWhy(os.Args[2:])
case "telemetry":
// The person's door onto the anonymous-usage pipe: what it is doing,
// exactly what would leave, and the switch. It emits nothing itself.
return runTelemetry(os.Args[2:])
case "manual":
// Everything codeaf knows about itself, read straight (manual.go). It
// is the same corpus the chat's manual tool reads, printed as it is
Expand Down Expand Up @@ -301,11 +321,10 @@ const (
// and where taking it out of the group's lines had silently removed it. One
// source of truth, two places it is read.
var handWorkFooter = ` the three differ by how much thinking happens first: do plans and may split
the job, exec does not plan, run follows a plan somebody saved. None of them
takes --yolo: they run with nobody watching and nothing in them stops to ask.
What do and run can still refuse is a plan whose price crosses your limit,
and --yes-spend answers that in advance. All three end the same way, and
why is in --json's stop field:
the job, exec does not plan, run follows a plan somebody saved. None takes
--yolo, and What do and run can still refuse is a plan whose price crosses
your limit — --yes-spend answers that in advance. All three end the same
way, and why is in --json's stop field:
` + foldedExitLadder(2, helpWidth) + `
CODEAF_EXIT_CODES=legacy restores exec's old 2/3/4/5/6 for one release`

Expand Down Expand Up @@ -345,11 +364,13 @@ Look at what happened — read-only, no key, nothing spent
show today's self-spend receipts
codeaf why <task-id> [--db path]
what one piece of work did — its turns, tools, arguments, how it ended
codeaf telemetry
the anonymous usage counts: status, show, off, on
codeaf logs [--tail 40] [--follow] [--path] [--json] [--run id]
[--call id] [--tag t] [--model m] [--node n] [--body id]
every model call codeaf made — what was asked, which lane answered, what
came back. The filters are exact and combine; --json prints the rows as
they are on disk, --body one call's bodies. CODEAF_CALL_LOG=off is off
came back; --json prints the rows as on disk, --body one call's bodies.
The filters are exact and combine; CODEAF_CALL_LOG=off is off
codeaf models [--refresh]
the models this machine will use, and what each has been measured at
codeaf doctor [--db path]
Expand All @@ -374,8 +395,7 @@ Housekeeping — changes state on disk or on the network
codeaf devices
list the devices paired with this machine
codeaf devices revoke <name> [--all]
stop one device opening a conversation here; --all, every device of that
name
stop one device opening a conversation here, --all every device of it
codeaf notebook [--db path]
what it has learned, and what it has been corrected on
codeaf notebook retract|restore <seq> [--db path]
Expand All @@ -399,8 +419,8 @@ Plan work by hand — a plan you can read, edit and diff
[--out plan.json] [--model slug] [--plan-model slug]
codeaf plan run <plan.json> [--dir dir] [--parallel 8] [--out done.json]
[--yes-spend] [--model slug] [--plan-model slug]
a plan written to a file, then executed exactly as written. It is not
what most people want: nothing learnt mid-flight moves a frozen plan
a plan written to a file, then executed exactly as written. It is not what
most people want: nothing learnt mid-flight moves a frozen plan

Examples:
codeaf open the conversation you were having
Expand Down Expand Up @@ -502,6 +522,15 @@ than fighting your shell.
also record each call's whole request and response —
your prompts included. Off by default, and for one run
at a time.
CODEAF_TELEMETRY on (default). off turns the anonymous usage counts off;
` + "`codeaf telemetry`" + ` says what they are and what would
leave, DO_NOT_TRACK=1 does the same
CODEAF_TELEMETRY_ENDPOINT
where the usage counts go
(default https://agentfield.ai/api/oss/codeaf/telemetry);
set to empty to turn them off entirely
DO_NOT_TRACK=1 the ecosystem's own opt-out word, honoured as if it were
CODEAF_TELEMETRY=off

The user-facing knobs above — budgets, rhythm, the document reader, the vision
and media slots — are also the ` + "`/settings`" + ` sheet in the chat, which persists
Expand Down
Loading
Loading