sessions

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: 10 Imported by: 0

Documentation

Overview

Package sessions provides the shared types and logic for building a per-session summary from a chain of merkle nodes. It is consumed by both the API server (for /v1/sessions/summary) and the deck TUI (for rendering).

All functions in this package operate on *merkle.Node, not on any specific storage driver type. Callers are responsible for fetching and walking the ancestry chain; this package computes derived metadata over that chain.

Index

Constants

View Source
const (
	StatusCompleted = "completed"
	StatusFailed    = "failed"
	StatusAbandoned = "abandoned"
	StatusUnknown   = "unknown"
)

Status values returned by DetermineStatus.

Variables

This section is empty.

Functions

func BlocksHaveGitActivity

func BlocksHaveGitActivity(blocks []llm.ContentBlock) bool

BlocksHaveGitActivity reports whether the blocks contain a Bash tool call whose command invokes `git commit` or `git push`.

func BlocksHaveToolError

func BlocksHaveToolError(blocks []llm.ContentBlock) bool

BlocksHaveToolError reports whether any tool_result block is marked as an error.

func CopyModelCosts

func CopyModelCosts(costs map[string]ModelCost) map[string]ModelCost

CopyModelCosts returns a shallow copy of the map, or an empty map if nil.

func CostForTokens

func CostForTokens(pricing Pricing, inputTokens, outputTokens int64) (float64, float64, float64)

CostForTokens calculates cost using base input/output pricing. For cache-aware cost calculation, use CostForTokensWithCache.

func CostForTokensWithCache

func CostForTokensWithCache(pricing Pricing, inputTokens, outputTokens, cacheCreation, cacheRead int64) (float64, float64, float64)

CostForTokensWithCache calculates cost accounting for prompt caching. When cache token counts are available, base input tokens are:

baseInput = totalInput - cacheCreation - cacheRead

Each token type is priced at its respective rate.

func CountToolCalls

func CountToolCalls(blocks []llm.ContentBlock) int

CountToolCalls returns how many tool_use blocks are in the slice.

func CountToolResultErrors added in v0.13.0

func CountToolResultErrors(blocks []llm.ContentBlock) int

CountToolResultErrors returns how many tool_result blocks are marked errors.

func CountToolResults added in v0.13.0

func CountToolResults(blocks []llm.ContentBlock) int

CountToolResults returns how many tool_result blocks are in the slice.

func DetermineStatus

func DetermineStatus(leaf *merkle.Node, hasGitActivity bool, toolResultCount, toolErrorCount int) string

DetermineStatus classifies a session from its terminal (leaf) node, a git-activity flag, and the session's tool_result success/failure counts.

A single mid-conversation tool error is normal agentic behaviour (a failed grep, a missing file) that the model routinely recovers from, so it no longer marks the whole session failed. "Failed" now means the session either ended broken or was dominated by errors.

Precedence:

  1. Unrecovered terminal error → StatusFailed. The session ended on an error the model never came back from: a non-assistant leaf carrying a tool_result error (no assistant turn followed it), or an assistant leaf with a known-failing stop_reason (length / max_tokens / content_filter / *error*). An assistant turn after an error means the model saw it and responded — recovered, not failed.
  2. Git commit/push anywhere → StatusCompleted. Shipped work and ended clean (rule 1 already ruled out a broken ending); outranks the error-rate check so a noisy-but-recovered session that shipped reads as completed.
  3. tool_error_count / tool_result_count > 1/2 → StatusFailed. The session limped along mostly erroring.
  4. Assistant leaf with a known-terminal stop_reason → StatusCompleted. `tool_use` / `tool_use_response` count as terminal (designed terminus of a subagent dispatch / parallel-tool side-conversation — see PCC-560).
  5. Non-assistant leaf (no terminal error) → StatusAbandoned.
  6. Empty or otherwise unrecognised stop_reason → StatusUnknown.

func ExtractText

func ExtractText(blocks []llm.ContentBlock) string

ExtractText concatenates visible text across text, tool_output, and tool_use blocks, joined by newlines.

func ExtractToolCalls

func ExtractToolCalls(blocks []llm.ContentBlock) []string

ExtractToolCalls returns the names of all tool_use blocks.

func MergeModelCosts

func MergeModelCosts(target map[string]ModelCost, costs map[string]ModelCost)

MergeModelCosts adds the per-model values in costs into target in place. Does nothing if target is nil.

func NormalizeModel

func NormalizeModel(model string) string

NormalizeModel canonicalizes a model name: lowercased, trimmed, date suffixes stripped, and vendor-specific version markers rewritten so the result can be used as a key into PricingTable.

func StripTaggedSection

func StripTaggedSection(text, tag string) string

StripTaggedSection removes all occurrences of a given XML-like tagged section (e.g. <system-reminder>…</system-reminder>) from text.

Types

type IngestEnvelope added in v0.10.0

type IngestEnvelope struct {
	OrgID                  string          `json:"org_id"`
	AuthSubject            string          `json:"auth_subject"`
	HarnessID              string          `json:"harness_id"`
	HarnessSessionID       string          `json:"harness_session_id,omitempty"`
	HarnessVersion         string          `json:"harness_version,omitempty"`
	Cwd                    string          `json:"cwd,omitempty"`
	Name                   string          `json:"name,omitempty"`
	ParentHarnessSessionID *string         `json:"parent_harness_session_id,omitempty"`
	HarnessMetadata        json.RawMessage `json:"harness_metadata,omitempty"`
}

IngestEnvelope is the session-tracking envelope attached to a turn payload at the ingest HTTP boundary. It carries identity fields (org_id, auth_subject) plus the harness identifiers ingest uses to resolve a `sessions` row.

IngestEnvelope is held in pkg/sessions (rather than the `ingest` package itself) so both the HTTP handler in `ingest` and the session-aware worker code under `proxy/worker` can reference the same type without inverting the existing import graph (proxy/worker is a dependency of ingest, not the other way around).

Field semantics:

  • OrgID, AuthSubject: identity fields; MUST be set on every non-nil envelope. Empty values are not synthesized — they are persisted verbatim so the row stays attributable.
  • HarnessID: "claude" | "unknown" | other registered harness; empty is normalized to "unknown".
  • HarnessSessionID: opaque identifier for the harness session. REQUIRED when HarnessID != "unknown". When absent (or when HarnessID == "unknown"), ingest derives a synthetic id from the captured turn's Merkle root prefix.
  • ParentHarnessSessionID: fork-lineage hint, resolved server-side to the parent's `sessions.id` within this envelope's harness namespace. Parent and child are assumed to share a harness_id; cross-harness forks are not supported (the envelope carries no parent harness_id). When the parent's first turn hasn't landed yet, ingest placeholder-inserts the parent so the FK can be set.
  • HarnessMetadata: arbitrary JSON object merged into the `sessions.harness_metadata` column last-write-wins per key.

func (*IngestEnvelope) HarnessIDOrUnknown added in v0.10.0

func (e *IngestEnvelope) HarnessIDOrUnknown() string

HarnessIDOrUnknown returns the canonical harness_id for this envelope: the verbatim HarnessID if set, otherwise the sentinel "unknown". Centralized here so both ingest and worker agree on the normalization.

func (*IngestEnvelope) NeedsSyntheticHarnessSessionID added in v0.10.0

func (e *IngestEnvelope) NeedsSyntheticHarnessSessionID() bool

NeedsSyntheticHarnessSessionID reports whether ingest must derive a synthetic harness_session_id from the captured turn's Merkle root prefix instead of using the envelope-supplied value. Returns true when:

  • the envelope is nil (no session block at all);
  • HarnessID is "unknown" / empty; or
  • HarnessSessionID is empty for any other reason.

func (*IngestEnvelope) Validate added in v0.10.0

func (e *IngestEnvelope) Validate() error

Validate enforces wire-boundary invariants that the decoder cannot express in struct tags. It MUST be called at the ingest HTTP boundary, after JSON decode but before the envelope is handed to the worker / SessionIngester.

Rejected shapes (each maps to a 400-equivalent at the HTTP layer):

  • OrgID set but not parseable as a UUID. Empty OrgID is permitted (the storage layer maps it to a nil-UUID sentinel for callers without an org-scoped identity); a non-empty malformed value would otherwise pass the HTTP boundary and fail asynchronously in the worker, silently dropping the turn.
  • ParentHarnessSessionID present but pointing at "". The pointer model treats nil as "absent"; an explicit empty string is ambiguous between "absent" and "this session has no parent", which the ingest path would silently coerce to absent. Force the caller to omit the field instead.
  • HarnessMetadata that does not decode to a JSON object. The Postgres `||` JSONB operator on sessions.harness_metadata is defined as merge-objects; arrays concatenate and scalars replace, producing surprising column shapes.

type ModelCost

type ModelCost struct {
	Model        string  `json:"model"`
	InputTokens  int64   `json:"input_tokens"`
	OutputTokens int64   `json:"output_tokens"`
	InputCost    float64 `json:"input_cost"`
	OutputCost   float64 `json:"output_cost"`
	TotalCost    float64 `json:"total_cost"`
	SessionCount int     `json:"session_count"`
}

ModelCost is a per-model aggregate of cost and token usage within a session or a collection of sessions.

type NodeTokens

type NodeTokens struct {
	Input         int64
	Output        int64
	Total         int64
	CacheCreation int64
	CacheRead     int64
}

NodeTokens holds all token counts for a node, including cache breakdown.

func TokensForNode

func TokensForNode(n *merkle.Node) NodeTokens

TokensForNode extracts token counts from a merkle node's Usage metadata. Returns a zero-valued NodeTokens when Usage is nil.

type Pricing

type Pricing struct {
	Input      float64 `json:"input"`
	Output     float64 `json:"output"`
	CacheRead  float64 `json:"cache_read"`
	CacheWrite float64 `json:"cache_write"`
}

Pricing is the per-million-tokens price for a single model.

func PricingForModel

func PricingForModel(pricing PricingTable, model string) (Pricing, bool)

PricingForModel looks up pricing for a model name, trying the normalized form first and falling back to the raw string.

type PricingTable

type PricingTable map[string]Pricing

PricingTable maps model names (after normalization) to their Pricing.

func DefaultPricing

func DefaultPricing() PricingTable

DefaultPricing returns hardcoded pricing per million tokens for supported models.

Last verified: 2026-07-09 Sources:

Anthropic cache multipliers: CacheWrite = 1.25x input, CacheRead = 0.10x input. OpenAI cache: CacheWrite = 1x input and CacheRead = 0.50x input for older models; GPT-5.6+ uses CacheWrite = 1.25x input and CacheRead = 0.10x input.

To override at runtime, pass a JSON file path to LoadPricing.

func LoadPricing

func LoadPricing(path string) (PricingTable, error)

LoadPricing loads a pricing table, applying JSON overrides from path if set. An empty path returns DefaultPricing unchanged.

type SessionSummary

type SessionSummary struct {
	ID               string    `json:"id"`
	HarnessID        string    `json:"harness_id,omitempty"`
	HarnessSessionID string    `json:"harness_session_id,omitempty"`
	Label            string    `json:"label"`
	Model            string    `json:"model"`
	Project          string    `json:"project"`
	AgentName        string    `json:"agent_name,omitempty"`
	Status           string    `json:"status"`
	StartTime        time.Time `json:"start_time"`
	EndTime          time.Time `json:"end_time"`
	// Duration is wall-clock session length. The client fold
	// (summaryFromSessionItem) prefers ended_at when present, else last_seen_at,
	// minus started_at. The server-side sort column sessions.duration_ns (a
	// STORED generated column, see migrations/*_sessions_sort.up.sql) is
	// unconditionally last_seen_at - started_at. The two agree only because
	// sessions.ended_at is never written today; when it starts getting
	// populated, reconcile the generated column (or move duration into
	// derivation) so the sort key matches this displayed value — altering a
	// STORED generated column is a full-table rewrite.
	Duration     time.Duration `json:"duration_ns"`
	InputTokens  int64         `json:"input_tokens"`
	OutputTokens int64         `json:"output_tokens"`
	InputCost    float64       `json:"input_cost"`
	OutputCost   float64       `json:"output_cost"`
	TotalCost    float64       `json:"total_cost"`
	ToolCalls    int           `json:"tool_calls"`
	MessageCount int           `json:"message_count"`
	SessionCount int           `json:"session_count,omitempty"`

	// Truncated is true when this summary was built from a partial chain
	// because an ancestor's parent_hash could not be resolved in the
	// current store. MissingParent names the hash that would have
	// continued the walk if the referenced node existed. Both fields are
	// informational — they signal that higher ancestors may live outside
	// this store (trimmed history, foreign chain, offloaded data) rather
	// than an error.
	Truncated     bool   `json:"truncated,omitempty"`
	MissingParent string `json:"missing_parent,omitempty"`
}

SessionSummary is the aggregate view of a single session. The deck TUI renders it (deck.SessionSummary aliases it), folding it from the /v1/sessions rows.

Jump to

Keyboard shortcuts

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