Skip to content

fix: prevent stale session detail and 60s update lag - #183

Merged
matt1398 merged 1 commit into
mainfrom
fix/sporadic-session-updates
Apr 27, 2026
Merged

matt1398 merged 1 commit into
mainfrom
fix/sporadic-session-updates

Conversation

@matt1398

@matt1398 matt1398 commented Apr 27, 2026 •

Copy link
Copy Markdown
Owner

DataCache now stores an optional file-state fingerprint (mtimeMs+size) alongside each entry. handleGetSessionDetail stats the JSONL file before the cache lookup and passes the fingerprint to get()/set(); a mismatch invalidates the entry. This is a safety net for FileWatcher events dropped by macOS FSEvents — without it, a missed event left a stale cache entry that survived manual refresh for up to the 10-minute TTL.

Also caps the renderer's adaptive session-refresh debounce at 5s (previously up to 60s for sessions with >1000 AI groups), so the UI never appears frozen while a session is actively streaming.

Summary by CodeRabbit

  • Bug Fixes

    • Improved cache invalidation to detect when session data has been modified on disk and properly refresh stale entries.
  • Performance

    • Optimized session refresh timing to provide faster updates, with response times now capped at 5 seconds maximum.

DataCache now stores an optional file-state fingerprint (mtimeMs+size)
alongside each entry. handleGetSessionDetail stats the JSONL file before
the cache lookup and passes the fingerprint to get()/set(); a mismatch
invalidates the entry. This is a safety net for FileWatcher events
dropped by macOS FSEvents — without it, a missed event left a stale
cache entry that survived manual refresh for up to the 10-minute TTL.

Also caps the renderer's adaptive session-refresh debounce at 5s
(previously up to 60s for sessions with >1000 AI groups), so the UI
never appears frozen while a session is actively streaming.
Copilot AI review requested due to automatic review settings April 27, 2026 03:48
@coderabbitai coderabbitai Bot added the bug Something isn't working label Apr 27, 2026

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request introduces a file-state fingerprinting mechanism to the session data cache, using file modification time and size to detect stale entries when file system events are missed. It also optimizes the renderer's adaptive debounce logic by capping the refresh interval at 5 seconds to maintain UI responsiveness during active streaming. A review comment suggests explicitly invalidating the cache entry if the file status check fails to ensure that deleted or inaccessible sessions do not persist in the cache until the TTL expires.

Comment thread src/main/ipc/sessions.ts
Comment on lines +228 to +230
} catch {
// Stat failure is non-fatal — fall through to the existence check below.
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

If stat fails (e.g., the file was deleted or is inaccessible), fingerprint remains undefined. In this case, the subsequent dataCache.get(cacheKey, fingerprint) call will skip the fingerprint check and return a cached entry if one exists. This means stale data for a deleted session could persist in the UI until the 10-minute TTL expires, even after a manual refresh.

Invalidating the cache entry in the catch block ensures that a failed stat correctly leads to a cache miss and a fresh existence check, which is consistent with the goal of preventing stale data from surviving manual refreshes.

Suggested change
} catch {
// Stat failure is non-fatal — fall through to the existence check below.
}
} catch {
// Stat failure is non-fatal — invalidate cache to ensure we don't serve stale data if the file was deleted.
dataCache.invalidate(cacheKey);
}

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Prevents stale session-detail caching when file watcher events are missed, and reduces perceived UI “freeze” by capping adaptive refresh debounce for large/streaming sessions.

Changes:

  • Add optional file-state fingerprinting (mtimeMs+size) to DataCache entries and invalidate on mismatch.
  • Update handleGetSessionDetail to stat() the session JSONL file, pass a fingerprint to cache get()/set(), and re-parse when stale.
  • Cap renderer adaptive session-refresh debounce to a 5s maximum for large sessions.

Reviewed changes

Copilot reviewed 3 out of 3 changed files in this pull request and generated 2 comments.

File Description
src/renderer/store/index.ts Caps adaptive refresh debounce to avoid long (up to 60s) UI update lag on large sessions.
src/main/services/infrastructure/DataCache.ts Stores optional fingerprints on cache entries and invalidates entries when the provided fingerprint differs.
src/main/ipc/sessions.ts Computes a session file fingerprint via stat() and uses it to avoid returning stale cached session detail.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +100 to +104
// Check fingerprint mismatch (file changed since cached)
if (fingerprint !== undefined && entry.fingerprint !== fingerprint) {
this.cache.delete(key);
return undefined;
}

Copilot AI Apr 27, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The fingerprint invalidation behavior doesn’t match the new doc comment: the comment says a mismatch invalidates the entry “when present on both set() and get()”, but the code invalidates whenever a caller provides a fingerprint and the cached entry’s fingerprint is different (including undefined). Either tighten the check to only compare when entry.fingerprint is defined, or update the comment to reflect that entries without a fingerprint are treated as stale when a fingerprinted get() occurs.

Copilot uses AI. Check for mistakes.
Comment thread src/main/ipc/sessions.ts
Comment on lines +224 to +230
try {
const filePath = projectScanner.getSessionPath(safeProjectId, safeSessionId);
const stats = await projectScanner.getFileSystemProvider().stat(filePath);
fingerprint = `${stats.mtimeMs}-${stats.size}`;
} catch {
// Stat failure is non-fatal — fall through to the existence check below.
}

Copilot AI Apr 27, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The comment in the stat() catch block says it will “fall through to the existence check below”, but on a cache hit the function returns immediately and never performs the later getSessionWithOptions null check. This can still serve a stale cached SessionDetail (or one for a deleted/missing file) for the full TTL when stat() fails. Consider bypassing the cache when fingerprinting fails (or explicitly verifying existence even on cache hits), and/or update the comment to reflect the actual control flow.

Copilot uses AI. Check for mistakes.
@coderabbitai

coderabbitai Bot commented Apr 27, 2026

Copy link
Copy Markdown
📝 Walkthrough

Walkthrough

Implemented fingerprint-based cache validation for session data using file modification time and size. Updated DataCache to store and validate fingerprints alongside cached entries. Adjusted session refresh debounce thresholds to trigger refreshes more frequently for large sessions.

Changes

Cohort / File(s) Summary
Fingerprint-based Cache Invalidation
src/main/ipc/sessions.ts, src/main/services/infrastructure/DataCache.ts
Added fingerprint computation from file mtimeMs and size to detect stale cache entries. Updated DataCache.get() and set() methods to accept optional fingerprint arguments; stale entries are deleted and treated as cache misses.
Session Refresh Debounce Adjustment
src/renderer/store/index.ts
Reduced scheduleSessionRefresh debounce thresholds: maximum delay lowered from ~60s to 5s, with intermediate thresholds adjusted to 2.5s and 1s for more frequent refresh triggers on large sessions.

Possibly related PRs

Suggested labels

bug

🚥 Pre-merge checks | ✅ 2
✅ Passed checks (2 passed)
Check name Status Explanation
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/main/ipc/sessions.ts (1)

340-354: ⚠️ Potential issue | 🟡 Minor

handleGetSessionMetrics bypasses the new fingerprint check.

This handler reads the same cacheKey as handleGetSessionDetail but calls dataCache.get(cacheKey) without a fingerprint, so an entry left stale by a dropped FileWatcher event will still be served here for up to the 10‑minute TTL — exactly the failure mode the PR is closing for session detail. If session-metrics callers are user-visible, please mirror the stat+fingerprint pattern; otherwise consider routing metrics through handleGetSessionDetail (or via a shared helper) so both stay consistent.

♻️ Suggested fix
-    const { sessionParser, dataCache } = registry.getActive();
+    const { projectScanner, sessionParser, dataCache } = registry.getActive();
     const safeProjectId = validatedProject.value!;
     const safeSessionId = validatedSession.value!;

-    // Try to get from cache first
     const cacheKey = DataCache.buildKey(safeProjectId, safeSessionId);
-    const cached = dataCache.get(cacheKey);
+
+    let fingerprint: string | undefined;
+    try {
+      const filePath = projectScanner.getSessionPath(safeProjectId, safeSessionId);
+      const stats = await projectScanner.getFileSystemProvider().stat(filePath);
+      fingerprint = `${stats.mtimeMs}-${stats.size}`;
+    } catch {
+      dataCache.invalidate(cacheKey);
+    }
+
+    const cached = dataCache.get(cacheKey, fingerprint);

     if (cached) {
       return cached.metrics;
     }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/main/ipc/sessions.ts` around lines 340 - 354, handleGetSessionMetrics
currently calls dataCache.get(cacheKey) directly and can return stale entries;
change it to mirror the stat+fingerprint pattern used in handleGetSessionDetail:
call dataCache.stat(cacheKey) to obtain the current fingerprint (and check
existence), then call dataCache.get(cacheKey, fingerprint) (or re-parse via
sessionParser.parseSession and update the cache when stat indicates miss) so
returned metrics respect the same fingerprint validation as
handleGetSessionDetail; reference functions/objects: handleGetSessionMetrics,
handleGetSessionDetail, DataCache.buildKey, dataCache.stat, dataCache.get, and
sessionParser.parseSession.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/main/ipc/sessions.ts`:
- Around line 220-233: The catch block after
projectScanner.getFileSystemProvider().stat can leave fingerprint undefined
which causes dataCache.get(cacheKey, fingerprint) to skip validation and return
stale data; update the catch to explicitly invalidate the cache entry (e.g.,
call the DataCache invalidation method for cacheKey or otherwise remove the
entry) before proceeding so a stale sessionDetail cannot be returned, and keep
the comment in sync; locate the logic around projectScanner.getSessionPath /
projectScanner.getFileSystemProvider().stat, the fingerprint variable, and the
subsequent dataCache.get call to implement the invalidation.

---

Outside diff comments:
In `@src/main/ipc/sessions.ts`:
- Around line 340-354: handleGetSessionMetrics currently calls
dataCache.get(cacheKey) directly and can return stale entries; change it to
mirror the stat+fingerprint pattern used in handleGetSessionDetail: call
dataCache.stat(cacheKey) to obtain the current fingerprint (and check
existence), then call dataCache.get(cacheKey, fingerprint) (or re-parse via
sessionParser.parseSession and update the cache when stat indicates miss) so
returned metrics respect the same fingerprint validation as
handleGetSessionDetail; reference functions/objects: handleGetSessionMetrics,
handleGetSessionDetail, DataCache.buildKey, dataCache.stat, dataCache.get, and
sessionParser.parseSession.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 65eabd7a-aae6-48a9-aaf6-31fe20418038

📥 Commits

Reviewing files that changed from the base of the PR and between 9745733 and 2b55f96.

📒 Files selected for processing (3)
  • src/main/ipc/sessions.ts
  • src/main/services/infrastructure/DataCache.ts
  • src/renderer/store/index.ts

Comment thread src/main/ipc/sessions.ts
Comment on lines +220 to +233
// Stat the JSONL file so we can fingerprint the cache entry.
// Without this, a missed FileWatcher event leaves a stale cache entry
// that survives manual refresh for up to 10 minutes (the TTL).
let fingerprint: string | undefined;
try {
const filePath = projectScanner.getSessionPath(safeProjectId, safeSessionId);
const stats = await projectScanner.getFileSystemProvider().stat(filePath);
fingerprint = `${stats.mtimeMs}-${stats.size}`;
} catch {
// Stat failure is non-fatal — fall through to the existence check below.
}

// Check cache first (returns undefined if fingerprint mismatches)
let sessionDetail = dataCache.get(cacheKey, fingerprint);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Misleading comment — there is no “existence check below.”

The catch comment claims the code falls through to an existence check, but the very next statement is dataCache.get(cacheKey, fingerprint) with fingerprint === undefined. Per DataCache.get, an undefined fingerprint skips validation, so on stat failure a stale cached entry will still be returned. That's the original bug this PR is meant to mitigate, just narrowed to the rare stat-failure window.

Either tighten the comment to reflect actual behavior, or invalidate the cache entry explicitly when stat fails so the safety net still applies.

♻️ Suggested clarification
     let fingerprint: string | undefined;
     try {
       const filePath = projectScanner.getSessionPath(safeProjectId, safeSessionId);
       const stats = await projectScanner.getFileSystemProvider().stat(filePath);
       fingerprint = `${stats.mtimeMs}-${stats.size}`;
     } catch {
-      // Stat failure is non-fatal — fall through to the existence check below.
+      // Stat failure is non-fatal. Without a fingerprint we can't validate the
+      // cached entry, so invalidate it to avoid serving a stale read; the
+      // downstream parse path will repopulate the cache.
+      dataCache.invalidate(cacheKey);
     }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/main/ipc/sessions.ts` around lines 220 - 233, The catch block after
projectScanner.getFileSystemProvider().stat can leave fingerprint undefined
which causes dataCache.get(cacheKey, fingerprint) to skip validation and return
stale data; update the catch to explicitly invalidate the cache entry (e.g.,
call the DataCache invalidation method for cacheKey or otherwise remove the
entry) before proceeding so a stale sessionDetail cannot be returned, and keep
the comment in sync; locate the logic around projectScanner.getSessionPath /
projectScanner.getFileSystemProvider().stat, the fingerprint variable, and the
subsequent dataCache.get call to implement the invalidation.

@matt1398
matt1398 merged commit baf41ef into main Apr 27, 2026
11 checks passed
pull Bot pushed a commit to stormychel/claude-devtools that referenced this pull request Apr 29, 2026
Fixes matt1398#186. Long sessions ran at ~10fps after v0.4.13 because PR matt1398#183
shrank the refresh debounce ceiling from 60s to 5s. The shorter window
exposed an existing architectural flaw: every `getSessionDetail` IPC
returned the full payload, with the renderer-side fingerprint check
happening *after* the expensive serialization. Frequent refreshes paid
near-real-refresh cost even when nothing on disk had changed.

Move the fingerprint check to the IPC boundary. The renderer caches the
opaque file-state fingerprint (mtimeMs+size) returned by main and passes
it back on the next call. When it matches, the handler short-circuits
to a tiny `{ unchanged: true }` sentinel before cache lookup or parsing.
The renderer detects the sentinel and exits without transforming or
re-rendering — bounded to one stat() + a sub-100B IPC payload.

With no-op cost effectively eliminated, the adaptive debounce can be
restored to sane values (1s ceiling for >500 groups, 500ms for >200,
300ms for >100), preserving the streaming-responsiveness intent of
PR matt1398#183 without the perf cost.

Backwards compatible: knownFingerprint is optional, so initial loads
and export still receive a full SessionDetail.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug Something isn't working

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants