From 14bf157f8681472e3d2848e39e3da09ce8d26f96 Mon Sep 17 00:00:00 2001 From: Emre K <110906681+kocaemre@users.noreply.github.com> Date: Sun, 8 Nov 2026 19:43:05 +0100 Subject: [PATCH 1/2] fix: write session pages during uncached directory conversion Fixes #406. Compute session metadata from loaded messages when a directory conversion skips the cache, so the index never links to missing session pages. Co-authored-by: Hermes Agent Signed-off-by: Emre K <110906681+kocaemre@users.noreply.github.com> --- claude_code_log/converter.py | 3 +++ test/test_date_filtered_runs.py | 18 ++++++++++++++++++ test/test_output_explicit.py | 6 +++++- 3 files changed, 26 insertions(+), 1 deletion(-) diff --git a/claude_code_log/converter.py b/claude_code_log/converter.py index 74c4307b..25a0a41b 100644 --- a/claude_code_log/converter.py +++ b/claude_code_log/converter.py @@ -4113,6 +4113,9 @@ def convert_jsonl_to( shown_sessions: Optional[set[str]] = None window_rows: Optional[Dict[str, SessionCacheData]] = None record_window = True + if input_path.is_dir() and cache_manager is None and generate_individual_sessions: + # The session writer needs rows even when no render cache was requested. + window_rows = _build_session_data_from_messages(loaded) if windowed: assert output_path is not None if input_path.is_dir(): diff --git a/test/test_date_filtered_runs.py b/test/test_date_filtered_runs.py index e8e9a97b..fbdc736d 100644 --- a/test/test_date_filtered_runs.py +++ b/test/test_date_filtered_runs.py @@ -182,6 +182,24 @@ def _missing(project: Path, shape: str) -> List[str]: return missing +def test_no_cache_hierarchy_writes_session_pages_linked_from_index( + tmp_path: Path, monkeypatch: pytest.MonkeyPatch +) -> None: + """An unfiltered no-cache run must not link to absent session pages.""" + project = _build_project(tmp_path) + monkeypatch.setenv("CLAUDE_CODE_LOG_CACHE_PATH", str(tmp_path / "cache.db")) + + process_projects_hierarchy(project.parent, jobs=1, use_cache=False) + + index = (project.parent / "index.html").read_text(encoding="utf-8") + for sid in SESSIONS: + page = project / f"session-{sid}.html" + assert f"session-{sid}.html" in index + assert page.is_file(), f"index links to missing {page.name}" + assert f"first {sid}" in page.read_text(encoding="utf-8") + assert not (tmp_path / "cache.db").exists() + + # The hierarchy pass converts only a project with a changed file, so it # reaches a filtered run through an append; a project converted on its own # reaches it with or without one. diff --git a/test/test_output_explicit.py b/test/test_output_explicit.py index 211e1b26..d84caf73 100644 --- a/test/test_output_explicit.py +++ b/test/test_output_explicit.py @@ -367,7 +367,11 @@ def test_grown_session_regenerates_without_cache(self, tmp_path: Path): r2 = runner.invoke(main, [str(proj), "--no-cache"]) assert r2.exit_code == 0, r2.output - assert "skipping regeneration" not in r2.output + # Session pages may be current; only the combined page must regenerate. + assert ( + "combined_transcripts.html is current, skipping regeneration" + not in r2.output + ) second = combined.read_text(encoding="utf-8") assert "SECOND_dir_marker" in second, ( "stale combined HTML served on a --no-cache directory re-run" From 195e51f93ea3f5487568a507e70670033e802581 Mon Sep 17 00:00:00 2001 From: Emre K <110906681+kocaemre@users.noreply.github.com> Date: Mon, 9 Nov 2026 01:11:27 +0100 Subject: [PATCH 2/2] fix: refresh uncached session pages when transcripts grow Signed-off-by: Emre K <110906681+kocaemre@users.noreply.github.com> --- claude_code_log/converter.py | 8 +++++--- test/test_date_filtered_runs.py | 18 ++++++++++++++++++ 2 files changed, 23 insertions(+), 3 deletions(-) diff --git a/claude_code_log/converter.py b/claude_code_log/converter.py index 25a0a41b..aa409b7e 100644 --- a/claude_code_log/converter.py +++ b/claude_code_log/converter.py @@ -5543,10 +5543,12 @@ def _generate_individual_session_files( or not session_file_path.exists() ) else: - # Fallback: no cache, or a format that tracks its own - # freshness (JSON) rather than the shared version marker. + # Without a cache there is no source fingerprint for an existing + # session page; the renderer's version check alone misses appends. + # JSON tracks its own version field when a cache is present. should_regenerate_session = ( - renderer.is_outdated(session_file_path) + cache_manager is None + or renderer.is_outdated(session_file_path) or from_date is not None or to_date is not None or not session_file_path.exists() diff --git a/test/test_date_filtered_runs.py b/test/test_date_filtered_runs.py index fbdc736d..3e0fd78e 100644 --- a/test/test_date_filtered_runs.py +++ b/test/test_date_filtered_runs.py @@ -200,6 +200,24 @@ def test_no_cache_hierarchy_writes_session_pages_linked_from_index( assert not (tmp_path / "cache.db").exists() +def test_no_cache_hierarchy_refreshes_existing_session_page_after_append( + tmp_path: Path, monkeypatch: pytest.MonkeyPatch +) -> None: + """An uncached rerun must refresh a session page when its source grows.""" + project = _build_project(tmp_path) + monkeypatch.setenv("CLAUDE_CODE_LOG_CACHE_PATH", str(tmp_path / "cache.db")) + process_projects_hierarchy(project.parent, jobs=1, use_cache=False) + page = project / "session-sess-alpha.html" + late = "LATE-UNCACHED-TURN" + assert late not in page.read_text(encoding="utf-8") + + _append(project / "sess-alpha.jsonl", _turn("sess-alpha", 2, 2, late)) + process_projects_hierarchy(project.parent, jobs=1, use_cache=False) + + assert late in page.read_text(encoding="utf-8") + assert not (tmp_path / "cache.db").exists() + + # The hierarchy pass converts only a project with a changed file, so it # reaches a filtered run through an append; a project converted on its own # reaches it with or without one.