Files
wehub-resource-sync f99010fae1
CI / lint (push) Failing after 1s
CI / frontend (push) Failing after 1s
CI / scripts (push) Failing after 1s
CI / Go Test (ubuntu-latest) (push) Failing after 0s
CI / frontend-node-25 (push) Failing after 1s
CI / docs (push) Failing after 0s
CI / coverage (push) Failing after 0s
CI / e2e (push) Failing after 0s
Docker / build-and-push (push) Failing after 1s
CI / integration (push) Failing after 4m43s
CI / Go Test (windows-latest) (push) Has been cancelled
CI / Desktop Unit Tests (Windows) (push) Has been cancelled
Desktop Artifacts / Desktop Build (Linux (arm64)) (push) Has been cancelled
Desktop Artifacts / Desktop Build (Linux) (push) Has been cancelled
Desktop Artifacts / Desktop Build (Windows) (push) Has been cancelled
Desktop Artifacts (macOS) / Desktop Build (macOS (aarch64)) (push) Has been cancelled
Desktop Artifacts (macOS) / Desktop Build (macOS (x86_64)) (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 12:30:36 +08:00

313 lines
8.5 KiB
Go

package db
import (
"context"
"database/sql"
"fmt"
"strings"
)
// UsageEvent records token and cost accounting that does not belong
// to a single message row. Hermes session-level usage is the first
// source, but the shape intentionally mirrors usage query totals.
type UsageEvent struct {
ID int64
SessionID string
MessageOrdinal *int
Source string
Model string
InputTokens int
OutputTokens int
CacheCreationInputTokens int
CacheReadInputTokens int
ReasoningTokens int
CostUSD *float64
CostStatus string
CostSource string
OccurredAt string
DedupKey string
}
func (db *DB) ensureUsageEventsSchemaLocked(w *writerHandle) error {
if _, err := w.Exec(`
CREATE TABLE IF NOT EXISTS usage_events (
id INTEGER PRIMARY KEY,
session_id TEXT NOT NULL REFERENCES sessions(id) ON DELETE CASCADE,
message_ordinal INTEGER,
source TEXT NOT NULL,
model TEXT NOT NULL,
input_tokens INTEGER NOT NULL DEFAULT 0,
output_tokens INTEGER NOT NULL DEFAULT 0,
cache_creation_input_tokens INTEGER NOT NULL DEFAULT 0,
cache_read_input_tokens INTEGER NOT NULL DEFAULT 0,
reasoning_tokens INTEGER NOT NULL DEFAULT 0,
cost_usd REAL,
cost_status TEXT NOT NULL DEFAULT '',
cost_source TEXT NOT NULL DEFAULT '',
occurred_at TEXT,
dedup_key TEXT NOT NULL DEFAULT ''
);
CREATE UNIQUE INDEX IF NOT EXISTS idx_usage_events_dedup
ON usage_events(session_id, source, dedup_key)
WHERE dedup_key != '';
CREATE INDEX IF NOT EXISTS idx_usage_events_session
ON usage_events(session_id);
CREATE INDEX IF NOT EXISTS idx_usage_events_occurred
ON usage_events(occurred_at);
`); err != nil {
return fmt.Errorf("creating usage_events: %w", err)
}
return nil
}
// ReplaceSessionUsageEvents replaces all usage events for one session
// in a single transaction.
func (db *DB) ReplaceSessionUsageEvents(
sessionID string, events []UsageEvent,
) error {
db.mu.Lock()
defer db.mu.Unlock()
tx, err := db.getWriter().Begin()
if err != nil {
return fmt.Errorf("beginning usage events tx: %w", err)
}
defer func() { _ = tx.Rollback() }()
if err := replaceSessionUsageEventsTx(tx, sessionID, events); err != nil {
return err
}
return tx.Commit()
}
func replaceSessionUsageEventsTx(
tx *sql.Tx, sessionID string, events []UsageEvent,
) error {
if _, err := tx.Exec(
`DELETE FROM usage_events WHERE session_id = ?`,
sessionID,
); err != nil {
return fmt.Errorf("deleting usage events: %w", err)
}
for _, ev := range events {
if ev.SessionID == "" {
ev.SessionID = sessionID
}
if ev.SessionID != sessionID {
return fmt.Errorf(
"usage event session_id %q does not match %q",
ev.SessionID, sessionID,
)
}
var ordinal any
if ev.MessageOrdinal != nil {
ordinal = *ev.MessageOrdinal
}
var cost any
if ev.CostUSD != nil {
cost = *ev.CostUSD
}
var occurredAt any
if ev.OccurredAt != "" {
occurredAt = ev.OccurredAt
}
if _, err := tx.Exec(`
INSERT INTO usage_events (
session_id, message_ordinal, source, model,
input_tokens, output_tokens,
cache_creation_input_tokens, cache_read_input_tokens,
reasoning_tokens, cost_usd, cost_status, cost_source,
occurred_at, dedup_key
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
ev.SessionID, ordinal, ev.Source, ev.Model,
ev.InputTokens, ev.OutputTokens,
ev.CacheCreationInputTokens, ev.CacheReadInputTokens,
ev.ReasoningTokens, cost, ev.CostStatus, ev.CostSource,
occurredAt, ev.DedupKey,
); err != nil {
return fmt.Errorf("inserting usage event: %w", err)
}
}
return nil
}
// GetUsageEvents returns usage events for one session in stable order.
// UsageEventFingerprints returns exact ordered fingerprints of
// stored usage events keyed by session ID. Used by PG push fast-paths
// to detect usage-only changes without N+1 queries.
func (db *DB) UsageEventFingerprints(
sessionIDs []string,
) (map[string]string, error) {
out := make(map[string]string, len(sessionIDs))
if len(sessionIDs) == 0 {
return out, nil
}
for _, id := range sessionIDs {
out[id] = ""
}
const batchSize = 900
for start := 0; start < len(sessionIDs); start += batchSize {
end := min(start+batchSize, len(sessionIDs))
if err := db.appendUsageEventFingerprints(
out, sessionIDs[start:end],
); err != nil {
return nil, err
}
}
return out, nil
}
func (db *DB) appendUsageEventFingerprints(
out map[string]string, sessionIDs []string,
) error {
placeholders := make([]string, len(sessionIDs))
args := make([]any, len(sessionIDs))
for i, id := range sessionIDs {
placeholders[i] = "?"
args[i] = id
}
rows, err := db.getReader().Query(`
SELECT session_id, message_ordinal, source, model,
input_tokens, output_tokens,
cache_creation_input_tokens, cache_read_input_tokens,
reasoning_tokens, cost_usd, cost_status, cost_source,
occurred_at, dedup_key
FROM usage_events
WHERE session_id IN (`+strings.Join(placeholders, ",")+`)
ORDER BY session_id, COALESCE(occurred_at, ''), id`,
args...,
)
if err != nil {
return err
}
defer rows.Close()
builders := make(map[string]*strings.Builder)
for rows.Next() {
var sessionID string
var ordinal sql.NullInt64
var source, model, costStatus, costSource string
var inputTokens, outputTokens int
var cacheCreationInputTokens, cacheReadInputTokens int
var reasoningTokens int
var cost sql.NullFloat64
var occurredAt, dedupKey sql.NullString
if err := rows.Scan(
&sessionID, &ordinal, &source, &model,
&inputTokens, &outputTokens,
&cacheCreationInputTokens, &cacheReadInputTokens,
&reasoningTokens, &cost, &costStatus, &costSource,
&occurredAt, &dedupKey,
); err != nil {
return err
}
b := builders[sessionID]
if b == nil {
b = &strings.Builder{}
builders[sessionID] = b
}
occurred := ""
if occurredAt.Valid {
occurred = occurredAt.String
}
// Sanitize before measuring so fingerprints match the
// PG-readback fingerprint, whose values were sanitized
// at insert time (see SanitizeUTF8).
source = SanitizeUTF8(source)
model = SanitizeUTF8(model)
costStatus = SanitizeUTF8(costStatus)
costSource = SanitizeUTF8(costSource)
dedupKey.String = SanitizeUTF8(dedupKey.String)
fmt.Fprintf(b,
"%t|%d|%d:%s|%d:%s|%d|%d|%d|%d|%d|%t|%g|%d:%s|%d:%s|%d:%s|%d:%s;",
ordinal.Valid,
ordinal.Int64,
len(source), source,
len(model), model,
inputTokens,
outputTokens,
cacheCreationInputTokens,
cacheReadInputTokens,
reasoningTokens,
cost.Valid,
cost.Float64,
len(costStatus), costStatus,
len(costSource), costSource,
len(occurred), occurred,
len(dedupKey.String), dedupKey.String,
)
}
if err := rows.Err(); err != nil {
return err
}
for id, b := range builders {
out[id] += b.String()
}
return nil
}
// UsageEventFingerprint returns exact ordered fingerprint for one session.
func (db *DB) UsageEventFingerprint(sessionID string) (string, error) {
fps, err := db.UsageEventFingerprints([]string{sessionID})
if err != nil {
return "", err
}
return fps[sessionID], nil
}
func (db *DB) GetUsageEvents(
ctx context.Context, sessionID string,
) ([]UsageEvent, error) {
rows, err := db.getReader().QueryContext(ctx, `
SELECT id, session_id, message_ordinal, source, model,
input_tokens, output_tokens,
cache_creation_input_tokens, cache_read_input_tokens,
reasoning_tokens, cost_usd, cost_status, cost_source,
occurred_at, dedup_key
FROM usage_events
WHERE session_id = ?
ORDER BY COALESCE(occurred_at, ''), id`,
sessionID,
)
if err != nil {
return nil, fmt.Errorf("querying usage events: %w", err)
}
defer rows.Close()
var out []UsageEvent
for rows.Next() {
var ev UsageEvent
var ordinal sql.NullInt64
var cost sql.NullFloat64
var occurred sql.NullString
if err := rows.Scan(
&ev.ID, &ev.SessionID, &ordinal, &ev.Source, &ev.Model,
&ev.InputTokens, &ev.OutputTokens,
&ev.CacheCreationInputTokens, &ev.CacheReadInputTokens,
&ev.ReasoningTokens, &cost, &ev.CostStatus,
&ev.CostSource, &occurred, &ev.DedupKey,
); err != nil {
return nil, fmt.Errorf("scanning usage event: %w", err)
}
if ordinal.Valid {
v := int(ordinal.Int64)
ev.MessageOrdinal = &v
}
if cost.Valid {
v := cost.Float64
ev.CostUSD = &v
}
if occurred.Valid {
ev.OccurredAt = occurred.String
}
out = append(out, ev)
}
if err := rows.Err(); err != nil {
return nil, fmt.Errorf("iterating usage events: %w", err)
}
return out, nil
}