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
- func BlocksHaveGitActivity(blocks []llm.ContentBlock) bool
- func BlocksHaveToolError(blocks []llm.ContentBlock) bool
- func CopyModelCosts(costs map[string]ModelCost) map[string]ModelCost
- func CostForTokens(pricing Pricing, inputTokens, outputTokens int64) (float64, float64, float64)
- func CostForTokensWithCache(pricing Pricing, inputTokens, outputTokens, cacheCreation, cacheRead int64) (float64, float64, float64)
- func CountToolCalls(blocks []llm.ContentBlock) int
- func CountToolResultErrors(blocks []llm.ContentBlock) int
- func CountToolResults(blocks []llm.ContentBlock) int
- func DetermineStatus(leaf *merkle.Node, hasGitActivity bool, toolResultCount, toolErrorCount int) string
- func ExtractText(blocks []llm.ContentBlock) string
- func ExtractToolCalls(blocks []llm.ContentBlock) []string
- func MergeModelCosts(target map[string]ModelCost, costs map[string]ModelCost)
- func NormalizeModel(model string) string
- func StripTaggedSection(text, tag string) string
- type IngestEnvelope
- type ModelCost
- type NodeTokens
- type Pricing
- type PricingTable
- type SessionSummary
Constants ¶
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 ¶
CopyModelCosts returns a shallow copy of the map, or an empty map if nil.
func CostForTokens ¶
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:
- 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.
- 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.
- tool_error_count / tool_result_count > 1/2 → StatusFailed. The session limped along mostly erroring.
- 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).
- Non-assistant leaf (no terminal error) → StatusAbandoned.
- 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 ¶
MergeModelCosts adds the per-model values in costs into target in place. Does nothing if target is nil.
func NormalizeModel ¶
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 ¶
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 ¶
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 ¶
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: https://platform.claude.com/docs/en/about-claude/pricing
- OpenAI: https://platform.openai.com/docs/pricing
- DeepSeek: https://api-docs.deepseek.com/quick_start/pricing
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.