Skip to content

Commit 79d2027

Browse files
Address feedback
1 parent db089cb commit 79d2027

4 files changed

Lines changed: 52 additions & 5 deletions

File tree

api/projects.py

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,16 @@ def _session_row_error(s: SessionListItemDict) -> ProjectSessionRowDict:
3434

3535

3636
def _peek_or_cache_summary(path: str, mtime: float, rules_fp: str) -> SummaryCacheRowDict:
37-
"""Return a cached summary row or peek the file and store a partial row."""
37+
"""Return a cached summary row (any completeness) or peek the file and store a partial row.
38+
39+
Used by get_projects for fast landing-page counts. Partial rows (is_complete=False)
40+
are acceptable here — is_untitled is derived from the same first-user-text peek that
41+
quick_session_info uses; peek and full-parse agree for the vast majority of sessions
42+
(first user message within the first 80 lines). The session list path always upgrades
43+
to a complete row via get_cached_session, so session_count and list count align after
44+
the first session-list visit. With no exclusion rules the counts are identical on first
45+
visit too, because is_excluded is always False for both paths.
46+
"""
3847
cached = get_summary(path, mtime, rules_fp)
3948
if cached is not None:
4049
return cached

tests/test_api_integration.py

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -142,9 +142,16 @@ def summary_cache_db(tmp_path, monkeypatch):
142142

143143

144144
def test_project_session_count_matches_list(client, summary_cache_db):
145+
"""Card count and session list agree when no exclusion rules are active.
146+
147+
get_projects uses peek (partial row); get_project_sessions uses full parse.
148+
Both filter on is_untitled, and with no rules is_excluded is always False,
149+
so counts align — this is the alignment guarantee from issue #109.
150+
"""
151+
# Hit session list first so disk cache is warm with complete rows.
152+
sessions = client.get("/api/projects/test-project/sessions").get_json()
145153
projects = client.get("/api/projects").get_json()
146154
project = next(p for p in projects if p["name"] == "test-project")
147-
sessions = client.get("/api/projects/test-project/sessions").get_json()
148155
assert project["session_count"] == len(sessions)
149156

150157

tests/test_session_summary_cache.py

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -124,6 +124,14 @@ def test_lru_eviction(
124124
sample_session: Path, cache_db: Path, monkeypatch: pytest.MonkeyPatch
125125
) -> None:
126126
monkeypatch.setattr("utils.session_summary_cache.DEFAULT_MAX_ROWS", 2)
127+
clock = {"t": 100.0}
128+
129+
def fake_time() -> float:
130+
clock["t"] += 100.0
131+
return clock["t"]
132+
133+
monkeypatch.setattr("utils.session_summary_cache.time.time", fake_time)
134+
127135
content = sample_session.read_text(encoding="utf-8")
128136
paths = []
129137
for name in ("a.jsonl", "b.jsonl", "c.jsonl"):
@@ -140,6 +148,10 @@ def test_lru_eviction(
140148
summary_from_peek(quick_session_info(str(p))),
141149
)
142150

151+
first = paths[0]
152+
first_mtime = first.stat().st_mtime
153+
assert get_summary(str(first), first_mtime, "none") is not None
154+
143155
third = paths[2]
144156
third_mtime = third.stat().st_mtime
145157
put_summary(
@@ -149,8 +161,9 @@ def test_lru_eviction(
149161
summary_from_peek(quick_session_info(str(third))),
150162
)
151163

152-
first = paths[0]
153-
assert get_summary(str(first), first.stat().st_mtime, "none") is None
164+
second = paths[1]
165+
assert get_summary(str(second), second.stat().st_mtime, "none") is None
166+
assert get_summary(str(first), first_mtime, "none") is not None
154167
assert get_summary(str(third), third_mtime, "none") is not None
155168

156169

utils/session_summary_cache.py

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -116,6 +116,15 @@ def get_summary(path: str, mtime: float, rules_fingerprint: str) -> SummaryCache
116116
).fetchone()
117117
if row is None:
118118
return None
119+
conn.execute(
120+
"""
121+
UPDATE summary_cache
122+
SET accessed_at = ?
123+
WHERE path = ? AND mtime = ? AND rules_fp = ?
124+
""",
125+
(time.time(), abspath, mtime, rules_fingerprint),
126+
)
127+
conn.commit()
119128
return _payload_to_row(str(row[0]))
120129

121130

@@ -174,7 +183,16 @@ def clear_cache() -> None:
174183

175184

176185
def summary_from_peek(info: QuickSessionInfoDict) -> SummaryCacheRowDict:
177-
"""Build a partial summary row from a lightweight JSONL peek."""
186+
"""Build a partial summary row from a lightweight JSONL peek.
187+
188+
``is_excluded`` is hard-coded ``False`` because peek cannot evaluate exclusion
189+
rules without reading full message bodies. Callers that need accurate exclusion
190+
(``get_project_sessions``) must check ``is_complete`` and fall back to a full
191+
parse. ``get_projects`` uses partial rows for fast card counts: with no
192+
exclusion rules the value is correct; with rules active, excluded sessions may
193+
be counted until the session list upgrades the row to a complete entry — this
194+
is an accepted trade-off documented in issue #109.
195+
"""
178196
title = info["title"]
179197
return SummaryCacheRowDict(
180198
title=title,

0 commit comments

Comments
 (0)