goose

package
v0.1.1 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Oct 4, 2026 License: MIT Imports: 14 Imported by: 0

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

View Source
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

func New

func New() adapter.Adapter

New returns a Goose adapter.

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) Collect

func (a Adapter) Collect(ctx context.Context, src adapter.Source) (adapter.Observation, error)

Collect reads a source in full.

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

func (Adapter) DisplayName() string

DisplayName returns the human-friendly name.

func (Adapter) ID

func (Adapter) ID() string

ID returns the stable tool identifier.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL