Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
42 changes: 42 additions & 0 deletions services/summary_cache.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
CACHE_DIR = Path.home() / ".cache" / "cursor-chat-browser"
PROJECTS_CACHE_FILE = CACHE_DIR / "projects.json"
COMPOSER_MAP_CACHE_FILE = CACHE_DIR / "composer-id-to-ws.json"
INVALID_WORKSPACE_ALIASES_CACHE_FILE = CACHE_DIR / "invalid-workspace-aliases.json"
TAB_SUMMARIES_PREFIX = "tab-summaries-"


Expand Down Expand Up @@ -238,6 +239,47 @@ def set_cached_composer_id_to_ws(
)


def get_cached_invalid_workspace_aliases(
fingerprint: dict[str, Any],
) -> dict[str, str] | None:
"""Load cached invalid-workspace alias map when the fingerprint matches.

Args:
fingerprint: Storage mtime/rules digest.

Returns:
``{invalid_id: replacement_id}`` on hit, else ``None``.
"""
data = _read_cache_file(INVALID_WORKSPACE_ALIASES_CACHE_FILE)
if not data:
return None
if not _fingerprint_equal(data.get("fingerprint"), fingerprint):
return None
aliases = data.get("invalid_workspace_aliases")
if not isinstance(aliases, dict):
return None
return {str(k): str(v) for k, v in aliases.items()}
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Outdated


def set_cached_invalid_workspace_aliases(
fingerprint: dict[str, Any],
aliases: dict[str, str],
) -> None:
"""Persist invalid-workspace alias map under *fingerprint*.

Args:
fingerprint: Invalidation fingerprint paired with *aliases*.
aliases: ``{invalid_id: replacement_id}`` from alias inference.
"""
_write_cache_file(
INVALID_WORKSPACE_ALIASES_CACHE_FILE,
{
"fingerprint": fingerprint,
"invalid_workspace_aliases": aliases,
},
)


def _tab_summaries_path(workspace_id: str) -> Path:
safe = hashlib.sha256(workspace_id.encode("utf-8")).hexdigest()[:16]
return CACHE_DIR / f"{TAB_SUMMARIES_PREFIX}{safe}.json"
Expand Down
105 changes: 104 additions & 1 deletion services/workspace_context.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,22 +2,27 @@

from __future__ import annotations

import os
import sqlite3
from dataclasses import dataclass, replace
from dataclasses import dataclass, field, replace
from typing import Any

from models import Bubble
from services.workspace_db import (
COMPOSER_ROWS_WITH_HEADERS_SQL,
build_composer_id_to_workspace_id,
build_composer_id_to_workspace_id_cached,
collect_invalid_workspace_ids,
collect_workspace_entries,
global_storage_db_path,
load_bubble_map,
load_project_layouts_map,
safe_fetchall,
)
from services.workspace_resolver import (
create_project_name_to_workspace_id_map,
create_workspace_path_to_id_map,
infer_invalid_workspace_aliases,
)


Expand All @@ -32,6 +37,7 @@ class WorkspaceContext:
workspace_path_to_id: dict[str, str]
project_layouts_map: dict[str, list[str]]
bubble_map: dict[str, Bubble]
invalid_workspace_aliases: dict[str, str] = field(default_factory=dict)


def _entries(
Expand Down Expand Up @@ -135,3 +141,100 @@ def enrich_workspace_context_from_global_db(
if not updates:
return ctx
return replace(ctx, **updates)


def resolve_invalid_workspace_aliases_cached(
ctx: WorkspaceContext,
global_db: sqlite3.Connection,
workspace_path: str,
rules: list[Any],
*,
nocache: bool = False,
project_layouts_map: dict[str, list[str]] | None = None,
) -> dict[str, str]:
"""Return invalid-workspace alias map, using the summary-cache fingerprint.

Computes ``infer_invalid_workspace_aliases`` at most once per storage
fingerprint (same mtime key as composer-map / tab-summary caches). When
*ctx* already carries a populated ``invalid_workspace_aliases`` field,
that value is returned without touching disk or the global DB roster.
Comment thread
coderabbitai[bot] marked this conversation as resolved.

Args:
ctx: Workspace maps from :func:`resolve_workspace_context_cached`.
global_db: Open global ``state.vscdb`` connection.
workspace_path: Cursor ``workspaceStorage`` root.
rules: Exclusion rule token lists (fingerprint input).
nocache: When ``True``, bypass disk cache reads and writes.
project_layouts_map: Pre-loaded layouts; loaded from *global_db* when
``None``.

Returns:
``{invalid_id: replacement_id}``, or ``{}`` when every workspace is valid.
"""
if not ctx.invalid_workspace_ids:
return {}

from services.summary_cache import (
fingerprint_workspace_storage,
get_cached_invalid_workspace_aliases,
nocache_enabled,
set_cached_invalid_workspace_aliases,
)
from utils.workspace_path import get_cli_chats_path

gdb = global_storage_db_path(workspace_path)
cli_path = get_cli_chats_path()
fingerprint = fingerprint_workspace_storage(
workspace_path,
ctx.workspace_entries,
global_db_path=gdb if os.path.isfile(gdb) else None,
rules=rules,
cli_chats_path=cli_path if os.path.isdir(cli_path) else None,
)
if not nocache_enabled(request_nocache=nocache):
cached = get_cached_invalid_workspace_aliases(fingerprint)
if cached is not None:
return cached

layouts = (
project_layouts_map
if project_layouts_map is not None
else load_project_layouts_map(global_db)
)
composer_rows = safe_fetchall(global_db, COMPOSER_ROWS_WITH_HEADERS_SQL)
aliases = infer_invalid_workspace_aliases(
composer_rows=composer_rows,
project_layouts_map=layouts,
project_name_map=ctx.project_name_to_workspace_id,
workspace_path_map=ctx.workspace_path_to_id,
workspace_entries=ctx.workspace_entries,
bubble_map={},
composer_id_to_ws=ctx.composer_id_to_workspace_id,
invalid_workspace_ids=ctx.invalid_workspace_ids,
)
if not nocache_enabled(request_nocache=nocache):
set_cached_invalid_workspace_aliases(fingerprint, aliases)
return aliases


def with_invalid_workspace_aliases(
Comment thread
clean6378-max-it marked this conversation as resolved.
ctx: WorkspaceContext,
global_db: sqlite3.Connection,
workspace_path: str,
rules: list[Any],
*,
nocache: bool = False,
project_layouts_map: dict[str, list[str]] | None = None,
) -> WorkspaceContext:
"""Return *ctx* with ``invalid_workspace_aliases`` populated from cache."""
aliases = resolve_invalid_workspace_aliases_cached(
ctx,
global_db,
workspace_path,
rules,
nocache=nocache,
project_layouts_map=project_layouts_map,
)
if aliases is ctx.invalid_workspace_aliases:
return ctx
return replace(ctx, invalid_workspace_aliases=aliases)
21 changes: 8 additions & 13 deletions services/workspace_listing.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@
nocache_enabled,
set_cached_projects,
)
from services.workspace_context import resolve_invalid_workspace_aliases_cached
from services.workspace_db import (
COMPOSER_ROWS_WITH_HEADERS_SQL,
collect_workspace_entries,
Expand All @@ -41,7 +42,6 @@
from utils.workspace_path import get_cli_chats_path
from services.workspace_resolver import (
build_composer_ids_by_workspace,
infer_invalid_workspace_aliases,
infer_workspace_name_from_layouts,
lookup_workspace_display_name,
)
Expand Down Expand Up @@ -124,18 +124,13 @@ def _build_workspace_projects_uncached(
project_layouts_map = load_project_layouts_map(global_db)

bubble_map: dict[str, Bubble] = {}
invalid_workspace_aliases: dict[str, str] = {}
if invalid_workspace_ids:
invalid_workspace_aliases = infer_invalid_workspace_aliases(
composer_rows=composer_rows,
project_layouts_map=project_layouts_map,
project_name_map=project_name_map,
workspace_path_map=workspace_path_map,
workspace_entries=workspace_entries,
bubble_map=bubble_map,
composer_id_to_ws=composer_id_to_ws,
invalid_workspace_ids=invalid_workspace_ids,
)
invalid_workspace_aliases = resolve_invalid_workspace_aliases_cached(
ctx,
global_db,
workspace_path,
rules,
project_layouts_map=project_layouts_map,
)
Comment thread
coderabbitai[bot] marked this conversation as resolved.

for row in composer_rows:
composer = parse_composer_data_row(
Expand Down
64 changes: 25 additions & 39 deletions services/workspace_tabs.py
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,10 @@
nocache_enabled,
set_cached_tab_summaries,
)
from services.workspace_context import resolve_workspace_context_cached
from services.workspace_context import (
resolve_invalid_workspace_aliases_cached,
resolve_workspace_context_cached,
)
from services.workspace_db import (
COMPOSER_ROWS_WITH_HEADERS_SQL,
collect_workspace_entries,
Expand All @@ -67,7 +70,6 @@
)
from utils.workspace_path import get_cli_chats_path
from services.workspace_resolver import (
infer_invalid_workspace_aliases,
lookup_workspace_display_name,
matching_workspace_ids_for_folder,
)
Expand Down Expand Up @@ -453,18 +455,14 @@ def _build_workspace_tab_summaries_uncached(

composer_rows = safe_fetchall(global_db, COMPOSER_ROWS_WITH_HEADERS_SQL)
Comment thread
clean6378-max-it marked this conversation as resolved.

invalid_workspace_aliases: dict[str, str] = {}
if invalid_workspace_ids:
invalid_workspace_aliases = infer_invalid_workspace_aliases(
composer_rows=composer_rows,
project_layouts_map=project_layouts_map,
project_name_map=project_name_map,
workspace_path_map=workspace_path_map,
workspace_entries=workspace_entries,
bubble_map={},
composer_id_to_ws=composer_id_to_ws,
invalid_workspace_ids=invalid_workspace_ids,
)
invalid_workspace_aliases = resolve_invalid_workspace_aliases_cached(
ctx,
global_db,
workspace_path,
rules,
nocache=nocache,
project_layouts_map=project_layouts_map,
)

for row in composer_rows:
composer = parse_composer_data_row(
Expand Down Expand Up @@ -582,22 +580,15 @@ def assemble_single_tab(
return {"error": "Conversation not found"}, 404

project_layouts_map: dict[str, list[str]] = {}
invalid_workspace_aliases: dict[str, str] = {}
project_layouts_map[composer_id] = load_project_layouts_for_composer(
global_db, composer_id,
)
if invalid_workspace_ids:
composer_rows_for_aliases = safe_fetchall(global_db, COMPOSER_ROWS_WITH_HEADERS_SQL)
invalid_workspace_aliases = infer_invalid_workspace_aliases(
composer_rows=composer_rows_for_aliases,
project_layouts_map=project_layouts_map,
project_name_map=project_name_map,
workspace_path_map=workspace_path_map,
workspace_entries=workspace_entries,
bubble_map={},
composer_id_to_ws=composer_id_to_ws,
invalid_workspace_ids=invalid_workspace_ids,
)
invalid_workspace_aliases = resolve_invalid_workspace_aliases_cached(
Comment thread
clean6378-max-it marked this conversation as resolved.
Outdated
ctx,
global_db,
workspace_path,
rules,
)

bubble_map = load_bubbles_for_composer(
global_db, composer_id, parse_warnings=parse_warnings,
Expand Down Expand Up @@ -723,18 +714,13 @@ def assemble_workspace_tabs(
# Get composer data entries with conversations
composer_rows = safe_fetchall(global_db, COMPOSER_ROWS_WITH_HEADERS_SQL)

invalid_workspace_aliases: dict[str, str] = {}
if invalid_workspace_ids:
invalid_workspace_aliases = infer_invalid_workspace_aliases(
composer_rows=composer_rows,
project_layouts_map=project_layouts_map,
project_name_map=project_name_map,
workspace_path_map=workspace_path_map,
workspace_entries=workspace_entries,
bubble_map=bubble_map,
composer_id_to_ws=composer_id_to_ws,
invalid_workspace_ids=invalid_workspace_ids,
)
invalid_workspace_aliases = resolve_invalid_workspace_aliases_cached(
Comment thread
clean6378-max-it marked this conversation as resolved.
Outdated
ctx,
global_db,
workspace_path,
rules,
project_layouts_map=project_layouts_map,
)

for row in composer_rows:
composer = parse_composer_data_row(
Expand Down
18 changes: 18 additions & 0 deletions tests/test_summary_cache.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,9 @@

from services.summary_cache import (
fingerprint_workspace_storage,
get_cached_invalid_workspace_aliases,
get_cached_projects,
set_cached_invalid_workspace_aliases,
set_cached_projects,
)

Expand All @@ -29,6 +31,9 @@ def setUp(self):
self.cache_patch = patch.object(summary_cache, "CACHE_DIR", self.tmp.name)
self.cache_patch.start()
summary_cache.PROJECTS_CACHE_FILE = Path(self.tmp.name) / "projects.json"
summary_cache.INVALID_WORKSPACE_ALIASES_CACHE_FILE = (
Path(self.tmp.name) / "invalid-workspace-aliases.json"
)

def tearDown(self):
self.cache_patch.stop()
Expand Down Expand Up @@ -84,6 +89,19 @@ def test_workspace_files_fingerprint_round_trip(self):
assert hit is not None
self.assertEqual(hit[0], projects)

def test_invalid_workspace_aliases_cache_hit(self):
fp = {"version": 1, "workspace_path": "/ws", "global_db_mtime_ns": 100}
aliases = {"broken-ws": "good-ws"}
set_cached_invalid_workspace_aliases(fp, aliases)
hit = get_cached_invalid_workspace_aliases(fp)
self.assertEqual(hit, aliases)

def test_invalid_workspace_aliases_cache_miss_on_fingerprint_change(self):
fp1 = {"version": 1, "workspace_path": "/ws", "global_db_mtime_ns": 100}
fp2 = {**fp1, "global_db_mtime_ns": 101}
set_cached_invalid_workspace_aliases(fp1, {"broken-ws": "good-ws"})
self.assertIsNone(get_cached_invalid_workspace_aliases(fp2))


if __name__ == "__main__":
unittest.main()
Loading
Loading