Documentation
¶
Overview ¶
Package goose implements the event-level adapter for the Goose CLI.
Goose keeps ONE SQLite database per data root — <data-dir>/sessions/sessions.db — and writes a purpose-built usage ledger into it. This adapter reads that ledger, `usage_ledger`, and nothing else that counts tokens:
usage_ledger(id, session_id, created_timestamp, model,
input_tokens, output_tokens, total_tokens,
cache_read_tokens, cache_write_tokens,
cost, cost_source, is_compaction)
WHY THE LEDGER AND NOT `sessions`. The `sessions` table carries lifetime counters stamped at the session's created_at, and its `total_tokens` column is ASSIGNED, not accumulated: goose's own UPDATE sets `total_tokens = ?` from the CURRENT turn while incrementing `accumulated_total_tokens` beside it (session_manager.rs record_usage_metrics). Reading `total_tokens` as a total reports the last turn as though it were the session; reading the ledger AND `accumulated_*` double counts, because SUM(usage_ledger) == accumulated_* by construction. So this adapter reads the ledger and NEVER selects a token column of `sessions` — only its `working_dir` (project) and `provider_name` (billing identity), neither of which is a counter. TestQueryReadsNoSessionCounters parses the queries and fails on the day one appears.
CARRIED-FORWARD ROWS ARE INCLUDED. A row with cost_source='carried_forward' is inserted as MAX(sessions.accumulated_* - SUM(usage_ledger.*), 0) under a WHERE that only fires when the accumulator is AHEAD of the ledger: it is the GAP, not a duplicate. Filtering it out undercounts. Those rows carry NO model (goose's INSERT ... SELECT never binds one), so a model-is-required guard — the obvious way to drop junk rows — would silently delete exactly the reconciliation this ledger depends on.
TOKEN SPLIT. Goose normalises EVERY provider to a cache-INCLUSIVE input: "input_tokens is the total input including cache read/write tokens; the cache fields are breakdown subsets of it" (token_usage.rs), and its total is input + output with the cache already inside. aiusage's accounting is the Anthropic one — input EXCLUSIVE of cache, and pricing charges input, cache read and cache write as three separate lines (pricing.Rates.Cost) — so the cached tokens are subtracted out of input here. Passing goose's input through unchanged would bill every cached token twice: once at the input rate, once at the cache rate. TotalTokens stays the provider's own number, which the split reconciles to exactly.
COST. `cost` is NULL under every provider goose has no price for (measured on this machine: an ollama session, cost and cost_source both NULL), and an unpriced event is not a free one — a NULL cost stays nil, never 0. A real cost is stamped with the source goose recorded it under ("goose-provider_reported" / "goose-estimated" / "goose-carried_forward"), and the collector preserves this vendor stamp. Its pricing ladder applies only when the source supplies no usable cost.
ACTIVITY. Tool calls come from `messages`, which is a different table with a watermark of its own. They are NEVER attributed: usage_ledger rows carry no message id, and two ledger rows commonly share one created_timestamp (both of the tool-call session's rows landed on the same second locally), so a timestamp match would be a positional guess. UsageDedupKey stays empty and the store reports the calls as unattributed rather than as free. See activity.go.
CRITICAL: strictly read-only. The DSN is mode=ro plus query_only(1) plus busy_timeout — never immutable=1, because goose holds this database open in WAL mode and writes it live; an immutable reader ignores the WAL and would read a database frozen at its last checkpoint.
Index ¶
- Constants
- func New() adapter.Adapter
- type Adapter
- func (Adapter) Capabilities() model.ToolCapability
- func (a Adapter) Collect(ctx context.Context, src adapter.Source) (adapter.Observation, error)
- func (a Adapter) CollectIncremental(ctx context.Context, src adapter.Source, cp *model.SourceCheckpoint) (adapter.Observation, error)
- func (a Adapter) Discover(ctx context.Context, cfg adapter.DiscoverConfig) ([]adapter.Source, error)
- func (Adapter) DisplayName() string
- func (Adapter) ID() string
Constants ¶
const ( // PathRootEnv relocates every Goose directory at once. goose reads it as an // absolute path only (`validated_path_root` filters on is_absolute) and // derives its data directory as <root>/data, so the database this adapter // opens is <GOOSE_PATH_ROOT>/data/sessions/sessions.db — NOT // <GOOSE_PATH_ROOT>/sessions/sessions.db. PathRootEnv = "GOOSE_PATH_ROOT" // DataHomeEnv is the XDG data root goose falls back to when PathRootEnv is // unset: <XDG_DATA_HOME>/goose, defaulting to ~/.local/share/goose. DataHomeEnv = "XDG_DATA_HOME" )
Variables ¶
This section is empty.
Functions ¶
Types ¶
type Adapter ¶
type Adapter struct{}
Adapter reads the Goose session database. Read-only.
func (Adapter) Capabilities ¶
func (Adapter) Capabilities() model.ToolCapability
Capabilities declares what this project can say about Goose.
Cost is VENDOR-reported: goose.go stamps priceSource from the provider figure the usage_ledger carries, and a NULL cost is left unpriced rather than stamped 0 — so a goose row is either vendor-valued or unpriced, never an estimate. Activity is RECORDED BUT UNATTRIBUTED because usage_ledger rows carry no message id and two of them commonly share one second, so a timestamp match would be a positional guess.
func (Adapter) CollectIncremental ¶
func (a Adapter) CollectIncremental(ctx context.Context, src adapter.Source, cp *model.SourceCheckpoint) (adapter.Observation, error)
CollectIncremental reads only what the checkpoint has not seen: ledger rows above the rowid watermark and message rows above their own. Both tables are append-only in goose (INTEGER PRIMARY KEY AUTOINCREMENT, no UPDATE anywhere, and AUTOINCREMENT never reuses an id after a session delete), which is what makes a rowid watermark sound rather than merely convenient.
A nil cp is a full read. The checkpoint is written ONLY when the read completed cleanly: advancing the file stamps past an unreadable row would gate the whole database shut and never retry it.
func (Adapter) Discover ¶
func (a Adapter) Discover(ctx context.Context, cfg adapter.DiscoverConfig) ([]adapter.Source, error)
Discover locates each <data-dir>/sessions/sessions.db that exists as a regular file.
func (Adapter) DisplayName ¶
DisplayName returns the human-friendly name.