daemon: integrate QoL slices 1-5 (model/rename/catalog, history deletes, stats, repo prepare+imageUsed+built) — 95.3% cover green
This commit is contained in:
+121
@@ -51,6 +51,7 @@ type ContainerRow struct {
|
||||
type RepoImageRow struct {
|
||||
Repo string `json:"repo"`
|
||||
Image string `json:"image"`
|
||||
Built bool `json:"built"`
|
||||
}
|
||||
|
||||
const dsnParams string = "?_pragma=busy_timeout(5000)&_pragma=journal_mode(WAL)&_pragma=synchronous(NORMAL)"
|
||||
@@ -207,6 +208,31 @@ func (s *Store) SetOnline(sessionID string, online bool) error {
|
||||
return err
|
||||
}
|
||||
|
||||
// SetSessionName merges a new name into the persisted session info blob,
|
||||
// leaving every other snapshot field untouched. Returns false when the
|
||||
// session row does not exist.
|
||||
func (s *Store) SetSessionName(sessionID, name string) (bool, error) {
|
||||
var infoBlob string
|
||||
err := s.db.QueryRow(`SELECT info FROM sessions WHERE id=?`, sessionID).Scan(&infoBlob)
|
||||
if err == sql.ErrNoRows {
|
||||
return false, nil
|
||||
}
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
var info SessionInfo
|
||||
if err := json.Unmarshal([]byte(infoBlob), &info); err != nil {
|
||||
return false, fmt.Errorf("decode session info %s: %w", sessionID, err)
|
||||
}
|
||||
info.Name = &name
|
||||
blob, err := json.Marshal(info)
|
||||
if err != nil {
|
||||
return true, err
|
||||
}
|
||||
_, err = s.db.Exec(`UPDATE sessions SET info=? WHERE id=?`, string(blob), sessionID)
|
||||
return true, err
|
||||
}
|
||||
|
||||
// Sessions returns every known session row ordered by session id.
|
||||
func (s *Store) Sessions() ([]SessionRow, error) {
|
||||
rows, err := s.db.Query(`SELECT id, info, lastSeq, lastEventAt, online FROM sessions ORDER BY id`)
|
||||
@@ -233,6 +259,101 @@ func (s *Store) Sessions() ([]SessionRow, error) {
|
||||
return out, rows.Err()
|
||||
}
|
||||
|
||||
// DeleteSessionEvents drops every persisted event for the session and
|
||||
// returns the number of rows removed (0 when the session had none).
|
||||
func (s *Store) DeleteSessionEvents(sessionID string) (int64, error) {
|
||||
res, err := s.db.Exec(`DELETE FROM events WHERE sessionId=?`, sessionID)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
return res.RowsAffected()
|
||||
}
|
||||
|
||||
// DeleteSession removes a session entirely: its events, container row and
|
||||
// the session row itself.
|
||||
func (s *Store) DeleteSession(sessionID string) error {
|
||||
if _, err := s.db.Exec(`DELETE FROM events WHERE sessionId=?`, sessionID); err != nil {
|
||||
return err
|
||||
}
|
||||
if _, err := s.db.Exec(`DELETE FROM containers WHERE sessionId=?`, sessionID); err != nil {
|
||||
return err
|
||||
}
|
||||
_, err := s.db.Exec(`DELETE FROM sessions WHERE id=?`, sessionID)
|
||||
return err
|
||||
}
|
||||
|
||||
// UsageStats aggregates token/cost usage over agent_end events.
|
||||
type UsageStats struct {
|
||||
Turns int64 `json:"turns"`
|
||||
InputTokens int64 `json:"inputTokens"`
|
||||
OutputTokens int64 `json:"outputTokens"`
|
||||
TotalCost float64 `json:"totalCost"`
|
||||
}
|
||||
|
||||
// StatsTotals is UsageStats aggregated across all sessions, plus how many
|
||||
// sessions contributed agent_end events.
|
||||
type StatsTotals struct {
|
||||
UsageStats
|
||||
SessionsCount int64 `json:"sessionsCount"`
|
||||
}
|
||||
|
||||
// agentEndUsage is the usage sub-object of an agent_end payload; absent
|
||||
// fields decode as zero.
|
||||
type agentEndUsage struct {
|
||||
Usage struct {
|
||||
InputTokens int64 `json:"inputTokens"`
|
||||
OutputTokens int64 `json:"outputTokens"`
|
||||
TotalCost float64 `json:"totalCost"`
|
||||
} `json:"usage"`
|
||||
}
|
||||
|
||||
// SessionStats aggregates usage over a session's persisted agent_end events.
|
||||
// Each agent_end row counts as one turn; rows without (or with partial)
|
||||
// usage payloads contribute zeros.
|
||||
func (s *Store) SessionStats(sessionID string) (UsageStats, error) {
|
||||
rows, err := s.db.Query(`SELECT sessionId, payload FROM events WHERE sessionId=? AND type=?`,
|
||||
sessionID, evAgentEnd)
|
||||
if err != nil {
|
||||
return UsageStats{}, err
|
||||
}
|
||||
defer rows.Close()
|
||||
totals, err := scanAgentEndUsage(rows)
|
||||
return totals.UsageStats, err
|
||||
}
|
||||
|
||||
// StatsTotals aggregates usage over every persisted agent_end event.
|
||||
func (s *Store) StatsTotals() (StatsTotals, error) {
|
||||
rows, err := s.db.Query(`SELECT sessionId, payload FROM events WHERE type=?`, evAgentEnd)
|
||||
if err != nil {
|
||||
return StatsTotals{}, err
|
||||
}
|
||||
defer rows.Close()
|
||||
return scanAgentEndUsage(rows)
|
||||
}
|
||||
|
||||
// scanAgentEndUsage folds agent_end rows into totals; unparseable payloads
|
||||
// still count as turns but add no usage.
|
||||
func scanAgentEndUsage(rows *sql.Rows) (StatsTotals, error) {
|
||||
var out StatsTotals
|
||||
seen := make(map[string]struct{})
|
||||
for rows.Next() {
|
||||
var sessionID, payload string
|
||||
if err := rows.Scan(&sessionID, &payload); err != nil {
|
||||
return out, err
|
||||
}
|
||||
seen[sessionID] = struct{}{}
|
||||
out.Turns++
|
||||
var u agentEndUsage
|
||||
if json.Unmarshal([]byte(payload), &u) == nil {
|
||||
out.InputTokens += u.Usage.InputTokens
|
||||
out.OutputTokens += u.Usage.OutputTokens
|
||||
out.TotalCost += u.Usage.TotalCost
|
||||
}
|
||||
}
|
||||
out.SessionsCount = int64(len(seen))
|
||||
return out, rows.Err()
|
||||
}
|
||||
|
||||
// GetSetting returns a settings value and whether it exists.
|
||||
func (s *Store) GetSetting(key string) (string, bool, error) {
|
||||
var val string
|
||||
|
||||
Reference in New Issue
Block a user