Files
nucleic/Sources/NucleicCore/Persistence/SessionMetadataStore.swift
T
abkslmandnucleic 5e00404575 nvrsion: promote trunk to dev
Nucleic-Promote: 1
Co-authored-by: Nucleic <[email protected]>
2026-06-27 19:33:40 -07:00

73 lines
3.6 KiB
Swift

import Foundation
/// A persisted, derived rollup of one session's transcript activity — the home
/// dashboard's message count and per-day histogram, cached so the dashboard never
/// has to re-scan transcripts (RUNTIME §4). Keyed by session; rebuildable at any time
/// from the transcript, so it's a pure cache. `lastSeq` is the transcript cursor the
/// rollup was computed at: when a session's live `lastSeq` advances past it the entry
/// is stale and gets recomputed.
public struct ActivityCacheEntry: Sendable, Equatable {
public let lastSeq: UInt64
/// Total user messages in the transcript.
public let messageCount: Int
/// User-message counts bucketed by start-of-day.
public let activityByDay: [Date: Int]
/// Total tokens used across the transcript (every turn's input + cache + output + reasoning;
/// see `Usage.totalTokens`). Drives the dashboard's total-tokens metric.
public let tokenCount: Int
/// Tokens-used totals bucketed by start-of-day — the per-day series the activity grid shades
/// by (falling back to `activityByDay` for days with no recorded usage) and the backing for a
/// token time series. Empty for transcripts that predate usage capture.
public let tokensByDay: [Date: Int]
public init(
lastSeq: UInt64, messageCount: Int, activityByDay: [Date: Int],
tokenCount: Int = 0, tokensByDay: [Date: Int] = [:]
) {
self.lastSeq = lastSeq
self.messageCount = messageCount
self.activityByDay = activityByDay
self.tokenCount = tokenCount
self.tokensByDay = tokensByDay
}
}
/// The metadata persistence surface (RUNTIME §4). Event bodies stay in the JSONL
/// transcript; this stores only what must be *queried* — session lists, status,
/// resume pointers, the live approval queue, and the derived dashboard activity cache.
///
/// SessionController depends on this protocol, not on GRDB directly, so the event
/// pipeline stays storage-agnostic and tests can use an in-memory fake.
public protocol SessionMetadataStore: Sendable {
func saveProject(_ project: Project) async throws
func loadProjects() async throws -> [Project]
func deleteProject(id: ProjectID) async throws
/// Insert-or-update a session row (the SessionController calls this as metadata
/// changes — status transitions, lastSeq, diffstat).
func saveSession(_ session: Session) async throws
func loadSession(id: SessionID) async throws -> Session?
func loadSessions(projectID: ProjectID) async throws -> [Session]
/// Every session across all projects in one query — the launch/dashboard fast path,
/// so the sidebar and dashboard load without a per-project round-trip.
func loadAllSessions() async throws -> [Session]
func deleteSession(id: SessionID) async throws
/// The full dashboard activity cache, keyed by session id.
func loadActivityCache() async throws -> [SessionID: ActivityCacheEntry]
/// Insert-or-update one session's cached activity rollup.
func saveActivityCache(_ sessionID: SessionID, _ entry: ActivityCacheEntry) async throws
/// Record an approval as pending (resolved_at IS NULL).
func saveApproval(_ request: ApprovalRequest) async throws
/// Mark an approval resolved with the winning decision.
func resolveApproval(_ resolved: ApprovalResolved) async throws
func pendingApprovals(sessionID: SessionID) async throws -> [ApprovalRequest]
/// Insert-or-update a captured idea (the home "to-do" inbox).
func saveTodo(_ todo: Todo) async throws
/// All todos, most-recently-updated first.
func loadTodos() async throws -> [Todo]
func deleteTodo(id: TodoID) async throws
}