From fb833d4a0a6cb9190388c0afbb170667004c310a Mon Sep 17 00:00:00 2001 From: Alan Wizemann Date: Fri, 24 Apr 2026 01:27:06 +0200 Subject: [PATCH] fix(projects): open HermesDataService before filtering sessions MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Sessions tab was showing "This project has N attributed sessions, but none are in the recent history. They may have been deleted from Hermes." on projects with valid sidecar entries and actual sessions present in state.db. Root cause: the VM never opened the DB handle. `HermesDataService` is an actor with a lazily-initialised SQLite pointer. Every query method short-circuits to `[]` when `db == nil`. Callers have to open/refresh the handle explicitly — InsightsViewModel does it (line 106), ActivityViewModel does it (line 60). ProjectSessionsViewModel was constructed fresh per project, never inherited a shared service, and never called refresh() itself, so fetchSessions returned empty on every load and the filter against the (correctly-populated) sidecar map produced zero matches. The empty-state message ("may have been deleted") fired on that false-negative. The data was fine all along: sqlite3 ~/.hermes/state.db confirmed both attributed sessions with source='acp', parent_session_id IS NULL — they pass fetchSessions's WHERE clause cleanly. The sidecar mappings were correct. The file watcher was firing. The only missing piece was the DB-open precondition. Fix: `_ = await dataService.refresh()` before fetchSessions, mirroring the pattern used by every other feature VM that consumes HermesDataService. Also adds a `close()` on the VM + an onDisappear handler on the view, so the handle doesn't dangle once the tab isn't visible — same cleanup ActivityView has. This is NOT forward-only. Existing sidecar entries that currently show the misleading empty-state will surface correctly as soon as users rebuild — no data migration, no re-create-the-chat, no backfill. The bug was "couldn't read what was already there," not "lost old data." 93/93 Swift tests still pass. No test change — the fix is an integration-level call-ordering detail that isn't meaningfully testable without mocking HermesDataService (overkill for a two-line fix). Co-Authored-By: Claude Opus 4.7 (1M context) --- .../ViewModels/ProjectSessionsViewModel.swift | 20 +++++++++++++++++++ .../Projects/Views/ProjectSessionsView.swift | 6 ++++++ 2 files changed, 26 insertions(+) diff --git a/scarf/scarf/Features/Projects/ViewModels/ProjectSessionsViewModel.swift b/scarf/scarf/Features/Projects/ViewModels/ProjectSessionsViewModel.swift index 8d6a56c..8d20a8b 100644 --- a/scarf/scarf/Features/Projects/ViewModels/ProjectSessionsViewModel.swift +++ b/scarf/scarf/Features/Projects/ViewModels/ProjectSessionsViewModel.swift @@ -51,6 +51,17 @@ final class ProjectSessionsViewModel { return } + // Open (or re-open for remote) the DB handle before querying. + // `HermesDataService` is an actor with a lazily-initialised + // SQLite pointer; every query method short-circuits to `[]` + // when `db == nil`. This VM constructs its own service + // instance (separate from ChatViewModel / InsightsVM / + // ActivityVM), so we have to open it ourselves. Same + // pattern used by those other VMs (`refresh()` rather than + // `open()` because refresh also re-pulls the remote-server + // snapshot on each call — local is a cheap no-op). + _ = await dataService.refresh() + // Fetch a generous page; we filter client-side by attribution // map membership. The 200 ceiling matches other feature VMs // (ActivityViewModel, InsightsViewModel). HermesDataService @@ -73,4 +84,13 @@ final class ProjectSessionsViewModel { emptyStateHint = nil } } + + /// Release the underlying DB handle. Safe to call repeatedly; the + /// service re-opens on the next `load()`. Mirrors the pattern in + /// ActivityViewModel.swift:80 — view calls this on `.onDisappear` + /// so file descriptors and the SQLite cache don't dangle once + /// the tab isn't visible. + func close() async { + await dataService.close() + } } diff --git a/scarf/scarf/Features/Projects/Views/ProjectSessionsView.swift b/scarf/scarf/Features/Projects/Views/ProjectSessionsView.swift index 3440fef..cd5ea2f 100644 --- a/scarf/scarf/Features/Projects/Views/ProjectSessionsView.swift +++ b/scarf/scarf/Features/Projects/Views/ProjectSessionsView.swift @@ -44,6 +44,12 @@ struct ProjectSessionsView: View { .onChange(of: fileWatcher.lastChangeDate) { Task { await viewModel?.load() } } + .onDisappear { + // Release the SQLite handle so it doesn't dangle once + // the user leaves this tab. `load()` will re-open next + // time. Mirrors ActivityView's disappear cleanup. + Task { await viewModel?.close() } + } } // MARK: - Header