These pages document master, which is unreleased and in development. The Quick Start installs the latest stable release; anything newer than that tag is marked in the text.
Session Analytics — user cookbook
Session Analytics reads the transcripts your AI coding assistant leaves on disk (Claude Code, Aider, Pi), stores them in a local database, and opens a Studio where you can see what happened in your sessions, find what to fix in your harness, and measure whether the numbers can be trusted. Nothing leaves your machine unless you point the judge at a cloud model on purpose.
This page is the “how do I” guide. The reference for every command and
design decision is scripts/session_analytics/README.md; the Studio’s
Learn tab serves both in-app.
1. What you get
Section titled “1. What you get”| Page | The question it answers |
|---|---|
| Dashboard | How much have I done, what did it cost, how fast does the agent answer, where did the errors and money go? |
| Sessions | Which sessions are real work (probe runs and two-turn tests are hidden by default; one toggle shows them). |
| Session page | What was said, turn by turn, with the agent’s response time — and three analyses a judge writes over the whole transcript: Agent Tuning (what to change in your CLAUDE.md, permissions, hooks, skills, with the diff), Prompt Coaching (which of your prompts were vague and what to send instead), Efficiency (where the turns went, and the script, hook, skill or rule that would remove each detour). |
| Ask | A question about your sessions in words, answered by the judge LLM through read-only lookups (sessions, turn text, analyses, patterns, the graph), each lookup shown so the answer can be checked. |
| Team | Who on the team is active right now (a heartbeat in the last five minutes), what they are on, what sessions cost per developer, per project and over today / 7 days / 30 days, and the alerts: budgets passed, runaway sessions, and auto-build runs burning a cap (§8.5). Reads the shared team store (§8.4); on a local SQLite store it shows one developer. |
| Runs | Every attempt of the auto-build driver recorded on this machine, read from its ledger: the outcome in the driver’s own words (landed, terminated_policy, or none yet — never pass/fail), why it stopped, rounds per phase, cost against the cap with the estimated part distinguished, which requirements had verifiers and went green, the policy decisions taken, the PR — and the human verdict on that PR, set here (§8.6). |
| Graph | One session or project and everything it is connected to, every relationship named; a catalogue of questions answered as tables, charts or drawn on the canvas. |
| Analysis | The pipeline as steps — load sessions, build the graph, run the judge, compute KPIs — with a funnel of counts and a Judge quality card. |
| Benchmark | What this repository’s benchmark harness found: attempts by result with the cost and duration of the sessions they produced, and the predicted pass rate per project. Empty until you run the harness and link its runs (Settings → Benchmarks, then Link benchmark runs). |
| Learn | The project’s own docs, skills, agents and wiki, read in-app; each Agent Tuning finding links to the guide that explains the fix. |
| Settings | The .env this tool reads, edited with help text and a path picker. |
2. Requirements
Section titled “2. Requirements”- Python 3.11+ with the package’s dependencies. The repo’s
.venvis what./scripts/session-analyticsuses;startcreates it and installs what is missing. - Node 20+ for the Studio (
studio/, a Next.js app;startrunsnpm installthe first time). - Optional, local: Ollama for the judge and for embeddings. The packaged default judge is Ollama, so no session text leaves the machine unless you change it.
- Optional: PostgreSQL instead of SQLite (a compose file is provided),
the
kuzupackage for the Graph tab (installed with the rest).
Everything below is run from the repository root.
3. Configure
Section titled “3. Configure”3.1 The one required setting: the database
Section titled “3.1 The one required setting: the database”Configuration is a single gitignored .env in the repo root. The CLI, the
API and the Studio all read the same file; setup and the Settings page
both write it.
./scripts/session-analytics setup # guided, one time; writes .envOr write the file yourself. The only required key is the database:
# SQLite file — note FOUR slashes for an absolute pathCCT_SA_DB=sqlite:////Users/you/.cct/session-analytics.db
# …or PostgreSQL# CCT_SA_DB=postgresql://cct:cct@localhost:5433/session_analytics
CCT_SA_DBwas calledCCT_SA_DSNuntil September 2026. An old.envkeeps working; the next Save from Settings rewrites it under the new name. The CLI flag is--db(--dsnstill accepted).
Any command takes --db … to use a store without touching .env.
Precedence, lowest to highest: packaged defaults → ~/.cct/session-analytics.json
→ .env → real environment variables → command-line flags.
3.2 The other keys
Section titled “3.2 The other keys”| Key | What it does | Default |
|---|---|---|
CCT_SA_KUZU_PATH |
Where the Kùzu graph for the Graph tab lives. A directory such as ~/.cct works: the store file session-analytics-graph is created inside it. A file path is used as given. |
~/.cct/session-analytics-graph |
CCT_SA_REDACTION |
What is stripped before anything is written: none, code (strip code blocks and tool inputs, keep prose), metadata-only (no text at all). |
code |
CCT_SA_SOURCE_CLAUDE_CODE, CCT_SA_SOURCE_AIDER |
Where each assistant keeps its transcripts. | ~/.claude/projects, ~ |
CCT_SA_JUDGE_BACKEND / CCT_SA_JUDGE_MODEL |
The judge for every copilot: ollama (local), claude-code (the claude CLI on your PATH), or openai (any OpenAI-compatible endpoint: LM Studio, vLLM, OpenAI, Azure). Empty model = the backend’s default. |
ollama, `` |
CCT_SA_OLLAMA_URL |
Ollama’s address. | http://localhost:11434 |
CCT_SA_JUDGE_BASE_URL, CCT_SA_JUDGE_API_KEY |
For the openai backend only. |
|
CCT_SA_JUDGE_WORKERS |
Parallel judge calls. | 2 |
CCT_SA_TEAM_ACTIVE_WINDOW |
Seconds a heartbeat counts as active on the Team tab. | 300 |
CCT_SA_BUDGET_TEAM_DAILY_USD, CCT_SA_BUDGET_TEAM_MONTHLY_USD, CCT_SA_BUDGET_DEVELOPER_DAILY_USD, CCT_SA_BUDGET_PROJECT_DAILY_USD |
Budgets the Team tab and team alerts check (§8.5); blank = none. |
none |
CCT_SA_RUNAWAY_RECENT_MINUTES, _MAX_TURNS_RECENT, _RECENT_TURNS, _MAX_ERROR_SHARE, _MIN_TURNS_FOR_ERROR_SHARE, _MAX_COST_RECENT_USD |
What makes a still-running session a runaway (§8.5). | 60, 300, 50, 0.5, 20, 20 |
CCT_SA_AUTO_BUILD_ROOT |
The .cct folder the auto-build driver wrote its ledgers under, for the Runs tab (§8.6); relative = from the repository root. |
.cct |
CCT_SA_EMBED_BACKEND, CCT_SA_EMBED_MODEL |
Embeddings for session similarity (Ollama; nomic-embed-text works well). |
ollama, none |
CCT_SA_NOISE_MIN_TURNS, CCT_SA_NOISE_MIN_DURATION_SECONDS, CCT_SA_NOISE_PATH_PATTERNS |
What the Studio hides as noise (see §5.2). | 3, 60, /cct-probe,/private/var/folders/,/tmp/ |
CCT_DEVELOPER_ID |
Your id on multi-developer stores. | git user.email local part, else local |
Per-project settings (redaction override, ingest opt-out, transcript
archive opt-in) live in ~/.cct/session-analytics.json. The file is
strict JSON — no comments — and this example can be copied as is:
{ "projects": { "my-project": { "trace_archive": true }, "scratch": { "ingest": "off" } }, "project_ids": [ { "match": "/repo/my-project", "id": "my-project" } ]}trace_archive: true keeps the full (redacted) text for Ask and the
whole-session analyses; ingest: "off" never ingests that project. A
project’s key is its git repository name; project_ids maps a path
fragment to a key when a session’s directory is not a git checkout.
3.3 From the Studio instead
Section titled “3.3 From the Studio instead”Settings edits the keys a first run needs — the database, the graph
store, redaction, the judge (backend, model, base URL, API key, workers,
Ollama URL) and your developer id — each with a (?) explanation, a
file picker for the two paths, a “test connection” probe, and a pill
saying whether the API is reachable. It also tells you when the running
server is using a different database than the one saved in the form (a
--db flag on the command line wins over .env).
The source roots, the embedding settings and the noise thresholds are
not in that form: set them in .env or the environment, and the
per-project block in ~/.cct/session-analytics.json.
4. Start
Section titled “4. Start”./scripts/session-analytics serve # API on 127.0.0.1:8765, Studio on http://localhost:3000serve reads .env. To point it elsewhere for one run:
./scripts/session-analytics serve --db sqlite:////Users/you/.cct/sa.db --graph-path /Users/you/.cctFirst time on a machine, start does everything in one go — creates the
venv, installs the Studio’s packages, writes .env with defaults, ingests,
and opens the browser:
./scripts/session-analytics startStop with Ctrl+C. The API binds to localhost only.
4.1 Ports
Section titled “4.1 Ports”The defaults are 8765 for the API and 3000 for the Studio. Both are
flags, and serve wires them together: the Studio is told where the
API is, and the API’s origin allowlist is told where the Studio is.
./scripts/session-analytics serve --api-port 8766 --ui-port 3001 # then open http://localhost:3001./scripts/session-analytics start --api-port 8766 --ui-port 3001 # same flags on startIf serve fails with “address already in use”, something is holding a
default port — often an earlier serve that was never stopped. See who:
lsof -nP -iTCP:8765 -iTCP:3000 -sTCP:LISTENStop it (kill <pid>) or start on other ports. Running the Studio by
hand (cd studio && npm run dev) needs NEXT_PUBLIC_API_BASE set to the
API’s address when it is not the default.
5. Load sessions
Section titled “5. Load sessions”5.1 Ingest
Section titled “5.1 Ingest”./scripts/session-analytics ingest # all adapters, incremental./scripts/session-analytics ingest --copilot claude-code # one adapter./scripts/session-analytics ingest --full # re-parse everything (idempotent)./scripts/session-analytics doctor # counts + whether sources are reachableIncremental is the default: only new or changed transcripts are read. Re-ingesting a session replaces its turns. Archived text, the whole-session analyses and your human labels are anchored so they survive that; the judge’s per-turn labels are dropped with the turns and come back on the next judge run.
Choose what to load. Years of copilot history can be gigabytes of transcripts; you do not have to read them all at once. Nothing is parsed to list them:
./scripts/session-analytics ingest --list # what would be loaded: newest first, size, loaded/new./scripts/session-analytics ingest --since 2026-09-01 # modified on/after a date (or an ISO datetime)./scripts/session-analytics ingest --limit 20 # the newest 20 (after --since)./scripts/session-analytics ingest --session-id <id> --session-id <id> # exactly these (ids from --list)On the Analysis page the Load sessions step shows the same list
under Which sessions: filter by date and count, or tick sessions,
and the button says what it will read (“Load 3 selected · 12.1 MB”).
Run all steps uses the same selection. watch runs ingest in a
loop:
./scripts/session-analytics watch --interval 155.2 What is hidden as noise, and why
Section titled “5.2 What is hidden as noise, and why”Probe runs in temp directories and two-turn smoke tests are real rows in
the store and, on a developer’s machine, outnumber real work. Every list
and aggregate in the Studio leaves them out by default, and says so: the
Dashboard’s Sessions stat, the Sessions page’s Show excluded (n)
toggle and the Analysis funnel all show the same excluded count. A
session is noise when it has fewer than min_turns turns, lasted under
min_duration_seconds, or its path contains one of path_patterns.
Benchmark-linked sessions are never noise. This is decided at query
time, so changing the thresholds needs no re-ingest. Opening a session
by id and the judge always see everything; Ask follows the pages —
its lists, searches and aggregates leave noise out, and a lookup of
one session by id still reads that session.
5.3 Keep the full text (opt-in)
Section titled “5.3 Keep the full text (opt-in)”Ingest keeps a 500-character redacted preview per turn. For the Session page’s full Timeline, Ask, and the whole-session analyses, opt a project into the archive (§3.2) and run:
./scripts/session-analytics archiveArchived text goes through the same redaction as ingest, under the stricter of the configured mode and the mode the session was ingested with.
6. Read a session
Section titled “6. Read a session”Open Sessions, pick one. The Timeline shows every turn with its
text (or the reason there is none: “preview only” until the project is
archived), the seconds since the previous turn (+8s; assistant turns
are what the response-time card summarises), tool names, and the judge’s
labels once it has run. A card at the top lists the slowest turns.
The three analysis tabs are generated on demand by the configured judge over the whole transcript, then stored:
- Pick a model if the configured one is not installed (the page lists what Ollama has; a missing configured model is flagged).
- Generate. One model call, usually one to three minutes locally.
- Every finding cites its turns; a turn chip jumps to that turn on the Timeline. A config change comes with a diff and a Copy button; an efficiency finding comes with a starter file for the script, hook, skill or rule that would remove it. The category badge links into Learn at the guide that explains the fix.
- The footer says which judge and model produced it, whether it saw the
full archive or previews only, how many characters, and whether the
transcript was truncated to fit the model. Weigh the findings
accordingly: a 3B local model over previews produces thin advice; the
claude-codejudge or a long-context model produces useful advice.
Re-generate never destroys a good result: if the re-run fails, the earlier result is kept and the page says so.
7. Run the judge
Section titled “7. Run the judge”The judge labels each turn (corrections, questions, commands, rework, misunderstanding, helpfulness, sentiment, a 1–5 quality) so the Dashboard’s label distribution, the KPIs, and cost-by-sentiment mean something. It calls a model per turn; the local Ollama default keeps that free.
The judge is configured in one place: Settings → LLM-as-Judge (backend, model, workers, URL). The Model field lists what the saved backend URL actually serves; choose Other… to type a name it does not list. Analysis → step 3, LLM judge runs that judge — it shows which one and links back to Settings; it is not a second place to choose. Set how many turns and run. The step reports as it goes: turns done of total, labelled and failed, the rate and time left, and the reason for the last failure — a wrong model name or a dead backend shows on the first turn, not after fifty. Every label is written as it arrives, so a run you stop keeps what it labelled. From the CLI:
./scripts/session-analytics analyze --limit 200 # configured judge./scripts/session-analytics analyze --judge ollama:llama3.2:latest # explicit./scripts/session-analytics analyze --judge claude-code: --limit 50 # the claude CLI./scripts/session-analytics kpis # roll labels up per sessionTurns with no text (tool-result turns under redaction) are skipped: there is nothing to judge.
7.0 A judge on another machine (e.g. a DGX Spark)
Section titled “7.0 A judge on another machine (e.g. a DGX Spark)”Both local backends can live on other hardware; the Studio only needs
its URL. For vLLM serving qwen38-27b on a Spark at 192.168.1.23:8001
(the setup in the Learn section Judge on a DGX Spark), the whole
configuration is:
- Settings → LLM-as-Judge: Backend
openai(the OpenAI-compatible backend — vLLM speaks that API), Base URLhttp://192.168.1.23:8001(with or without/v1; it is added when missing), API key blank. - Save. The Model dropdown now lists what that server serves —
pick
qwen38-27b. Save again. - Test judge LLM (next to Test database): one tiny
prompt to the saved judge. Expect
openai:qwen38-27b answered in 0.7s: {"ok": true}. A failure shows the server’s own reason (a wrong model name, a wrong port). - Analysis → step 3, Run llm judge. Verified on that Spark: about 10 s per turn.
For Ollama on the Spark instead: Backend ollama, Ollama URL
http://192.168.1.23:11434 (Ollama must be started with
OLLAMA_HOST=0.0.0.0), then the same Save → pick model → Test judge LLM.
| You run on the Spark | Settings → LLM-as-Judge |
|---|---|
vLLM (vllm serve …, or any OpenAI-compatible server) |
Backend openai · Base URL http://<spark>:8001 · Model from the list |
Ollama (OLLAMA_HOST=0.0.0.0 ollama serve) |
Backend ollama · Ollama URL http://<spark>:11434 · Model from the list |
Qwen3-family models “think” by default and can spend the whole answer
budget on it; the judge sends enable_thinking: false with every
request (what a working curl to vLLM also needs) and falls back to a
plain request on servers that do not know the field. The transcript
text goes to that machine under the configured redaction level (§3.2);
nothing else leaves.
How to stand the Spark up in the first place — Ollama, vLLM in a container, Qwen3.8-27B, the exact run sequence — is in Learn under Judge on a DGX Spark (three pages: setup & cookbook, the vLLM operator manual, the runbook).
7.1 Is the judge right? (validation)
Section titled “7.1 Is the judge right? (validation)”Everything the labels feed rests on the judge, so measure it. Two comparisons are built in, both on the Analysis page’s Judge quality card and in the CLI:
Run against run — the same rubric under a second name over the same turns. At temperature 0 a model agrees with itself; what this shows is which labels the judge ever fires at all.
./scripts/session-analytics analyze --rubric-name heuristic-v1-rerun --only-labelled-by heuristic-v1./scripts/session-analytics labels agreement heuristic-v1 heuristic-v1-rerunHuman against judge — the one that carries information. Label a sample yourself; the CSV shows exactly the text the judge saw:
./scripts/session-analytics labels sample --n 50 --out sample.csv # 25 user + 25 assistant turns# fill the nine yes/no columns (blank = not applicable), sentiment, quality 1–5./scripts/session-analytics labels import sample.csv --labeler you./scripts/session-analytics analyze --only-labelled-by human:you # judge the same turns./scripts/session-analytics labels agreement human:you heuristic-v1Every figure comes with its n; under 20 pairs it is shown but is not
evidence. Cohen’s κ sits beside raw agreement because a label that is
almost always “no” agrees by chance. A stronger judge over the same
human sample is one command away (--judge claude-code: --rubric-name heuristic-v1-claude --only-labelled-by human:you).
8. The graph
Section titled “8. The graph”./scripts/session-analytics graph --rebuild # seconds, even for 100k turns./scripts/session-analytics embed # session embeddings (needs an Ollama embedding model)./scripts/session-analytics similar # SIMILAR_TO edges from the embeddingsOr press Build knowledge graph on the Analysis page (noise sessions are left out, the same as everywhere else in the Studio).
The Graph tab is a picture of one thing and what it is connected to, with the relationship written on every edge. Pick a session (or a project) at the top:
- A session in the middle; around it the workspace, model, copilot
and developer it ran with (
IN_WORKSPACE,USED_MODEL,RAN_ON,BY_DEVELOPER); then the tools it called most, one bubble per tool sized by calls and ringed red when it errored (INVOKED, up to 20); outside, the files it touched most (ACCESSED_FILE, up to 12) and the sessions most like it with their scores (SIMILAR_TO, up to 8). Every list says “12 of 31” when it is a cut. - Click a tool for every call of it, by the turn that made it, each
linking into the transcript; click a file for the sessions that
touched it most (up to 40, with the total); click a similar
session to recentre on it; click the workspace (or the
breadcrumb) to go up to the project, where the newest sessions (up to
60) are the bubbles,
SIMILAR_TOedges run between them, and each session is linked to the model it ran on, so the models are hubs. - Ask the graph a question below the picture: a catalogue of
questions in words — which tools fail most and where, tools that
precede an error, files touched by the most sessions,
sessions most similar to one, which model runs the longest sessions,
sessions per project, similar sessions across projects — grouped as
Find, One thing, Across everything. Each answers as a table, a
bar chart, or drawn on the canvas when the answer is nodes and
relationships. The catalogue is data
(
config_data/graph-queries.json): add a question by adding an entry. Raw read-only Cypher stays under a fold, with the schema beside it.
Clusters are gone. On real data, transitive components were too coarse to mean anything: at the default similarity threshold the graph fell into a few large components, and raising the threshold to 0.85 gave 11 clusters (the largest 45 sessions) at the price of 29 sessions in no cluster — tuning traded giant components for unclustered sessions rather than fixing the abstraction (the threshold table is on #65). Similar sessions (§8.1) and the neighbourhoods above are what survives.
8.1 Similar sessions
Section titled “8.1 Similar sessions”The Similar tab on a session page lists the sessions closest to it by meaning — “have I done this before, and how did it go” — each with its score, project, model, tags, and a reason in words: the same project, the tools few sessions use that both used, shared error types, shared files. It needs two things, both in the Analysis pipeline:
- Settings → Embeddings: choose an embedding model. The list holds
only the models your Ollama can embed with — chat models such as
qwen3.6 are left out, because Ollama refuses to embed with them.
Ollama has no default one;
nomic-embed-textis small and enough (ollama pull nomic-embed-text, then pick it from the list and Save). Test embedding shows the vector size and time. - Analysis → Embed sessions, then Find similar sessions — or just press the button on the Similar tab, which runs both (and the graph build first if it has not run). Run all steps includes them. Embedding 119 sessions with nomic-embed-text took about 8 seconds on a laptop.
Sessions loaded later are embedded on the next pass; envelopes are kept, not recomputed. The list is a snapshot of the last pass, and “no neighbours” means no other session scored above the similarity threshold — a result, not a failure.
8.2 Ask
Section titled “8.2 Ask”Ask is a chat over your store. Type a question — “which of my sessions had the most tool errors, and what went wrong in the worst one?”, “what did I work on last week, project by project?”, “find the sessions where I asked about pytest fixtures” — and the judge LLM from Settings answers it by looking things up, never by guessing. It has nine read-only lookups: list sessions (by project, model, copilot, date, tag, sorted), one session’s facts, a window of its turns as text, its stored analyses, full-text search of what was said, tool and error patterns, similar sessions, one of the Graph page’s catalogue questions, and the store-wide totals. Each lookup appears as it happens — the tool, its arguments, a one-line summary, how long the model took to decide — and a fold under it holds exactly what the judge was given, so the answer can be checked against the rows. A session the answer cites is linked only when a lookup returned it; an id the model names that no lookup returned is shown as unverified. Follow-ups see the earlier exchanges. Stop stops listening; a lookup the judge is already working on finishes on its own (nothing is written either way).
What the model reads is what the analyses read: every turn’s redacted
preview, and the full text where a project opted into the archive
(§5.3). Nothing is written. The judge is the one in Settings — there is
no other — and it must be able to follow instructions to answer in
JSON; the Spark’s qwen38-27b (§7.0) answers a four-lookup question in
about a minute, a small Ollama model in a few. A model that cannot keep
to the JSON shape ends the exchange with its reply shown, and Test
judge LLM in Settings is the first thing to check.
8.3 Benchmark runs
Section titled “8.3 Benchmark runs”If you run this repository’s benchmark harness (benchmarks/README.md),
each attempt leaves a run-record.json and a score.json under a runs
folder (the harness’s default is runs/ at the repository root). Set
that folder under Settings → Benchmarks, then press Link benchmark
runs on the Analysis page — or on the Benchmark page, which offers the
same step. It stores every attempt’s result, for every benchmark
backend, and links Claude Code runs whose run record names a Claude
Code session that is loaded; a linked session is never hidden as noise,
however short it was. Older runs whose records carry no session id, or
name a session you have not loaded, keep their outcomes but link
nothing — the page says which of those the last scan found. With the
folder unset the step is skipped by Run all, and the Benchmark page
says what to set.
8.4 A team store
Section titled “8.4 A team store”Every page above reads one store, and that store can be shared. The
plane for a team is one Postgres database that every developer’s
ingest writes to — no daemon beyond Postgres, no login: whoever holds
the database credentials can read and write, so put it behind your
LAN or VPN and hand out the DSN accordingly (identity is the derived
developer_id, an attribution, not authentication).
- Start Postgres once, on a machine the team can reach:
docker compose -f scripts/session_analytics/docker-compose.yml up -d(or any Postgres 16). Its DSN is the team DSN. - Every developer sets
CCT_SA_DBto that DSN under Settings → Database (or in.env), presses Test database, and keeps./scripts/session-analytics watchrunning: sessions and heartbeats land in the shared store at the watch interval. - Open Team in anyone’s Studio. It says “Shared team store
(postgres)”, lists every developer with a green dot for a heartbeat
in the last five minutes, their current project / phase / feature
from the newest heartbeat, and cost and sessions for today, 7 days
and 30 days; the Projects table splits the same by repository. In
a terminal,
./scripts/session-analytics team statusprints the developers table (--jsonfor the payload).
“Active” is last-seen, never a liveness verdict: a heartbeat older
than the window (team.active_window_seconds, default 300, or
CCT_SA_TEAM_ACTIVE_WINDOW) reads as idle, and a developer who has
never sent one reads as “no heartbeat yet”. Costs sum priced turns
only; a * marks a window where some priceable turns had no price,
and an em dash means nothing in the window was priced.
One person can end up under several developer ids — developer_id is
derived at ingest (flag > CCT_DEVELOPER_ID > config > git email
local-part > local), and a machine or a setting changing splits them.
List the ids that are one person under team.aliases in
~/.cct/session-analytics.json ({"i-am-goga": "Gosha", "local": "Gosha"}), or as CCT_SA_TEAM_ALIASES=i-am-goga=Gosha,local=Gosha in
.env, and the Team tab and team status fold them into one row under
that name, summing sessions, turns and cost and taking the newest of
their heartbeats; the configured name wins over the developer table’s
own. Nothing is rewritten — the store keeps every id, the fold happens
at read time, and each row’s merged_ids (in --json and the API
payload) names every id folded into it. An entry that is not id=Name
refuses at config load rather than quietly dropping the alias.
8.5 Budgets, runaway and auto-build cap alerts
Section titled “8.5 Budgets, runaway and auto-build cap alerts”The Team tab opens with an Alerts card, and ./scripts/session-analytics team alerts prints the same list. Three kinds:
- Budgets. Under Settings → Team set any of: team per day, team per 30 days, per developer per day, per project per day (USD; blank = no budget). A scope at 80% of its budget is a warning, at 100% a breach. Budgets count priced turns only; every alert names how many priceable turns had no price, because the true spend is then higher, and a window with nothing priced never breaches.
- Runaway sessions. Always on. A session whose newest turn is
within the last 60 minutes is flagged when it produced more than 300
turns in that time, or spent more than $20, or more than half of its
last 50 turns errored (given at least 20 of them). Each alert names
the developer, the project, the session, the figure and the
threshold, and links to the session. Thresholds live under
team.runawayin the config file (CCT_SA_RUNAWAY_*in.env). - Auto-build runs burning a cap. Always on, and read from the same ledgers the Runs tab reads (§8.6) — no extra setting. A run the driver has not concluded is flagged at 80% of the cost cap it was admitted with (a warning) and again at 100% (a breach), and the same two thresholds apply to its wall-clock cap. The alert names the feature, the run’s attempt id, and the figure against the cap as a percentage; the cost figure is metered spend plus the driver’s estimate for an unmetered reviewer, with the estimated portion named, because that is what the cap is set against. A run with no cap recorded, or a cap of zero, yields no alert. Finished runs never alert — they are history, read on the Runs tab. The last configuration line says whether the ledgers were read at all and how many runs are still running, so a quiet card is never mistaken for an unread one.
Alerts are derived from the store on every read; nothing is stored, so the audit trail is the rows themselves. Nothing is terminated: a runaway is flagged for a person to look at.
For a cron job or a pipeline step, the exit code is the alarm:
./scripts/session-analytics team alerts # exit 1 on a breach./scripts/session-analytics team alerts --fail-on warning./scripts/session-analytics team alerts --json # the full record8.6 Auto-build runs and the human verdict
Section titled “8.6 Auto-build runs and the human verdict”The auto-build driver (scripts/auto-build-loop.sh) leaves a ledger for
every attempt under .cct/auto-build/<feature>/; finished ledgers you
move aside by hand go under .cct/auto-build-archive/. The Runs tab
reads both on every refresh — nothing from a ledger is stored — and
shows, per run:
- the outcome as the driver wrote it:
landed,terminated_policy, or “no outcome yet” with the status. It is never collapsed to pass/fail; a run that stopped at a policy boundary is a different thing from one that failed; - why it stopped: the disposition reason and detail from
termination.json; - the reviewer probe, the fallbacks and the earlier terminations: the
readiness probe preflight put the gating reviewer through (who
answered, with what verdict, in how long, at what cost), a phase whose
newest review round changed reviewer mid-round (who produced no
review, who gated the round instead — a phase that ended on a round
gated by one reviewer no longer stands on a fallback, so it lists
none), and every termination the run already survived before this
state (the kept
termination-<epoch>.jsonrecords; the currenttermination.json, if any, is the disposition shown beside it, not counted here) — so a run that was resumed and landed reads as “landed after 1 earlier termination”, not as a plain landing; - phases and rounds: planned, done, review rounds and the reviewer’s verdict per phase, fix sessions, commits;
- cost against the cap, with the metered figure and the driver’s conservative estimate for an unmetered reviewer named separately (solid and hatched in the bar) and the wall clock against its cap;
- verifiers: how many requirements had a verifier at admission and how many went green — or that they never ran;
- policy decisions: the journal events where the driver decided something (a termination, a park, a review-state reset, a skipped artifact, a merge decision, a cap change, a waiver);
- the PR, and the human verdict on it: merged unmodified, merged with fixes, or rejected, with a note. This is the one fact the ledger cannot write. It is stored in the analytics store keyed by the run’s attempt id, so it survives the ledger being archived or pruned.
The page polls while any run has not concluded and stops when every
run has. A run whose state file was written within
auto_build.active_window_seconds (default 15 minutes) is shown as
live; one that has gone quiet in a long build phase is still polled,
with how long since its last state write. No ledger writes a score today, so each run says “no scores
recorded” rather than drawing an empty chart.
./scripts/session-analytics runs # every run, one line each./scripts/session-analytics runs show 47908-474720888 # one run with its events and triage report./scripts/session-analytics runs label 47908-474720888 merged_with_fixes --note "FR-3 fixed by hand"./scripts/session-analytics runs unlabel 47908-474720888The ledger root is CCT_SA_AUTO_BUILD_ROOT (Settings → Auto-build
runs); the default is this repository’s own .cct.
9. Everything else
Section titled “9. Everything else”./scripts/session-analytics search "pricing config" --limit 20 # archived text, ranked./scripts/session-analytics export --table sessions --format csv --out sessions.csv./scripts/session-analytics export --table all --format parquet --out ./export/./scripts/session-analytics correlate --runs-root runs # same as Analysis → Link benchmark runs./scripts/session-analytics runs # auto-build runs and their verdicts (§8.6)./scripts/session-analytics mcp # MCP server over the store./scripts/session-analytics list # adapters + judges registeredPostgreSQL instead of SQLite:
docker compose -f scripts/session_analytics/docker-compose.yml up -d10. Troubleshooting
Section titled “10. Troubleshooting”| You see | Cause | Do |
|---|---|---|
error: no database configured |
No .env, no --db. |
setup, or --db sqlite:////abs/path.db (four slashes). |
| Settings says the server uses a different database than the form | serve was started with --db. |
Either is fine; the banner tells you which store every number comes from. |
| Graph tab: “the graph store … could not be opened” | The store file at CCT_SA_KUZU_PATH is corrupt or locked by a running build. |
Delete the corrupt file (~/.cct/session-analytics-graph by default), then run Build knowledge graph. |
| Graph tab: “has not been built yet” | Never built. | Analysis → Build knowledge graph. |
Session page: model 'llama3' not found |
The configured judge has no model and Ollama’s default is not pulled. | Pick an installed model in the page’s judge picker, or set CCT_SA_JUDGE_MODEL. |
| “The judge did not answer … Remote end closed connection” | The model does not fit in memory at the whole-session context size. | Use a smaller model for whole-session analyses. |
| “hit its answer cap before finishing” | A small model would not keep to the list limits. | Re-generate, or a stronger model. |
| Timeline says “preview only” | The project is not opted into the archive. | §5.3. |
| Sessions page is empty but the store has data | Everything matched the noise rule. | Tick Show excluded, or relax CCT_SA_NOISE_*. |
| Dashboard “Median agent response” is “—” | No turn carries a timestamp on both sides (Aider transcripts have none per turn). | Expected; the measured-n note says how many turns were measured. |
serve fails: “address already in use” |
A default port is held — usually an earlier serve still running. |
lsof -nP -iTCP:8765 -iTCP:3000 -sTCP:LISTEN, stop it, or serve --api-port 8766 --ui-port 3001 (§4.1). |
Studio pages stuck on “Loading…” after npm install |
The dev server’s cache predates the install. | Restart serve. |
CodeQL / CI mentions fs/browse |
The Settings path picker is a deliberate local directory browser, loopback-only. | Nothing; it is by design. |
11. Privacy, in one paragraph
Section titled “11. Privacy, in one paragraph”Redaction (code by default) runs before anything is written or sent
anywhere. The packaged judge and embedding backends are local Ollama;
session text reaches a cloud model only if you set claude-code or an
openai endpoint as the judge, and the Analysis page warns before a
batch run. The API binds to 127.0.0.1 and rejects cross-origin writes.
.env may hold an API key and is gitignored. Per project you can turn
ingest off entirely or tighten redaction beyond the global mode.