Documentation
¶
Overview ¶
Package store persists per-session token/cost history so the dashboard can render a cumulative-cost chart (V13) that survives daemon restarts.
The store is a thin wrapper over SQLite (github.com/mattn/go-sqlite3, CGo — picked for raw retrieval speed; see project_v13_storage_decision). One table (cost_points) holds append-only samples; a compound index on (session, ts) keeps Range queries cheap regardless of how many sessions have landed rows.
Required server.go wiring (coordinator owns this) ¶
Open the DB after `hub := events.NewHub(0)`:
costDB, err := store.OpenCostStore(filepath.Join(config.Dir(), "ctm.db"))
if err != nil { return nil, fmt.Errorf("open cost db: %w", err) }
Attach it to the Server struct (field `cost store.CostStore`) and close it in Run's shutdown path:
defer costDB.Close()
Subscribe a goroutine to the hub that writes `quota_update` events carrying `session` + token triples into the store — see store.SubscribeQuotaWriter for the helper.
Mount the handler in registerRoutes:
mux.Handle("GET /api/cost", authHF(api.Cost(s.cost)))
Package store — quota_update → cost_points subscriber.
Wired in server.go (coordinator-owned):
costDone := make(chan struct{})
go func() {
defer close(costDone)
store.SubscribeQuotaWriter(runCtx, hub, costDB)
}()
No cancellation channel is returned; the goroutine exits when SubscribeQuotaWriter's ctx is cancelled (wired off the daemon's root ctx so shutdown draining matches the attention/webhook pattern in server.go).
Index ¶
- Constants
- func ComputeCostMicros(input, output, cache int64) int64
- func SubscribeQuotaWriter(ctx context.Context, hub *events.Hub, store CostStore, ready chan<- struct{})
- func SubscribeToolCallWriter(ctx context.Context, hub *events.Hub, idx ToolCallIndexer, ...)
- type CostStore
- type Point
- type SearchMatch
- type SearchStore
- type ToolCallIndexer
- type Totals
Constants ¶
const ( PriceInputPerMillionMicros int64 = 3_000_000 PriceOutputPerMillionMicros int64 = 15_000_000 PriceCachePerMillionMicros int64 = 300_000 )
Per-million-token input/output/cache prices used to compute cost_usd_micros. Values track Claude Sonnet 4.5 public pricing (input $3/Mt, output $15/Mt, cache read $0.30/Mt). We store integers (USD * 1_000_000) rather than floats so SUM() stays exact across the seven-day window.
Variables ¶
This section is empty.
Functions ¶
func ComputeCostMicros ¶
ComputeCostMicros returns USD * 1e6 for the given token triple. Exported for tests and so handlers never have to redo the math.
func SubscribeQuotaWriter ¶
func SubscribeQuotaWriter(ctx context.Context, hub *events.Hub, store CostStore, ready chan<- struct{})
SubscribeQuotaWriter subscribes to the hub's quota_update stream and persists each per-session update as a cost_points row. Blocks until ctx is cancelled or the subscription is closed by the hub.
If ready is non-nil, it is closed as soon as the hub subscription is registered — callers that need to race-free publish between the "start this goroutine" and "publish the first event" lines should wait on ready first. Nil is fine for production, where events arrive asynchronously from the quota ingester well after startup.
Write errors are logged and swallowed — a failed persistence must not take down the daemon, and the next update will carry the same cumulative token counts so no data is lost permanently.
func SubscribeToolCallWriter ¶
func SubscribeToolCallWriter( ctx context.Context, hub *events.Hub, idx ToolCallIndexer, ready chan<- struct{}, )
SubscribeToolCallWriter subscribes to every tool_call event and writes a searchable row to the FTS index. Returns when ctx is cancelled or the subscription channel closes. `ready`, if non-nil, is closed once the subscription attaches — tests wait on it to avoid racing the hub.
Types ¶
type CostStore ¶
type CostStore interface {
// Insert appends a batch of points in a single transaction. A nil
// or empty slice is a no-op (returns nil).
Insert(points []Point) error
// Range returns every point for session (or all sessions if
// session == "") with ts ∈ [since, until], sorted oldest-first.
Range(session string, since, until time.Time) ([]Point, error)
// Totals aggregates all points with ts >= since across every
// session. The caller picks the time window; the store has no
// opinion about what "total" means.
Totals(since time.Time) (Totals, error)
// Close releases the underlying DB handle. Idempotent.
Close() error
}
CostStore is the persistence seam. Handlers depend on the interface so tests can swap in an in-memory fake.
func OpenCostStore ¶
OpenCostStore opens (or creates) the SQLite DB at path and applies the V13 schema. Callers should Close() on shutdown.
WAL + NORMAL sync is used for write throughput; busy_timeout=5000ms keeps the handler-side Writer from erroring under light contention with the quota-subscriber goroutine.
type Point ¶
type Point struct {
TS time.Time
Session string
InputTokens int64
OutputTokens int64
CacheTokens int64
CostUSDMicros int64 // USD * 1_000_000
}
Point is a single persisted cost sample.
type SearchMatch ¶
SearchMatch mirrors api.SearchMatch in wire form. Exported so the server.go adapter can type-assert cleanly (shared field shape keeps the api package free of the store dependency).
type SearchStore ¶
type SearchStore interface {
// IndexToolCall appends one searchable row. Idempotency is the
// caller's problem — OpenCostStore wipes the FTS table on boot so
// the tailer's offset-0 replay repopulates it fresh.
IndexToolCall(session, tool, content string, ts time.Time) error
// SearchFTS returns at most limit matches for q, optionally filtered
// by session. The boolean return reports truncation.
SearchFTS(q, sessionFilter string, limit int) ([]SearchMatch, bool, error)
}
SearchStore is the persistence seam for the FTS5 index. sqliteCostStore implements both CostStore and SearchStore so a single *sql.DB handle backs every V13/V19 write path.
type ToolCallIndexer ¶
ToolCallIndexer is satisfied by sqliteCostStore. Kept narrow so tests can swap in a spy.