storage

package
v0.24.0 Latest Latest
Warning

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

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

Documentation

Overview

Package storage

Index

Constants

View Source
const (
	// RawTurnSourceWire marks turns captured on the wire
	// (Envoy → extproc → ingest): request verbatim as sent to the
	// provider, response reduced from the SSE stream by the capture
	// adapter.
	RawTurnSourceWire = "wire"

	// RawTurnSourceTranscript marks turns ingested from a harness
	// on-disk transcript (parentUuid causal records + subagent
	// metadata).
	RawTurnSourceTranscript = "transcript"
)

Raw-turn source discriminators. The raw layer is source-agnostic so every capture origin lands in the same substrate; the deriver treats rows uniformly.

View Source
const DefaultListLimit = 50

DefaultListLimit is the page size used when ListOpts.Limit is zero.

View Source
const MaxListLimit = 5000

MaxListLimit is the maximum permitted page size. Drivers clamp ListOpts.Limit to this value.

Set high (5000) because the AncestryChains hot path has a large fixed cost per request (the recursive CTE setup) and a tiny incremental cost per leaf — measured at ~260ms regardless of whether limit is 200 or 1000 against the brian_large store. Forcing callers to paginate at small page sizes multiplies the fixed cost. The deck's full Overview load drops from ~17s @ limit=200 (50 round trips) to ~3s @ limit=2000 (5 round trips) just from the math.

Memory impact: each row carries ~200 bytes (the CTE doesn't ship the heavy `content` blob — see the label_hint extraction in pkg/storage/ent/driver/driver.go). 5000 leaves × ~30 avg depth × 200 bytes ≈ 30 MB peak per request, which is fine for an API server backing one deck instance.

View Source
const SkillSortDownloads = "downloads"

SkillSortDownloads is the SkillListOpts.Sort value for most-downloaded order.

Variables

View Source
var ErrInvalidContent = errors.New("invalid content for storage")

ErrInvalidContent signals that a write failed because the payload content itself is unstorable — JSON carrying escape sequences or bytes a Postgres JSONB column rejects (SQLSTATE 22P05 unsupported Unicode escape, 22021 invalid byte sequence) — rather than any infrastructure fault.

The distinction drives behavior at the boundaries: an HTTP handler maps it to a client 4xx (the data is the problem; retrying the identical bytes will never succeed) instead of a 502 that reads as a gateway outage, and the derive worker can quarantine such a row instead of re-attempting it on every poll forever. Drivers wrap the underlying pg error with this sentinel; callers test with errors.Is.

View Source
var ErrSkillVersionConflict = errors.New("skill version number already exists")

ErrSkillVersionConflict is returned by CreateSkillVersion when the (skill_id, version_number) pair already exists — i.e. a concurrent publish claimed the same number. Callers translate it into a retry rather than a 500.

Functions

This section is empty.

Types

type BackfillSessionStatusResult added in v0.13.0

type BackfillSessionStatusResult struct {
	// Scanned is the number of session rows visited.
	Scanned int
	// Updated is the number whose status row was (re)written.
	Updated int
}

type Chain added in v0.4.0

type Chain struct {
	// Nodes is the walk output in node-first order: index 0 is the node
	// the walk started from, and the last element is either a real root
	// or the last resolvable node whose parent could not be found.
	Nodes []*merkle.Node

	// Incomplete is true when the walk stopped short of a real root,
	// whether because a parent_hash could not be resolved (see
	// MissingParent) or because a cycle was detected (see CycleDetected).
	Incomplete bool

	// MissingParent is the parent_hash that could not be resolved in this
	// store. Only set when Incomplete is true due to a dangling pointer;
	// left empty for cycle-detected incompletes (the cycle-triggering
	// hash is present in Nodes already).
	MissingParent string

	// CycleDetected is true when the walk stopped because it was about
	// to re-visit a hash already in Nodes. Drivers guard every walk with
	// a per-chain seen-set so a corrupt parent edge can never spin
	// forever; when this flag trips, the chain is the largest
	// acyclic prefix reachable from the starting hash.
	//
	// In practice this never trips on a healthy store. It exists because
	// a single corrupted parent_hash would otherwise hang any endpoint
	// that walks ancestry, which is a blast radius out of proportion to
	// the likelihood.
	CycleDetected bool
}

Chain is the result of walking a node's parent edges back toward a root.

A Chain whose Incomplete field is false reached a legitimate root (a node whose parent_hash is nil or empty). A Chain whose Incomplete field is true stopped because a parent_hash pointed at a node that is not currently present in this store — see MissingParent. The nodes in Nodes are still valid; the store simply can't resolve any higher ancestors from here.

This state is expected on large or long-lived stores that trim/offload older data, merge content from foreign sources, or receive chains whose ancestors live in another store altogether. Callers should treat it as informational (a signal to render a marker, or trigger a future "thaw" lookup against another source), not as corruption.

func (*Chain) Complete added in v0.4.0

func (c *Chain) Complete() bool

Complete reports whether the walk reached a real root.

type Cursor added in v0.4.0

type Cursor struct {
	// CreatedAt is the head-node timestamp of the last item on the prior page.
	CreatedAt time.Time `json:"t"`

	// Hash is the head-node hash of the last item on the prior page.
	// Used as a tiebreaker when multiple nodes share a CreatedAt.
	Hash string `json:"h"`
}

Cursor is the decoded form of an opaque ListOpts.Cursor token. It is exported for driver implementations; clients should treat the encoded string as opaque.

func DecodeCursor added in v0.4.0

func DecodeCursor(token string) (Cursor, error)

DecodeCursor parses an opaque cursor token. An empty token returns the zero Cursor without error, meaning "start from the most recent".

func (Cursor) Encode added in v0.4.0

func (c Cursor) Encode() string

Encode returns the opaque base64 representation of the cursor.

type DeriveQueue added in v0.16.0

type DeriveQueue interface {
	// MarkDeriveDirty queues (or re-bumps) one harness session.
	MarkDeriveDirty(ctx context.Context, orgID, harnessID, harnessSessionID string) error

	// ListDeriveDirty returns sessions whose dirty mark has settled
	// (DirtiedAt <= dirtiedBefore), oldest first, capped at limit.
	ListDeriveDirty(ctx context.Context, dirtiedBefore, firstDirtiedBefore time.Time, limit int32) ([]DeriveQueueEntry, error)

	// GetDeriveDirty re-reads one queue entry. Returns nil (no error)
	// when the session is clean.
	GetDeriveDirty(ctx context.Context, orgID, harnessID, harnessSessionID string) (*DeriveQueueEntry, error)

	// ClearDeriveDirty removes the entry only if its DirtiedAt is
	// unchanged from e.DirtiedAt. Returns false when the session was
	// re-dirtied (or already cleared) in the meantime.
	ClearDeriveDirty(ctx context.Context, e DeriveQueueEntry) (bool, error)

	// SweepDeriveDirty enqueues every harness session with raw-layer
	// activity at or after activeSince (the worker's slow backstop for
	// lost marks, bounded so a restart doesn't stampede the queue with
	// all of history). The zero time sweeps every session. Sessions
	// already queued keep their DirtiedAt. Returns how many sessions
	// were newly enqueued.
	SweepDeriveDirty(ctx context.Context, activeSince time.Time) (int64, error)

	// DeriveQueueStats reports queue depth and the oldest dirty mark.
	// Cheap (one aggregate over the small dirty-queue table); the
	// worker calls it every poll and the readiness probe leans on it
	// as the "store reachable, queue pollable" check.
	DeriveQueueStats(ctx context.Context) (DeriveQueueStats, error)
}

DeriveQueue is an optional capability for a Driver: the dirty-session queue feeding the derive worker. Marking is at-least-once and idempotent (an upsert that bumps DirtiedAt); deriving is idempotent (re-run prunes 0) — together they make a lost clear or duplicate mark cost only a redundant derive, never lost data.

Only drivers that host the raw layer implement this (Postgres does; in-memory intentionally does not). Callers MUST type-assert.

type DeriveQueueEntry added in v0.16.0

type DeriveQueueEntry struct {
	// OrgID is the canonical UUID string; empty means "no org context"
	// and maps to the nil-UUID sentinel, mirroring raw_turns.org_id.
	OrgID string

	HarnessID        string
	HarnessSessionID string

	// DirtiedAt is when the most recent raw turn dirtied the session.
	// The worker debounces on it and clears the entry only if it is
	// unchanged since read — a bump mid-derive survives the clear.
	DirtiedAt time.Time

	// FirstDirtiedAt is when the session was first marked dirty in this
	// queued window (it survives re-marks, unlike DirtiedAt). The worker
	// derives a continuously-streaming session — whose DirtiedAt never
	// settles past the debounce cutoff — once FirstDirtiedAt crosses the
	// max-lag bound, so live views see bounded lag.
	FirstDirtiedAt time.Time
}

DeriveQueueEntry is one dirty harness session awaiting re-derivation. The key is the deriver's natural unit — the harness triple — NOT a sessions-row id: a sessions row is not guaranteed to exist when a raw turn lands (transcript ingest writes only a raw row).

type DeriveQueueStats added in v0.16.0

type DeriveQueueStats struct {
	// Depth is the number of queued (dirty) sessions.
	Depth int64

	// OldestDirtiedAt is the oldest dirty mark still queued — "derive
	// lag" is now minus this. Zero when the queue is empty.
	OldestDirtiedAt time.Time
}

DeriveQueueStats is a point-in-time summary of the dirty-session queue, feeding the worker's depth/lag gauges and readiness probe.

type Driver

type Driver interface {
	// Get retrieves a node by its hash.
	Get(ctx context.Context, hash string) (*merkle.Node, error)

	// GetByParent retrieves all nodes that have the given parent hash.
	// Pass nil to get root nodes.
	GetByParent(ctx context.Context, parentHash *string) ([]*merkle.Node, error)

	// Put stores a node. Returns true if the node was newly inserted,
	// false if it already exists. If the node already exists, this should be
	// a no-op. Put provides automatic deduplication via content-addressing in the dag.
	Put(ctx context.Context, node *merkle.Node) (bool, error)

	// Has checks if a node exists by its hash.
	Has(ctx context.Context, hash string) (bool, error)

	// List returns all nodes in the store.
	List(ctx context.Context) ([]*merkle.Node, error)

	// Roots returns all root nodes (nodes with no parent).
	Roots(ctx context.Context) ([]*merkle.Node, error)

	// Leaves returns all leaf nodes (nodes with no children).
	Leaves(ctx context.Context) ([]*merkle.Node, error)

	// ListSessions returns a page of leaf nodes ordered by created_at descending,
	// optionally filtered by ListOpts. The returned Page.NextCursor is empty
	// when there are no further pages.
	//
	// "Session" here is the API-layer concept: a leaf node identifies the head
	// of a conversation chain. Filters apply to the leaf node itself, not to
	// any ancestor in the chain.
	ListSessions(ctx context.Context, opts ListOpts) (*Page[*merkle.Node], error)

	// CountSessions returns aggregate counts for the slice of data matching
	// the filter in opts. Pagination fields on opts (Limit, Cursor) are ignored.
	CountSessions(ctx context.Context, opts ListOpts) (SessionStats, error)

	// Ancestry returns the path from a node back to its root (node first, root last).
	Ancestry(ctx context.Context, hash string) ([]*merkle.Node, error)

	// AncestryChain is Ancestry with a marker describing how the walk
	// terminated. When the walk stops at a parent_hash whose target is not
	// present in this store, the returned Chain has Incomplete=true and
	// MissingParent set to that parent_hash. The nodes in Chain.Nodes are
	// still valid; this state is expected on stores that trim older data or
	// merge content from foreign sources, and is not an error.
	AncestryChain(ctx context.Context, hash string) (*Chain, error)

	// AncestryChains returns a Chain for each input hash, batched per depth
	// level so the cost scales with maximum chain depth rather than the
	// product of starting-node count and depth. Shared ancestors across
	// starts are fetched once.
	//
	// The returned map is keyed by each starting hash. Starts that are not
	// present in the store are omitted from the map rather than surfaced as
	// errors — callers that need a strict "every start must resolve" check
	// should compare the map's keys against their input slice.
	//
	// Use this instead of looping over AncestryChain when walking many
	// leaves (e.g. the /v1/sessions/summary handler): the per-leaf loop
	// issues O(N_starts × depth) queries, which on a real store with tens
	// of thousands of leaves will not complete in any reasonable time.
	AncestryChains(ctx context.Context, hashes []string) (map[string]*Chain, error)

	// LoadDag takes a node hash and returns the full graph.
	LoadDag(ctx context.Context, hash string) (*merkle.Dag, error)

	// Open initializes the backing store and makes it ready for use.
	Open(ctx context.Context) error

	// UpdateUsage updates token / duration usage metadata on an existing node.
	UpdateUsage(ctx context.Context, hash string, usage *llm.Usage) error

	// Depth returns the depth of a node (0 for roots).
	Depth(ctx context.Context, hash string) (int, error)

	// Close closes the store and releases any resources.
	Close() error
}

Driver defines the interface for persisting and retrieving nodes in a storage backend. The Driver is the primary interface for working with pkg/merkle - it handles storage, retrieval, and traversal of nodes per the storage implementor.

type IngestTurnRequest added in v0.10.0

type IngestTurnRequest struct {
	// Session is the optional session-tracking envelope. nil is a
	// legitimate value (legacy clients): implementations fall back
	// to a Merkle-derived synthetic harness_session_id.
	Session *sessions.IngestEnvelope

	// Nodes is the chain of nodes for this turn, ordered root-to-leaf.
	// The first element is the conversation root; the last is the
	// assistant response. ParentHash linkage between successive
	// elements must already be set by NewNode at the call site.
	Nodes []*merkle.Node

	// InputTokens / OutputTokens / CostUSD are the per-turn deltas
	// applied to the sessions counters when at least one node was
	// newly inserted. Pre-computed by the worker (which owns the
	// pricing lookup) so this layer stays free of llm/pricing
	// dependencies.
	InputTokens  int64
	OutputTokens int64
	CostUSD      float64

	// DerivedTitle folds a title-gen shadow call's output onto the
	// session (sessions.derived_title). Empty for every other call
	// kind; non-empty values overwrite (the harness regenerates the
	// title as the session evolves, so the latest wins).
	DerivedTitle string
}

IngestTurnRequest bundles every input the SessionIngester needs. Built by the worker pool from a single Job + the chain of content-addressed nodes derived from that Job's request/response pair.

type IngestTurnResult added in v0.10.0

type IngestTurnResult struct {
	// SessionID is the resolved/created sessions row id as a 36-char
	// canonical UUID string. Stable across retries (idempotent on
	// natural key).
	SessionID string

	// NewNodes lists the nodes that were actually inserted on this
	// call (i.e. nodes.hash was not already present). May be shorter
	// than the input chain on retries.
	NewNodes []*merkle.Node

	// CountersUpdated is true when the call bumped sessions counters.
	// False for pure-retry calls where every node hash already
	// existed (idempotent retries must not double-count).
	CountersUpdated bool
}

IngestTurnResult reports what the call actually did. Used by the worker to drive downstream side effects (publisher events, vector embeddings) for newly-inserted nodes only.

type ListOpts added in v0.4.0

type ListOpts struct {
	// Limit is the maximum number of items to return. If zero, DefaultListLimit
	// is used. Values larger than MaxListLimit are clamped.
	Limit int

	// Cursor is an opaque pagination token from a prior Page.NextCursor.
	// Empty means start from the most recent.
	Cursor string

	// Filters. Empty / nil values mean "no filter on this field".
	Project  string
	Agent    string
	Model    string
	Provider string
	Since    *time.Time
	Until    *time.Time
}

ListOpts controls filtering and cursor pagination for session listings.

All filter fields are AND-combined and apply to the head (leaf) node of each session. Empty string and nil pointer fields are treated as "no filter".

Pagination is keyset-based on (CreatedAt DESC, Hash DESC). Callers should treat Cursor as opaque; use the NextCursor returned in Page.

func (ListOpts) Normalize added in v0.4.0

func (o ListOpts) Normalize() ListOpts

Normalize returns a copy of opts with Limit clamped to [1, MaxListLimit]. A zero Limit is replaced with DefaultListLimit.

type ModelTokenStats added in v0.8.0

type ModelTokenStats struct {
	InputTokens         int64
	OutputTokens        int64
	CacheCreationTokens int64
	CacheReadTokens     int64
}

ModelTokenStats is the per-model token rollup returned inside SessionStats. Cost is intentionally not computed here — pricing lives in pkg/sessions and is applied by the API handler.

type ModelUsage added in v0.16.0

type ModelUsage struct {
	Model        string  `json:"model"`
	Calls        int64   `json:"calls"`
	InputTokens  int64   `json:"input_tokens"`
	OutputTokens int64   `json:"output_tokens"`
	CostUSD      float64 `json:"cost_usd"`
}

ModelUsage is one model's contribution to a session: how many llm calls ran on it and what they spent. Cost is priced at derive time, so the per-model share is spend-weighted (a fan-out of cheap subagent calls never out-votes the expensive main-spine model).

type NotFoundError

type NotFoundError struct {
	Hash string
}

NotFoundError is returned when a node doesn't exist in the store.

func (NotFoundError) Error

func (e NotFoundError) Error() string

type Page added in v0.4.0

type Page[T any] struct {
	Items []T

	// NextCursor is empty when there are no more pages.
	NextCursor string
}

Page is a generic paginated result envelope.

type ParentRef added in v0.4.0

type ParentRef struct {
	Hash       string
	ParentHash *string
}

ParentRef is a lightweight (hash, parent_hash) tuple used by integrity checks and bulk traversal code that only needs the DAG edges, not the full node bucket JSON.

type ParentRefLister added in v0.4.0

type ParentRefLister interface {
	ListParentRefs(ctx context.Context) ([]ParentRef, error)
}

ParentRefLister is an optional capability for drivers that can return every node's parent edge in a single efficient query. Drivers that do not implement it are assumed to be unsuitable for bulk integrity checks over large stores.

type RawTurnHeader added in v0.16.0

type RawTurnHeader struct {
	ID            int64
	Source        string
	Provider      string
	AgentName     string
	RequestID     string
	ReceivedAt    time.Time
	Meta          json.RawMessage
	RequestBytes  int64
	ResponseBytes int64
}

RawTurnHeader is one wire-log row: capture identity and sizes, no payloads. The operator surface onto the raw layer.

type RawTurnRecord added in v0.16.0

type RawTurnRecord struct {
	// OrgID is the canonical UUID string from the validated session
	// envelope. Empty means "no org context" and maps to the nil-UUID
	// sentinel, mirroring nodes.org_id.
	OrgID string

	// Source is one of the RawTurnSource* constants.
	Source string

	Provider  string
	AgentName string

	// HarnessID / HarnessSessionID echo the session envelope's natural
	// key so a session's raw turns are queryable without decoding the
	// envelope JSONB. Empty when the turn carried no envelope.
	HarnessID        string
	HarnessSessionID string

	// RequestID is the capture adapter's per-call id (extproc forwards
	// the Envoy request id). It dedupes retried POSTs of the same
	// captured turn; empty disables deduplication for the row.
	RequestID string

	// RawRequest is the provider request body, verbatim.
	RawRequest json.RawMessage

	// Response is the reduced provider response, verbatim from the
	// envelope.
	Response json.RawMessage

	// Meta is the capture adapter's metadata block (model, stream,
	// upstream status, byte counts, elapsed seconds, …), verbatim.
	Meta json.RawMessage

	// SessionEnvelope is the session-tracking block, verbatim.
	SessionEnvelope json.RawMessage

	// ReceivedAt is populated by the store on read; ignored on write
	// (the database stamps insertion time).
	ReceivedAt time.Time

	// ID is the store-assigned monotonic row id, populated on read.
	ID int64
}

RawTurnRecord is one immutable captured turn, stored verbatim before any parsing or projection. The JSON payloads are the exact bytes from the ingest envelope — never re-marshaled through parsed structs — so fields unknown to the current build survive for later derivers.

type RawTurnStore added in v0.16.0

type RawTurnStore interface {
	// PutRawTurn appends one captured turn. Returns false when the
	// row was deduplicated (same org + request id already stored).
	PutRawTurn(ctx context.Context, rec RawTurnRecord) (bool, error)

	// ListRawTurns scans the raw layer in insertion order, returning
	// up to pageSize rows with id greater than afterID. Pass 0 to
	// start from the beginning.
	ListRawTurns(ctx context.Context, afterID int64, pageSize int32) ([]RawTurnRecord, error)

	// CountRawTurns reports the total number of raw rows.
	CountRawTurns(ctx context.Context) (int64, error)
}

RawTurnStore is an optional capability for a Driver: an append-only, immutable store of full capture envelopes. The derived layer (nodes, edges, typing) is a pure re-runnable function of these rows — see pkg/derive.

Only drivers that can host the raw layer implement this (Postgres does; in-memory intentionally does not). Callers MUST type-assert.

type SessionBackfillRequest added in v0.10.0

type SessionBackfillRequest struct {
	Session      *sessions.IngestEnvelope
	NodeHashes   []string
	StartedAt    time.Time
	LastSeenAt   time.Time
	InputTokens  int64
	OutputTokens int64
	TurnCount    int64
}

type SessionBackfillResult added in v0.10.0

type SessionBackfillResult struct {
	SessionID   string
	NodesLinked int
}

type SessionBackfiller added in v0.10.0

type SessionBackfiller interface {
	BackfillSession(ctx context.Context, req SessionBackfillRequest) (SessionBackfillResult, error)
}

SessionBackfiller is an optional Driver capability for linking legacy node rows to a session table row after the fact. It is deliberately separate from SessionIngester: backfill does not insert nodes, it only UPSERTs session identity and stamps existing nodes that still have no session_id.

type SessionIdentity added in v0.10.0

type SessionIdentity struct {
	HarnessID        string `json:"harness_id,omitempty"`
	HarnessSessionID string `json:"harness_session_id,omitempty"`
}

SessionIdentity is the harness-level identity attached to a persisted sessions row. For Claude Code captures, HarnessID is "claude" and HarnessSessionID is Claude's own session id.

type SessionIngester added in v0.10.0

type SessionIngester interface {
	// IngestTurn stores every node in `nodes` (root first, leaf
	// last; ParentHash must already chain correctly) inside a single
	// Tx that also resolves/UPSERTs the sessions row, optionally
	// resolves the parent_session_id FK, stamps session_id onto each
	// newly-inserted node, and rolls up the counters.
	IngestTurn(ctx context.Context, req IngestTurnRequest) (IngestTurnResult, error)
}

SessionIngester is an optional capability for a Driver: it folds session resolution, node insert, and counter rollup into a single transaction.

Only drivers that can host the sessions table implement this. The Postgres driver does; the in-memory driver intentionally does not (it has no concept of sessions, and tests that exercise the classic Put-only path still want the legacy behavior). Callers like the worker pool MUST type-assert against this interface before invoking it.

Implementations MUST satisfy these invariants:

  • All side effects (sessions UPSERT, parent placeholder insert, nodes inserts, counter UPDATE) commit atomically. A panic or error on any step rolls back every other step.
  • The resolved sessions row is keyed by (org_id, harness_id, harness_session_id). When the inbound envelope is nil, or HarnessID is "unknown", or HarnessSessionID is empty, implementations derive a synthetic harness_session_id from the captured turn's Merkle root prefix.
  • Counters (turn_count, total_input_tokens, total_output_tokens, total_cost_usd) are incremented ONLY when at least one new node was actually inserted in this call. A duplicate envelope (every node hash already present) is a true no-op on counters — this preserves end-to-end idempotency for retried POSTs.

type SessionListOpts added in v0.16.0

type SessionListOpts struct {
	Limit       int
	Sort        SessionSortField // zero value == SortLastActive
	Dir         SortDirection    // zero value == SortDesc
	CursorVal   *string          // boundary row's sort column, canonical ::text form
	CursorID    *string          // boundary row's id (UUID), the keyset tiebreak
	Since       *time.Time
	Until       *time.Time
	AuthSubject string
}

SessionListOpts parameterizes the sessions-list read: keyset cursor ordered by any sortable column (default last_seen_at DESC, id DESC), an optional activity window, and an optional attribution filter. The since/until window filters on last_seen_at. AuthSubject "" lists every user's sessions; non-empty is an exact match on the gateway-stamped JWT subject.

type SessionRecord added in v0.12.0

type SessionRecord struct {
	ID                string
	HarnessID         string
	HarnessSessionID  string
	Name              string // empty when not set
	Cwd               string // empty when not set
	HarnessVersion    string // empty when not set
	ParentSessionID   string // empty when not set
	StartedAt         time.Time
	LastSeenAt        time.Time
	EndedAt           *time.Time // nil when session is still live
	HarnessMetadata   map[string]any
	TotalInputTokens  int64
	TotalOutputTokens int64
	TotalCostUsd      float64
	TurnCount         int
	// DerivedStatus is the chain-aware session status (completed / failed /
	// abandoned / unknown), denormalized at ingest. 'unknown' until the first
	// turn lands or, for pre-feature rows, until the status backfill runs.
	DerivedStatus string
	// Model is the dominant conversation-spine model, folded at derive
	// time (sessions.derived_model). Empty until the session derives.
	Model string
	// ModelUsage is the per-model spend breakdown folded at derive time
	// across every thread (sessions.model_usage), cost-weighted so the
	// share reflects spend rather than call count. Nil until the session
	// derives; ordered dominant-model-first (by cost).
	ModelUsage []ModelUsage
	Preview    string // first user turn text, truncated; empty when unavailable
	// AuthSubject is the gateway-stamped JWT subject (the WorkOS user id)
	// captured at ingest. Empty for rows captured before the edge began
	// stamping the x-paper-auth-subject header.
	AuthSubject string
	// SortVal is the canonical ::text form of this row's active sort column,
	// populated by ListSessionRecords so the API can mint an exact next cursor.
	// Empty on records returned by point lookups.
	SortVal string
}

SessionRecord is the flat sessions-table row surfaced by GET /v1/sessions. Fields absent in the DB (NULL) are represented as empty strings or nil/zero values so API callers never have to unwrap optional pgtype wrappers.

type SessionSortField added in v0.22.0

type SessionSortField string

SessionSortField is the validated column a sessions-list page is ordered by. The zero value sorts by last activity (the historical default).

const (
	SortLastActive    SessionSortField = "last_active"
	SortStartedAt     SessionSortField = "started_at"
	SortTurnCount     SessionSortField = "turn_count"
	SortTotalCost     SessionSortField = "total_cost_usd"
	SortTotalTokens   SessionSortField = "total_tokens"
	SortDurationNs    SessionSortField = "duration_ns"
	SortDerivedStatus SessionSortField = "derived_status"
	SortAuthSubject   SessionSortField = "auth_subject"
)

func ParseSessionSortField added in v0.22.0

func ParseSessionSortField(raw string) (SessionSortField, bool)

ParseSessionSortField validates a raw sort key. Empty string is the default (last_active). ok is false for any unrecognized value.

type SessionStats added in v0.4.0

type SessionStats struct {
	// SessionCount is the number of distinct first-class sessions touched by
	// the matching nodes (keyed on nodes.session_id). Nodes with no
	// session_id — legacy or non-session-tracked writers, including the
	// in-memory driver — do not contribute, so a store with no session rows
	// reports 0. See StemCount for the leaf-based metric.
	SessionCount int

	// StemCount is the number of leaf nodes (Merkle chains) matching the
	// filter. This is the value SessionCount reported before the
	// sessions table existed; it only surfaces on the legacy node-layer
	// /v1/stats fallback.
	StemCount int

	// TurnCount is the number of nodes (turns) matching the filter.
	TurnCount int

	// RootCount is the number of root nodes (no parent) matching the filter.
	RootCount int

	// CompletedCount is the number of distinct sessions whose chain-aware
	// derived_status is "completed" (computed at ingest via
	// pkg/sessions.DetermineStatus, read from the sessions table). Like
	// SessionCount it is session-grained: a store with no session rows
	// reports 0.
	CompletedCount int

	// InputTokens / OutputTokens are SUMs over the matching node set,
	// taken from prompt_tokens / completion_tokens columns.
	InputTokens  int64
	OutputTokens int64

	// CacheCreationTokens / CacheReadTokens are SUMs of the cache-aware
	// token columns. Surfaced so a caller (typically the API handler) can
	// fold cost via pkg/sessions.CostForTokensWithCache.
	CacheCreationTokens int64
	CacheReadTokens     int64

	// TotalDurationNs is the wall-clock span MAX(created_at) − MIN(created_at)
	// over the matching node set, in nanoseconds. It is NOT a sum of per-call
	// durations: nodes.total_duration_ns is currently never populated by the
	// proxy (see PCC-514), so SUMming the column would always return 0.
	// Wall-clock span is meaningful for a dashboard "Agent Time" card and is
	// the same shape that pkg/sessions.BuildSummary uses per session.
	TotalDurationNs int64

	// ToolCalls is the number of tool_use content blocks across the
	// matching node set.
	ToolCalls int

	// PerModel breaks tokens down by (normalized) model so the API layer
	// can apply per-model pricing without the storage driver having to
	// know about pricing tables. Keys are normalized model names; nodes
	// with no model are excluded.
	PerModel map[string]ModelTokenStats
}

SessionStats is the aggregate result of CountSessions for a given filter.

All numeric aggregates are computed over the set of nodes matching the supplied ListOpts filter; they are not restricted to nodes that are part of a matching leaf session. This mirrors the long-standing TurnCount semantic: the filter is per-node, not per-chain.

type SessionStatusBackfiller added in v0.13.0

type SessionStatusBackfiller interface {
	BackfillSessionStatus(ctx context.Context) (BackfillSessionStatusResult, error)
}

SessionStatusBackfiller is an optional Driver capability that recomputes the denormalized derived_status (and the sticky has_git_activity flag, tool_result_count, and tool_error_count) for sessions whose rows predate the ingest-time status computation. It walks each session's nodes with the same signal helpers ingest uses, so a backfilled store matches what live ingest would have written. Idempotent and safe to run online.

type SkillCounts added in v0.20.0

type SkillCounts struct {
	Total int64
	Mine  int64
}

SkillCounts are the per-tab totals for a search: every matching skill, and how many the caller authored. "team" is derived as Total - Mine.

type SkillListOpts added in v0.20.0

type SkillListOpts struct {
	Query     string // name/description/tag search (empty = no filter)
	Author    string // only skills authored by this subject ("mine")
	NotAuthor string // exclude this subject ("team")
	// Sort selects the ordering and which cursor column applies:
	// "downloads" orders by download_count DESC (keyset on CursorDownloads);
	// anything else orders by updated_at DESC (keyset on CursorTs). Both
	// tiebreak on id DESC.
	Sort            string
	CursorTs        *time.Time // recent keyset: updated_at of the prior page's last row
	CursorDownloads *int64     // downloads keyset: download_count of that row
	CursorID        string     // keyset tiebreak: id of that last row
	Limit           int        // page size; zero falls back to DefaultListLimit
}

SkillListOpts controls a single keyset page of skills. Query, Author and NotAuthor are all optional filters; an empty string disables each.

type SkillRecord added in v0.20.0

type SkillRecord struct {
	// ID is the opaque, immutable identity (the route/URL key), mirroring
	// sessions. Slug is a cosmetic, non-unique display label and SKILL.md
	// filename derived from the name.
	ID                      string
	Slug                    string
	Name                    string
	Description             string
	Type                    string // "workflow" | "domain-knowledge" | "prompt-template"
	Version                 string // semver, e.g. "0.1.0"
	Visibility              string // "private" | "team"
	Tags                    []string
	Content                 string // markdown body
	IsAIGenerated           bool
	GeneratedFromSessionIDs []string
	ParentID                string // empty when not a fork
	// AuthorSubject is the WorkOS user id (JWT sub) of the creator, stamped
	// from the gateway-trusted x-paper-auth-subject header the same way
	// sessions.auth_subject is captured. Empty when no header was present.
	AuthorSubject string
	// DownloadCount is a real usage signal — how many times the SKILL.md has
	// been downloaded.
	DownloadCount int64
	CreatedAt     time.Time
	UpdatedAt     time.Time
}

SkillRecord is the flat skills-table row surfaced by the /v1/skills API. It mirrors the console's Skill shape so a generated skill round-trips through persistence without a separate projection. Fields absent in the DB (parent_id NULL) are represented as empty strings so API callers never unwrap optional pgtype wrappers.

type SkillVersionRecord added in v0.20.0

type SkillVersionRecord struct {
	SkillID       string
	VersionNumber int
	Semver        string
	Changelog     string
	Content       string
	AuthorSubject string
	PublishedAt   time.Time
}

SkillVersionRecord is one immutable published snapshot of a skill's content. The skill's working/current content lives on SkillRecord.Content; versions are history only.

type SortColumn added in v0.22.0

type SortColumn struct {
	// contains filtered or unexported fields
}

SortColumn is an opaque, SQL-safe sort target: a physical column name and the Postgres type the keyset cursor value is cast back to. Its fields are unexported and there is no exported constructor, so the only SortColumns that exist are the entries in sessionSortColumn below. A SortColumn interpolated into an ORDER BY clause or a ::cast therefore cannot carry an attacker-controlled identifier by construction — the type system enforces the allowlist that a bare string could only enforce by convention.

func SessionSortColumn added in v0.22.0

func SessionSortColumn(f SessionSortField) (SortColumn, bool)

SessionSortColumn resolves a validated sort field to its opaque SortColumn (physical column + cursor cast type). ok is false for unknown fields — the injection guard: an unrecognized field never yields a SortColumn, so it can never reach SQL.

func (SortColumn) Cast added in v0.22.0

func (c SortColumn) Cast() string

Cast is the Postgres type the keyset cursor value is cast back to (bigint, numeric, timestamptz, text) — likewise allowlist-sourced and SQL-safe.

func (SortColumn) Col added in v0.22.0

func (c SortColumn) Col() string

Col is the physical column name, safe to interpolate into SQL because it can only have originated from the allowlist.

type SortDirection added in v0.22.0

type SortDirection string

SortDirection is the validated order direction.

const (
	SortAsc  SortDirection = "asc"
	SortDesc SortDirection = "desc"
)

func ParseSortDirection added in v0.22.0

func ParseSortDirection(raw string) (SortDirection, bool)

ParseSortDirection validates a raw direction. Empty string defaults to desc.

type SpanLinkRecord added in v0.16.0

type SpanLinkRecord struct {
	FromTraceID string
	FromSpanID  string
	FromIO      string
	ToTraceID   string
	ToSpanID    string
	ToIO        string
	Kind        string
}

SpanLinkRecord is a dataflow edge between spans, possibly across traces (compaction seams).

type SpanModelReader added in v0.16.0

type SpanModelReader interface {
	ListSessionSpanModel(ctx context.Context, sessionID string) ([]SpanTurnRecord, []SpanRecord, []SpanLinkRecord, error)
	ListTraceSummaries(ctx context.Context, sessionID string) ([]TraceSummaryRecord, error)
	GetTraceDetail(ctx context.Context, orgID, traceID string) (*SpanTurnRecord, []SpanRecord, []SpanLinkRecord, error)
	GetSpanRecord(ctx context.Context, orgID, traceID, spanID string) (*SpanRecord, error)
	ListRawTurnHeaders(ctx context.Context, orgID, harnessID, harnessSessionID string) ([]RawTurnHeader, error)
}

SpanModelReader serves the span projection for session UIs.

type SpanRecord added in v0.16.0

type SpanRecord struct {
	TraceID      string
	SpanID       string
	ParentSpanID string
	Kind         string
	Name         string
	Status       string
	CallKind     string
	ThreadID     string
	Model        string
	StopReason   string
	StartedAt    time.Time
	DurationNS   int64
	// Seq is the deriver's emit ordinal within the trace —
	// presentation order, since started_at ties inside one llm call.
	Seq       int64
	Input     json.RawMessage
	Output    json.RawMessage
	Usage     json.RawMessage
	RawTurnID int64
	NodeHash  string
}

SpanRecord is one observed unit of work within a trace. Input and Output hold delta-only content-block arrays; Usage is the llm.Usage JSON for llm spans.

type SpanStats added in v0.16.0

type SpanStats struct {
	TurnCount           int
	RootCount           int
	SessionCount        int
	CompletedCount      int
	InputTokens         int64
	OutputTokens        int64
	CacheCreationTokens int64
	CacheReadTokens     int64
	TotalDurationNS     int64
	TotalCostUSD        float64
	ToolCalls           int
}

SpanStats is the span-layer aggregate behind /v1/stats: trace-grain rollups summed over a time window, so the dashboard numbers agree with the session detail and trace views. TotalDurationNS is the sum of trace durations (agent time), not a wall-clock window.

type SpanStatsReader added in v0.16.0

type SpanStatsReader interface {
	AggregateSpanStats(ctx context.Context, orgID string, since, until *time.Time) (SpanStats, error)
}

SpanStatsReader is the capability interface for span-layer stats. Backends without the span projection fall back to the node-layer CountSessions aggregate.

type SpanTurnRecord added in v0.16.0

type SpanTurnRecord struct {
	TraceID    string
	SessionID  string
	UserPrompt string
	// ResponsePreview is the derive-time fold of the closing spine llm
	// call's text output — the turn card's answer line.
	ResponsePreview   string
	Synthetic         string
	Status            string
	StartedAt         time.Time
	EndedAt           *time.Time
	DurationNS        int64
	TotalInputTokens  int64
	TotalOutputTokens int64
	// Main* counts only conversation-spine llm calls; the difference
	// from Total* is shadow spend.
	MainInputTokens     int64
	MainOutputTokens    int64
	CacheReadTokens     int64
	CacheCreationTokens int64
	TotalCostUSD        float64
}

SpanTurnRecord is one user-visible turn (trace).

type TraceSummaryRecord added in v0.16.0

type TraceSummaryRecord struct {
	SpanTurnRecord
	SpanCount int
}

TraceSummaryRecord is a turn header with its span count — the lazy session-detail row (no payloads).

Directories

Path Synopsis
Package postgres
Package postgres
Package storagetest provides shared Ginkgo specs that any storage.Driver implementation can run against.
Package storagetest provides shared Ginkgo specs that any storage.Driver implementation can run against.

Jump to

Keyboard shortcuts

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