store

package
v0.5.6 Latest Latest
Warning

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

Go to latest
Published: Jul 12, 2026 License: AGPL-3.0 Imports: 22 Imported by: 0

Documentation

Overview

Package store is the akari-server data layer: a Postgres connection pool, the startup migration runner, and the query methods the rest of the server uses.

Index

Constants

View Source
const DefaultSort = "updated"

DefaultSort is the global session list's order when none is requested: most recently active first, matching the feed's "find a recent run" purpose.

View Source
const ModelFallbackListCap = 100

ModelFallbackListCap is the standard cap on how many fallback rows a caller reads for one session. sessions.model_fallback_count is the session-wide total and rides every view; this list is only the first N by occurrence, so a reader stays bounded no matter how pathological the session (a transcript that fell back thousands of times cannot blow up a tooltip, a transcript-notice map, or an MCP payload). Callers pass this to SessionModelFallbacks and lean on the count for the true total.

View Source
const (
	TranscriptTailTurns = 50
)

The web transcript renders a bounded window instead of the whole session (an unbounded server render is what froze the tab on long sessions). These constants bound every windowed read:

  • TranscriptTailTurns: how many user turns the initial page and each "Show earlier" fetch cover. A turn is one user message plus the assistant run that follows, so the window boundary always lands on a prompt, never mid-answer.
  • transcriptPageMessageCap: the hard message bound behind the turn count, so a pathological session (one prompt, thousands of assistant rows) still cannot blow up a single fetch.
  • transcriptSeedLookback: how many rows immediately before a window ride along unrendered, to prime the render walker's carried state (reply latency, shed detection) at the window boundary. A prompt is almost always within a few rows of its reply, so a short fixed lookback recovers the boundary instruments without scaling the read; a turn whose anchor sits deeper than the lookback shows no latency stamp, which the plan accepts.

Variables

View Source
var AllInsightsPanels = InsightsPanels{
	FleetMix: true, Gallery: true, Velocity: true, Tools: true,
	Churn: true, Economics: true, Subagents: true,
}

AllInsightsPanels asks for every instrument group, the fleet /insights page's set.

View Source
var ErrBlobHashMismatch = errors.New("uploaded blob bytes do not match the declared hash")

ErrBlobHashMismatch reports that the uploaded bytes did not hash to the declared key, so the body was not stored. The handler maps it to a 400: it is the client's error, not a server fault.

View Source
var ErrBlobNotUploaded = errors.New("referenced tool body is not present in the CAS")

ErrBlobNotUploaded reports that a tool body the transcript references by sha256 is not in the CAS. Under the client-CAS protocol the client uploads every body before the transcript that references it, so this means an out-of-order or dropped upload; the parse leaves the cursor for a retry rather than inventing a dangling reference.

View Source
var ErrChunkNotLineAligned = errors.New("chunk must be non-empty and end on a newline")

ErrChunkNotLineAligned reports a chunk that is empty or does not end on a newline. The ingest protocol requires every stored byte to rest on a JSONL line boundary so the server only ever parses complete lines; the server enforces it rather than trusting the client.

View Source
var ErrInvalidGrant = errors.New("invalid or expired grant")

ErrInvalidGrant is returned when an authorization code or refresh token is unknown, already used, expired, or revoked. It maps to the OAuth "invalid_grant" error at the token endpoint.

View Source
var ErrInvalidInvite = errors.New("invalid or already used invite token")

ErrInvalidInvite is returned when registration presents an invite token that is unknown, already redeemed, or expired.

View Source
var ErrNotFound = errors.New("not found")

ErrNotFound is returned by lookups that match no row.

View Source
var ErrOAuthRegistrationLimit = errors.New("oauth registration limit reached")

ErrOAuthRegistrationLimit is returned when the shared rolling-hour ceiling has been reached. The database transaction that enforces it is serialized across server replicas, so a deployment cannot multiply durable growth by adding processes.

View Source
var QualityBandPanels = InsightsPanels{Tools: true, Churn: true}

QualityBandPanels is the project quality band's set (authed and public): the signal series come with the core, and the band renders the Tools instrument beside them.

Functions

func HashBytes

func HashBytes(content []byte) string

HashBytes returns the lowercase hex sha256 of content, the key the CAS uses.

func HashString

func HashString(content string) string

HashString returns the lowercase hex sha256 of content. It hashes in place (the digest consumes the string in 64-byte blocks), so a large body is never copied into a byte slice just to be hashed.

func IsSortKey

func IsSortKey(key string) bool

IsSortKey reports whether key names a sortable column of the global session list, so the handler can reject an unknown or tampered sort param before it reaches the query builder.

Types

type APIToken

type APIToken struct {
	ID         int64
	UserID     int64
	Name       string
	Scope      string
	CreatedAt  time.Time
	LastUsedAt *time.Time
	RevokedAt  *time.Time
}

APIToken is a stored client token (the secret itself is never stored, only its hash).

type Analytics

type Analytics struct {
	Series []DayPoint
	Models []Breakdown
	Agents []Breakdown
	// Users is the by-owning-user split, the same shape as Models and Agents. The
	// overview only renders it once more than one user has usage in scope (see
	// analyticsByUser): a single-user instance or a single-user filter (the public
	// overview, scoped by the handler to one account) gains nothing from a breakdown
	// of one row, so the row still populates here and the view decides whether to
	// show it.
	Users           []Breakdown
	TotalCost       float64
	TotalIn         int64
	TotalOut        int64
	TotalCacheRead  int64
	TotalCacheWrite int64
	// TotalReasoning is the window's reasoning-token volume, summed from the by-agent split
	// like the other totals. It sits beside TotalTokens rather than inside it (see
	// Breakdown.Reasoning for why), so the Tokens tile shows it as a distinct class without
	// disturbing the headline-equals-sum-of-series reconciliation the four billed classes hold.
	TotalReasoning int64
	Sessions       int
	// CostIncomplete is true when any usage event in the window carried token
	// volume but no price, so TotalCost is a lower bound. The headline Cost tile
	// shows the "$X+" marker when set, matching how a single session flags an
	// incomplete cost. It is the OR of the by-agent slices, the same rows the
	// headline totals are summed from.
	CostIncomplete bool
	// Cache is the prompt-cache effectiveness over the same scope: hit rate, the
	// dollars caching saved, and the prompt-token split. It reads from the same dated
	// usage_events base as the totals (see CacheStats), so the Cache tile reconciles
	// with the Tokens tile beside it rather than counting usage the panel drops.
	Cache CacheStats
}

Analytics is everything the inline charts render from, scoped by an AnalyticsFilter (a project or the whole instance, a trailing window, and an optional user/agent/machine narrowing).

func (Analytics) HasData

func (a Analytics) HasData() bool

HasData reports whether there is anything worth charting, so the view can show an empty state instead of an axis with no line.

func (Analytics) TotalTokens

func (a Analytics) TotalTokens() int64

TotalTokens is the combined token volume across all four classes (input, output, cache read, cache write). The overview's Tokens readout shows this one figure and keeps the per-class split behind its tooltip.

type AnalyticsFilter

type AnalyticsFilter struct {
	ProjectID int64
	Since     time.Time
	// Until is an exclusive upper bound on occurred_at; the zero value means no upper
	// bound (every figure through the latest event). The OG card sets it to the end
	// of the current day so its headline and caption cover exactly the days its
	// heatmap draws (the grid stops at today), rather than folding a future-dated
	// event into the total that no visible cell accounts for. The live pages leave it
	// zero.
	Until   time.Time
	UserIDs []int64
	// Username scopes by a single account name, the form the project page's filter
	// carries. It is independent of UserIDs (the overview's multi-select by id): an
	// unknown name resolves to no session, the same empty result the session list's
	// u.username = $ predicate gives, so the panel and the table stay in lockstep
	// even for a stale or mistyped user rather than the panel falling back to every
	// user while the table shows nothing.
	Username string
	Agent    string
	Machine  string
	// OmitUsers skips the by-owning-user cost split in Analytics (analyticsByUser,
	// leaving Analytics.Users nil) when the caller renders no by-user breakdown. The
	// split groups every matching row by user and materializes one aggregate per
	// distinct user, so its size grows with the scope's user count. Standalone callers
	// such as the project OG card set this when no authenticated view shares their
	// result. Insights ignores it: its
	// optional instrument groups are named per call via InsightsPanels instead.
	OmitUsers bool
	// Bucket, when non-empty ("day" or "week"), asks Insights to also compute the trend
	// series (Insights.Trends): the same scoped cohort aggregated into a time-bucket grid
	// for the charts. It is the one Insights input the cost/token Analytics path ignores;
	// leaving it empty (the OG card, any caller that renders no charts) skips the extra
	// per-bucket scans entirely, so Insights stays the distributions-only snapshot it was.
	Bucket string
}

AnalyticsFilter scopes an Analytics query. The zero value is the whole instance, all of history, every user, every agent and machine. ProjectID 0 means all projects; a zero Since means all of history; an empty UserIDs/Agent/Machine means no scoping on that dimension. The project page sets Agent/User/Machine so its usage panel reflects the same filter as its session table, which keeps the panel headline and the rows beneath it reconciled under a filter rather than letting the headline stay instance-wide while the rows narrow.

type AnnounceParams

type AnnounceParams struct {
	UserID          int64
	Agent           string
	SourceSessionID string
	ProjectID       int64
	Kind            string
	GitBranch       string
	Cwd             string
	Machine         string
	// Terminal is the client's assertion that this session is finished (an
	// `akari sync --finalize` on an ephemeral host). It is persisted sticky (OR'd
	// onto the stored flag) and OR'd into the server-side idle checks so the session
	// grades immediately rather than waiting out the abandoned-idle window. A normal
	// watch-loop announce leaves it false.
	Terminal bool
}

AnnounceParams carries what a client reports when announcing a session. Kind is the session's classification ("remote", "standalone", or "orphaned"); it gates the downgrade guard in Announce.

type AnnounceResult

type AnnounceResult struct {
	SessionID    int64
	StoredBytes  int64
	PrefixSHA256 string
}

AnnounceResult is the server's authoritative view of a session's raw store.

type AttachmentDelta

type AttachmentDelta struct {
	MessageOrdinal int
	SHA256         string
	Body           string
	Bytes          int64
	MediaType      string
	Filename       string
}

AttachmentDelta is one attachments row (today a lifted image). Like a tool body it reaches the CAS by one of two paths: when the client lifted the image, SHA256 names the already-uploaded blob and the rebuild records the reference with no blob write; otherwise Body holds the decoded bytes inline for the server to store. Bytes and MediaType describe the decoded image so the row carries its size and type without fetching the blob.

type AttachmentView

type AttachmentView struct {
	MessageOrdinal int
	SHA256         string
	MediaType      string
	ByteLen        int64
	Filename       string
}

AttachmentView is one attachment (today a lifted image) rendered under its message: the blob key plus enough metadata to show or link the image without fetching it. The bytes are served on demand through the session-scoped blob route.

type AuthCode

type AuthCode struct {
	ClientID      string
	UserID        int64
	RedirectURI   string
	CodeChallenge string
	Scope         string
	Resource      string
}

AuthCode is the data an authorization code carries from the authorize step to the token exchange: who approved it, for which client and redirect, the PKCE challenge the exchange must answer, and the scope and audience it grants.

type Blob

type Blob struct {
	SHA256      string
	ByteLen     int64
	MediaType   string
	ContentType string
}

Blob is a stored content-addressed body: its key, stored size, semantic media type, and storage content type. The bytes themselves live in a Postgres large object referenced by lo_oid and are stored exactly as the client uploaded them. ContentType names how those bytes are encoded (application/octet-stream verbatim, or application/zstd compressed): the server never (de)compresses, so it serves this as the response's Content-Encoding and lets the client decode. ByteLen is the stored (possibly compressed) size; the raw body size lives on the tool_calls row.

type Breakdown

type Breakdown struct {
	Label      string
	CostUSD    float64
	Input      int64
	Output     int64
	CacheRead  int64
	CacheWrite int64
	// Reasoning is the slice's reasoning-token volume, the class some agents (codex, pi)
	// report for the model's hidden deliberation. It is tracked and shown on its own but
	// deliberately excluded from Tokens(): reasoning is neither a prompt nor a cache class,
	// so folding it into the bar-sizing total would double-count against the billed classes
	// and unsettle the headline-equals-sum reconciliation. It surfaces as its own figure.
	Reasoning int64
	Sessions  int
	// CostIncomplete is true when this slice folded in a usage event that carried
	// real token volume but no price (an unpriced model), so the slice's cost is a
	// lower bound. It lets a by-model or by-agent row show the same "$X+" marker the
	// per-session figures use rather than an exact cost that understates the slice.
	CostIncomplete bool
}

Breakdown is one slice of a by-model or by-agent split: a label with its rolled cost, its token volume broken out by class, and how many sessions it touched. The per-class split lets a slice both size its bar on the full token volume and reproduce the same hover card every other token figure carries.

func (Breakdown) Tokens

func (b Breakdown) Tokens() int64

Tokens is the all-class token volume (input, output, cache read, cache write) for the slice. The breakdown bars size and label on this, so a model's share reflects everything it was billed for. Sizing on uncached in/out alone (the old behavior) made cache-heavy models like Claude read as mispriced: a bar a third the width of its cost, because the cache tokens that drove the cost were absent from the figure beside it.

type CacheStats

type CacheStats struct {
	Input      int64 // uncached prompt tokens, billed at the input rate
	Output     int64
	CacheRead  int64 // prompt tokens served from cache (the discounted read)
	CacheWrite int64 // prompt tokens written to cache (creation)
	SavingsUSD float64
	// SavingsIncomplete is true when some cached read or write volume rode an unpriced
	// model, so that model's saving is omitted and SavingsUSD is partial. Unlike cost,
	// this is NOT a clean lower bound: an omitted model's saving can be negative (a
	// Claude cache write is priced above input, a cost paid up front), so the true
	// figure could be lower OR higher than what is shown. The Cache readout flags it
	// "partial" rather than the cost figures' "$X+" lower-bound marker.
	SavingsIncomplete bool
}

CacheStats summarizes prompt-cache effectiveness over a scope: the prompt token volume split into uncached input, cached reads, and cache writes (creation), the output volume alongside, and the USD caching saved versus paying the uncached input rate for the same prompt tokens. It backs the Cache readout on the overview, project, and session views, the cache counterpart to the cost and token figures beside it.

func (CacheStats) HasData

func (c CacheStats) HasData() bool

HasData reports whether any prompt tokens were seen, so a view can show an empty state instead of a 0% hit rate and a $0 saving on a scope with no usage.

func (CacheStats) HitRate

func (c CacheStats) HitRate() float64

HitRate is the share of prompt tokens served from cache, 0..1. Cache writes count as misses: a token written to cache was read fresh on that turn, and only a later read of it is a hit. Zero when there are no prompt tokens, so a no-usage scope reads 0% rather than dividing by zero.

func (CacheStats) PromptTokens

func (c CacheStats) PromptTokens() int64

PromptTokens is the total prompt-side token volume: uncached input plus cached reads plus cache writes. Output is excluded; caching is a prompt-side economy, so the hit rate measures the prompt, not the completion.

type ChurnFile

type ChurnFile struct {
	ProjectID int64
	Project   string // the project's display label (remote_key, or display_name for a standalone/orphaned project)
	Path      string
	Edits     int
	Sessions  int
}

ChurnFile is one file's edit thrash over a scope: how many times it was edited and across how many sessions. The edits are deduped (a replayed transcript re-emits prior edits, so the raw rows over-count), the same dedup the tool analytics and the per-session signals apply. The file is keyed per project on its worktree-invariant relative path (the projection's file_rel_path, coalesced onto the absolute file_path when no relative form exists), so one repo file edited from several git worktrees reads as a single row rather than one per checkout. Project and ProjectID carry the owning project so the panel can label each bar and two projects that share a relative path stay distinct.

type ChurnNode added in v0.3.0

type ChurnNode struct {
	Project  string
	Folder   string
	Path     string
	Edits    int
	Sessions int
}

ChurnNode is one row of the file-churn tree: a project, folder, or file, with the edits it absorbed and the sessions that returned to it. Project and Folder locate it in the drill hierarchy; Path is the full worktree-relative path for a file node.

type ChurnTrend added in v0.3.0

type ChurnTrend struct {
	ReEdits       []int // deduped edits of hot files per bucket
	Files         []int // hot files (edited more than once in the window) per bucket
	Tree          []ChurnNode
	Clipped       int // hot files beyond the tree cap, noted rather than shown
	TotalReEdits  int // deduped edits of hot files across the window (sums the tree's edits, before the cap)
	TotalHotFiles int // distinct files re-edited (more than once) across the window
	// Projects is the uncapped count of distinct projects the hot-file cohort spans, before the
	// tree's maxChurnTreeFiles cap. The treemap uses it to tell a genuinely single-project window
	// (root at that project's folders) from a multi-project one whose capped tree happens to show
	// one project (keep the project-level breakdown). Reading the capped tree's project list
	// instead would misjudge a window whose top-N files all sit in one project while a clipped
	// file belongs to another.
	Projects int
}

ChurnTrend is the edit-thrash read over time plus the tree the treemap drills: how much re-editing happened per bucket and across how many hot files, and the per-file edit counts grouped by project and folder. The re-edit figures count only hot files (edited more than once across the window), the same set the tree renders, so the headline totals reconcile with the file breakdown below them.

type ConcurrencyStats

type ConcurrencyStats struct {
	FleetPeak       int       // most sessions active simultaneously, anywhere in scope
	FleetPeakAt     time.Time // when the fleet peak was first reached
	BusiestUser     string    // the user whose own sessions overlapped the most
	BusiestUserPeak int       // that user's peak simultaneous sessions
	AvgConcurrent   float64   // total active session-time over the span the sessions cover
	Sessions        int       // sessions with a measurable span in scope
}

ConcurrencyStats answers "how many sessions ran at once" over a scope: the fleet's peak overlap and when it happened, the single busiest user's peak, and the average concurrency across the active span. It reads from session start/end spans, so it sees the same scoped sessions the rest of the Insights page does. A session with no parsed start or end (or an end before its start) carries no measurable span and sits out.

func (ConcurrencyStats) HasData

func (c ConcurrencyStats) HasData() bool

HasData reports whether any scoped session had a measurable span, so the panel can show an empty state rather than a peak of zero.

type ContextBucket added in v0.3.0

type ContextBucket struct {
	Lo    int64
	Hi    int64
	Count int
}

ContextBucket is one log-scale bin of the peak-context histogram: [Lo, Hi) tokens and how many sessions peaked inside it.

type ContextHealthStats

type ContextHealthStats struct {
	Sessions          int   // scoped sessions with a measured peak, the rate denominator
	PeakTokensP50     int64 // median session peak context, in tokens
	PeakTokensP90     int64 // 90th-percentile session peak context
	PeakTokensMax     int64 // heaviest single session peak
	TotalResets       int64 // inferred context resets summed over the cohort
	SessionsWithReset int   // sessions that reset context at least once
}

ContextHealthStats is the cohort's context-load picture over a scope: how heavy the scoped sessions' contexts got and how often they shed that context. The peak percentiles read straight off the stored per-session peaks (quality.ContextHealthFolder, materialized by the settle pass or a reparse); the reset figures sum the inferred compaction/clear counts. The cohort is the scoped sessions carrying a current-version signals row whose peak is measured (a session with no usage stores NULL and is left out), so every rate divides by the same measured set. A stale or missing signals row contributes nothing, the same way the quality distribution folds it into unknown, so the panel never mixes a half-rebuilt view.

func (ContextHealthStats) HasData

func (h ContextHealthStats) HasData() bool

HasData reports whether the scope carried any measured session, so the panel can show a note rather than a row of zeroes for a window with no context-measured sessions.

type ContextMarker added in v0.3.0

type ContextMarker struct {
	Tokens int64
	Kind   string
}

ContextMarker annotates the histogram with an order statistic at a token position. Kind names the statistic ("p50", "p90", "max"); the axis label is formatted at render time in web, so the store carries no presentation strings.

type DayPoint

type DayPoint struct {
	Day        time.Time
	CostUSD    float64
	Input      int64
	Output     int64
	CacheRead  int64
	CacheWrite int64
}

DayPoint is one day's aggregated usage, the unit of the analytics time series. Days with no priced usage events are absent rather than zero-filled; the chart layer decides how to render gaps.

type DueSession added in v0.3.0

type DueSession struct {
	ID         int64
	Agent      string
	EpochStale bool
}

DueSession is one session the parse worker should rebuild: its projection is behind its raw bytes or behind the running parser epoch. EpochStale says the epoch is what made it due (possibly alongside new bytes), which is the subset a fleet rebuild's progress counts.

type Economics added in v0.3.0

type Economics struct {
	CostCompleted []float64 // dollars spent in bucket i by sessions that completed
	CostAbandoned []float64 // dollars spent in bucket i by sessions that abandoned (outcome='abandoned')
	CostOther     []float64 // dollars spent in bucket i by every other outcome (errored, unknown, ungraded)
	CacheSavings  []float64 // dollars caching saved in bucket i, priced per day and model
	CacheHitRate  []float64 // cache-read share of all prompt-side tokens (input+read+write), percent
	CacheMeasured []bool    // whether bucket i had prompt-side tokens, so a 0 rate reads as measured 0% not "no data"

	TotalSpend         float64 // all spend across the window, every outcome
	TotalAbandoned     float64 // spend by abandoned sessions across the window
	AbandonedSharePct  float64
	TotalCacheSavings  float64
	CacheHitRateLatest float64 // the latest measured bucket's hit rate (a real 0% included), 0 when no bucket was measured

	// CostIncomplete is true when the window folded in a token-bearing usage event with no
	// price, so every spend figure here is a lower bound, the same flag Analytics carries.
	CostIncomplete bool
	// AbandonedIncomplete is CostIncomplete narrowed to the abandoned subset: true when an
	// abandoned session's usage carried token volume with no price, so TotalAbandoned alone is a
	// lower bound. It is separate from CostIncomplete because a window can be incomplete on its
	// completed spend while its abandoned spend is fully priced (or the reverse), so the
	// abandoned subfigure must carry its own marker rather than the whole window's.
	AbandonedIncomplete bool
	// CacheSavingsIncomplete is true when cached read or write volume rode a model the pricing
	// table cannot price, so the savings total omits it. The omitted term can be either sign, so
	// this is "partial", not a lower bound, matching CacheStats.SavingsIncomplete.
	CacheSavingsIncomplete bool
}

Economics is the per-bucket money read: spend split by outcome class (completed, abandoned, and everything else) so the three bands sum to the window's total spend and the abandon rate carries a dollar figure, plus what caching saved.

type FacetCount

type FacetCount struct {
	Value string
	Count int
}

FacetCount is one filter value and how many sessions carry it, for a faceted filter rail that shows counts beside each option.

type FacetValues

type FacetValues struct {
	Agents   []string
	Machines []string
	Users    []string
}

FacetValues holds the distinct filter values available within a project's sessions, for populating the session-list filter dropdowns.

type FileChurn

type FileChurn struct {
	Files   []ChurnFile
	Clipped int // churned files beyond the shown list
}

FileChurn is the cohort's edit-thrash picture: the files edited more than once in the window, the most-edited first. A file touched once is not churn and never appears. It is the fleet counterpart to the per-session edit_churn signal, pointing at the paths a fleet kept returning to (a sign of a hard spot, a flaky change, or a moving target).

func (FileChurn) HasData

func (c FileChurn) HasData() bool

HasData reports whether any file was edited more than once, so the panel can show a note rather than an empty list for a window with no repeated edits.

type FleetMix added in v0.3.0

type FleetMix struct {
	Models []ModelSeries

	// NewestModel and NewestFirst name the newest arrival: the scoped model whose
	// first tokens EVER land latest, provided that first day falls inside this grid
	// past its first bucket. Arrival is a fleet-history fact, not a window-relative
	// one, and it is computed over every model, not just the kept bands. Both halves
	// of that matter: a window-relative "first bucket with tokens" would call any
	// incumbent with a quiet first day an arrival on a short window, and the top-N
	// fold (ranked by whole-window tokens) buries a true arrival in "other" on a long
	// one; either way two windows would name different "newest" models over the same
	// corpus. NewestModel is empty (NewestFirst -1) when nothing arrived inside the
	// window: incumbents predate it, and "unknown" usage is skipped because a batch
	// of unattributed tokens is not a model's story.
	NewestModel string
	NewestFirst int
}

FleetMix is the per-bucket token share by model, the stacked-area read of a model migration: a new model eating an incumbent's share shows up here as one band growing as another shrinks, without reading release notes.

func (FleetMix) HasData added in v0.3.0

func (f FleetMix) HasData() bool

HasData reports whether any model carried tokens in the window.

type Gallery struct {
	Rows  []GallerySession
	Total int

	// Window-wide summary figures, computed over the full cohort rather than the capped Rows, so
	// the headline medians and the priciest and longest callouts describe every session in the
	// window and not just the most recent maxGalleryPoints kept for the scatter payload.
	MedianDurationS        float64
	MedianCostUSD          float64
	MedianCompletedCostUSD float64
	PriciestDurationS      float64
	PriciestCostUSD        float64
	LongestDurationS       float64
	LongestCostUSD         float64
	// CostIncomplete is true when any session in the cohort folded a token-bearing unpriced
	// event, so the cost summaries (median cost, priciest) are lower bounds.
	CostIncomplete bool
}

Gallery is the per-session scatter: one point per fully-spanned session in the window (capped for the payload), with Total the full count so the panel can note a sample.

type GallerySession added in v0.3.0

type GallerySession struct {
	DurationS      float64
	CostUSD        float64
	CostIncomplete bool // the session's cost rollup folded a token-bearing unpriced event
	Archetype      string
	Grade          string
	Outcome        string
}

GallerySession is one dot in the session gallery: a fully-spanned session placed by how long it ran and what it cost, coloured by archetype and carrying its grade and outcome.

type GlobalFacetValues

type GlobalFacetValues struct {
	Agents   []FacetCount
	Machines []FacetCount
	Users    []FacetCount
	Projects []ProjectFacet
}

GlobalFacetValues holds the filter options for the cross-project session view, each with a count so the rail reads like an instrument.

type Insights

type Insights struct {
	Quality     QualityDistribution
	Archetypes  []LabeledCount
	Concurrency ConcurrencyStats
	Velocity    VelocityStats
	Tools       ToolStats
	Hygiene     PromptHygiene
	Churn       FileChurn
	Context     ContextHealthStats

	// Trends is the time-bucketed read of the same cohort: every distribution above drawn
	// as a series over the window's day or week buckets. It is computed only when the
	// filter names a bucket (AnalyticsFilter.Bucket), so a distributions-only caller pays
	// nothing for it and leaves it nil. It shares the snapshot transaction, so a bucket
	// series reconciles exactly with the rolled-up number above it.
	Trends *Trends
}

Insights is everything the Insights page renders for a scope: the quality distribution (grades and outcomes), the archetype mix, the concurrency figures, the velocity cadence, the tool reliability and mix, the prompt-hygiene rates, the file churn, and the context-load figures. It is the cross-cutting counterpart to Analytics (which is about cost and tokens), scoped by the same AnalyticsFilter so a window or a per-user narrowing applies to both surfaces alike.

func (Insights) HasData

func (i Insights) HasData() bool

HasData reports whether any scoped session carried signals, so the page can show an empty state instead of a row of zero bars on a scope with no sessions.

type InsightsPanels added in v0.5.1

type InsightsPanels struct {
	// FleetMix is the per-bucket token share by model (Trends.FleetMix).
	FleetMix bool
	// Gallery is the per-session duration-by-cost scatter and its summary figures
	// (Trends.Gallery).
	Gallery bool
	// Velocity covers the velocity instrument as a whole: the rolled-up cadence figures
	// (Insights.Velocity), the per-bucket series (Trends.Velocity), the hour-of-week
	// rhythm grid (Trends.Rhythm), and the concurrency figures (Insights.Concurrency)
	// the instrument's readouts annotate. These are the message-scan panels, the most
	// expensive reads on the page, so a surface without the instrument skips them all.
	Velocity bool
	// Tools is the tool reliability/mix instrument: Insights.Tools and Trends.Tools.
	Tools bool
	// Churn is the file-churn read the Tools instrument's churn tab draws:
	// Insights.Churn and Trends.Churn.
	Churn bool
	// Economics is the spend-by-outcome and cache-savings series (Trends.Economics).
	Economics bool
	// Subagents is the delegation read (Trends.Subagents).
	Subagents bool
}

InsightsPanels names the optional instrument groups one Insights call computes. The quality core (the grade/outcome distribution, archetypes, hygiene, context health, and, when the filter names a bucket, the signal trend series) is always computed: it is cheap (one row per session off sessions and session_signals), and Quality gates Insights.HasData, so every surface needs it. Everything else is opt-in, because the callers genuinely differ: the fleet /insights page renders all seven instruments, but the project page's quality band renders only the signal series plus the Tools instrument, and it used to pay for the gallery, velocity, economics, rhythm, and subagent scans anyway. A skipped group leaves its Insights (and Trends) fields zero, which the JSON payload serializes as empty series no chart mount reads.

type Invite added in v0.2.2

type Invite struct {
	ID         int64
	Note       string
	CreatedBy  string
	CreatedAt  time.Time
	ExpiresAt  *time.Time
	RedeemedBy *string
	RedeemedAt *time.Time
}

Invite is an issued invite token as shown on the account page: enough to judge its status (unused, expired, or redeemed by whom) without exposing the token itself, which is never stored beyond its hash.

type LabeledCount

type LabeledCount struct {
	Key   string
	Count int
}

LabeledCount is one bar in a distribution: a canonical key (the stored value, or "" for the unscored grade bucket) and how many sessions fell in it. The view maps the key to a display label and a colour; the store keeps the raw key so the two layers stay decoupled.

type MCPMessage added in v0.5.5

type MCPMessage struct {
	Message
	ContentBytes          int64
	ThinkingTextBytes     int64
	ContentSHA256         string
	ThinkingTextSHA256    string
	ContentTruncated      bool
	ThinkingTextTruncated bool
}

MCPMessage is a transcript row whose large text fields may be previews. The byte lengths always describe the stored values, which lets the MCP layer publish a retrievable reference without first materializing the full field.

type Message

type Message struct {
	Ordinal      int
	Role         string
	Content      string
	ThinkingText string
	Model        string
	HasThinking  bool
	HasToolUse   bool
	// ThinkingBytes is the turn's reasoning-trace weight (plaintext length where the agent
	// logs it, else the encrypted payload length; see parser.Message.ThinkingBytes). The
	// transcript's per-message thinking band estimates the turn's reasoning tokens from it
	// when the agent reports no exact count (Usage.Reasoning), the same estimate the session
	// and fleet reads use.
	ThinkingBytes int
	Timestamp     *time.Time
	// Prompt-hygiene facts, meaningful only when PromptFactsCurrent is true.
	PromptShort        bool
	PromptNoCode       bool
	PromptDigest       int64
	PromptFactsCurrent bool
	// Usage is this turn's rolled-up token load and cost, folded per message ordinal from the
	// session's usage_events in the same read (a LEFT JOIN, not a second query), so the transcript
	// stamps a message with its own context and cost without holding a second session-sized map
	// beside the message slice. It is nil when the ordinal carried no attributed usage_events row
	// (an ordinal with no dated usage has no turn load to show), the same "no entry" the old
	// per-ordinal usage map expressed by a missing key.
	Usage *TurnUsage
	// DuplicatePrompt is true when this user turn repeats an earlier eligible prompt's digest
	// verbatim, computed in SQL over the same eligible set gatherPromptHygiene's duplicate count
	// uses (role user, content_length > 0, current prompt facts, a non-null digest, and NOT
	// prompt_short), so a transcript "repeat" badge and the stored duplicate_prompt_count read the
	// same set. The first occurrence of a digest is never marked (it is the original); every later
	// eligible occurrence is. An ineligible row is never marked.
	DuplicatePrompt bool
}

Message is one transcript row for rendering. The prompt-hygiene facts (PromptShort, PromptNoCode, PromptDigest) are the fixed-size verdicts quality.ClassifyPrompt materialized on the row when the message was written; they carry no prompt body, so the full-transcript read stays fixed-size per row and never re-reads content to classify it.

PromptFactsCurrent gates them: it is true only when the digest is non-NULL (a real classified human turn). Every row is rewritten by the epoch rebuild, so stored facts are always the running classifier's output; the flag only distinguishes a classified prompt from a non-prompt row (assistant turn, empty user turn), so the renderer badges exactly the turns the aggregate counted.

type MessageDelta

type MessageDelta struct {
	Ordinal       int
	Role          string
	Content       string
	ThinkingText  string
	ThinkingBytes int
	Model         string
	HasThinking   bool
	HasToolUse    bool
	Timestamp     time.Time
}

MessageDelta is one message row. Each ordinal appears exactly once: the reducer folds a whole session in one pass, so Content and ThinkingText are the complete text of the turn. ThinkingBytes is the reasoning-trace weight the observed-thinking signal sums (see parser.Message.ThinkingBytes): the plaintext length where the agent logs it, else the encrypted payload length, so a redacted turn still records its volume.

type ModelFallback added in v0.2.5

type ModelFallback struct {
	MessageOrdinal     *int
	FromModel          string
	ToModel            string
	Trigger            string
	RefusalCategory    string
	RefusalExplanation string
	DeclinedInput      *int64
	DeclinedOutput     *int64
	DeclinedCacheWrite *int64
	DeclinedCacheRead  *int64
	OccurredAt         *time.Time
	DedupKey           string
}

ModelFallback is one recorded model fallback: a Claude Fable turn the safety classifier declined and re-served on a lower model (see migration 0034). The row is merged from the several transcript lines of one logical fallback, so a field may be unset when only one source line was seen: MessageOrdinal is nil on a system-only row, the declined token counts are nil until the assistant side merged in, and RefusalCategory/RefusalExplanation are empty until the system entry merged in. FromModel and ToModel are the models the turn fell FROM (Fable) and TO (the served lower model).

type ModelSeries added in v0.3.0

type ModelSeries struct {
	Model       string
	Share       []float64
	First       int
	WindowShare float64
}

ModelSeries is one model's token share across the bucket grid: Share[i] is the model's percent of bucket i's total tokens, and First is the first bucket index it appears in (so a model that arrived mid-window draws a line only from its arrival, not a flat zero run before it existed). WindowShare is the model's percent of the whole window's tokens, the figure-level answer to "which model did the work": a headline must not read the trailing bucket, which is the current partial day or week and often empty, naming an arbitrary model at 0%. Models are ordered by total tokens descending, with an "other" fold of the long tail last.

type OAuthClient

type OAuthClient struct {
	ID           string
	ClientName   string
	RedirectURIs []string
	CreatedAt    time.Time
}

OAuthClient is a dynamically registered MCP client (a coding agent). Clients are public: they authenticate with PKCE and hold no secret, so this is identity and the redirect allowlist only.

type OAuthGrant

type OAuthGrant struct {
	ClientID    string
	ClientName  string
	Scope       string
	ConnectedAt time.Time
	LastUsedAt  time.Time
}

OAuthGrant summarizes a client a user has connected, for the account page's "connected apps" list. One row per client the user has live (unrevoked) tokens for, with when it was first connected and when it last authenticated a request or redeemed a refresh token.

type OAuthTokenParams

type OAuthTokenParams struct {
	AccessHash       string
	RefreshHash      string // empty for no refresh token
	ClientID         string
	UserID           int64
	Scope            string
	Resource         string
	AccessExpiresAt  time.Time
	RefreshExpiresAt *time.Time
}

OAuthTokenParams is one issued token pair and its bindings.

type OGImage added in v0.3.0

type OGImage struct {
	PNG         []byte
	GeneratedAt time.Time
}

OGImage is a cached Open Graph preview card: the rendered PNG bytes and when they were generated. The generated_at stamp drives the TTL (a request past the cache window re-renders) and the cleanup sweep (an expired card is pruned). It is the one read shape for all three card caches (overview, project, session).

type OffsetMismatchError

type OffsetMismatchError struct{ StoredBytes int64 }

OffsetMismatchError reports that an append was attempted at the wrong offset; StoredBytes is the server's current cursor, which the client should resume at.

func (OffsetMismatchError) Error

func (e OffsetMismatchError) Error() string

type ProjFallback added in v0.2.5

type ProjFallback struct {
	MessageOrdinal     *int
	FromModel          string
	ToModel            string
	Trigger            string
	RefusalCategory    string
	RefusalExplanation string
	DeclinedInput      *int
	DeclinedOutput     *int
	DeclinedCacheWrite *int
	DeclinedCacheRead  *int
	OccurredAt         time.Time
	DedupKey           string
}

ProjFallback is one model-fallback observation: a Claude Fable turn the safety classifier declined and re-served on a lower model. One logical fallback arrives across several transcript lines that share DedupKey (Claude splits one API message into several assistant entries, plus a separate system entry for the refusal detail), so the fold merges observations on DedupKey: the assistant side brings MessageOrdinal and the declined token counts, the system side brings Trigger/RefusalCategory/RefusalExplanation, and each field fills from whichever line carried it. A field the source did not observe is left at its unset default (MessageOrdinal nil, token counts nil, strings empty) so the merge can tell "unset" from a real value and never overwrites a filled field with a blank.

type ProjToolCall

type ProjToolCall struct {
	MessageOrdinal int
	CallIndex      int
	ToolName       string
	Category       string
	FilePath       string
	Detail         string
	InputBody      string
	InputSHA256    string
	InputBytes     int64
	InputMediaType string
	CallUID        string
}

ProjToolCall is one tool_calls row. The input body lives in the CAS, by one of two paths: InputBody holds the bulky input inline and the rebuild writes it and records the sha256; or InputSHA256 is already set because the client lifted the body to the CAS at upload time and left a sentinel, so the reference is recorded with no blob write. Exactly one of InputBody / InputSHA256 is set when there is an input. CallUID is the agent's call id, which the fold uses to patch the result that arrives on a later line. Detail is the bounded human-scannable summary of the input (a command, pattern, URL, or description) the UI shows when a call has no file_path; it is empty when the input has no summarizable key or was lifted before the field existed.

type ProjUsage

type ProjUsage struct {
	MessageOrdinal *int
	Model          string
	Input          int
	Output         int
	CacheWrite     int
	CacheRead      int
	Reasoning      int
	CostUSD        *float64
	OccurredAt     time.Time
	DedupKey       string
	SourceOffset   int64
	SourceIndex    int
}

ProjUsage is one usage event as the reducer saw it, pre-dedup. SourceOffset and SourceIndex identify the transcript line it came from; together with DedupKey they drive the in-memory dedup (see foldUsage).

type ProjectFacet

type ProjectFacet struct {
	ID    int64
	Key   string
	Name  string
	Kind  string
	Count int
}

ProjectFacet is one project option in the global session filter: enough to label, color, and link it, plus its session count.

type ProjectParams

type ProjectParams struct {
	RemoteKey   string
	Host        string
	Owner       string
	Repo        string
	DisplayName string
	Kind        string
}

ProjectParams is the project identity carried by an announce request before the server knows the row id. Keeping this with Announce lets the downgrade guard run before a local project is inserted, so an old client cannot recreate an unused orphaned project row for a session already stuck to a remote.

type ProjectSummary

type ProjectSummary struct {
	ID           int64
	RemoteKey    string
	Host         string
	Owner        string
	Repo         string
	DisplayName  string
	Kind         string
	SessionCount int
	TotalCostUSD float64
	TotalInput   int64
	TotalOutput  int64
	// Cache token rollups back the projects-index tokens column, whose hover
	// detail breaks the total into in, out, cache read, and cache write (the same
	// four classes the overview heatmap surfaces per day).
	TotalCacheRead  int64
	TotalCacheWrite int64
	// CostIncomplete is true when any session folded into this project's totals
	// carries an unpriced usage event, so the rolled-up cost is a lower bound. It
	// is the OR of the per-session cost_incomplete flags, letting the index render
	// the same "$X+" marker the per-session rows show instead of an exact figure
	// that silently understates an aggregate built from incomplete sessions.
	//
	// Like every other figure on this index, it is rollup-scoped (every surviving
	// usage row), so it can read true for a project whose only unpriced usage is
	// undated while the all-time analytics panel, which drops undated rows off its
	// time axis, reads exact. That is the one documented rollup-vs-analytics gap,
	// the same one the token and cost totals carry (see, in package store's tests,
	// TestUndatedUsageIsTheOnlyRollupAnalyticsGap): the flag tracks each surface's
	// own displayed total rather than diverging from it.
	CostIncomplete bool
	// LastActivity is the most recent session activity in the project: max over its
	// sessions' last_active_at (last-event time), not their updated_at write time,
	// so a reparse of the project's sessions does not float it to the top of the
	// projects index. NULL for a project with no sessions.
	LastActivity *time.Time
	// OverviewPublic gates whether the project's usage overview resolves for
	// logged-out viewers at /p/<id>. Every read that returns a ProjectSummary
	// populates it from projects.overview_public (Project, PublicProjectOverview, and
	// the ListProjects rollup), so the flag reads the same for a given project across
	// surfaces rather than one projection carrying a stale false the others contradict.
	OverviewPublic bool
}

ProjectSummary is one row of the projects index: a project plus rolled-up session counts and token/cost totals.

func (ProjectSummary) TotalTokens

func (p ProjectSummary) TotalTokens() int64

TotalTokens is the sum of every token class for a project: input, output, and both cache directions. It is the headline figure for the projects-index tokens column, matching how the overview heatmap totals a day.

type ProjectionDelta

type ProjectionDelta struct {
	Messages    []MessageDelta
	ToolCalls   []ProjToolCall
	ToolResults []ToolResultDelta
	Usage       []ProjUsage
	Attachments []AttachmentDelta
	Fallbacks   []ProjFallback

	Started time.Time
	Ended   time.Time
}

ProjectionDelta is everything one whole-session parse produces: the rows to write and the session's timestamp span. It carries the reducer's raw view; the rebuild's in-memory fold (dedup, result patching, fallback merge, prompt facts, rollups) runs over it with the complete session in hand, so no row or counter here needs to be correct under partial information.

type PromptHygiene

type PromptHygiene struct {
	Prompts            int // human prompts across the scoped sessions, the rate denominator
	Short              int // prompts under the terse-word threshold
	Duplicate          int // prompts repeating an earlier one verbatim
	NoCodeContext      int // change requests that pointed at no code
	Sessions           int // scoped sessions carrying a current-version signals row
	UnstructuredStarts int // of those sessions, how many opened with an unstructured prompt
}

PromptHygiene is the cohort's input-quality picture over a scope: how many of the window's human prompts were terse, repeated, or asked for a change without pointing at code, and how many sessions opened with an unstructured prompt. The counts come from the stored per-session signals (aggregated by the settle pass or a reparse from the per-message hygiene columns quality.ClassifyPrompt materializes at insert), summed over the sessions carrying a current-version row so the numerators and the Prompts denominator cover the same set. A stale or missing signals row contributes nothing to either, the same way the quality distribution folds it into the unknown bucket, so the panel never mixes a half-rebuilt view.

func (PromptHygiene) HasData

func (h PromptHygiene) HasData() bool

HasData reports whether the scope carried any measured prompt, so the panel can show a note rather than a row of zero-over-zero rates for a window with no signalled sessions.

type PublicSessionSnapshot added in v0.5.5

type PublicSessionSnapshot struct {
	Audit              SessionAudit
	Page               TranscriptPage
	Outline            []Message
	Tools              []ToolCallView
	ProjectionRevision int64
}

PublicSessionSnapshot is the bounded state rendered by a public session page or one of its earlier-page requests. ProjectionRevision identifies the committed projection so a browser cannot append rows from two rebuilds. Outline and Tools are nil on an earlier-page request (before != nil): the outline rail already covers the whole session and does not change page to page, so only the first load and a stale-revision resync (which rerenders the whole body) need to pay for the read.

type QualityDistribution

type QualityDistribution struct {
	Grades   []LabeledCount // canonical order: A, B, C, D, F, then "" (unscored)
	Outcomes []LabeledCount // canonical order: completed, errored, abandoned, unknown
	Sessions int            // total scoped sessions (every session falls in one bucket)
	// Graded is how many scoped sessions carry a gated (current-version, non-stale) grade,
	// the complement of the unscored bucket. The Insights Grades panel reads it as a
	// coverage figure ("N% graded"): the share of the cohort a letter grade actually
	// speaks for, so a distribution dominated by the unscored bar reads as thin coverage
	// rather than a real spread. It is Sessions minus the unscored count, but computed in
	// the same grade scan with a FILTER so it needs no second pass over the cohort.
	Graded int
}

QualityDistribution is the Insights page's quality summary over a scope: how the scoped sessions split across letter grades and across outcomes, plus the total. Every scoped session contributes to exactly one bucket of each split: a session whose signals row is missing or was written by an older scoring version (before its backfill reparse) reads as unscored and unknown rather than vanishing, so the splits cover the same session set the archetype distribution and the session count do, and the three reconcile. The parsed views are gated during a reparse, so a reader never sees a half-rebuilt distribution.

type RhythmGrid added in v0.3.0

type RhythmGrid struct {
	Cells [][]int
}

RhythmGrid is the hour-of-week activity heatmap: Cells[dow][hour] is the message-plus-tool volume, dow 0 = Monday through 6 = Sunday, hour 0..23 in UTC.

func (RhythmGrid) HasData added in v0.3.0

func (r RhythmGrid) HasData() bool

HasData reports whether any cell carried volume.

type SearchSnippet added in v0.2.2

type SearchSnippet struct {
	// Text is the trimmed content window, with leading/trailing ellipses already
	// applied when the window was cut from a longer message.
	Text string
	// MatchStart and MatchEnd are the byte offsets of the matched run within Text,
	// so MatchStart <= MatchEnd <= len(Text). They are equal (both zero) when Text
	// is empty.
	MatchStart int
	MatchEnd   int
}

SearchSnippet is a window of a matching message's content around the first occurrence of the search query, with the match's offsets within the window so a renderer can wrap exactly the matched run in a highlight without re-scanning. The window is computed in Go from the raw matching content (fetched by a lateral join in the search query) rather than in SQL, so the offsets are byte positions into Text and stay exact for the template's three-part split (before, match, after). A zero-value snippet (empty Text) means no match was carried.

func (SearchSnippet) Has added in v0.2.2

func (s SearchSnippet) Has() bool

Has reports whether the snippet carries a match, so a renderer can choose the snippet line over the title line only when a search actually produced one.

type SessionAudit added in v0.4.0

type SessionAudit struct {
	Detail    SessionDetail
	Signals   SessionSignals
	Subagents []SubagentRow
	// Fallbacks is the header tile's capped fallback list (ModelFallbackListCap rows),
	// loaded only when Detail's rollup counted one, from the same snapshot as the
	// count, so the tile's list and its "plus N more" remainder always reconcile.
	Fallbacks []ModelFallback
}

SessionAudit is everything the session instruments read together, from one MVCC snapshot: the session row, its current signals, and its direct subagents with their verdicts. The stat band shows the session's own cost beside the subagents' costs; a rebuild committing between separate reads could update sessions.total_cost_usd under the page and make those figures disagree, so they are pinned the same way the feed pins its rows (analyticsSnapshot's reasoning at single-session scale).

type SessionCard added in v0.3.0

type SessionCard struct {
	// Project identity for the heading. Kind selects the label form (a local project shows
	// its name, a remote one its key), the same choice the session page's heading makes.
	ProjectKind string
	ProjectName string
	ProjectKey  string
	// Title is the session's first user message, squashed and capped like the page's title,
	// empty when the session has no user message (so the card skips the title line).
	Title string
	// MessageCount and TotalTokens are the session's own rollups (the four token classes
	// folded), the same figures the session header shows.
	MessageCount int
	TotalTokens  int64
	// StartedAt and EndedAt bound the session's span. The card derives both its DURATION
	// figure and its activity strip from these two (never last_active_at), so the card's
	// duration matches the page's Duration tile, which dashes when either is absent.
	StartedAt *time.Time
	EndedAt   *time.Time
	// Grade is the session's letter grade, non-nil only when the session carries a gated
	// (current-version, non-stale) signals grade, so an unscored or superseded session dashes
	// the QUALITY figure rather than showing a stale letter.
	Grade *string
	// Activity is the session's usage bucketed over its span into the requested number of
	// buckets, each the summed all-class token volume of the events in that slice. It is nil
	// when the session has no measured span (no start, no end, or a non-positive interval),
	// which the card draws as an empty strip. Its length is exactly the requested bucket count
	// when a span exists, so the histogram is bounded no matter how many usage events a long
	// session carries: the bucketing happens in SQL, not by materializing one point per event.
	Activity []int64
}

SessionCard is everything the session OG card renders from, read as one consistent snapshot by SessionCard: the project identity the heading uses, the session's title and rolled-up figures, its span, its gated quality grade, and the activity strip pre-bucketed to a bounded histogram. Grouping every source read into one repeatable-read snapshot is deliberate: the card was stitched from the handler's session-detail read and a later, separate activity read, so an append or reparse between them could cache a card whose foot totals described one session version while its activity strip described another. One snapshot means the totals, the grade, and the strip all describe the same instant.

type SessionDetail

type SessionDetail struct {
	SessionSummary
	OwnerID     int64
	ProjectID   int64
	ProjectKey  string
	ProjectName string
	ProjectKind string
	Cwd         string
	ParentID    *int64
	// TotalCacheSavingsUSD is the session's rolled-up prompt-cache saving (folded by the
	// rebuild beside total_cost_usd), so the Cache tile reads it in O(1) instead of
	// scanning usage_events on every live refresh. CacheSavingsIncomplete flags that some
	// cached volume rode an unpriced model, so the figure is partial (and, unlike cost,
	// not a clean lower bound: an omitted model's saving can be either sign).
	TotalCacheSavingsUSD   float64
	CacheSavingsIncomplete bool
}

SessionDetail adds the owning project to a session summary, plus the rolled-up prompt-cache saving the session header's Cache tile renders. The saving lives only on the detail (not the list summary): the session list shows no cache tile, so the extra per-model-priced rollup rides only the single-session read that needs it.

type SessionFeedCursor

type SessionFeedCursor struct {
	ID int64
}

SessionFeedCursor marks a position in the session feed: the id of the last row a page returned. The feed pages on the session id, which is immutable, rather than on updated_at. That matters for a client paging the whole feed across separate requests: a session re-activated mid-walk (its updated_at bumped by ingest or a re-parse) keeps the same id, so it never jumps past the cursor and drops out of a later page the way an updated_at keyset would silently skip it. Paging stays O(N), not the O(N^2) an OFFSET scan would cost.

type SessionFilter

type SessionFilter struct {
	ProjectID int64
	Agent     string
	Machine   string
	Username  string
	// Query, when set, restricts the list to sessions with at least one message
	// whose content matches it case-insensitively (an ILIKE substring, with the LIKE
	// metacharacters escaped so a literal % or _ in the query matches itself). It
	// drives the match from the messages side so the content trigram index serves it
	// (see ListAllSessions), and it also windows a per-row snippet around the first
	// match. The empty string is no content filter.
	Query string
	// IncludeEmpty keeps sessions whose parse produced no readable message
	// (message_count = 0) in the list. The default excludes them: they clutter the
	// global feed without carrying anything to read. Setting it restores the old
	// behavior of listing every session regardless of message count.
	IncludeEmpty bool
	// IncludeSubagents keeps subagent sessions (relationship_type = 'subagent') in the
	// global feed. The default excludes them: a fleet's spawned reviewers, fan-out
	// workers, and spec-extraction batches vastly outnumber the top-level runs a reader
	// is looking for, and each already rolls up under its parent's detail page. Setting
	// it shows the whole tree. Continuations (a resumed session) stay visible either way:
	// they are real work a reader started, not machinery a parent spun up. It is applied
	// in ListAllSessions, not the shared conds(), so it narrows only the browse feed and
	// leaves the count, facet, MCP-feed, and Insights drill-through queries counting every
	// session (see ListAllSessions).
	IncludeSubagents bool
	// Since bounds the list to sessions last active at or after this instant,
	// matching the analytics window so a project page's session list and its usage
	// panel cover the same range. The zero time means no lower bound.
	Since time.Time
	// Grade narrows by the Insights Grades panel's buckets: a letter "A".."F" matches
	// sessions with a usable current-version signals row carrying that grade, and the
	// sentinel "unscored" matches the panel's catch-all (explicit NULL-grade row, stale
	// or non-current row, or no row at all). The empty string is no grade filter. See
	// conds() for the exact match.
	Grade string
	// Outcome narrows by the Outcomes panel's buckets: "completed", "abandoned", and
	// "errored" match a usable current-version signals row with that outcome, and
	// "unknown" matches the panel's catch-all (an explicit unknown row or a missing/
	// stale one). The empty string is no outcome filter. See conds().
	Outcome string
	// Range is the trailing-window key (a web.DateRanges key like "30d") that produced
	// Since, carried so the URL builder can re-emit ?range= and the chip can label the
	// window. It is display-and-URL state only: the query narrows by Since, never by
	// this string, so the store ignores it. The empty string means no windowing.
	Range string
	// RequireSpan narrows to sessions with a measured span (a parsed start and end and a
	// non-negative duration), the exact cohort the Insights Concurrency panel sweeps (see
	// spanFilter in analytics_concurrency.go). The busiest-user drill sets it so the
	// linked feed matches the panel's session set rather than a looser "all this user's
	// sessions in the window", which would list sessions the panel never counted. The
	// zero value applies no span constraint.
	RequireSpan bool
	// Sort names the column the global session list is ordered by (see
	// sessionSortColumns). The empty string means DefaultSort. Desc selects
	// descending order. Together they back the click-to-sort table headers; an
	// unknown Sort falls back to DefaultSort in the query builder.
	Sort   string
	Desc   bool
	Limit  int
	Offset int
	// After is a keyset cursor: the id of the last row the reader has already seen, so
	// "Show more" fetches the page strictly after it in the current sort order rather
	// than re-reading rows 1..N with a doubled limit. Zero means the first page. It applies
	// only to the four keyset-sortable feed orders (updated, tokens, messages, cost, in
	// sessionKeysetColumns); an After set under any other sort is ignored, since those
	// orders are not offered a "Show more" cursor. See ListAllSessions.
	After int64
	// AfterVal is the cursor row's sort value as the page observed it (the last visible row's
	// last_active_at, total_tokens, message_count, or total_cost_usd, formatted for its column
	// type). It is carried so the resume boundary stays fixed at what the reader already saw:
	// the sort columns are mutable projection fields (activity bumps last_active_at, a rebuild
	// moves the cost/token/message counts), so resolving the boundary live from the cursor row
	// on the next request would let it drift and duplicate or skip rows. Empty falls back to a
	// live scalar-subquery lookup of the cursor row's value, which is what a legacy cursor URL
	// (id only) still gets. It is meaningful only alongside a keyset-sortable After.
	AfterVal string
}

SessionFilter narrows a session list. Empty fields are ignored.

type SessionPage

type SessionPage struct {
	Sessions  []SessionSummary
	Remainder SessionRemainder
}

SessionPage is a project's windowed session table: the capped rows the page shows, newest-active first, plus the aggregate of every windowed session that did not fit (Remainder). Shown rows plus Remainder reproduce the usage panel's headline, since both the rows and the remainder derive from the one dated-usage base the panel sums.

type SessionReducer added in v0.3.0

type SessionReducer interface {
	Feed(region []byte, baseOffset int64) error
	Finish() ProjectionDelta
}

SessionReducer folds a session's raw bytes into one whole-session delta. RebuildSession constructs one per rebuild, feeds it every stored region in offset order, and calls Finish exactly once for the completed delta. Feed is pure CPU: it runs inside the rebuild transaction, so it must not perform I/O.

type SessionRemainder

type SessionRemainder struct {
	Sessions       int
	Input          int64
	Output         int64
	CacheRead      int64
	CacheWrite     int64
	CostUSD        float64
	CostIncomplete bool
}

SessionRemainder is the aggregate of the windowed sessions the capped table did not show: how many, their per-class token volume, their summed cost, and whether any of them carried unpriced usage. The project page renders it as a footer so the visible rows plus this line reconcile with the usage panel headline even when more sessions match than the table caps at. It carries all four token classes (not just a total) so the footer can show the same breakdown card every other token figure does, and its CostIncomplete is a bool_or over the hidden sessions alone, so the footer flags "$X+" only when a hidden session is the unpriced one, never because a visible row was.

func (SessionRemainder) Has

func (r SessionRemainder) Has() bool

Has reports whether any windowed sessions fell outside the capped table, so the project page shows the reconciling footer only when rows were actually withheld.

func (SessionRemainder) Tokens

func (r SessionRemainder) Tokens() int64

Tokens is the hidden tail's all-class token volume, the figure the footer's total shows with the per-class split behind its card, matching every other token readout.

type SessionRow

type SessionRow struct {
	SessionSummary
	ProjectID   int64
	ProjectKey  string
	ProjectName string
	ProjectKind string
	// Grade is the session's letter grade (A..F) from its current, non-stale signals
	// row, nil when the session is unscored or has not settled yet. Outcome is that
	// row's outcome (completed / abandoned / errored / unknown), empty when no current
	// row exists. Both come from a LEFT JOIN in globalSessionSelect gated by
	// signalsCurrent(), so a feed row's grade and outcome match the drill filters and
	// the Insights panels rather than reading a stale verdict.
	Grade   *string
	Outcome string
	// Search is the content-match snippet for this row, populated only when the
	// list was run with a Query filter: a window of the first matching message's
	// content centered on the match, so the feed can show what the session said
	// around the search term. It is the zero value on an unfiltered list.
	Search SearchSnippet
	// Tree is the whole-work-item rollup for this row: its own cost plus every
	// subagent it fanned out, and the count of those subagents. It is filled only by
	// the feed path (ListAllSessions attaches it after the page is scanned), so a row
	// read outside the feed carries the zero-value rollup (no fan-out). The feed shows
	// the fan-out only when SubagentCount > 0, since a session that spawned nothing has
	// no work-item cost beyond the cost the row already shows.
	Tree TreeRollup
}

SessionRow is one row of the global (cross-project) session list: a session summary plus the project it ran in, so a reader can scan and filter every session in one place without first choosing a project.

type SessionSignals

type SessionSignals struct {
	SessionID            int64
	Outcome              string
	OutcomeConfidence    string
	Score                *int    // nil when the session is unscored
	Grade                *string // nil when the session is unscored
	ToolCalls            int
	ToolFailures         int
	ToolRetries          int
	EditChurn            int
	LongestFailureStreak int
	// Prompt-hygiene counts describe the human's input, not the agent's work, so they
	// ride alongside the tool-health counts but never feed the score. PromptCount is the
	// classifier's base (non-empty human prompts), the denominator the counts are read
	// against.
	PromptCount          int
	ShortPromptCount     int
	DuplicatePromptCount int
	NoCodeContextCount   int
	UnstructuredStart    bool
	// Context-health figures describe resource load, not the agent's work, so like the
	// hygiene counts they ride alongside the score without feeding it. Both are nil when
	// the session had no usage to measure, so the UI can tell "unmeasured" apart from a
	// measured zero. PeakContextTokens is the heaviest single-turn context the session
	// reached; ContextResetCount is how many inferred context resets (compactions or
	// clears) it went through.
	PeakContextTokens *int64
	ContextResetCount *int
	// Observed-thinking figures: like context health they describe the work's shape, not
	// its quality, so they never feed the score. All four are measured together from the
	// session's assistant turns and are nil when it had none (nothing to measure, so the
	// UI reads absence rather than "off"). AssistantTurns is the denominator, ThinkingTurns
	// how many carried a reasoning block (zero reads as "off"), ThinkingTailTokens the
	// session's headline volume (the mean of the hardest tenth of its thinking turns, in
	// estimated reasoning tokens), and ThinkingPeakTokens the single hardest turn. The band
	// is an absolute cut on the token scale (see quality.ThinkingBucketForTokens).
	AssistantTurns     *int
	ThinkingTurns      *int
	ThinkingTailTokens *int
	ThinkingPeakTokens *int
}

SessionSignals is a session's stored behavioral signals: its outcome, its quality score and grade (nil when unscored), and the tool-health counts the score is built from. It is the read shape of the session_signals row, derived from the session's own projection and materialized by the settle pass once the session settles, or re-derived on reparse.

func (SessionSignals) HasContextHealth

func (s SessionSignals) HasContextHealth() bool

HasContextHealth reports whether the session had usage to measure, so the UI can show the context readout only when there is a real figure rather than a blank stand-in. Peak and reset count are populated together, so testing the peak is enough.

func (SessionSignals) HasHygieneSignal

func (s SessionSignals) HasHygieneSignal() bool

HasHygieneSignal reports whether any prompt-hygiene signal fired, so the UI can omit the input readout for a session whose prompts were all clean.

func (SessionSignals) HasThinkingMeasure added in v0.3.0

func (s SessionSignals) HasThinkingMeasure() bool

HasThinkingMeasure reports whether the session had assistant turns to measure thinking over. The four thinking fields are populated together, so testing the denominator is enough. A measured session with zero ThinkingTurns reads as "off"; an unmeasured one shows no thinking readout at all.

func (SessionSignals) HasToolActivity

func (s SessionSignals) HasToolActivity() bool

HasToolActivity reports whether the session ran any tools, so the UI can omit the tool-health detail for a pure-conversation session that has none.

func (SessionSignals) Scored

func (s SessionSignals) Scored() bool

Scored reports whether the session carries a score and grade, so the UI can show a grade tile or fall back to the outcome alone for an unscored (unknown, no-signal) session.

func (SessionSignals) ThinkingBucket added in v0.3.0

func (s SessionSignals) ThinkingBucket() quality.ThinkingBucket

ThinkingBucket is the session's absolute band: off when no turn reasoned, else the band its hardest-decile-mean volume reaches on the token scale (quality.ThinkingBucketForTokens). It reads as ThinkingOff when unmeasured too, so a caller should gate on HasThinkingMeasure first to tell "no read" from "measured off".

func (SessionSignals) ThinkingCoverage added in v0.3.0

func (s SessionSignals) ThinkingCoverage() float64

ThinkingCoverage is the share of the session's assistant turns that carried a reasoning block, in [0, 1]. Zero when unmeasured or when no turn reasoned.

type SessionSnapshot added in v0.4.0

type SessionSnapshot struct {
	Audit   SessionAudit
	Page    TranscriptPage
	Outline []Message
	Tools   []ToolCallView
	// DupIDs counts tool-call ids appearing on more than one row (a replayed
	// transcript), read with the shape: it summarizes the same tool_calls rows the
	// page renders, so it must not straddle a rebuild against them.
	DupIDs int
}

SessionSnapshot is everything the live session body renders from one MVCC snapshot. Outline and Tools are nil when the read skipped the shape (a quiet append tick, see SessionAppendByID); the fragment then carries no shape swap, which is right because no turns changed.

type SessionSummary

type SessionSummary struct {
	ID               int64
	Agent            string
	Machine          string
	GitBranch        string
	Username         string
	MessageCount     int
	UserMessageCount int
	// ModelFallbackCount is how many times this session's Claude Fable turns were declined by
	// the safety classifier and re-served on a lower model (see migration 0034). It is read from
	// the sessions.model_fallback_count rollup so every summary read surfaces it in O(1). Every
	// SessionSummary query loads it (the shared sessionSelect, the cross-project feed row, and the
	// single-session header) so the count is truthful wherever a summary is published, including a
	// subagent row: the MCP DTO reports it as an always-present field, so a summary that skipped the
	// column would falsely read zero on a child session that actually fell back.
	ModelFallbackCount int
	TotalInput         int64
	TotalOutput        int64
	TotalCacheWrite    int64
	TotalCacheRead     int64
	TotalCostUSD       float64
	CostIncomplete     bool
	Visibility         string
	PublicID           *string
	StartedAt          *time.Time
	EndedAt            *time.Time
	// LastActiveAt is when the session was last active: its last event timestamp
	// (ended_at), falling back to the row's creation time for a transcript that
	// carried no timestamps. It is the feed's "updated" recency, read from the
	// generated last_active_at column rather than the row's updated_at write time,
	// so a reparse (which restamps updated_at to now) never makes a days-old session
	// read as freshly updated. See migration 0033.
	LastActiveAt *time.Time
	// Title is the session's first user message, squashed to single-spaced and
	// capped for display, so a row is recognizable by what the run was about rather
	// than only its metadata. It is empty for a session with no user message. The
	// figure is fetched by a LEFT JOIN LATERAL over the first ordinal user message,
	// so it rides the list read rather than a per-row lookup.
	Title string
}

SessionSummary is one row of a session list (project view, search results).

type SignalTrends added in v0.3.0

type SignalTrends struct {
	// Grades holds the per-bucket grade share. GradeShare[i] maps a grade key
	// (A/B/C/D/F/"" for unscored) to its percent of bucket i's sessions.
	GradeShare []map[string]float64
	GPA        []float64 // grade-point average per bucket over the graded sessions, 0..4

	// ArchetypeShare holds the per-bucket archetype mix. ArchetypeShare[i] maps an archetype
	// key (quick/standard/deep/marathon/automation) to its percent of bucket i's sessions,
	// banded by the same archetypeCaseExpr the distribution uses so the trend and the rolled-up
	// mix read one numeric source. Unlike the grade and outcome series it needs no signals join:
	// a session's shape is a fact of its own columns (user turns, message count, span), so an
	// ungraded session still bands, and the denominator is every started session in the bucket.
	ArchetypeShare []map[string]float64

	CompletedRate []float64 // percent of bucket i's sessions that completed
	AbandonedRate []float64 // percent that abandoned
	OutcomeTotal  []int     // sessions in bucket i (the rate denominator)
	// Raw per-bucket outcome counts behind the rates, so the outcome chart's magnitude bars draw
	// the store's completed/abandoned/other partition exactly rather than deriving a warn segment
	// as total-completed, which folds errored and unknown into the abandoned colour and drifts
	// from the abandoned-rate line. Other is OutcomeTotal minus these two.
	CompletedCount []int
	AbandonedCount []int

	// Hygiene rates, each a percent of the bucket's prompts (or sessions, for
	// unstructured starts), gated on the current prompt-facts version.
	HygieneTerse        []float64
	HygieneRepeated     []float64
	HygieneNoCode       []float64
	HygieneUnstructured []float64

	ContextResets []int // inferred context resets summed per bucket

	// ContextHistogram is the window-wide distribution of per-session peak context, a
	// log-scale histogram (not per bucket). Markers carries the p50/p90/max annotations.
	ContextHistogram []ContextBucket
	ContextMarkers   []ContextMarker
}

SignalTrends is the per-bucket read of the settle-pass signals: how grades, outcomes, prompt hygiene, and context resets moved over the window. Every series is gated the same way the distributions are (current signals version, not stale), so an ungraded bucket reads as unscored rather than zero.

type Store

type Store struct {
	Pool *pgxpool.Pool
	// contains filtered or unexported fields
}

Store wraps a Postgres connection pool.

func Open

func Open(ctx context.Context, databaseURL string) (*Store, error)

Open connects to Postgres and verifies the connection.

func (*Store) Analytics

func (s *Store) Analytics(ctx context.Context, f AnalyticsFilter) (Analytics, error)

Analytics aggregates usage for the charts, scoped by f (see AnalyticsFilter): project or whole instance, a trailing window, and an optional user/agent/machine narrowing applied uniformly to every base.

Every figure derives from one base set, the scoped dated usage_events, so the headline totals, the daily series, the by-model split, and the by-agent split all reconcile by construction: sum the per-day cells, or the by-model tokens, or the by-agent tokens, and you get the headline tokens, every time. This is deliberate. The figures used to come from three different sources (tokens from the daily series, cost from the session rollups, the by-model split from a separate usage_events query that dropped unnamed models), so the headline and the rows beneath it could disagree by an order of magnitude. They are the same base now, grouped three ways (by day, by model, by agent), with the headline summed from one of them.

Every base shares the one filter the time axis forces: occurred_at IS NOT NULL. An undated usage event has no day to plot, so counting it in the headline but not in the daily cells would make the total exceed the sum of the chart, the exact drift this view exists to avoid. So the overview counts dated usage only, uniformly. In practice that excludes nothing: Claude, Codex, and pi all stamp the turn a usage line belongs to, so a NULL occurred_at is a malformed transcript to fix at ingest, not usage to scatter across the dashboard.

The headline totals are summed from the by-agent split rather than queried again: a session has exactly one agent, so the per-agent rows partition the usage cleanly and their sum is the grand total with no double counting. It reads inside one read-only REPEATABLE READ transaction so every grouped query and the Cache tile share a single MVCC snapshot: the headline token classes and the Cache split are the same sums regrouped, so a concurrent ingest landing between two pooled reads would let them disagree by the usage that arrived mid-render. Unlike AnalyticsSnapshot this takes no reparse-lock gate (a live panel tolerates a snapshot that falls during a reparse, where each session is atomically old or new), and being read-only it takes no locks that could block ingest.

func (*Store) AnalyticsSnapshot

func (s *Store) AnalyticsSnapshot(ctx context.Context, f AnalyticsFilter) (a Analytics, ok bool, err error)

AnalyticsSnapshot reads Analytics as a single consistent snapshot that is guaranteed not to straddle a fleet rebuild, for the OG card render. It runs the grouped queries inside one REPEATABLE READ transaction, so they all read one MVCC snapshot rather than several independently-timed reads. The first statement in that transaction is the epoch-staleness check (see epochGatedSnapshot), which both establishes the snapshot and reads the corpus state at that instant: if every session sits at the running parser epoch, the snapshot is a single-epoch corpus and an epoch rollout that starts later cannot alter the frozen snapshot. When sessions are still behind the epoch, it returns ok=false and no analytics, so the caller skips the render rather than caching a mixed aggregate.

func (*Store) Announce

func (s *Store) Announce(ctx context.Context, p AnnounceParams) (AnnounceResult, error)

Announce upserts the session row (latest announce wins for mutable metadata), ensures its raw-store row exists, and returns the current cursor and hash.

Remote attribution is sticky: once a session resolves to a git-remote project, a later announce that can no longer find a remote (standalone or orphaned, because the folder lost its origin or was deleted) does not move it to a local project. Backed-up work keeps its repo grouping rather than sliding into an orphaned bucket the moment its checkout is removed. An upgrade in the other direction (a local session that gains a remote) is allowed and re-homes it.

func (*Store) AnnounceWithProject

func (s *Store) AnnounceWithProject(ctx context.Context, p AnnounceParams, project ProjectParams) (AnnounceResult, error)

AnnounceWithProject upserts the project and the session in one transaction. For non-remote announces it first applies the sticky remote guard; when the guard wins, the local project is never inserted. The HTTP ingest path uses this form because it receives a project identity rather than a project id.

func (*Store) AppendChunk

func (s *Store) AppendChunk(ctx context.Context, sessionID, offset int64, data []byte) (newStoredBytes int64, err error)

AppendChunk appends data at the given offset as a new raw chunk row. If offset does not match the server's current byte_len it returns OffsetMismatchError with the truth and makes no change. The prefix hash is advanced by resuming the stored sha256 state and folding in only the new bytes, so appending is work proportional to the chunk, not to the whole session.

func (*Store) ArchetypeDistribution

func (s *Store) ArchetypeDistribution(ctx context.Context, f AnalyticsFilter) ([]LabeledCount, error)

ArchetypeDistribution buckets the scoped sessions by shape on its own pooled connection for the Insights page. The snapshot path threads archetypeDistributionFrom so every panel reads one MVCC snapshot.

func (*Store) Attachments

func (s *Store) Attachments(ctx context.Context, sessionID int64) ([]AttachmentView, error)

Attachments returns all of a session's attachments, ordered by the message they hang on, for the web renderer. Bounded readers pass an ordinal range to AttachmentsInRange.

func (*Store) AttachmentsInRange

func (s *Store) AttachmentsInRange(ctx context.Context, sessionID int64, minOrdinal, maxOrdinal int) ([]AttachmentView, error)

AttachmentsInRange returns the attachments hanging on messages in the inclusive ordinal window [minOrdinal, maxOrdinal], so a bounded transcript read fetches only the attachments for the messages it returned.

func (*Store) AuthCodeForExchange added in v0.5.5

func (s *Store) AuthCodeForExchange(ctx context.Context, codeHash string) (AuthCode, error)

AuthCodeForExchange loads an authorization code for endpoint validation without consuming it. The token endpoint checks the client, redirect, and PKCE binding before RedeemAuthCode atomically consumes the code and stores its token pair.

func (*Store) AvgQualityScore added in v0.3.0

func (s *Store) AvgQualityScore(ctx context.Context, f AnalyticsFilter) (*float64, error)

AvgQualityScore is the mean quality score across the scoped sessions that carry a gated (non-stale) grade, or nil when none is scored. It shares the analytics filter (clauseFor on s.started_at, so a windowed scope counts sessions that started in the window) and the same signals gate the quality distribution uses, so it speaks for exactly the graded cohort the Insights Grades panel counts rather than a different set. The public project OG card reads it and rounds it to a representative letter grade (via quality.GradeFor), so the card's single QUALITY figure summarizes the same graded sessions the page's grade distribution draws. It is nil, not zero, when no scored session is in scope, so the card can dash an unmeasured figure rather than print a zero that would read as a real (failing) average.

func (*Store) BackfillMessageContentHashes added in v0.5.5

func (s *Store) BackfillMessageContentHashes(ctx context.Context) (int64, error)

BackfillMessageContentHashes fills content_sha256 and thinking_text_sha256 for every message row still at migration 0049's unbackfilled sentinel (never a valid digest). The migration only adds the columns, their default, and a trigger that stamps new and rewritten rows; it deliberately does not backfill the existing corpus itself, because a full-table UPDATE inside the migration's own transaction would hold the ADD COLUMN's access-exclusive lock on messages, the hottest table, for as long as the backfill took (see migration 0041 for the established pattern of deferring bulk population out of the migration transaction). This instead commits in bounded, primary-key-ordered batches with a pause between them, so a large corpus never blocks concurrent reads or writes and the pool stays available to live traffic throughout the pass.

It is safe to run concurrently with normal operation: a session a rebuild rewrites arrives with its rows already stamped by the messages_content_hashes trigger (rebuild deletes and re-inserts every row), so the backfill only ever touches rows no rebuild has reached yet, and FOR UPDATE SKIP LOCKED steps around any row a concurrent rebuild is rewriting right now rather than waiting on it. It is idempotent across restarts: the sentinel itself is the worklist, so a process that stops mid-backfill just resumes where the sentinel says work remains next time it runs.

func (*Store) BlobMeta

func (s *Store) BlobMeta(ctx context.Context, sha256hex string) (Blob, error)

BlobMeta returns a blob's stored size, media type, and storage content type without reading its body. The content type lets the serve path set Content-Encoding so the client decodes a compressed blob, while the server never touches the bytes.

func (*Store) CacheStats

func (s *Store) CacheStats(ctx context.Context, f AnalyticsFilter) (CacheStats, error)

CacheStats aggregates prompt-cache effectiveness over the analytics scope. It shares the analytics base exactly: the scoped dated usage_events (occurred_at IS NOT NULL), grouped by model, so the cache figures reconcile with the usage panel they sit beside rather than counting undated usage the panel drops off its time axis. Savings is folded per model in Go because pricing is compiled into the binary, not in the database, so the rate gap that defines a saving is not a column to sum.

It reads on its own pooled connection. The snapshot path (AnalyticsSnapshot) instead threads its transaction through cacheStats, so the Cache tile and the token totals come from one MVCC snapshot and one connection rather than two.

func (*Store) CheckRegistrationInvite added in v0.5.5

func (s *Store) CheckRegistrationInvite(ctx context.Context, inviteHash string) error

CheckRegistrationInvite cheaply rejects a registration that cannot possibly pass the invite gate before the caller performs expensive password hashing. It is intentionally only a preflight: Register repeats the decision while holding its registration lock and atomically consumes the invite, so a first user, expiry, redemption, or concurrent registration racing this read cannot weaken the actual account-creation gate.

func (*Store) Close

func (s *Store) Close()

Close releases the connection pool.

func (*Store) ConcurrencyStats

func (s *Store) ConcurrencyStats(ctx context.Context, f AnalyticsFilter) (ConcurrencyStats, error)

ConcurrencyStats computes the scope's overlap figures for the Insights page. Fleet peak, busiest user, and the average plus session count are three separate reads over the same span set, so it wraps them in one repeatable-read, read-only snapshot: a concurrent ingest between the reads could otherwise return a peak from one cohort with a session count and average from another. Insights threads its own snapshot through concurrencyStatsFrom; this is the standalone equivalent. The snapshot takes no row locks, so it never blocks ingest.

func (*Store) ContextHealth

func (s *Store) ContextHealth(ctx context.Context, f AnalyticsFilter) (ContextHealthStats, error)

ContextHealth aggregates the scoped sessions' context-load figures on its own pooled connection. Insights instead threads its snapshot transaction through contextHealthFrom, so the measured cohort shares one MVCC snapshot with the other panels.

func (*Store) CountAllSessions added in v0.2.2

func (s *Store) CountAllSessions(ctx context.Context, f SessionFilter) (total, empty int, err error)

CountAllSessions counts the sessions ListAllSessions would return for the same filter (ignoring Limit and Offset), plus how many empty sessions the default hides. It shares conds() with ListAllSessions so the two can never disagree about which rows match.

It exists for tests and verification only, NOT the render path: the /sessions footer no longer counts the corpus (that count(*) was O(total), the very cost the incremental-efficiency gate flagged), so nothing in a request calls this. The drift-guard tests keep it to pin the drill filters' bucket semantics against the Insights panel counts, and TestCountAllSessionsAgreement uses it to hold the list-vs-count invariant the shared conds() guarantees.

The empty count is a FILTER aggregate over the same matched set, so it reflects the sessions hidden BY the empty filter within the current agent/project/query scope, not a fleet-wide zero-message count. When IncludeEmpty is set the empty rows are already in Total, and Empty reports how many of them are the zero-message ones.

func (*Store) CreateAPIToken

func (s *Store) CreateAPIToken(ctx context.Context, userID int64, name, scope, tokenHash string) (int64, error)

CreateAPIToken stores a token's hash with a scope and returns its row id.

func (*Store) CreateAuthCode

func (s *Store) CreateAuthCode(ctx context.Context, codeHash string, c AuthCode, expiresAt time.Time) error

CreateAuthCode stores an authorization code's hash with everything the token exchange will need to validate and honor it.

func (*Store) CreateInvite

func (s *Store) CreateInvite(ctx context.Context, tokenHash string, createdBy int64, note string, expiresAt *time.Time) (int64, error)

CreateInvite stores an invite token hash issued by an admin.

func (*Store) CreateOAuthClient

func (s *Store) CreateOAuthClient(ctx context.Context, id, name string, redirectURIs []string, limit int) error

CreateOAuthClient stores a dynamic client registration if fewer than limit clients were created in the preceding hour. A transaction-scoped advisory lock makes the count-and-insert decision atomic across every server replica.

func (*Store) CreateOAuthToken

func (s *Store) CreateOAuthToken(ctx context.Context, p OAuthTokenParams) error

CreateOAuthToken stores an issued access/refresh token pair.

func (*Store) CreateWebSession

func (s *Store) CreateWebSession(ctx context.Context, id string, userID int64, expiresAt time.Time) error

CreateWebSession persists a browser session.

func (*Store) DeleteExpiredOGImages

func (s *Store) DeleteExpiredOGImages(ctx context.Context, olderThan time.Time) (int64, error)

DeleteExpiredOGImages removes cached overview cards stamped before the cutoff, the housekeeping the cleanup loop runs.

func (*Store) DeleteExpiredProjectOGImages added in v0.3.0

func (s *Store) DeleteExpiredProjectOGImages(ctx context.Context, olderThan time.Time) (int64, error)

DeleteExpiredProjectOGImages removes cached project cards stamped before the cutoff, the housekeeping the cleanup loop runs beside DeleteExpiredOGImages.

func (*Store) DeleteExpiredSessionOGImages added in v0.3.0

func (s *Store) DeleteExpiredSessionOGImages(ctx context.Context, olderThan time.Time) (int64, error)

DeleteExpiredSessionOGImages removes cached session cards stamped before the cutoff, the housekeeping the cleanup loop runs beside DeleteExpiredOGImages.

func (*Store) DeleteSession

func (s *Store) DeleteSession(ctx context.Context, sessionID int64) error

DeleteSession removes a session and everything derived from it. The foreign keys cascade messages, tool calls, usage events, attachments, and the raw bytes; child sessions have their parent pointer nulled. Any CAS blobs the session referenced are left for a later SweepBlobs to reclaim. Authorization (owner or admin) is enforced by the caller, so this deletes unconditionally by id and returns ErrNotFound when nothing matched.

func (*Store) DeleteWebSession

func (s *Store) DeleteWebSession(ctx context.Context, id string) error

DeleteWebSession removes a browser session (logout).

func (*Store) DeriveSessionRollups added in v0.5.1

func (s *Store) DeriveSessionRollups(ctx context.Context, sessionID int64) error

DeriveSessionRollups rewrites one session's insights rollups from its current projection rows, in a transaction of its own. Production code never needs it (rebuildTx and the announce path maintain the rollups inline); it exists for tests that seed projection rows with direct INSERTs, bypassing the rebuild, and must bring the rollups along through the same derivation SQL the write path runs.

func (*Store) DueSessions added in v0.3.0

func (s *Store) DueSessions(ctx context.Context, epoch int, limit int) ([]DueSession, error)

DueSessions returns up to limit sessions due for a rebuild, in no particular order. A session is due when the last successful rebuild did not cover its current bytes (parsed_byte_len <> byte_len) or ran at an earlier epoch (attemptedEpoch behind the running one). The epoch comparisons are deliberately monotonic (behind-only, never <>): during a rolling deploy the old binary would otherwise see the new binary's stamps as "different" and rebuild them with the old parser, downgrading projections the fleet already advanced. Rows stamped ahead of the running epoch are left alone entirely, even when byte-dirty; the instance running the newer binary picks them up on its next wake or tick.

There is no page cursor because none is needed: every session a drain processes leaves this scan's result set before the next page is fetched. A successful rebuild stamps it current, a deterministic parser failure pins it (parse_error markers covering its bytes at the running epoch), and an operational failure parks it on a retry backoff (RecordRebuildBackoff; the worker treats a failure of THAT write as fatal to the drain, which is what makes this guarantee hold). So "the next page" is simply the first limit ready sessions again, and a drain loops until the page comes back empty.

The query is a UNION of three arms rather than one OR so that each arm terminates at its own LIMIT on its own partial index, whatever the planner's current statistics say (a single OR-ed predicate under an outer LIMIT has been observed to plan as a full index walk). Each arm reads an index that already excludes what the drain must not visit, so rows parked on a future retry cost nothing here however many the corpus accumulates:

  • dirty_ready: byte-dirty, undeferred (the every-chunk wake's arm);
  • epoch_ready: attemptedEpoch behind the running epoch, undeferred (the fleet-rebuild arm; the expression folds the failed-parse pin in, so a session whose failure covers its current bytes at the running epoch or above is absent: retrying identical input would fail identically, and an attempt recorded ahead belongs to a newer binary);
  • retry_elapsed: one range over deferral times picks up exactly the parked retries whose backoff has elapsed. This arm needs no dirty-or-stale recheck because a deferral only exists on a session that was due when its rebuild failed, and everything that advances a session clears the deferral in the same statement.

The epoch-staleness gates deliberately do NOT honor the deferral: a backing-off rebuild is deferred, not cancelled, so the corpus is still mixed and the gate staying up is the honest answer (the safe direction of asymmetry; the gates must never read done while the scan still has work).

func (*Store) DuplicateCallUIDCount

func (s *Store) DuplicateCallUIDCount(ctx context.Context, sessionID int64) (int, error)

DuplicateCallUIDCount returns how many of a session's tool-call ids appear on more than one row. The GROUP BY runs in the database against the (session_id, call_uid) index, so the result is a bounded scalar and the session view can flag a repeated id without loading or grouping the calls in process memory. It is normally zero; a non-zero count means the transcript replayed a turn (a resumed or compacted Claude session repeats a tool_use id), which the view surfaces as a chip so a genuinely malformed id reuse is visible rather than silent.

func (*Store) EpochStaleCount added in v0.3.0

func (s *Store) EpochStaleCount(ctx context.Context, epoch int) (int, error)

EpochStaleCount counts the sessions whose projection was last rebuilt behind the running epoch and can still be rebuilt at it (attemptedEpoch, which folds in both the monotonic behind-only comparison and the failed-parse pin). A nonzero count means a fleet rebuild is draining (a deploy with a bumped epoch, a first boot after the pipeline migration, or an operator-marked scope), which is what the parsed-page gate and the rebuild progress bar key on. A row stamped or pinned at the running epoch or ahead is not this binary's work: counting it would wedge the gate and the bar on sessions the drain will never advance.

func (*Store) EpochStaleExists added in v0.3.0

func (s *Store) EpochStaleExists(ctx context.Context, epoch int) (bool, error)

EpochStaleExists answers whether any session is epoch-stale, with the same predicate as EpochStaleCount but stopping at the first hit. It backs the cross-instance fleet gate (Worker.FleetStatus), which only needs the boolean; counting the whole backlog on a page request would pay for precision the gate throws away.

func (*Store) EpochStaleReadyCount added in v0.3.0

func (s *Store) EpochStaleReadyCount(ctx context.Context, epoch int) (int, error)

EpochStaleReadyCount counts the epoch-stale sessions a drain can rebuild RIGHT NOW: EpochStaleCount minus the rows parked on a future retry backoff. It is the drain's opening count (fleet mode and the progress denominator), which runs on every chunk wake: counting the honest total there would pay O(deferred backlog) per append just to decide there is nothing to do, since persistent operational failures stay epoch-stale for as long as they keep failing. The two arms mirror the due scan's, so each lands on a ready index (epoch_ready, retry_elapsed) and parked rows are never visited. The fleet GATE keeps the honest total-side answer (EpochStaleExists over the full index): a deferred rebuild still makes the corpus mixed; it is only not this drain's workload.

func (*Store) FileChurn

func (s *Store) FileChurn(ctx context.Context, f AnalyticsFilter) (FileChurn, error)

FileChurn computes which files a scope edited repeatedly on its own pooled connection for the Insights page. The snapshot path threads fileChurnFrom so every panel reads one MVCC snapshot.

func (*Store) GlobalFacets

func (s *Store) GlobalFacets(ctx context.Context) (GlobalFacetValues, error)

GlobalFacets returns the busiest agents, machines, usernames, and projects across all sessions, each with its session count, ordered busiest first. The counts are read from the session_facets rollup (maintained incrementally by a trigger on the sessions table, see migration 0005), so each category is a bounded top-N index read rather than a GROUP BY over the whole sessions table. It backs the global Sessions view's filter rail.

Reconciliation gap (intentional and pinned): the rollup counts EVERY session, including the empty (message_count = 0) parse-failures the default feed hides. So a facet's count is the whole-corpus count and can exceed the rows a default-feed click on that facet lists, by exactly the empty sessions carrying that value. Clicking the facet and then showing empties (the footer's toggle, empty=1) reconciles them: the facet count equals the IncludeEmpty feed count for that facet. The gap is not closed in the rollup on purpose: the facet trigger fires only on the facet columns (agent, machine, user_id, project_id), never on message_count (migration 0005's UPDATE OF clause deliberately excludes it so live ingest's per-message projection updates do not churn the rollup). A message_count-aware rollup would have to fire on every ingest append, reintroducing exactly that churn. The relationship (facet_count == IncludeEmpty feed count >= default feed count) is pinned by TestGlobalFacetsReconcileEmptyHidden so it cannot drift silently.

func (*Store) HasEmptySessions added in v0.2.2

func (s *Store) HasEmptySessions(ctx context.Context, f SessionFilter) (bool, error)

HasEmptySessions reports whether the filter's scope holds at least one empty (zero-message) session, so the footer can decide whether the empty-hidden toggle would change anything without counting how many. It exists so the /sessions render path stays bounded: the old toggle carried a count K that was the same O(total) aggregate the footer no longer runs, and the toggle only needs the yes/no.

The probe is EXISTS over the filter's other conditions with message_count = 0 added, so Postgres stops at the first matching row (index-bounded, O(1)-ish) instead of scanning the scope. It forces IncludeEmpty on so the empty rows are in scope to be found regardless of the current toggle: the caller asks "are there empties here", not "does the current list include them".

func (*Store) Insights

func (s *Store) Insights(ctx context.Context, f AnalyticsFilter, panels InsightsPanels) (Insights, error)

Insights is the single-scope read of the page's panels: one filter, one shared MVCC snapshot. It is a one-element InsightsRanges call; the project and public-project quality bands come through here.

func (*Store) InsightsRanges added in v0.5.4

func (s *Store) InsightsRanges(ctx context.Context, filters []AnalyticsFilter, panels InsightsPanels) ([]Insights, error)

InsightsRanges gathers each filter's panels concurrently against one shared MVCC snapshot. The panels of one filter overlap: the quality split's session total, the archetype split, and the cohort denominators of the hygiene, context, and tool panels all describe the same scoped session set, so reading them on independent connections that each took their own snapshot would let a concurrent ingest land between two panels and make the page disagree with itself (the quality total and the archetype total, say, off by the one session that arrived mid-render).

The filters share that snapshot too, and one clock: every window's trailing bound and trend grid measure back from the same instant, and every window's panels read the same corpus state. That is what lets the fleet /insights refresher compute all five trailing windows in one pass that cannot disagree with itself: a session that lands mid-pass is either in every window that spans it or in none.

A control transaction fixes the snapshot and exports it (pg_export_snapshot); every panel then runs on its own pooled connection under a read-only REPEATABLE READ transaction that imports that exact snapshot (SET TRANSACTION SNAPSHOT). So the overlapping totals still reconcile exactly, as they did under the old single-transaction sequential read, but the panels' wall time is now the slowest single panel rather than their sum: the aggregate queries the page runs no longer serialize on one connection. The control transaction stays open until every panel has imported the snapshot (it is the last thing released), which is what keeps the exported snapshot valid for the importers.

Unlike AnalyticsSnapshot it takes no reparse-lock gate: a live page tolerates a snapshot that falls during a reparse (each session is atomically old or new in it, since ReparseSession commits per session), it just must not straddle two snapshots within one render. The panels fail fast on the first error (the errgroup cancels the rest), and every transaction is read-only and takes no row locks, so the call never blocks ingest.

func (*Store) ListAPITokens

func (s *Store) ListAPITokens(ctx context.Context, userID int64) ([]APIToken, error)

ListAPITokens returns a user's tokens, newest first.

func (*Store) ListAllSessions

func (s *Store) ListAllSessions(ctx context.Context, f SessionFilter) (rows []SessionRow, hasMore bool, err error)

ListAllSessions returns one page of sessions across every project matching the filter, newest first, and whether more rows match beyond the page. A zero ProjectID means "all projects"; the other fields narrow the set exactly as ListSessions does. This backs the global Sessions view and the Overview's recent-activity feed.

The page is bounded by the filter's Limit, and the hasMore return reports whether at least one more row matches past it: the query asks for limit+1 rows and, when it comes back full, trims the extra and flags hasMore. The handler thus learns "is there a next page" without a second count(*) over the whole matching history, so the render cost stays linear in the page rather than the corpus.

Every row carries its first-user-message Title. When Query is set, each row also carries a SearchSnippet windowed around the first match. The match window is bounded in SQL (see snippetSQLWindowLen): the LATERAL locates the match with strpos and selects only a substring around it, so a huge message never rides back whole just to yield a short snippet. Go finishes the windowing so the offsets are exact byte positions the template can split on.

func (*Store) ListInvites added in v0.2.2

func (s *Store) ListInvites(ctx context.Context) ([]Invite, error)

ListInvites returns every invite token ever issued, newest first, joined to the creator's and (if redeemed) the redeemer's username so the account page can render status without a second lookup per row.

func (*Store) ListOAuthGrants

func (s *Store) ListOAuthGrants(ctx context.Context, userID int64) ([]OAuthGrant, error)

ListOAuthGrants returns the clients a user has live tokens for, one row per client, newest connection first. It backs the account page's "connected apps" list. Revoked and fully expired tokens are excluded, so a disconnected client drops off the list.

func (*Store) ListProjects

func (s *Store) ListProjects(ctx context.Context) ([]ProjectSummary, error)

ListProjects returns every project with rolled-up stats, most recently active first.

func (*Store) ListSessions

func (s *Store) ListSessions(ctx context.Context, f SessionFilter) ([]SessionSummary, error)

ListSessions returns sessions matching the filter, newest first. Its Since bound and its order both key on last_active_at (last-event time), so this is the "sessions last active in a window, most recently active first" list, immune to a reparse restamping updated_at. It differs deliberately from ListAllSessions, which binds Since to started_at to match the Insights quality window (see the note on CountAllSessions and TestQualityDrilldownWindowsOnStartedAt).

func (*Store) ListUsers

func (s *Store) ListUsers(ctx context.Context) ([]User, error)

ListUsers returns every account, id and username only, ordered by username, to populate the overview's per-user activity filter. The password hash is left zero: this list names identities for a scope control, it does not carry the credential.

func (*Store) MCPMessagesAfter added in v0.5.5

func (s *Store) MCPMessagesAfter(ctx context.Context, sessionID int64, after *int, limit int, byteBudget int64, previewChars int) ([]MCPMessage, bool, bool, error)

MCPMessagesAfter returns an ordinal-keyset page whose worst-case JSON string expansion stays within byteBudget. PostgreSQL applies the cumulative bound before sending text over the connection, so many medium messages and one huge message both keep server memory proportional to the configured response cap. A first row that cannot fit is returned as previews so the caller can advance the cursor and attach authenticated references to the stored fields.

func (*Store) MarkEpochStale added in v0.3.0

func (s *Store) MarkEpochStale(ctx context.Context, agent string) (int, error)

MarkEpochStale forces a rebuild of every session (or one agent's sessions) by resetting their stored parser_epoch to 0, which is behind every real epoch. It is how the admin Reparse button and the `akari-server reparse` CLI trigger a fleet rebuild: mark the scope due, wake the worker, and the ordinary drain does the rest. The failure markers reset too: the operator asked for the whole scope, so previously failed sessions get one fresh attempt (and re-record their failure if the bytes still do not parse). It returns how many sessions were marked.

func (*Store) MessageCount

func (s *Store) MessageCount(ctx context.Context, sessionID int64) (int, error)

MessageCount returns a session's current message count from its rollup.

func (*Store) MessageText added in v0.5.5

func (s *Store) MessageText(ctx context.Context, sessionID int64, ordinal int, field, sha256 string) (string, error)

MessageText reads one message field for an authenticated MCP resource. The fixed field switch keeps the query static and rejects invented URI suffixes. It locates the row by session and ordinal alone, then verifies the sha256 the caller presents (baked into the resource URI when the reference was minted) against the field's digest recomputed from the live column, rather than filtering by the stored hash column: the column can still carry migration 0049's unbackfilled sentinel value on an old row, and a stored-column match would either reject a legitimate reference before the background backfill reaches that row or, worse, never recompute at all. Recomputing here instead means resolution works regardless of backfill progress and, as a side effect, reproduces the original invalidation semantics exactly: a reference whose field content changed since it was minted (a rebuild, an edit) recomputes to a different digest and is correctly refused.

func (*Store) Messages

func (s *Store) Messages(ctx context.Context, sessionID int64) ([]Message, error)

Messages returns a session's whole transcript in order, each row carrying its per-turn usage (Usage) and duplicate-prompt verdict (DuplicatePrompt) folded in the same read. The web renderer wants the full session in one pass; bounded readers (the MCP transcript window) use MessagesAfter instead so peak memory does not scale with session size.

Both the duplicate-prompt verdict and the per-turn usage are read from stored per-message rows (duplicate_prompt on the messages row and the message_turn_usage rollup, both materialized by the rebuild's in-memory fold), not folded from whole-session windows here. So the live body fragment (handleSessionBody) re-fetching this on every SSE append reads bounded indexed rows and does no growing whole-session usage aggregation or message-window scan for either.

func (*Store) MessagesAfter

func (s *Store) MessagesAfter(ctx context.Context, sessionID int64, after *int, limit int) ([]Message, error)

MessagesAfter returns the next window of a session's transcript ordered by ordinal, starting strictly after the given ordinal (after == nil for the first window). It pages by keyset on ordinal rather than OFFSET: each call walks the messages primary key (session_id, ordinal) straight to the resume point and reads only the next `limit` rows, so reading a whole session window by window costs O(N), not the O(N^2/limit) an OFFSET walk would (Postgres re-skips the already returned prefix on every page). limit is clamped to [1, 2000].

It deliberately does NOT fold the per-turn usage or the duplicate-prompt flag (both left empty on the returned messages): those require a whole-session scan, and running them here per page would make a client paging a long transcript pay O(N) whole-session work on each of O(N/limit) pages. The MCP transcript window (its only caller) renders neither, so the bounded read stays bounded.

func (*Store) Migrate

func (s *Store) Migrate(ctx context.Context, migrationFS embed.FS) error

Migrate applies every embedded migration not yet recorded, in lexical order, each inside its own transaction. It is safe to run on every startup.

func (*Store) MissingBlobs

func (s *Store) MissingBlobs(ctx context.Context, shas []string) ([]string, error)

MissingBlobs reports which of a set of candidate hashes the CAS does not hold, and atomically (re)pins every hash it does hold. The client calls this before uploading tool bodies: a body the server already has (from an earlier sync, or any other session, since the CAS dedupes globally) is reported absent from the missing set and so not re-sent, but it is pinned here so it survives the sweep until the transcript chunk that references it commits. Without the pin a present but unreferenced, unpinned blob could be reclaimed in the window between this check and the transcript append, stranding a sentinel with no body.

The whole check-and-pin runs in one transaction so the pin is durable before the client is told a body is present. Pinning takes the blob rows FOR KEY SHARE (via the upsert's FK validation) which conflicts with the sweep's FOR UPDATE, so a body cannot be both reported-present and swept.

func (*Store) NextRebuildRetryDelay added in v0.5.5

func (s *Store) NextRebuildRetryDelay(ctx context.Context, epoch int) (time.Duration, bool, error)

NextRebuildRetryDelay returns the delay until the earliest parked operational retry this parser epoch can act on. The eligibility predicates mirror the retry arm of DueSessions: during a rolling deploy, an older worker must ignore a retry on a projection already stamped by a newer binary. Otherwise an elapsed, ineligible row would repeatedly arm a zero-delay timer while every drain correctly left it untouched.

PostgreSQL computes the delay against its own clock because it also writes parse_retry_at; application/database clock skew must not stretch or collapse the backoff. The boolean is false when no eligible rebuild is parked.

func (*Store) OAuthAccessAuth

func (s *Store) OAuthAccessAuth(ctx context.Context, accessHash string) (userID int64, scope string, expiresAt time.Time, err error)

OAuthAccessAuth resolves a presented access-token hash to its owner, scope, and expiry, rejecting expired and revoked tokens. It is the MCP endpoint's bearer check; the expiry it returns lets the caller mirror the real token lifetime.

func (*Store) OAuthClient

func (s *Store) OAuthClient(ctx context.Context, id string) (OAuthClient, error)

OAuthClient looks up a registered client by id.

func (*Store) OutlineMessages added in v0.4.0

func (s *Store) OutlineMessages(ctx context.Context, sessionID int64) ([]Message, error)

OutlineMessages returns every message of a session with bounded columns, for the outline rail and the flow ribbon: full coverage (one entry per turn even when the transcript window is partial) at a fixed cost per row, independent of message size.

func (*Store) OverviewOGImage

func (s *Store) OverviewOGImage(ctx context.Context, userID int64) (OGImage, error)

OverviewOGImage loads the cached preview card for a user, addressed by id, or ErrNotFound when none is cached yet. See ogImage for why this read carries no visibility join (the public serve path reads through PublicOverviewCard).

func (*Store) Project

func (s *Store) Project(ctx context.Context, id int64) (ProjectSummary, error)

Project returns one project's identity (without rollups), including whether its overview is published, so the signed-in project page can render the publicity control's current state without a second query.

func (*Store) ProjectCardSnapshot added in v0.3.0

func (s *Store) ProjectCardSnapshot(ctx context.Context, f AnalyticsFilter) (a Analytics, avgScore *float64, ok bool, err error)

ProjectCardSnapshot reads everything the project OG card renders from (the windowed analytics and the mean quality score) as one epoch-gated snapshot, so the card's figures and its QUALITY grade describe the same instant. The two reads sat in separate pooled queries before, which let a rebuild or a signal refresh land in the gap: the card could then cache pre-rebuild token totals beside a partially rebuilt quality grade, a torn projection the page a visitor lands on never shows. Folding both into the one repeatable-read snapshot AnalyticsSnapshot already uses (and its epoch gate) means the average is read from the same MVCC snapshot as the totals, so the card reconciles with the page's Insights grade distribution for the same project and window. ok is false when a fleet rebuild is draining at the snapshot point, exactly as AnalyticsSnapshot, so the caller skips the render rather than caching a mixed card.

func (*Store) ProjectOGImage added in v0.3.0

func (s *Store) ProjectOGImage(ctx context.Context, projectID int64) (OGImage, error)

ProjectOGImage loads the cached preview card for a project, addressed by id, or ErrNotFound when none is cached yet. See ogImage for why this read carries no visibility join (the public serve path reads through PublicProjectCard).

func (*Store) ProjectOverviewSnapshot added in v0.5.5

func (s *Store) ProjectOverviewSnapshot(ctx context.Context, f AnalyticsFilter) (Analytics, Insights, error)

ProjectOverviewSnapshot reads the usage panel and quality band from one repeatable-read transaction. Public and unfiltered authenticated project views share the completed result in the HTTP snapshot cache, so this refresh path favors one immutable database generation over the ordinary Insights fan-out across several imported snapshot transactions.

func (*Store) ProjectSparklines

func (s *Store) ProjectSparklines(ctx context.Context, days int) (map[int64][]float64, error)

ProjectSparklines returns, per project id, a fixed-length slice of daily cost over the last `days` days (index 0 oldest, last index today, UTC). Projects with no recent usage are simply absent from the map; the index view renders a flat line for them. One query buckets in Go so the index stays a single round trip regardless of project count.

func (*Store) PromptHygiene

func (s *Store) PromptHygiene(ctx context.Context, f AnalyticsFilter) (PromptHygiene, error)

PromptHygiene aggregates the scoped sessions' prompt-hygiene counts on its own pooled connection. Insights instead threads its snapshot transaction through promptHygieneFrom, so the measured cohort shares one MVCC snapshot with the other panels.

func (*Store) PublicOverviewCard

func (s *Store) PublicOverviewCard(ctx context.Context, username string) (User, OGImage, bool, error)

PublicOverviewCard resolves a username to its account and reads that account's cached Open Graph card in one query, gated on overview_public = TRUE. Folding the public check, the user lookup, and the card read into a single statement is what keeps the /u/<username>/og.png serve atomic: a split (resolve the user, then read the card) leaves a window where a concurrent unpublish between the two steps could serve a card for an overview that just went private. found is false when the name is unknown or the overview is not public, so the caller 404s the link. When found is true the account is public; card.PNG is nil when no card is cached yet (the LEFT JOIN yields NULLs), which the caller renders on demand. Only the fields the card path needs are loaded; the password hash is left zero.

func (*Store) PublicOverviewUser

func (s *Store) PublicOverviewUser(ctx context.Context, username string) (User, error)

PublicOverviewUser resolves a username to its account for the logged-out overview page, only while that account's overview is public. The flag is folded into the WHERE clause, so an unknown or unpublished name yields ErrNotFound and the link 404s. Only the fields the public page needs are loaded; the password hash is left zero.

func (*Store) PublicProjectCard added in v0.3.0

func (s *Store) PublicProjectCard(ctx context.Context, id int64) (ProjectSummary, OGImage, bool, error)

PublicProjectCard resolves a project id to its identity and reads that project's cached Open Graph card in one query, gated on overview_public = TRUE. Folding the public check, the project lookup, and the card read into a single statement keeps the /p/<id>/og.png serve atomic: a split (resolve the project, then read the card) leaves a window where a concurrent unpublish between the two steps could serve a card for an overview that just went private. found is false when the id is unknown or the overview is not public, so the caller 404s the link. When found is true the project is public; card.PNG is nil when no card is cached yet (the LEFT JOIN yields NULLs), which the caller renders on demand. It returns the same identity fields PublicProjectOverview does, which the card render uses for the heading.

func (*Store) PublicProjectOverview added in v0.2.5

func (s *Store) PublicProjectOverview(ctx context.Context, id int64) (ProjectSummary, error)

PublicProjectOverview resolves a project id to its identity for the logged-out overview page, only while that project's overview is public. The flag is folded into the WHERE clause, so an unknown or unpublished id yields ErrNotFound and the link 404s. It returns the same identity fields Project does (no rollups), which the public page pairs with a windowed analytics read.

func (*Store) PublicSessionByID added in v0.5.5

func (s *Store) PublicSessionByID(ctx context.Context, publicID string, before *int) (PublicSessionSnapshot, error)

PublicSessionByID loads a published session and one transcript window from a single repeatable-read snapshot. before is nil for the trailing window (also the whole-session outline and tool metadata the rail needs) and names the first currently rendered ordinal when paging backward (the outline read is skipped, since the fragment it feeds does not render one). Publication is checked inside the same transaction as every rendered row.

func (*Store) PublicSessionCard added in v0.3.0

func (s *Store) PublicSessionCard(ctx context.Context, publicID string) (int64, OGImage, bool, error)

PublicSessionCard resolves a session's public id to its numeric id and reads that session's cached Open Graph card in one query, gated on visibility = 'public'. Folding the public check, the id lookup, and the card read into a single statement keeps the /s/<public_id>/og.png serve atomic: a split (resolve the session, then read the card) leaves a window where a concurrent unpublish between the two steps could serve a card for a session that just went private. found is false when the public id is unknown or the session is not public, so the caller 404s the link. When found is true the session is public; card.PNG is nil when no card is cached yet (the LEFT JOIN yields NULLs), which the caller renders on demand from the session's own rollups by id.

func (*Store) PublishOverview

func (s *Store) PublishOverview(ctx context.Context, userID int64) error

PublishOverview marks a user's own usage overview public, so /u/<username> resolves for logged-out viewers. The address is the username, so there is no capability id to mint: the call only flips the gate.

func (*Store) PublishProjectOverview added in v0.2.5

func (s *Store) PublishProjectOverview(ctx context.Context, projectID int64) error

PublishProjectOverview marks a project's usage overview public, so /p/<id> resolves for logged-out viewers. Projects are fleet-global rather than owned, so (unlike PublishSession) there is no owner check: any signed-in caller may flip the gate, matching the route's requireFull guard. The address is the project id, so there is no capability id to mint. A missing project touches no row and is ErrNotFound rather than a silent no-op.

func (*Store) PublishSession

func (s *Store) PublishSession(ctx context.Context, sessionID, userID int64, candidateID string) (string, error)

PublishSession marks a session public and returns its public id, minting a new one only if it had none. The owner check is folded into the WHERE clause, so a session that does not belong to the user returns ErrNotFound and is untouched. Re-publishing an already-public session keeps the existing id, so a shared link stays valid across repeated publishes.

func (*Store) PutBlob

func (s *Store) PutBlob(ctx context.Context, sha, mediaType, contentType string, r io.Reader) error

PutBlob stores a content-addressed body uploaded directly by the client and pins it against the sweep for blobPinTTL. The bytes the client sends are the STORED bytes (raw or zstd-compressed, as contentType declares); the server stores them verbatim and never (de)compresses, so it stays off the compression CPU path. The body streams in from r in bounded slices so neither side holds the whole body in memory: a 500 MiB tool result lands as a large object without a 500 MiB buffer. The stored bytes are verified against the claimed sha256 (which is the hash of the stored bytes), so a corrupt upload cannot poison the CAS; the server does not validate that a zstd-declared body actually decompresses, since that would cost the CPU this design avoids and the key already pins the exact bytes.

No database lock is held across the network read. An already-present body is pinned and committed in a short transaction before its (redundant) body is drained, so a slow duplicate upload cannot block the sweep's FOR UPDATE behind a FOR KEY SHARE held across a client-controlled read. A new body is written inside one transaction (Postgres large objects require it), but that transaction holds no lock on any existing row until it inserts the new blobs row at the end, so it does not block the sweep either.

func (*Store) PutOverviewOGImage

func (s *Store) PutOverviewOGImage(ctx context.Context, userID int64, png []byte, generatedAt time.Time) (bool, error)

PutOverviewOGImage stores the rendered preview card for a user's published overview. The guarded-upsert semantics (and why the stamp is the data snapshot's instant, not the write time) live on putOGImage, shared by all three card caches.

func (*Store) PutProjectOGImage added in v0.3.0

func (s *Store) PutProjectOGImage(ctx context.Context, projectID int64, png []byte, generatedAt time.Time) (bool, error)

PutProjectOGImage stores the rendered preview card for a project's published overview. The guarded-upsert semantics live on putOGImage, shared by all three card caches.

func (*Store) PutSessionOGImage added in v0.3.0

func (s *Store) PutSessionOGImage(ctx context.Context, sessionID int64, png []byte, generatedAt time.Time) (bool, error)

PutSessionOGImage stores the rendered preview card for a published session. The guarded-upsert semantics live on putOGImage, shared by all three card caches.

func (*Store) QualityDistribution

func (s *Store) QualityDistribution(ctx context.Context, f AnalyticsFilter) (QualityDistribution, error)

QualityDistribution aggregates the scoped sessions' grades and outcomes. The grade split and the outcome split are separate scans, so it wraps them in one repeatable-read, read-only snapshot: the session total, the grade buckets, and the outcome buckets then all describe the same scoped cohort, where a concurrent session insert or signals_stale change between the two scans could otherwise pair a grade split from one cohort with an outcome split from another. Insights threads its own snapshot through qualityDistributionFrom so its panels reconcile against each other; this is the standalone equivalent. The snapshot takes no row locks, so it never blocks ingest.

func (*Store) RebuildSession added in v0.3.0

func (s *Store) RebuildSession(ctx context.Context, sessionID int64, epoch int, r SessionReducer) error

RebuildSession rebuilds a session's entire projection from its stored raw bytes in one transaction: it streams the raw regions through the reducer, folds the resulting whole-session delta in memory (usage dedup, tool-result patching, fallback merging, prompt facts, rollups), deletes the old derived rows, bulk-inserts the new ones, stamps the parse cursor and epoch, and re-grades the session's signals. It is the ONLY write path for derived data: ingest appends raw bytes and wakes the parse worker, and the worker calls this.

Because the delete and the rewrite commit together, a concurrent reader never sees the session empty or half built: it sees the prior projection until this transaction commits, then the new one. Any failure rolls the whole rebuild back, so a parser error on malformed bytes or an operational store/CAS error leaves the prior projection intact rather than a cleared session.

The raw bytes are never modified. Raw regions are read in bounded batches (rebuildRegionBytes), so the raw side of the parse never holds the whole transcript in one buffer; the folded delta is whole-session by design.

Failures split in two. A reducer error (r.Feed rejecting the bytes) is deterministic: re-running would fail identically, so the attempt is recorded on the failure markers (parse_error plus the epoch and raw length it tried), committed with no projection writes (the feed runs before any). The bookkeeping of the last successful rebuild (parsed_byte_len, parser_epoch) is deliberately NOT advanced: the surviving projection keeps reading as the epoch that actually built it, and the session's signals flip stale so the settle pass regrades them under the current scoring. The due scan skips a session only while its recorded failure matches its current bytes and the running epoch, so it neither hot-loops on the same bad bytes nor goes silent forever: new bytes or a new epoch retry it, and a chunk that landed mid-parse keeps it due (the marker records the length that was read, not the newer one). The reducer's error is still returned so the worker can log and count it. An operational error (a store or CAS failure, a cancellation) rolls everything back and stamps nothing, so the next drain retries.

func (*Store) RecordRebuildBackoff added in v0.3.0

func (s *Store) RecordRebuildBackoff(ctx context.Context, sessionID int64) error

RecordRebuildBackoff defers a session's next rebuild attempt after an operational failure (the rebuild rolled back; the session is still due). It runs in its own transaction, after the failed one, and is best-effort: if this write fails too, the session simply retries on the next wake, which is the pre-backoff behavior. The deferral doubles on consecutive failures, from 30 seconds to a one-hour ceiling, so a failure that does not clear on its own (a CAS blob the client never uploaded) costs the drain one attempt per backoff window instead of one per chunk wake. Everything that changes the situation clears the marker for an immediate retry: a successful rebuild, a recorded deterministic failure, new bytes, a raw reset, an operator reparse.

func (*Store) RedeemAuthCode added in v0.5.5

func (s *Store) RedeemAuthCode(ctx context.Context, codeHash string, p OAuthTokenParams) error

RedeemAuthCode consumes a valid authorization code and stores the token pair in one transaction. A token insert failure rolls the code consumption back, while the conditional update remains the single-use gate for concurrent exchanges.

func (*Store) RefreshSessionSignals

func (s *Store) RefreshSessionSignals(ctx context.Context, sessionID int64) error

RefreshSessionSignals recomputes one session's signals in its own transaction. It is the standalone form the settle tick (RefreshSettledSignals) and the tests use; a rebuild calls gradeSignalsTx inside its existing transaction instead, so the signals commit with the projection rather than in a second round trip.

It grades only a session whose parse state is settled at the running epoch: the attempted epoch (see attemptedEpoch) equals it, and the raw bytes are either fully parsed or covered by the recorded deterministic failure. Anything else is skipped, leaving signals_stale set, because a grade written now would be cleared as current while it is not:

  • Attempted epoch ahead (a newer binary's rebuild OR its recorded failure, seen during a rolling deploy): this binary's scoring code does not match, and grading would clear the flag with nothing left to make the newer binary redo it.
  • Attempted epoch behind, or bytes neither parsed nor pinned (a rebuild is due, e.g. a finalize racing the parse worker right after the last chunk landed): the pending rebuild supersedes anything graded from the current projection, so grading now could stamp signals from a projection that does not cover the bytes.
  • Pinned failure at the running epoch: gradeable. The drain will never advance the session, so the settle pass grades the surviving projection under the current scoring, which is the failure model's contract.

The rebuild path needs no such check: it grades the projection it just stamped, at its own epoch, inside the same transaction. An unset epoch (0, a store wired without SetParserEpoch, as in tests) skips nothing, matching the other epoch gates.

func (*Store) RefreshSettledSignals

func (s *Store) RefreshSettledSignals(ctx context.Context) (int, error)

RefreshSettledSignals recomputes signals for every settled session marked stale. It is the catch-up half of signal grading: a rebuild grades a session that is already settled or terminal in its own transaction, so this tick exists for the sessions that settle BETWEEN rebuilds (the last rebuild ran while the session was live, so its grade was withheld and signals_stale stayed set). It grades them once, after they have been idle past the abandoned threshold, off the ingest hot path.

A session is due when signals_stale is set AND it is either settled (ended_at at least abandonedIdleMinutes in the past) or terminal (the client declared it finished via `akari sync --finalize`, so it is gradeable now without waiting out the idle window). The flag is the single-table marker that replaces a cross-table due predicate: a rebuild of a live session leaves it set, and a grade clears it only for a settled-or-terminal session. So it captures every way a stored grade can fall behind its source, without a join the settle scan would have to evaluate per row:

  • Never graded (a fresh ingest, or a session that predates signals): the column defaults true, so it is due from creation until the first grade.
  • Graded before the session settled (a rebuild that ran while the session was still live, so its outcome was not yet stable): that rebuild left the flag set because the session was not idleLongEnough, so this tick re-grades it once settled.

(A projection change needs no flag write of its own: a rebuild always ends by grading or, for a live session, leaving the flag set, so the grade can never silently trail the projection.)

It drains the whole due backlog in bounded batches through two keyset scans, each resuming strictly after the last row of the previous batch, and a session drops out of its scan the moment it is graded, so the pass reads only the due rows via a partial index, O(D_due) per wake rather than O(settled history):

  • the settled-by-idle tail in (ended_at, id) order (idx_sessions_signals_stale), and
  • terminal stale sessions in id order (idx_sessions_terminal_stale), which are gradeable regardless of ended_at and so cannot ride the settled drain's ended_at cursor (a terminal transcript can carry a NULL ended_at yet still be gradeable, so keying the terminal drain on id keeps the due-query scope matched to gatherSignalFacts).

A session that is both settled and terminal is graded by whichever scan reaches it first and skipped by the other (its signals_stale is cleared), so it is never graded twice. Each session is refreshed in its own transaction so one slow session never holds a broad lock, cancellation stops the drain between sessions, and it returns how many it refreshed.

func (*Store) Register

func (s *Store) Register(ctx context.Context, username, passwordHash, inviteHash string) (User, error)

Register creates a user. The first account ever created becomes admin and needs no invite; every later account must present an unredeemed, unexpired invite token (by its hash), which is redeemed atomically with the insert.

func (*Store) ResetRaw

func (s *Store) ResetRaw(ctx context.Context, sessionID int64) error

ResetRaw clears a session's raw store and its derived rows so the next chunk starts from zero. Dropping the tool_calls and attachments can orphan CAS blobs; like any deletion or rebuild, those are reclaimed by a later SweepBlobs rather than synchronously here, so a client reset stays cheap.

It takes the parent session row lock and then the session_raw lock, the locks AppendChunk and RebuildSession serialize on, so a reset cannot interleave with an in-flight append or rebuild and leave behind a chunk row or projection rows for a session it just zeroed. Taking the session row before session_raw matches DeleteSession's order, so the two cannot deadlock.

func (*Store) RevokeAPIToken

func (s *Store) RevokeAPIToken(ctx context.Context, userID, tokenID int64) error

RevokeAPIToken marks a user's token revoked. It is a no-op if the token does not belong to the user.

func (*Store) RevokeInvite added in v0.2.2

func (s *Store) RevokeInvite(ctx context.Context, id int64) error

RevokeInvite deletes a STILL-OPEN invite token by id: unredeemed and not past its expiry. It is a no-op (not an error) if the id does not exist or the invite is no longer open (redeemed or expired), matching RevokeAPIToken's idempotent shape; the caller (an admin-only form handler) redirects either way.

The predicate is exactly the one revocability policy the whole app shares: an invite is revocable iff it is still redeemable. It matches classifyInvite in the account view (which shows Revoke only for the still-open case) and Register's redemption gate (redeemed_at IS NULL AND (expires_at IS NULL OR expires_at > now())), so the page's control, this write path, and the redemption never disagree about which invites can still be acted on. A direct POST to revoke an expired or redeemed invite therefore does nothing, just as the missing Revoke button implies.

Aligning on redeemed_at IS NULL also closes a race with Register: Register redeems an invite inside its registration transaction and only then patches redeemed_by. Scoping the delete to unredeemed rows makes the two mutually exclusive on the same row: whichever commits first, the other matches nothing (Register sees the row deleted and returns ErrInvalidInvite; the delete sees redeemed_at set and removes nothing).

func (*Store) RevokeOAuthGrant

func (s *Store) RevokeOAuthGrant(ctx context.Context, userID int64, clientID string) error

RevokeOAuthGrant revokes every live token a user holds for one client, disconnecting it. It is scoped to the user, so a request cannot revoke another account's grant.

func (*Store) RotateOAuthToken

func (s *Store) RotateOAuthToken(ctx context.Context, oldRefreshHash string, p OAuthTokenParams) (clientID string, userID int64, scope, resource string, err error)

RotateOAuthToken redeems a refresh token for a fresh access/refresh pair. It rewrites the existing row in place (so the grant stays one row per connection) only if the presented refresh token is live, returning the bindings the new access token inherits. A refresh token that is unknown, revoked, or past its expiry matches no row and yields ErrInvalidGrant. Rotating the refresh hash on every use makes refresh tokens single-use, so a leaked-and-replayed refresh token is caught the next time the legitimate client refreshes.

func (*Store) SessionAppendByID added in v0.4.0

func (s *Store) SessionAppendByID(ctx context.Context, sessionID int64, after int) (SessionSnapshot, error)

SessionAppendByID loads the live append: the audit bundle (the fragment refreshes the instruments out-of-band on every tick) and the rows past `after`, plus the whole-session shape only when rows actually landed. A quiet tick (raw bytes ahead of the rebuild) changes no turns, so it skips both the shape read and the swap it would feed. A missing session returns ErrNotFound.

func (*Store) SessionAuditByID added in v0.4.0

func (s *Store) SessionAuditByID(ctx context.Context, sessionID int64) (SessionAudit, error)

SessionAuditByID loads a session's audit bundle from one repeatable-read snapshot. A missing session returns ErrNotFound.

func (*Store) SessionCacheStats

func (s *Store) SessionCacheStats(ctx context.Context, sessionID int64) (CacheStats, error)

SessionCacheStats recomputes one session's cache effectiveness by scanning its usage rows and pricing per model. Unlike the scoped CacheStats it counts ALL the session's usage, dated or not, so its prompt totals match the session's token rollups (sessions.total_*); the scoped path keeps the dated guard to match the time-bounded panel instead. That mirrors the one documented rollup-versus-analytics gap the cost and token figures already carry: a per-session figure counts every usage row, an analytics figure counts only the dated rows it can plot.

The session header no longer calls this on the hot path: it reads the same figure off the total_cache_savings_usd rollup (folded per row at parse time), which is O(1) rather than a per-refresh scan. This full recompute stays as the independent oracle the reconciliation test prices the rollup against, so a drift between the parse-time fold and a from-scratch per-model recompute fails a test rather than shipping a wrong tile.

func (*Store) SessionCard added in v0.3.0

func (s *Store) SessionCard(ctx context.Context, sessionID int64, buckets int) (SessionCard, bool, error)

SessionCard reads all of one session's OG-card inputs as a single repeatable-read, read-only snapshot, bucketing the activity strip into at most buckets buckets in SQL. found is false when the session id is unknown. A single session is rebuilt atomically during a reparse (never a half-built row), so this snapshot reads either the wholly-old or the wholly-new session, never a torn mix, which is why the session card needs no reparse-lock gate the aggregate cards take: the one snapshot is consistency enough for a single session.

func (*Store) SessionDetailByID

func (s *Store) SessionDetailByID(ctx context.Context, id int64) (SessionDetail, error)

SessionDetailByID loads a session by numeric id.

func (*Store) SessionDetailByPublicID

func (s *Store) SessionDetailByPublicID(ctx context.Context, publicID string) (SessionDetail, error)

SessionDetailByPublicID loads a published session by its public id.

func (*Store) SessionEarlierByID added in v0.4.0

func (s *Store) SessionEarlierByID(ctx context.Context, sessionID int64, before int) (SessionDetail, TranscriptPage, error)

SessionEarlierByID loads the "Show earlier" fragment's inputs from one snapshot: the session row and the window strictly before `before`. The window's rows carry their own tools, attachments, and fallback notices, and the fragment refreshes nothing out-of-band, so nothing else is read.

func (*Store) SessionFacets

func (s *Store) SessionFacets(ctx context.Context, projectID int64) (FacetValues, error)

SessionFacets returns the distinct agents, machines, and usernames present in a project's sessions, each sorted, for the project view's filter controls.

func (*Store) SessionFeed

func (s *Store) SessionFeed(ctx context.Context, f SessionFilter, limit int, cursor *SessionFeedCursor) ([]SessionRow, *SessionFeedCursor, error)

SessionFeed returns one page of the cross-project feed (newest session first) and the cursor for the page after it (nil when this page is the last). It applies the same filters as ListAllSessions but pages by keyset on the immutable session id descending rather than OFFSET: each page resumes from the prior page's last id and reads only the next `limit` rows, so a client paging the whole feed never re-skips the rows it already saw and a concurrent updated_at bump cannot drop a row from the walk. Each row still carries its updated_at, so a caller can order by recency within a page. limit is clamped to [1, 500] (default 100).

The Since bound windows on s.started_at, the same column ListAllSessions and CountAllSessions use, so SessionFilter.Since has ONE meaning across every query that shares the filter: "started in the window". A session started before the window but bumped inside it is out of all three consistently, rather than in the MCP feed but out of the web drill. The keyset still pages on id (immutable), so the window column choice does not affect paging stability.

func (*Store) SessionMeta

func (s *Store) SessionMeta(ctx context.Context, sessionID int64) (userID int64, agent string, err error)

SessionMeta returns the owning user and agent of a session, or ErrNotFound.

func (*Store) SessionModelFallbacks added in v0.2.5

func (s *Store) SessionModelFallbacks(ctx context.Context, sessionID int64, limit int) ([]ModelFallback, error)

SessionModelFallbacks returns a session's recorded model fallbacks in a stable order (by when they occurred, then by dedup_key so rows with no timestamp still order deterministically), capped at limit rows so the read stays bounded on a pathological session. It reads the merged model_fallbacks rows the projection built. A limit of zero or less means no cap; callers on hot paths pass ModelFallbackListCap.

func (*Store) SessionOGImage added in v0.3.0

func (s *Store) SessionOGImage(ctx context.Context, sessionID int64) (OGImage, error)

SessionOGImage loads the cached preview card for a session, addressed by id, or ErrNotFound when none is cached yet. See ogImage for why this read carries no visibility join (the public serve path reads through PublicSessionCard).

func (*Store) SessionRawTo

func (s *Store) SessionRawTo(ctx context.Context, w io.Writer, sessionID, limit int64) (written int64, truncated bool, total int64, err error)

SessionRawTo streams a session's raw uploaded bytes (the lossless JSONL the client sent, the source every projection is rebuilt from) to w in upload order, writing at most limit bytes. It returns the number of bytes written, whether the session held more than was written (so the caller can flag a truncated read), and the session's full raw length. A limit of zero or less means no cap. This is the raw underlying data behind the parsed transcript, exposed so an agent can inspect exactly what was ingested rather than only the projection. A missing session returns ErrNotFound.

func (*Store) SessionReferencesBlob

func (s *Store) SessionReferencesBlob(ctx context.Context, sessionID int64, sha256hex string) (bool, error)

SessionReferencesBlob reports whether a session points at a blob, through a tool call's input or result or through an attachment. Blob serving is gated on this so a session can never read a blob it does not reference, even though the CAS dedupes content across sessions.

Lifting Codex images to the CAS means a session view fetches a blob per rendered image as well as per tool body, so this runs once per blob on every open and live refresh: it must stay logarithmic in the session's references, not scan them. Each arm is a hash-leading index lookup: tool_calls by (input_sha256, session_id) and (result_sha256, session_id), attachments by (sha256, session_id). The parameter is cast to char(64) so it matches the bpchar columns and their indexes; a bare text parameter would compare bpchar against text, cast the columns, and fall back to scanning the session's whole slice of tool calls and attachments.

func (*Store) SessionSignalsByID

func (s *Store) SessionSignalsByID(ctx context.Context, sessionID int64) (SessionSignals, error)

SessionSignalsByID reads a session's up-to-date stored signals. A session with no usable row reads as an unknown, unscored result rather than an error, so the session page renders a neutral state instead of a stale or missing grade. A row is usable only when the session is not flagged signals_stale, so the header and the fleet aggregates gate on the same flag and agree on exactly which grades count. A refresh of a still-live session leaves the flag set, so a not-yet-stable outcome (abandoned versus unknown turns on the idle gap) never reaches a reader before the settle tick pins it.

Gating on the flag rather than a refreshed_at >= updated_at comparison is deliberate. updated_at also moves on metadata-only writes (an announce re-announce, an owner reassignment) that leave the grade valid, so keying reads on it would strand those grades unread while the settle tick, which keys on the flag, never revisits them.

func (*Store) SessionSnapshotByID added in v0.4.0

func (s *Store) SessionSnapshotByID(ctx context.Context, sessionID int64) (SessionSnapshot, error)

SessionSnapshotByID loads the full session view: the audit bundle, the transcript's tail window, and the whole-session shape. A missing session returns ErrNotFound.

func (*Store) SetParserEpoch added in v0.3.0

func (s *Store) SetParserEpoch(epoch int)

SetParserEpoch records the running binary's parser epoch for the epoch-staleness gate (see epochGatedSnapshot). Call it once at wiring time, before serving traffic.

func (*Store) Subagents

func (s *Store) Subagents(ctx context.Context, parentID int64) ([]SubagentRow, error)

Subagents returns the sessions whose parent is the given session, each with its verdict and its first-prompt title, so the parent's subagents table reads as what each child was asked to do and how it ended rather than a bare id list. The verdict comes from the same signalsCurrent-gated LEFT JOIN the feed row uses, so a child's outcome here matches its own session page.

func (*Store) SweepBlobs

func (s *Store) SweepBlobs(ctx context.Context) (int, error)

func (*Store) TokenAuth

func (s *Store) TokenAuth(ctx context.Context, tokenHash string) (userID int64, scope string, err error)

TokenAuth resolves a presented token hash to its owner and scope, rejecting revoked tokens, and stamps last_used_at.

func (*Store) ToolCalls

func (s *Store) ToolCalls(ctx context.Context, sessionID int64) ([]ToolCallView, error)

ToolCalls returns all of a session's tool calls as metadata, for the web renderer. Bounded readers pass a message-ordinal range to ToolCallsInRange.

func (*Store) ToolCallsInRange

func (s *Store) ToolCallsInRange(ctx context.Context, sessionID int64, minOrdinal, maxOrdinal int) ([]ToolCallView, error)

ToolCallsInRange returns the tool calls hanging on messages in the inclusive ordinal window [minOrdinal, maxOrdinal], so a bounded transcript read fetches only the calls for the messages it returned rather than the whole session.

func (*Store) ToolStats

func (s *Store) ToolStats(ctx context.Context, f AnalyticsFilter) (ToolStats, error)

ToolStats computes the scope's tool volume, reliability, and mix for the Insights page. The deduped tool-call numerator and the turn denominator are separate reads, so it wraps them in one repeatable-read, read-only snapshot: a concurrent projection update between them could otherwise pair TotalCalls from one cohort with Turns from another, making ToolsPerTurn a mixed-snapshot figure. Insights threads its own snapshot through toolStatsFrom; this is the standalone equivalent. The snapshot takes no row locks, so it never blocks ingest.

func (*Store) TranscriptAfter added in v0.4.0

func (s *Store) TranscriptAfter(ctx context.Context, sessionID int64, after int) (TranscriptPage, error)

TranscriptAfter reads the rows strictly after `after`, for the live SSE append: the client names the last ordinal it rendered and receives just the turns that follow, with the seed rows that let the walker stamp the boundary. When more rows exist than the cap, More is set and the caller should fall back to a whole-window re-render rather than append a fragment with a gap after it.

func (*Store) TranscriptTail added in v0.4.0

func (s *Store) TranscriptTail(ctx context.Context, sessionID int64, before *int) (TranscriptPage, error)

TranscriptTail reads the trailing window of a session's transcript: up to TranscriptTailTurns user turns (bounded by transcriptPageMessageCap messages) ending at the transcript's end, or strictly before `before` when the reader is paging earlier ("Show earlier" passes its first rendered ordinal). The window starts on a turn boundary (a user message) whenever one exists within the cap; a session with fewer turns than the window simply starts at its beginning.

func (*Store) UnpublishOverview

func (s *Store) UnpublishOverview(ctx context.Context, userID int64) error

UnpublishOverview hides a user's public overview by clearing the gate flag. The URL is the username and never changes, so re-publishing later brings the same /u/<username> back.

func (*Store) UnpublishProjectOverview added in v0.2.5

func (s *Store) UnpublishProjectOverview(ctx context.Context, projectID int64) error

UnpublishProjectOverview hides a project's public overview by clearing the gate flag. The URL is the project id and never changes, so re-publishing later brings the same /p/<id> back.

func (*Store) UnpublishSession

func (s *Store) UnpublishSession(ctx context.Context, sessionID, userID int64) error

UnpublishSession returns a session to internal visibility and clears its public id, so the old link stops resolving rather than merely flipping a flag. It is owner-scoped; a session the user does not own yields ErrNotFound.

func (*Store) UpsertProject

func (s *Store) UpsertProject(ctx context.Context, remoteKey, host, owner, repo, displayName, kind string) (int64, error)

UpsertProject inserts the project keyed by its remote/synthetic key, or refreshes last_seen on an existing one, returning the project id. The kind is updated on conflict so a standalone folder that is later deleted transitions to orphaned in place (its key, machine + path, is unchanged), and one that gains a remote is never re-resolved here: a remote session carries its own remote key.

func (*Store) UpsertProxyUser added in v0.2.5

func (s *Store) UpsertProxyUser(ctx context.Context, username string) (User, error)

UpsertProxyUser resolves the account for a username asserted by a trusted reverse proxy, provisioning it on first sight (auth_source "proxy", no password, not admin). It is the just-in-time provisioning behind proxy-header auth: the proxy has already authenticated the user against the org's identity, so the first request under a new identity mints the local account and every later one resolves it.

The common path is a plain read (the account already exists), so the steady state costs one indexed lookup, not a write, on the per-request auth path. Only a genuinely new identity takes the insert, and ON CONFLICT DO NOTHING absorbs the race between two concurrent first requests for the same user: whichever loses the insert still reads the row back.

An existing username is adopted regardless of its auth_source, including a local password account or an admin: in this deployment the proxy is the authority on identity, so an assertion for "grace" is grace. The insert never rewrites an existing row, so adopting a local account does not strip its password or flip its source. Operators who do not want that overlap keep the local and federated namespaces disjoint (the deployment notes in docs/development.md spell this out).

func (*Store) UserByID

func (s *Store) UserByID(ctx context.Context, id int64) (User, error)

UserByID looks up a user by id, including its overview publicity state, which the account page and the overview badge read for the signed-in owner.

func (*Store) UserByUsername

func (s *Store) UserByUsername(ctx context.Context, username string) (User, error)

UserByUsername looks up a user by username. password_hash is read through COALESCE so a federated account's NULL hash surfaces as "" rather than a scan error.

func (*Store) VelocityStats

func (s *Store) VelocityStats(ctx context.Context, f AnalyticsFilter) (VelocityStats, error)

VelocityStats computes the scope's cadence figures for the Insights page. Latency, active time with message count, and tool count are three separate reads over the message timeline, so it wraps them in one repeatable-read, read-only snapshot: a concurrent projection update between them could otherwise combine active minutes from one message timeline with a tool count from another, making ToolsPerActiveMin and the headline cadence internally inconsistent. Insights threads its own snapshot through velocityStatsFrom; this is the standalone equivalent. The snapshot takes no row locks, so it never blocks ingest.

func (*Store) WebSession

func (s *Store) WebSession(ctx context.Context, id string) (userID int64, err error)

WebSession resolves a session cookie id to its user, rejecting expired ones.

func (*Store) WindowSessionPage

func (s *Store) WindowSessionPage(ctx context.Context, f SessionFilter) (SessionPage, error)

WindowSessionPage returns the project page's session table: the capped rows that contributed dated usage inside the filter's window, each carrying its in-window token and cost sums rather than its all-time rollup, plus the aggregate of the windowed sessions beyond the cap. It shares the analytics base exactly (the same dated usage_events under the same project, window, agent, user, and machine scope, grouped per session instead of summed whole), which is what lets the rows be a partition of the usage panel's headline where the lifetime rollups ListSessions returns would overcount a session whose usage predates the window.

The cap keeps a project with thousands of windowed sessions from rendering an unbounded table. So the visible rows alone need not sum to the headline; the remainder closes the gap. It is queried, not subtracted from the panel: a boolean OR like cost_incomplete cannot be undone by subtraction (a visible unpriced row would wrongly mark the priced tail incomplete), so the tail is aggregated directly over the hidden sessions, carrying its own per-class sums, cost, and bool_or flag. The remainder query runs only when the cap actually engaged.

func (*Store) WriteBlobPrefixTo

func (s *Store) WriteBlobPrefixTo(ctx context.Context, w io.Writer, sha256hex string, limit int64) (mediaType string, err error)

WriteBlobPrefixTo streams at most limit bytes of a blob's body to w and returns its media type; limit <= 0 means the whole body. The large-object reader pulls only the bytes it copies, so a small limit transfers a small prefix rather than the whole object: a capped preview of a bulky CAS body is O(limit), not O(blob). A caller that needs to flag truncation compares limit against the stored byte_len from BlobMeta.

func (*Store) WriteBlobTo

func (s *Store) WriteBlobTo(ctx context.Context, w io.Writer, sha256hex string) (mediaType string, err error)

WriteBlobTo streams a blob's whole body to w and returns its media type. Large object reads must run in a transaction, so the copy happens inside one.

type SubagentRow added in v0.4.0

type SubagentRow struct {
	SessionSummary
	Grade   *string
	Outcome string
}

SubagentRow is one direct child session in a parent's subagents table: the summary every session list shows, plus the child's own verdict so the fold summary can say "2 failed" and the table can flag the children worth opening. Grade is nil and Outcome empty when the child has no current signals row (unsettled, or stale under a newer epoch), the same LEFT JOIN convention the feed row uses.

func (SubagentRow) Failed added in v0.4.0

func (r SubagentRow) Failed() bool

Failed reports whether this child ended in an error, the one outcome the fold summary counts as failed: an abandoned child is the parent stopping it, not the child failing.

type SubagentStats added in v0.3.0

type SubagentStats struct {
	DelegateShare []float64 // percent of a bucket's root sessions that delegated
	CostShare     []float64 // percent of a bucket's spend that ran through subagents

	FanoutOrder []string         // "one","twoThree","fourSeven","eightPlus"
	FanoutRows  []map[string]int // per bucket: fan-out band to count of delegating sessions

	SessionsThatDelegatePct  float64
	SubagentSessionsInWindow int
	CostThroughSubagentsPct  float64
	DeepestTree              int

	// CostShareIncomplete is true when a token-bearing unpriced event landed on either the
	// subagent numerator or the whole-window denominator, so the cost share is computed from
	// lower-bound dollars and reads as partial (the ratio can move either way).
	CostShareIncomplete bool
}

SubagentStats is the delegation read: how much of the fleet's work runs through subagents, how wide the fan-out gets, and the headline figures. DelegateShare and CostShare ride the bucket grid; Fanout is the per-bucket spread of how many subagents a delegating session spawned.

func (SubagentStats) HasData added in v0.3.0

func (s SubagentStats) HasData() bool

HasData reports whether any delegation happened in the window.

type ToolCallView

type ToolCallView struct {
	MessageOrdinal int
	CallIndex      int
	ToolName       string
	Category       string
	FilePath       string
	// FileRelPath is the worktree-relative form of FilePath (migration 0030), empty when the
	// path sits outside the session's workspace or no cwd was known. The transcript shows it in
	// preference to the absolute FilePath: the same repo file reads the same across worktrees.
	FileRelPath     string
	Detail          string
	InputSHA        string
	InputBytes      int64
	InputMediaType  string
	ResultSHA       string
	ResultBytes     int64
	ResultMediaType string
	ResultStatus    string
}

ToolCallView is one tool call rendered as metadata (the body lives in the CAS, fetched on demand by its sha256).

type ToolFailSeries added in v0.3.0

type ToolFailSeries struct {
	Name string
	Rate []float64
}

ToolFailSeries is one tool's error rate across the bucket grid, for the failure-trend lines drawn beside the fleet rate.

type ToolPoint added in v0.3.0

type ToolPoint struct {
	Name     string
	Calls    int
	Failures int
	Sessions int
	Category string
}

ToolPoint is one tool's whole-window reliability reading for the scatter: how much the fleet leans on it (calls, and the sessions that used it) against how often it errors.

func (ToolPoint) ErrorRate added in v0.3.0

func (t ToolPoint) ErrorRate() float64

ErrorRate is the tool's failure share as a percent, 0 when it never ran.

type ToolResultDelta

type ToolResultDelta struct {
	CallUID    string
	Body       string
	BodySHA256 string
	Bytes      int64
	MediaType  string
	Status     string
}

ToolResultDelta patches a tool call's result, matched by call id. The result body reaches the CAS by one of two paths, mirroring ProjToolCall: Body holds it inline for the server to write, or BodySHA256 is the reference the client already uploaded. Both are empty when the result carries no body.

type ToolStat

type ToolStat struct {
	Name     string
	Calls    int
	Failures int
}

ToolStat is one tool's volume and reliability over a scope: how many times it ran and how many of those runs failed. Calls and Failures are deduped (a replayed transcript re-emits prior tool calls, so the raw rows over-count), the same dedup the per-session signals apply, so the fleet figures reconcile with the session tiles.

func (ToolStat) ErrorRate

func (t ToolStat) ErrorRate() float64

ErrorRate is the tool's failure share, 0 when it never ran.

type ToolStats

type ToolStats struct {
	TotalCalls    int
	TotalFailures int
	Turns         int        // human prompts across the cohort (sum of user_message_count)
	Tools         []ToolStat // the busiest tools, most calls first (see maxToolBars)
	Clipped       int        // tools beyond the shown list, folded into the totals but not the bars
}

ToolStats is the fleet tool picture for a scope: the overall call and failure volume, the prompt count the tools-per-turn rate divides by, and the busiest tools with their own reliability. It answers "how much tool work happens, how reliable is it, and which tools carry it" over the same cohort the rest of the Insights page reads.

func (ToolStats) ErrorRate

func (t ToolStats) ErrorRate() float64

ErrorRate is the fleet failure share across every tool, 0 when nothing ran.

func (ToolStats) HasData

func (t ToolStats) HasData() bool

HasData reports whether the scope ran any tools, so the panel can show a note rather than an empty bar list for a pure-conversation window.

func (ToolStats) ToolsPerTurn

func (t ToolStats) ToolsPerTurn() float64

ToolsPerTurn is how many tool calls the fleet ran per human prompt, 0 when the cohort has no prompts (a window of pure automation). The denominator is every prompt in the cohort, so a conversation that needed no tools pulls the rate down rather than sitting outside the average.

type ToolTrends added in v0.3.0

type ToolTrends struct {
	Reliability []ToolPoint

	MixOrder []string             // category keys, busiest first, "other" last
	Mix      []map[string]float64 // per bucket: category key to percent of the bucket's calls

	FailFleet []float64        // fleet error rate per bucket, percent
	FailWorst []ToolFailSeries // the few worst tools' error rate per bucket
}

ToolTrends is the tools read three ways: the whole-window reliability scatter, the category mix over time, and the failure rate over time. Reliability is not bucketed (it is a snapshot of every tool); Mix and the failure series ride the shared grid.

type TranscriptPage added in v0.4.0

type TranscriptPage struct {
	// Msgs is the window itself, in ordinal order.
	Msgs []Message
	// Seed is up to transcriptSeedLookback rows immediately before Msgs, in ordinal
	// order, for walker priming only; the caller must not render them.
	Seed []Message
	// Tools, Attachments, and Fallbacks are the tool calls, attachments, and model
	// fallback notices hanging on Msgs, read in the same transaction as the rows
	// themselves. A rebuild committing between separate reads could otherwise pair one
	// projection's messages with another's chips, images, or notices at the same
	// ordinals, and an appended fragment would leave that mix in the DOM; carrying them
	// on the page pins all four to one snapshot.
	Tools       []ToolCallView
	Attachments []AttachmentView
	Fallbacks   []ModelFallback
	// HasEarlier reports whether any rows precede the window, so the renderer knows to
	// draw the "Show earlier" bar. EarlierCount is how many (for the bar's label).
	HasEarlier   bool
	EarlierCount int
	// More reports that rows beyond the window remain AFTER it. Only TranscriptAfter
	// sets it: a live append that hits the message cap means the client is too far
	// behind for an append to reconcile, and the handler should re-render the window
	// whole instead of leaving a gap in the DOM.
	More bool
}

TranscriptPage is one contiguous, full-fold window of a session's transcript: the rows to render, the unrendered seed rows that precede them, and what lies beyond the window on each side. Every row carries the same per-turn usage and duplicate-prompt fold the whole-session read carries, so a windowed transcript renders identically to the full one.

type TreeRollup added in v0.4.0

type TreeRollup struct {
	// SubagentCount is how many subagent sessions the root fanned out, counted over
	// the whole descendant subtree (a subagent that itself spawns subagents adds all
	// of them), not just the root's direct children.
	SubagentCount int
	// CostUSD is the summed cost of the root and every subagent in its subtree: the
	// price of the whole work item, which the row's own cost understates whenever the
	// session delegated work to subagents.
	CostUSD float64
	// CostIncomplete is true when any session folded into the subtree could not be
	// fully priced (an unknown model, a missing rate), so the rolled-up cost is a
	// floor rather than an exact figure and the UI can mark it approximate.
	CostIncomplete bool
}

TreeRollup is the whole-work-item view of a top-level session: its own cost plus every subagent it fanned out, folded together. A feed row shows its session's own tokens and cost, but a single prompt can spawn dozens of subagents whose spend is invisible at the row. The rollup names that hidden fan-out so a reader auditing where the money went sees the work item's true footprint, not just its root turn.

The zero value (no subagents, zero cost) is the correct reading for a session that spawned nothing, so a row with no fan-out simply carries no rollup chip.

type Trends struct {
	Unit         string      // "day" or "week", the bucket width every series shares
	BucketStarts []time.Time // bucket start instants, oldest to newest; the x axis
	Labels       []string    // formatted bucket labels, aligned to BucketStarts

	FleetMix  FleetMix       // token share by model per bucket
	Gallery   Gallery        // one point per fully-spanned session (duration x cost)
	Velocity  VelocityTrends // active hours, response latency, throughput per bucket
	Tools     ToolTrends     // reliability scatter, category mix, failure rate per bucket
	Churn     ChurnTrend     // re-edit trend plus the project/folder/file tree
	Signals   SignalTrends   // grades, outcomes, hygiene, context per bucket
	Economics Economics      // cost of quality and cache savings per bucket
	Subagents SubagentStats  // delegation share and fan-out per bucket
	Rhythm    RhythmGrid     // message + tool volume by hour of week
}

Trends is the time-bucketed counterpart to the Insights distributions: the same scoped cohort read as a grid of buckets (days or weeks, per AnalyticsFilter.Bucket) so the page can draw how the fleet moved over the window rather than one rolled-up number. Every per-bucket series is indexed against BucketStarts (oldest first), zero-filled so an empty week still draws a point and the range selector windows every chart together. It is computed only when the filter names a bucket; the distributions-only callers leave it nil.

func (*Trends) HasData added in v0.3.0

func (t *Trends) HasData() bool

HasData reports whether the trend grid carries any buckets to draw.

type TurnUsage added in v0.2.2

type TurnUsage struct {
	Input, Output, CacheRead, CacheWrite, Reasoning int64
	CostUSD                                         *float64
	CostIncomplete                                  bool
	ContextTokens                                   int64
}

TurnUsage is one message-turn's rolled-up usage, folded across the (possibly several, streamed) usage_events rows that share a message_ordinal. It rides on Message.Usage, folded in the transcript read's usage_agg CTE (see messageReadCTEs) rather than fetched as a separate per-ordinal map. Input, Output, CacheRead, CacheWrite, and Reasoning are the summed token classes.

CostUSD is the summed cost, but nil when EVERY contributing row's cost_usd was NULL: an unpriced model has no cost to show, and a summed zero would read as "this turn was free" rather than "we could not price it". A turn that mixes priced and unpriced rows returns the priced partial (a lower bound), matching how the session-level cost total treats an incomplete price.

CostIncomplete is true when the turn folded in a usage row that carried real token volume but no price, so CostUSD (when present) is a lower bound: the summed cost covers only the priced subset of the token classes the card shows beside it. It is the per-turn shape of the session and analytics costIncompleteExpr, so the turn's cost stamp gets the same "$X+" lower-bound marker those figures do rather than an exact-looking cost next to unpriced tokens. It is false for a fully-priced turn and for a fully-unpriced one (where CostUSD is nil and the card reads "unpriced" instead).

ContextTokens is the turn's context occupancy: Input + CacheRead + CacheWrite, output EXCLUDED. It is the size of the prompt presented that turn (what the model had to read), not the cumulative spend: output tokens are what the model produced, so they are not part of the context it carried in. This mirrors gatherContextHealth's definition exactly (the same three-class sum), so the per-message context stamp and the session's peak-context signal are measuring the same thing at two granularities. The divergence between this ordinal-grouped, attributed-only fold and the signal's raw fold is documented on the usage_agg CTE in messageReadCTEs and pinned by TestMessagesTurnUsageDivergesFromContextFold.

type User

type User struct {
	ID       int64
	Username string
	// PasswordHash is the argon2id PHC string for a local account, or empty for a
	// federated one (auth_source != "password"). It is read through COALESCE, so a
	// NULL hash surfaces as "" rather than a scan error; the login path treats an
	// empty hash as "no local password" and refuses it before ever calling the
	// verifier. See HasPassword.
	PasswordHash string
	// AuthSource is how the account authenticates: "password" for a local account,
	// or an external source ("proxy") for a federated one provisioned on the fly
	// from a trusted assertion.
	AuthSource string
	IsAdmin    bool
	CreatedAt  time.Time
	// OverviewPublic reports whether this account has published its own usage
	// overview to logged-out viewers at /u/<username>. It is loaded by UserByID
	// (the account page and the overview badge read the owner's own state through
	// it); ListUsers leaves it zero, since the per-user filter only needs
	// identities.
	OverviewPublic bool
}

User is an akari account.

func (User) HasPassword added in v0.2.5

func (u User) HasPassword() bool

HasPassword reports whether the account can log in with a local password. A federated account (provisioned from a trusted external assertion) has none, so the password login path must refuse it rather than calling the verifier with an empty hash.

type VelocityStats

type VelocityStats struct {
	ResponseP50       time.Duration // median prompt-to-first-reply latency, over every turn
	ResponseP90       time.Duration // 90th percentile, the slow-turn tail
	FirstResponseP50  time.Duration // median opening-turn latency (the prompt that pays the context load)
	MsgsPerActiveMin  float64       // messages per active minute (throughput, idle time excluded)
	ToolsPerActiveMin float64       // tool calls per active minute
	ActiveSeconds     float64       // total active time the rates divide by (the dead air between bursts removed)
	Turns             int           // prompt-to-reply pairs measured
	Sessions          int           // sessions that contributed at least one measured turn
}

VelocityStats answers "how fast does work move" over a scope: how long the agent takes to start replying after a prompt (the turn-cycle latency, at the median and the long tail), how slow the opening reply is on its own, and how densely messages and tool calls land during the time the session is actively working. It reads message timestamps, so it sees the same scoped cohort the rest of the Insights page does: sessions that started in the window.

func (VelocityStats) HasData

func (v VelocityStats) HasData() bool

HasData reports whether the scope carried any measurable cadence, so the panel can show a note rather than a row of dashes and zeros on a window with no timed turns.

func (VelocityStats) HasThroughput

func (v VelocityStats) HasThroughput() bool

HasThroughput reports whether there was any active time to divide by, so the panel can dash the per-minute rates rather than print a 0.0 that reads as a real measurement when the denominator is undefined (a single-message scope, or one whose every gap exceeds the idle cap).

type VelocityTrends added in v0.3.0

type VelocityTrends struct {
	ActiveHours []float64 // hands-on agent hours per bucket (dead air over the idle gap removed)
	WallHours   []float64 // wall-clock session span hours per bucket
	ResponseP50 []float64 // median prompt-to-first-reply latency, seconds
	ResponseP90 []float64 // 90th percentile, seconds
	ResponseP99 []float64 // 99th percentile, the long tail
	MsgsPerMin  []float64 // messages per active minute
	ToolsPerMin []float64 // tool calls per active minute
}

VelocityTrends is the per-bucket cadence: how much hands-on time the fleet logged against the wall-clock span it held open, how fast the agent started replying, and how densely messages and tools landed. Every series is aligned to the shared bucket grid.

Jump to

Keyboard shortcuts

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