From 3d86f11c6762b2807bd3890eccfd9c3fdcf246fd Mon Sep 17 00:00:00 2001 From: Andrew Blakeslee Moore Date: Sun, 5 Jul 2026 22:31:23 -0700 Subject: [PATCH] Session Cache Implementation Nucleic-Session: B2346642-3353-4BD5-9DE2-A8B77BD66C98 Co-authored-by: Nucleic --- .../NucleicRemote/Models/RemoteStore.swift | 42 +++++++++++++++++++ 1 file changed, 42 insertions(+) diff --git a/NucleicRemote/NucleicRemote/Models/RemoteStore.swift b/NucleicRemote/NucleicRemote/Models/RemoteStore.swift index 422f6d0..2445edf 100644 --- a/NucleicRemote/NucleicRemote/Models/RemoteStore.swift +++ b/NucleicRemote/NucleicRemote/Models/RemoteStore.swift @@ -647,6 +647,48 @@ final class RemoteStore: ObservableObject { return bySeq.values.sorted { $0.seq < $1.seq } } + // MARK: - Offline cache (SessionCache) + + /// Debounced write of the live session list to disk, so a read-only history survives a + /// disconnect / relaunch. + private func persistSummaries(_ list: [WireSessionSummary]) { + summaryPersistTask?.cancel() + summaryPersistTask = Task { [list] in + try? await Task.sleep(nanoseconds: 2_000_000_000) + guard !Task.isCancelled else { return } + await SessionCache.saveSummaries(list) + } + } + + /// Seed the open transcript from disk so a cached session's history shows instantly — even fully + /// offline. A live snapshot/backfill merges on top by seq (dedup), so this never double-counts. + private func loadCachedTranscript(_ sessionID: SessionID) { + Task { [weak self] in + let cached = await SessionCache.loadEvents(sessionID) + guard let self, self.openSessionID == sessionID, !cached.isEmpty else { return } + self.openEvents = Self.mergedEvents(cached, self.openEvents) + } + } + + /// Debounced write of the open transcript, called after each batch of events lands. + private func persistOpenTranscript() { + guard !demoMode, let id = openSessionID, !openEvents.isEmpty else { return } + let events = openEvents + transcriptPersistTask?.cancel() + transcriptPersistTask = Task { [events, id] in + try? await Task.sleep(nanoseconds: 1_500_000_000) + guard !Task.isCancelled else { return } + await SessionCache.saveEvents(events, for: id) + } + } + + /// Flush the open transcript to disk immediately (on close), cancelling any pending debounce. + private func flushOpenTranscript(_ id: SessionID, _ events: [AgentEvent]) { + transcriptPersistTask?.cancel() + guard !demoMode, !events.isEmpty else { return } + Task { await SessionCache.saveEvents(events, for: id) } + } + /// The flat sessions list: every connected Mac's sessions, deduped by id (ids are globally /// unique). Views handle sorting/grouping. private func aggregatedSessions() -> [WireSessionSummary] {