# analytics-node

Local privacy-first analytics MCP server for agent usage analytics.

It imports metadata from local client files (Codex, Copilot, Claude, Cursor, Antigravity, hook log), stores normalized data in a local SQLite DB, and exposes query tools for operational insight.

Adapter contract: malformed individual records are recoverable warnings when other valid records
exist in the same file. A source that cannot be read or has an incompatible schema raises a file-level
failure (`SOURCE_LOCKED_OR_UNREADABLE` or `SOURCE_SCHEMA_UNSUPPORTED`); the scanner stores `last_status = failed` and retries the unchanged file on
the next scan. Adapter counters are published only after the file transaction commits, so a failed
transaction contributes no imported rows or counters.

## Features

- Incremental scan/import with source-specific adapters.
- Runtime event storage with canonical types, without storing prompt/response payloads.
- Query tools with pagination and filters (`summary`, `models`, `sessions`, `events`).
- Imported-data cleanup workflow with dry-run/confirm (`analytics_delete_imported`).
- Privacy guardrails by design (no credentials/token parsing, no raw chat content persistence).

## Runtime event contract

The storage/API contract accepts these canonical `event_type` values: `mcp`, `skill`, `hook`, `agent_tool`, `agent_lifecycle`, and `hotword`.

Hook metadata:

- `hook_phase`: `lifecycle`, `prompt`, or `pretool`.
- `suggestion_outcome`: `none`, `mcp`, `skill`, or `mcp_and_skill`.
- `mcp_suggestion_count` and `skill_suggestion_count`: the number of MCP and Skill suggestions associated with the hook record.

Hotword and routing metadata:

- `hotword_mode`: `quick`, `agents`, or `quick_agents`.
- `context_mode`: `light` or `full`.
- `subagent_mode`: `off`, `requested`, or `required`.
- `quick_overridden`: `true`/`1` when a detected `!quick` request is resolved with full rather than light context.

These fields define the storage/API contract available to readers. They do not imply that every adapter or historical log already emits the related metadata. Until a source produces it, optional fields can be `NULL` and count fields can retain their default value (`0`).

Summary runtime-event metrics:

- `analytics_summary.event_type` accepts `mcp`, `skill`, `hook`, `agent_tool`, `agent_lifecycle`, or `hotword`. It filters event-derived sections: `event_totals`, Hook/Hotword/Skill/MCP metrics, `by_source.events`, `by_source.last_activity`, and `groups`.
- Sessions, messages, tokens, and models are not filtered by `event_type`. `hotword_metrics.prompts_total` remains a prompt denominator and is not narrowed by that filter.
- `hook_metrics.invocations` counts canonical Hook events with a known phase.
- `hook_metrics.outcome_evaluations` counts those invocations that have an outcome. It can be lower than `invocations` when legacy records have a known phase but no recorded outcome.
- Hook outcomes are `none`, `mcp`, `skill`, and `mcp_and_skill`; they describe only MCP/Skill suggestions produced by the Hook.
- `hook_metrics.suggested_invocations` counts outcomes with at least one MCP or Skill suggestion. `suggestion_rate` is its percentage from `0` to `100`, calculated over `outcome_evaluations`; `lifecycle`, `prompt`, and `pretool` count canonical Hook phases.
- `hook_metrics.by_hook` is a server-side breakdown of canonical Hook invocations, grouped by `hook_name`, `hook_event`, and `hook_phase`, ordered by invocation count and then names. Each row also provides outcome counts, suggested invocations, and `suggestion_rate` for that Hook grouping; the source-aware `by_source_hook` has the same fields.
- The GUI Hook view keeps lifecycle Hooks separate as `Eseguito`. Prompt-routing and PreTool Hooks show total activations separately from evaluated outcomes, unavailable outcomes, and the suggestion rate (calculated only on evaluated outcomes). These outcomes describe suggestions emitted by one canonical Hook activation, not the number of `McpHint` or `SkillHint` detail events and not whether a suggested target was used.
- Source-aware variants preserve the originating source in each row: `mcp_metrics.by_source_server`, `mcp_metrics.by_source_tool`, `skill_metrics.by_source_skill`, and `hook_metrics.by_source_hook`. They use the same source/date filters and self-event exclusion as their summary metrics; the existing `by_server`, `by_tool`, `by_skill`, and `by_hook` remain source-agnostic compatibility aggregates.
- MCP breakdowns retain legacy rows without structured names: real calls derive server/tool from `event_name` when possible, otherwise use `unknown-server` and `unknown-tool`. A `McpHint` without a classified server uses `unknown-server` and never treats `mcp_hint` as a server name. Skill breakdowns use `skill_name`, then `event_name` without a `skill.` prefix, then `unknown-skill`. The GUI renders these stable buckets as non-classified labels.
- `hotword_metrics.quick`, `agents`, and `quick_agents` are mutually exclusive Hotword modes.
- `hotword_metrics.quick_total` includes `quick + quick_agents`; `agents_total` includes `agents + quick_agents`.
- `quick_light` counts `quick` or `quick_agents` events resolved with light context. `orchestration_required` counts Hotword events that request required orchestration; neither proves that a subagent was executed.
- The GUI Hotword tab reads only these server-side metrics. It presents exclusive modes separately from the overlapping inclusive totals, and labels orchestration as requested rather than as completed subagent execution.
- `skill_metrics` separates hints, declarations, and all signals; `by_skill` has one row per Skill. `mcp_metrics.real_calls` counts observed MCP use, while `server_hints` and `tool_hints` count `McpHint` occurrences without implying use: respectively hints whose normalized `tool_name` is absent (`NULL`, empty, or whitespace-only) and hints with a non-empty tool. Repeated hints for the same target increase these counters. `by_server` and `by_tool` aggregate the same occurrences into one row per MCP server or server/tool pair; they are frequency breakdowns, not unique-target cardinalities. Consumers that need unique target counts must deduplicate these rows separately.
- Legacy records and empty filtered datasets return zero counters and empty breakdown arrays.
- Hotword metrics are separate from Hook MCP/Skill outcomes. They do not prove that a subagent was executed.
- `hook_log` contributes to metrics derived from runtime events, but not to session, token, or model metrics.

## Tool surface

- `analytics_status`: DB status and high-level counters.
- `analytics_scan`: import/refresh analytics from one or more sources.
- `analytics_scan_details`: privacy-safe details for the latest completed scan or a specified run.
- `analytics_summary`: aggregated counters by source/project/model/date.
- `analytics_timeseries`: daily UTC buckets for sessions, messages, events and observed tokens. `dense_range` is `true` only when both inclusive date limits are supplied within the supported range, filling absent days with zero; otherwise it is `false` and only days present in data are returned. Response metadata includes `timezone`, `range`, `bucket_fields`, and `filters_not_applicable_to_events`; sessions use `sessions.updated_at`, messages/tokens use `message_metrics.created_at`, and events use `runtime_events.occurred_at`. Self events are excluded; `model` and `include_technical` do not apply to events.
- `analytics_models`: model usage metrics.
- `analytics_sessions`: session-level listing and totals. Omitting `include_technical` preserves the legacy unfiltered list; passing `false` returns only main conversational sessions with at least one user message, while `true` returns all sessions.
- `analytics_events`: chronological list of individual normalized runtime events, not an aggregate. Each row retains `occurred_at`. Omitting `include_self` (or passing `true`) preserves the legacy behavior and includes self events; passing `false` excludes them from rows, `total`, and pagination.
- `analytics_delete_imported`: two-step deletion of imported rows (DB only).

## Supported sources

Core sources:

- `codex`
- `copilot`
- `claude`
- `cursor` (Cursor chat sessions, read from local SQLite storage; see "Cursor analytics" below)
- `antigravity` (Antigravity/Gemini IDE and CLI chat sessions, read from local JSONL transcripts; see "Antigravity analytics" below)
- `hook_log`
- `client_surface` distinguishes variants (`cli`, `vscode`, `vscode_insiders`, `code`, `desktop`, `unknown`)

Note on `DEFAULT_SCAN_SOURCES`: the default list of sources scanned by `analytics_scan` when no explicit `sources` argument is given is defined in `analytics-node/src/sources.ts` and currently is `["codex", "copilot", "claude", "cursor", "antigravity", "hook_log"]` (i.e. every entry in `ALL_SOURCES`). This list previously omitted `claude`, which made Claude Code analytics silently show nothing even though the adapter worked fine -- the bug was in what got *scanned*, not in reading already-imported data (the read tools like `analytics_models`/`analytics_sessions` already defaulted to including `claude`). If a source is ever added without adding it to `DEFAULT_SCAN_SOURCES`, the same class of bug recurs: no error, just quietly-empty analytics for that source. As defense in depth, the GUI's "Sincronizza (Scan)" button also passes the full source list explicitly instead of relying solely on this default.

Claude Code transcripts under `~/.claude/projects` are imported as `source = "claude"` and `client_surface = "code"`. Claude Desktop can still be detected as an installed client by the runtime, but its local storage is not currently an import source: it is excluded from file discovery, never read by `analytics_scan`, and cannot turn a scan into `partial` or `failed`.

Note on `SESSION_SOURCES`: `analytics-node/src/tools/models.ts` and `analytics-node/src/tools/sessions.ts` derive their allowed-source validation and SQL `IN (...)` filters from `SESSION_SOURCES` (also in `sources.ts`) instead of hand-typed lists, so adding a new session-bearing source only requires updating it in one place.

Note on empty codex/copilot session shells: Codex and VS Code Copilot can both emit resume/reconnect events (a fresh `session_meta` block, or a new event-log grouping) that carry zero messages, zero MCP calls, and zero lifecycle events before the next resume supersedes them. `codex.ts` and `vscode-copilot.ts` skip creating a session (and all child rows) for these empty shells; only a group with at least one message, MCP call, or lifecycle event becomes a session row. Migration 12 (`cleanup_empty_codex_copilot_session_shells`) is a one-time data cleanup that removes empty-shell rows already present in existing databases.

## Privacy and security constraints

- No prompt text, response text, or raw payload persistence.
- No credential/token parsing (`*token*`, `*auth*`, credential-like files are skipped).
- No OAuth/auth store reads (including VS Code/Copilot auth stores).
- Path disclosure is minimized in tool outputs and warnings.

## Configuration

Only supported env:

- `ANALYTICS_DB_PATH`

Default DB path:

- `~/.mcp-servers/analytics/analytics.sqlite`

Derived hook log path:

- `dirname(ANALYTICS_DB_PATH)/hooks/events.jsonl`

## Build and tests

- `npm run build`
- `npm run test`
- `npm run smoke` (build e smoke Analytics, senza migration recovery)
- `npm run test` (build, smoke Analytics e migration recovery)
- Dal workspace root, `npm run test:analytics` esegue il piano completo Analytics; il gate completo canonico e' `npm run test:ci` in CI.

## Scan behavior

`analytics_scan` is explicit only (no auto-scan at MCP startup).

Warnings use three severities: `info` for non-problematic diagnostics (for example
`COLLAB_AGENT_LIFECYCLE_DETECTED`), `warning` for recoverable partial records, and `error` for a
file that could not be imported. Run-level warnings remain in `scan_runs.warnings_json`; each
imported or failed file keeps its own sanitized warnings in `source_files.warnings_json`. The detail
tool returns only codes, severity, occurrence counts and privacy-safe file IDs: never local paths or
conversation contents.

During a retry, `source_files.last_status` becomes `pending` without clearing the diagnostics from
the prior completed attempt. Only the new `imported` or `failed` finalization replaces
`last_error_code`, `last_error_severity` and `warnings_json`. `analytics_scan_details.details` keeps
its existing fields and adds `source` plus `file_outcome`: `recovered` is an imported file with
diagnostics, `failed` is a non-imported file, and `unknown` is used for historical runs where the
outcome cannot be proven. Occurrences use a finite positive `details.count` when present, otherwise
one; historical warnings remain readable and recover `source` from an unambiguous file ID only.
The dashboard's persistent “Dettagli ultima scansione” control rereads the last completed run after
a refresh.

- `dry_run=true`: planning/check mode, no writes.
- `dry_run=false`: real import/update.
- `force=true`: bypass incremental skip policy when reimport is required.
- Prefer source-targeted scans over full scans.

Message counters:

- `sessions.message_count`: conversational messages only.
- `user_message_count` and `assistant_message_count`: conversational messages by role; the former is the functional message KPI.
- Codex `token_count` telemetry remains in `message_metrics` with `metadata_json.metric_kind = "telemetry"`; it preserves native token metrics but never contributes to conversational message counts.
- `analytics_summary.session_totals.messages` and `analytics_models.models[].messages` count only main user messages.
- `analytics_summary.by_source` uses the same main-session/user-message definition and only includes requested sources; `sources: []` is invalid.
- Optional array filters are omitted for defaults; when present they must be non-empty arrays of supported enum values (duplicates are deduplicated).
- Recognized Claude/Cursor metadata-only records do not create empty sessions. Claude can clean a recognized empty native session; Cursor does so only when composer metadata contains a valid recognized field (non-empty name/model or a valid timestamp) and no bubble row exists. Malformed, structurally foreign metadata and malformed or unrecognized Cursor bubbles never authorize cleanup. Existing event-only sessions are preserved by the cleanup migration.
- `scan_runs.messages_upserted` / `analytics_scan.messages_upserted` is a technical per-run import counter, not the functional message KPI.
- The conversational read paths (`analytics_sessions` with `include_technical=false`, summary, models and source breakdowns) require both `session_kind = main` and `user_message_count > 0`. This query-time rule also hides legacy Codex telemetry-only rows without deleting tokens or requiring a force scan.
- Codex parses JSONL records independently. Malformed lines and valid-but-unsupported records are privacy-safe recoverable warnings when another recognized record remains; an all-unrecognized file fails as `SOURCE_SCHEMA_UNSUPPORTED`. After a malformed or over-limit event-style line, implicit session context is discarded until a valid `session_meta` establishes it again.
- When Codex is selected, each non-dry scan also reads `~/.codex/session_index.jsonl` as append-only metadata, independently of transcript incremental skips. Only a valid `id` plus normalized `thread_name` is retained in memory; the last valid physical line wins, the file is never registered as a transcript, and its title is correlated only through `source_session_hash = HMAC(id)`. The title is merged into `sessions.metadata_json.thread_name` without a migration and exposed by `analytics_sessions.thread_name`. Invalid individual rows are aggregate privacy-safe recoverable warnings and do not discard other valid rows. A filesystem or stream error makes the whole index snapshot unusable: no partially read title is updated and the next Codex scan retries it. Title updates from a complete snapshot run in one database transaction: a SQL failure rolls back every title update, emits the privacy-safe error `CODEX_SESSION_INDEX_SYNC_FAILED`, and makes the scan `partial` when transcripts were imported or skipped (otherwise `failed`), without changing transcript file states or `files_failed`; the next Codex scan retries the synchronization.

Token and model contract:

- Observed tokens are always `input_tokens + output_tokens`. `reasoning_tokens`, `cache_read_tokens`, and `cache_write_tokens` remain separate metrics and do not change observed-token totals or GUI averages.
- `analytics_summary.token_totals` keeps the native counter availability/missing/partial coverage. The additive `model_coverage` block reports `known_observed_tokens`, `unknown_observed_tokens`, `total_observed_tokens`, and `known_percentage`; its total equals `token_totals.observed_tokens`, and `known_percentage` is `0` when there are no observed tokens.
- `preferred_model` is computed only from known models and observed tokens, with null-safe token sums and a stable lexical tie-break; it is `null` when no known model has observed tokens.
- `analytics_models` keeps the aggregation of known model names. Unknown-model data is instead one privacy-safe row per canonical source, marked by `is_unknown: true`, with `source`, `adapter`, `observed_tokens`, source/filtered-total percentages, `no_model_deduced: true`, and a deterministic `reason_breakdown`.
- `reason_breakdown[].count` is the number of underlying unknown diagnostic rows carrying that code, not a count of distinct sessions. Historical records without a recorded reason return `MODEL_REASON_UNDETERMINED`. Codex records `MODEL_FIELD_MISSING` or `MODEL_FIELD_INVALID` only when a structured `turn_context` proves that condition; other adapters and non-demonstrable cases remain undetermined. No model name is inferred.
- Hook analytics distinguishes a routing **SkillHint** (a skill structurally represented in the final routing output) from **SkillDeclared** (a `$skill` Markdown declaration in the current user submission). An explicit non-user top-level role excludes every direct field from declarations; assistant/system/tool messages and history do not produce declarations. `messages` is consulted only as a fallback for the last explicit user message, while legacy routing may still use the aggregate payload. Semantic identity removes duplicate ProjectFS guidance; a locally declared skill is represented without becoming a SkillHint, and a duplicate automatic proposal for that same skill is discarded while a distinct skill may use the remaining slot. This suppression is applied again after subagent control hints are added, so an agent-skill pairing can remain visible and structurally represented without producing a duplicate `SkillHint`. Skill associations survive composite, Git/ProjectFS replacement and hint merge as structured metadata; only associations still represented in the emitted output become `SkillHint`. Neither signal proves skill execution. Each `UserPromptSubmit` writes its user event and any skill signals in one ordered batch, while every signal remains a separate JSONL record. `event_totals.skill_hints` counts only hook-hint signals, `skill_declarations` counts declarations, and `skill_signals` counts both. Paths, prompts and rendered hints are not persisted for either signal.
- Codex JSONL is read as a bounded line stream: malformed or unsupported records are aggregated as privacy-safe warnings, while a valid `session_meta` establishes the context for subsequent `turn_context.payload.model` records that do not repeat a session id. A contextual `event_msg` with subtype `thread_settings_applied` can also supply `payload.thread_settings.model`; generic message payloads are never used to infer a model. Codex files are therefore not subject to the whole-file 50 MiB guard applied to non-streaming adapters; a single oversized line is discarded without retaining its contents.

Dashboard usage insights:

- The local GUI may present an illustrative **Usage Insight** that compares observed tokens with a fixed local catalogue of books and film screenplays. It makes no network request and does not add data to the Analytics MCP contract.
- The comparison is intentionally randomized at each GUI rendering: it selects one item from the fixed local catalogue using only the filtered observed-token total for the quantity. It never uses paths, conversation content, credentials or raw payloads. With zero observed tokens it displays a no-data message.
- Positive quantities never render as zero. The Italian formatted quantity and its singular/plural unit derive from the same rounding policy; the primary token definition remains `input_tokens + output_tokens`.

Recommended order:

1. `analytics_status`
2. read-only queries (`analytics_summary`, `analytics_models`, `analytics_events`, `analytics_sessions`)
3. `analytics_scan` only when stale/missing data is detected

## Delete imported data (M9)

`analytics_delete_imported` removes imported analytics rows from the local SQLite DB only.

Guardrails:

- Never deletes original client files (Codex/Copilot/Claude/Cursor/Antigravity/hook logs).
- Requires dry-run/confirm two-step flow for real deletion.
  - Binds each plan to sorted, privacy-safe ID-set digests for sessions, message metrics, runtime events, runtime event observations, actually deletable source files, and a random plan instance. Direct source-file references from every child table participate in orphan detection. Confirmation holds `BEGIN IMMEDIATE` from digest revalidation through explicit child-first deletion and consumes that exact plan atomically. Tokens are one-shot and cannot become valid again after reimport; two identical dry-runs intentionally return different tokens.
- Tracks operations in `delete_runs`.

Flow:

1. Plan (`dry_run=true`, default) -> returns `delete_plan_id` and matched counters.
2. Execute (`dry_run=false`, `confirm_delete=true`, `confirm_plan_id=<id>`).

Filters:

- `sources[]`
- `client_surfaces[]` (`cli`, `desktop`, `vscode`, `vscode_insiders`, `code`, `unknown`)
- `session_kinds[]` (`main`, `subagent`, `task`, `unknown`)
- `date_from/date_to` (ISO)
- or `period_type + period_value` (`day|month|year`)
- date and period filters are mutually exclusive.

Scopes:

- `all` (default)
- `sessions`
- `runtime_events`
- `source_files` (orphans only; warning `SOURCE_FILE_STILL_REFERENCED` when still linked)

## Integration notes

- `genera_mcp_json.sh` / `genera_mcp_json.ps1` include `analytics-mcp-server`.
- Generators propagate only `ANALYTICS_DB_PATH` when set.
- Codex hooks generator passes `--analytics-db-path` with safe Windows quoting.

## Hook log behavior

`hook_log` imports runtime events from `hooks/events.jsonl` (next to DB path).

- Produces `runtime_events` only.
- Does not create `sessions` or message content rows.
- `McpHint` is treated as hook hint (`event_type='hook'`), not real MCP usage. Its `mcp_server` is the suggested target, `tool_name` is present only when that target tool is determinate, and routing hints are recorded only after final hint composition.
- `SkillHint` is tracked as skill hint event.
- Optional Hook fields omitted by legacy log records remain compatible: outcomes stay `NULL`, counters stay `0`, and Hotword context fields stay `NULL` without a warning. Explicit but invalid structured values are imported with compatible fallbacks and a recoverable, privacy-safe `HOOK_FIELD_INVALID` warning that identifies only the field and line number. When all outcome and suggestion-count fields are present and valid, their semantic consistency is also checked: a contradictory combination produces a privacy-safe `HOOK_SUGGESTION_METADATA_INCONSISTENT` warning, preserves valid numeric counts, and stores the outcome as `NULL` so it is excluded from evaluated outcomes. Records with absent legacy fields remain outside that cross-check. `quick_overridden` is `NULL` when a Quick context cannot be evaluated or when Quick does not apply.
- An invalid explicit `hook_phase` produces the same recoverable warning; where the event is known, the importer falls back to its inferred phase.

## Skill operativa

- Primary skill: `mcp-analytics-operator`
- Path: `skills/mcp-analytics-operator/SKILL.md`
- References:
  - `skills/mcp-analytics-operator/references/analytics-workflow.md`
  - `skills/mcp-analytics-operator/references/privacy-rules.md`
  - `skills/mcp-analytics-operator/references/deletion-playbook.md`

Read-only workflow:

1. `analytics_status`
2. `analytics_summary`
3. `analytics_models`
4. `analytics_events`
5. `analytics_sessions` only if session-level detail is required

## Cursor analytics

Unlike every other source, Cursor does not write JSON/JSONL chat logs. Session data lives in a
per-root SQLite file always named `state.vscdb`, found under:

- Windows: `%APPDATA%\Cursor\User\globalStorage\state.vscdb` and `%APPDATA%\Cursor\User\workspaceStorage\<hash>\state.vscdb`
- macOS: `~/Library/Application Support/Cursor/User/globalStorage/state.vscdb` and `.../workspaceStorage/<hash>/state.vscdb`
- Linux: `~/.config/Cursor/User/globalStorage/state.vscdb` and `.../workspaceStorage/<hash>/state.vscdb`

Inside each `state.vscdb`, a generic key/value table `cursorDiskKV` (key TEXT PRIMARY KEY, value TEXT holding JSON) stores:

- Session metadata: `composerData:<composerId>` entries
- Individual messages: `bubbleId:<composerId>:<bubbleId>` entries

**This schema (`cursorDiskKV`, the `composerData:*`/`bubbleId:*` key shapes, and any tool-call
fields inside a bubble) is reverse-engineered from public community sources. It is not an
official, documented Cursor API/schema**, and it has been observed to shift across Cursor
releases. The adapter (`analytics-node/src/adapters/cursor.ts`) is written to degrade gracefully
instead of throwing whenever a field is missing or has an unexpected type/shape -- worst case a
session or a message is skipped with a warning, never a crashed scan.

Known limitations:

- Token usage does not appear to be exposed via this local storage at all, so Cursor sessions/messages always report `token_available = 0`, the same convention other adapters use when native token counters are unavailable.
- Tool/MCP-call extraction from a bubble is best-effort only: it is only emitted as a `runtime_event` when an `mcp__<server>__<tool>`-shaped name can be confidently resolved against the repo's MCP allowlist; anything else is silently dropped rather than guessed at.
- No prompt/response text is read out of the bubbles at all (consistent with the "no raw payload persistence" privacy rule below) -- only role, timestamp, and (when present) model/tool-call metadata.
- If Cursor is running and holding an exclusive lock on `state.vscdb`, or the file is corrupt/has an unexpected schema, the file is opened read-only (`sqlite3.OPEN_READONLY`) and recorded as a file-level failure (`SOURCE_LOCKED_OR_UNREADABLE`/`SOURCE_SCHEMA_UNSUPPORTED`) so it is retried later. A malformed record remains a warning when valid composers exist; if all metadata/bubble rows are malformed or semantically unrecognized, the file fails and prior normalized sessions are preserved. Warning details contain aggregate counts only, never raw metadata values.

## Antigravity analytics

Antigravity (Google's Antigravity IDE/CLI, built on the Gemini stack) writes one JSONL transcript
file per conversation at:

- `~/.gemini/antigravity/brain/<conversation-id>/.system_generated/logs/transcript.jsonl` (IDE variant)
- `~/.gemini/antigravity-cli/brain/<conversation-id>/.system_generated/logs/transcript.jsonl` (CLI variant)

with `transcript_full.jsonl` as a fallback filename only used when `transcript.jsonl` is absent for
that conversation-id -- the scanner never imports both for the same conversation, that would
double-count it. Unlike Copilot/Cursor, these roots are not split by OS/AppData location: they
always live directly under the user's home directory, matching this repo's own existing
install-side convention for `.gemini/antigravity/skills`, `.gemini/config/hooks.json`, `.gemini/agents`.

Each `brain/<conversation-id>` root also contains unrelated files that are **deliberately
excluded** and never treated as chat sessions: `config/`, `agents/`, `plugins/` (including this
repo's own `plugin.json`/`hooks.json` when installed there), a root-level `history.jsonl` index,
and a `conversations/` folder of unparseable protobuf `.pb` files. Only paths matching
`.../brain/<id>/.system_generated/logs/transcript(_full).jsonl` are ever treated as candidates for
this source -- this is a path-pattern check, not a filename/extension check, precisely to avoid
sweeping up config/plugin/history files that also happen to be JSON/JSONL.

**This schema (the `brain/<id>/.system_generated/logs/transcript*.jsonl` path convention, the
per-line `step_index`/`type`/`source`/`created_at`/`tool_calls` fields) is reverse-engineered from
public documentation/community sources, not an official Antigravity/Gemini API or schema** -- the
same posture as Cursor's `state.vscdb` schema below. The adapter
(`analytics-node/src/adapters/antigravity.ts`) degrades gracefully instead of throwing whenever a
line or field is missing/unexpected: a bad line is skipped with a warning, and a file where nothing
recognizable parses is flagged `SOURCE_SCHEMA_UNSUPPORTED` rather than producing an empty session.

Known limitations:

- Token usage is not exposed by this local storage, so Antigravity sessions/messages always report `token_available = 0`, the same convention Cursor uses.
- Tool/MCP-call extraction is best-effort only: a `tool_calls[].name` entry is only turned into a `runtime_event` when it matches the `mcp__<server>__<tool>` shape and resolves against the repo's MCP allowlist; anything else is silently dropped.
- No prompt/response text is read out of the `content` field at all (consistent with the "no raw payload persistence" privacy rule below) -- only `step_index`, `type`/`source` (for role), `created_at`, and `tool_calls[].name` are ever touched.
- `client_surface` is derived purely from which root the file was found under: `cli` for `.gemini/antigravity-cli`, `code` for `.gemini/antigravity`.

`summary.by_source[].messages` has the same KPI meaning as `session_totals.messages`: it is the
sum of `user_message_count` for `main` sessions only, and `by_source` contains only the sources
selected in the request. `hook_log` therefore reports zero sessions and messages, while it
contributes to metrics derived from runtime events. Claude Desktop storage is not imported, so its presence does not create
file-level warnings or failures.

## Troubleshooting (Windows/Ubuntu)

- DB unavailable: verify `ANALYTICS_DB_PATH` and directory permissions.
- Missing sessions/models/events: run targeted `analytics_scan` on the specific source and inspect warnings.
- Missing tokens: if native token counters are not present in source logs, token values remain unavailable (no estimation).
- Missing hook events: verify Sophia hooks are writing `events.jsonl` near analytics DB.
- Parser/source-format warning: treat as adapter-format warning and avoid forcing unsafe import.
- Delete blocked by plan mismatch: rerun dry-run and use the new `confirm_plan_id`.

## Smoke tests

Unified smoke coverage is maintained under `/tests` (single consolidated analytics smoke file for milestone progression).
