claude-code-log 1.4.0__tar.gz → 1.5.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (242) hide show
  1. claude_code_log-1.5.0/.devbox-foldyard/.gitignore +1 -0
  2. claude_code_log-1.5.0/.devbox-foldyard/foldyard-0.0.1-py3-none-any.whl +0 -0
  3. claude_code_log-1.5.0/.github/workflows/ci.yml +117 -0
  4. {claude_code_log-1.4.0 → claude_code_log-1.5.0}/CHANGELOG.md +60 -0
  5. {claude_code_log-1.4.0 → claude_code_log-1.5.0}/PKG-INFO +1 -1
  6. {claude_code_log-1.4.0 → claude_code_log-1.5.0}/claude_code_log/cache.py +273 -37
  7. {claude_code_log-1.4.0 → claude_code_log-1.5.0}/claude_code_log/cli.py +243 -13
  8. {claude_code_log-1.4.0 → claude_code_log-1.5.0}/claude_code_log/converter.py +611 -173
  9. {claude_code_log-1.4.0 → claude_code_log-1.5.0}/claude_code_log/dag.py +114 -15
  10. {claude_code_log-1.4.0 → claude_code_log-1.5.0}/claude_code_log/factories/meta_factory.py +8 -1
  11. {claude_code_log-1.4.0 → claude_code_log-1.5.0}/claude_code_log/factories/system_factory.py +3 -0
  12. {claude_code_log-1.4.0 → claude_code_log-1.5.0}/claude_code_log/factories/tool_factory.py +75 -0
  13. {claude_code_log-1.4.0 → claude_code_log-1.5.0}/claude_code_log/html/__init__.py +4 -0
  14. {claude_code_log-1.4.0 → claude_code_log-1.5.0}/claude_code_log/html/async_formatter.py +2 -2
  15. {claude_code_log-1.4.0 → claude_code_log-1.5.0}/claude_code_log/html/renderer.py +225 -33
  16. {claude_code_log-1.4.0 → claude_code_log-1.5.0}/claude_code_log/html/system_formatters.py +16 -13
  17. {claude_code_log-1.4.0 → claude_code_log-1.5.0}/claude_code_log/html/templates/components/global_styles.css +15 -0
  18. {claude_code_log-1.4.0 → claude_code_log-1.5.0}/claude_code_log/html/templates/components/message_styles.css +521 -11
  19. {claude_code_log-1.4.0 → claude_code_log-1.5.0}/claude_code_log/html/templates/components/session_nav.html +8 -0
  20. {claude_code_log-1.4.0 → claude_code_log-1.5.0}/claude_code_log/html/templates/components/session_nav_styles.css +8 -3
  21. {claude_code_log-1.4.0 → claude_code_log-1.5.0}/claude_code_log/html/templates/components/timeline.html +28 -6
  22. {claude_code_log-1.4.0 → claude_code_log-1.5.0}/claude_code_log/html/templates/transcript.html +310 -297
  23. {claude_code_log-1.4.0 → claude_code_log-1.5.0}/claude_code_log/html/tool_formatters.py +669 -46
  24. {claude_code_log-1.4.0 → claude_code_log-1.5.0}/claude_code_log/html/user_formatters.py +154 -1
  25. {claude_code_log-1.4.0 → claude_code_log-1.5.0}/claude_code_log/html/utils.py +268 -25
  26. {claude_code_log-1.4.0 → claude_code_log-1.5.0}/claude_code_log/json/renderer.py +12 -3
  27. {claude_code_log-1.4.0 → claude_code_log-1.5.0}/claude_code_log/markdown/renderer.py +372 -25
  28. claude_code_log-1.5.0/claude_code_log/migrations/007_subagents_fingerprint.sql +16 -0
  29. {claude_code_log-1.4.0 → claude_code_log-1.5.0}/claude_code_log/models.py +157 -7
  30. {claude_code_log-1.4.0 → claude_code_log-1.5.0}/claude_code_log/plugins.py +47 -9
  31. {claude_code_log-1.4.0 → claude_code_log-1.5.0}/claude_code_log/renderer.py +1073 -275
  32. {claude_code_log-1.4.0 → claude_code_log-1.5.0}/claude_code_log/utils.py +92 -3
  33. claude_code_log-1.5.0/claude_code_log/workflow.py +661 -0
  34. {claude_code_log-1.4.0 → claude_code_log-1.5.0}/dev-docs/agents.md +182 -4
  35. {claude_code_log-1.4.0 → claude_code_log-1.5.0}/dev-docs/application_model.md +64 -20
  36. {claude_code_log-1.4.0 → claude_code_log-1.5.0}/dev-docs/css-classes.md +4 -1
  37. claude_code_log-1.5.0/dev-docs/dag.md +721 -0
  38. {claude_code_log-1.4.0 → claude_code_log-1.5.0}/dev-docs/implementing-a-tool-renderer.md +24 -0
  39. {claude_code_log-1.4.0 → claude_code_log-1.5.0}/dev-docs/message-hierarchy.md +40 -36
  40. claude_code_log-1.5.0/dev-docs/messages/tools/Artifact-tool_result.json +24 -0
  41. claude_code_log-1.5.0/dev-docs/messages/tools/Artifact-tool_result.jsonl +1 -0
  42. claude_code_log-1.5.0/dev-docs/messages/tools/Artifact-tool_use.json +27 -0
  43. claude_code_log-1.5.0/dev-docs/messages/tools/Artifact-tool_use.jsonl +1 -0
  44. {claude_code_log-1.4.0 → claude_code_log-1.5.0}/dev-docs/messages.md +38 -1
  45. {claude_code_log-1.4.0 → claude_code_log-1.5.0}/dev-docs/plugins.md +89 -12
  46. {claude_code_log-1.4.0 → claude_code_log-1.5.0}/dev-docs/rendering-architecture.md +17 -8
  47. claude_code_log-1.5.0/dev-docs/workflows.md +341 -0
  48. {claude_code_log-1.4.0 → claude_code_log-1.5.0}/justfile +6 -3
  49. claude_code_log-1.5.0/mise.toml +8 -0
  50. {claude_code_log-1.4.0 → claude_code_log-1.5.0}/mkdocs.yml +8 -1
  51. {claude_code_log-1.4.0 → claude_code_log-1.5.0}/pyproject.toml +19 -2
  52. {claude_code_log-1.4.0 → claude_code_log-1.5.0}/uv.lock +23 -23
  53. claude_code_log-1.5.0/work/agent-hierarchies-design.md +229 -0
  54. claude_code_log-1.5.0/work/parse-memory-spike.md +140 -0
  55. claude_code_log-1.5.0/work/tui-output-fixes.md +75 -0
  56. claude_code_log-1.5.0/work/win-io.md +79 -0
  57. claude_code_log-1.5.0/work/xdist-import-cost.md +133 -0
  58. claude_code_log-1.4.0/.github/workflows/ci.yml +0 -87
  59. claude_code_log-1.4.0/dev-docs/dag.md +0 -701
  60. claude_code_log-1.4.0/mise.toml +0 -2
  61. claude_code_log-1.4.0/work/refactor-reindex-with-ghosting.md +0 -197
  62. claude_code_log-1.4.0/work/simplify-converter-renderer.md +0 -312
  63. {claude_code_log-1.4.0 → claude_code_log-1.5.0}/.claude/settings.json +0 -0
  64. {claude_code_log-1.4.0 → claude_code_log-1.5.0}/.claude/skills/tool-renderer/SKILL.md +0 -0
  65. {claude_code_log-1.4.0 → claude_code_log-1.5.0}/.github/workflows/docs.yml +0 -0
  66. {claude_code_log-1.4.0 → claude_code_log-1.5.0}/.gitignore +0 -0
  67. {claude_code_log-1.4.0 → claude_code_log-1.5.0}/.vscode/settings.json +0 -0
  68. {claude_code_log-1.4.0 → claude_code_log-1.5.0}/CLAUDE.md +0 -0
  69. {claude_code_log-1.4.0 → claude_code_log-1.5.0}/CONTRIBUTING.md +0 -0
  70. {claude_code_log-1.4.0 → claude_code_log-1.5.0}/LICENSE +0 -0
  71. {claude_code_log-1.4.0 → claude_code_log-1.5.0}/README.md +0 -0
  72. {claude_code_log-1.4.0 → claude_code_log-1.5.0}/claude_code_log/__init__.py +0 -0
  73. {claude_code_log-1.4.0 → claude_code_log-1.5.0}/claude_code_log/factories/__init__.py +0 -0
  74. {claude_code_log-1.4.0 → claude_code_log-1.5.0}/claude_code_log/factories/agent_metadata_factory.py +0 -0
  75. {claude_code_log-1.4.0 → claude_code_log-1.5.0}/claude_code_log/factories/assistant_factory.py +0 -0
  76. {claude_code_log-1.4.0 → claude_code_log-1.5.0}/claude_code_log/factories/attachment_factory.py +0 -0
  77. {claude_code_log-1.4.0 → claude_code_log-1.5.0}/claude_code_log/factories/priorities.py +0 -0
  78. {claude_code_log-1.4.0 → claude_code_log-1.5.0}/claude_code_log/factories/task_notification_factory.py +0 -0
  79. {claude_code_log-1.4.0 → claude_code_log-1.5.0}/claude_code_log/factories/teammate_factory.py +0 -0
  80. {claude_code_log-1.4.0 → claude_code_log-1.5.0}/claude_code_log/factories/transcript_factory.py +0 -0
  81. {claude_code_log-1.4.0 → claude_code_log-1.5.0}/claude_code_log/factories/user_factory.py +0 -0
  82. {claude_code_log-1.4.0 → claude_code_log-1.5.0}/claude_code_log/git_remote.py +0 -0
  83. {claude_code_log-1.4.0 → claude_code_log-1.5.0}/claude_code_log/html/ansi_colors.py +0 -0
  84. {claude_code_log-1.4.0 → claude_code_log-1.5.0}/claude_code_log/html/assistant_formatters.py +0 -0
  85. {claude_code_log-1.4.0 → claude_code_log-1.5.0}/claude_code_log/html/renderer_code.py +0 -0
  86. {claude_code_log-1.4.0 → claude_code_log-1.5.0}/claude_code_log/html/teammate_formatter.py +0 -0
  87. {claude_code_log-1.4.0 → claude_code_log-1.5.0}/claude_code_log/html/templates/components/edit_diff_styles.css +0 -0
  88. {claude_code_log-1.4.0 → claude_code_log-1.5.0}/claude_code_log/html/templates/components/filter_styles.css +0 -0
  89. {claude_code_log-1.4.0 → claude_code_log-1.5.0}/claude_code_log/html/templates/components/page_nav_styles.css +0 -0
  90. {claude_code_log-1.4.0 → claude_code_log-1.5.0}/claude_code_log/html/templates/components/project_card_styles.css +0 -0
  91. {claude_code_log-1.4.0 → claude_code_log-1.5.0}/claude_code_log/html/templates/components/pygments_styles.css +0 -0
  92. {claude_code_log-1.4.0 → claude_code_log-1.5.0}/claude_code_log/html/templates/components/search.html +0 -0
  93. {claude_code_log-1.4.0 → claude_code_log-1.5.0}/claude_code_log/html/templates/components/search_inline.html +0 -0
  94. {claude_code_log-1.4.0 → claude_code_log-1.5.0}/claude_code_log/html/templates/components/search_inline_script.html +0 -0
  95. {claude_code_log-1.4.0 → claude_code_log-1.5.0}/claude_code_log/html/templates/components/search_results_panel.html +0 -0
  96. {claude_code_log-1.4.0 → claude_code_log-1.5.0}/claude_code_log/html/templates/components/search_styles.css +0 -0
  97. {claude_code_log-1.4.0 → claude_code_log-1.5.0}/claude_code_log/html/templates/components/teammate_styles.css +0 -0
  98. {claude_code_log-1.4.0 → claude_code_log-1.5.0}/claude_code_log/html/templates/components/timeline_styles.css +0 -0
  99. {claude_code_log-1.4.0 → claude_code_log-1.5.0}/claude_code_log/html/templates/components/timezone_converter.js +0 -0
  100. {claude_code_log-1.4.0 → claude_code_log-1.5.0}/claude_code_log/html/templates/components/todo_styles.css +0 -0
  101. {claude_code_log-1.4.0 → claude_code_log-1.5.0}/claude_code_log/html/templates/index.html +0 -0
  102. {claude_code_log-1.4.0 → claude_code_log-1.5.0}/claude_code_log/image_export.py +0 -0
  103. {claude_code_log-1.4.0 → claude_code_log-1.5.0}/claude_code_log/json/__init__.py +0 -0
  104. {claude_code_log-1.4.0 → claude_code_log-1.5.0}/claude_code_log/markdown/__init__.py +0 -0
  105. {claude_code_log-1.4.0 → claude_code_log-1.5.0}/claude_code_log/markdown_plugins.py +0 -0
  106. {claude_code_log-1.4.0 → claude_code_log-1.5.0}/claude_code_log/migrations/001_initial_schema.sql +0 -0
  107. {claude_code_log-1.4.0 → claude_code_log-1.5.0}/claude_code_log/migrations/002_html_cache.sql +0 -0
  108. {claude_code_log-1.4.0 → claude_code_log-1.5.0}/claude_code_log/migrations/003_html_pagination.sql +0 -0
  109. {claude_code_log-1.4.0 → claude_code_log-1.5.0}/claude_code_log/migrations/004_html_pagination_variant.sql +0 -0
  110. {claude_code_log-1.4.0 → claude_code_log-1.5.0}/claude_code_log/migrations/005_session_team_name.sql +0 -0
  111. {claude_code_log-1.4.0 → claude_code_log-1.5.0}/claude_code_log/migrations/006_session_ai_title.sql +0 -0
  112. {claude_code_log-1.4.0 → claude_code_log-1.5.0}/claude_code_log/migrations/__init__.py +0 -0
  113. {claude_code_log-1.4.0 → claude_code_log-1.5.0}/claude_code_log/migrations/runner.py +0 -0
  114. {claude_code_log-1.4.0 → claude_code_log-1.5.0}/claude_code_log/parser.py +0 -0
  115. {claude_code_log-1.4.0 → claude_code_log-1.5.0}/claude_code_log/py.typed +0 -0
  116. {claude_code_log-1.4.0 → claude_code_log-1.5.0}/claude_code_log/renderer_timings.py +0 -0
  117. {claude_code_log-1.4.0 → claude_code_log-1.5.0}/claude_code_log/tui.py +0 -0
  118. {claude_code_log-1.4.0 → claude_code_log-1.5.0}/dev-docs/messages/assistant/assistant.json +0 -0
  119. {claude_code_log-1.4.0 → claude_code_log-1.5.0}/dev-docs/messages/assistant/assistant.jsonl +0 -0
  120. {claude_code_log-1.4.0 → claude_code_log-1.5.0}/dev-docs/messages/assistant/assistant_sidechain.json +0 -0
  121. {claude_code_log-1.4.0 → claude_code_log-1.5.0}/dev-docs/messages/assistant/assistant_sidechain.jsonl +0 -0
  122. {claude_code_log-1.4.0 → claude_code_log-1.5.0}/dev-docs/messages/assistant/thinking.json +0 -0
  123. {claude_code_log-1.4.0 → claude_code_log-1.5.0}/dev-docs/messages/assistant/thinking.jsonl +0 -0
  124. {claude_code_log-1.4.0 → claude_code_log-1.5.0}/dev-docs/messages/system/file_history_snapshot.json +0 -0
  125. {claude_code_log-1.4.0 → claude_code_log-1.5.0}/dev-docs/messages/system/file_history_snapshot.jsonl +0 -0
  126. {claude_code_log-1.4.0 → claude_code_log-1.5.0}/dev-docs/messages/system/queue_operation.json +0 -0
  127. {claude_code_log-1.4.0 → claude_code_log-1.5.0}/dev-docs/messages/system/queue_operation.jsonl +0 -0
  128. {claude_code_log-1.4.0 → claude_code_log-1.5.0}/dev-docs/messages/system/summary.json +0 -0
  129. {claude_code_log-1.4.0 → claude_code_log-1.5.0}/dev-docs/messages/system/summary.jsonl +0 -0
  130. {claude_code_log-1.4.0 → claude_code_log-1.5.0}/dev-docs/messages/system/system_info.json +0 -0
  131. {claude_code_log-1.4.0 → claude_code_log-1.5.0}/dev-docs/messages/system/system_info.jsonl +0 -0
  132. {claude_code_log-1.4.0 → claude_code_log-1.5.0}/dev-docs/messages/tools/AskUserQuestion-tool_result.json +0 -0
  133. {claude_code_log-1.4.0 → claude_code_log-1.5.0}/dev-docs/messages/tools/AskUserQuestion-tool_result.jsonl +0 -0
  134. {claude_code_log-1.4.0 → claude_code_log-1.5.0}/dev-docs/messages/tools/AskUserQuestion-tool_result_error.json +0 -0
  135. {claude_code_log-1.4.0 → claude_code_log-1.5.0}/dev-docs/messages/tools/AskUserQuestion-tool_result_error.jsonl +0 -0
  136. {claude_code_log-1.4.0 → claude_code_log-1.5.0}/dev-docs/messages/tools/AskUserQuestion-tool_use.json +0 -0
  137. {claude_code_log-1.4.0 → claude_code_log-1.5.0}/dev-docs/messages/tools/AskUserQuestion-tool_use.jsonl +0 -0
  138. {claude_code_log-1.4.0 → claude_code_log-1.5.0}/dev-docs/messages/tools/Bash-tool_result.json +0 -0
  139. {claude_code_log-1.4.0 → claude_code_log-1.5.0}/dev-docs/messages/tools/Bash-tool_result.jsonl +0 -0
  140. {claude_code_log-1.4.0 → claude_code_log-1.5.0}/dev-docs/messages/tools/Bash-tool_result_error.json +0 -0
  141. {claude_code_log-1.4.0 → claude_code_log-1.5.0}/dev-docs/messages/tools/Bash-tool_result_error.jsonl +0 -0
  142. {claude_code_log-1.4.0 → claude_code_log-1.5.0}/dev-docs/messages/tools/Bash-tool_use.json +0 -0
  143. {claude_code_log-1.4.0 → claude_code_log-1.5.0}/dev-docs/messages/tools/Bash-tool_use.jsonl +0 -0
  144. {claude_code_log-1.4.0 → claude_code_log-1.5.0}/dev-docs/messages/tools/BashOutput-tool_result.json +0 -0
  145. {claude_code_log-1.4.0 → claude_code_log-1.5.0}/dev-docs/messages/tools/BashOutput-tool_result.jsonl +0 -0
  146. {claude_code_log-1.4.0 → claude_code_log-1.5.0}/dev-docs/messages/tools/BashOutput-tool_use.json +0 -0
  147. {claude_code_log-1.4.0 → claude_code_log-1.5.0}/dev-docs/messages/tools/BashOutput-tool_use.jsonl +0 -0
  148. {claude_code_log-1.4.0 → claude_code_log-1.5.0}/dev-docs/messages/tools/Edit-tool_result.json +0 -0
  149. {claude_code_log-1.4.0 → claude_code_log-1.5.0}/dev-docs/messages/tools/Edit-tool_result.jsonl +0 -0
  150. {claude_code_log-1.4.0 → claude_code_log-1.5.0}/dev-docs/messages/tools/Edit-tool_result_error.json +0 -0
  151. {claude_code_log-1.4.0 → claude_code_log-1.5.0}/dev-docs/messages/tools/Edit-tool_result_error.jsonl +0 -0
  152. {claude_code_log-1.4.0 → claude_code_log-1.5.0}/dev-docs/messages/tools/Edit-tool_use.json +0 -0
  153. {claude_code_log-1.4.0 → claude_code_log-1.5.0}/dev-docs/messages/tools/Edit-tool_use.jsonl +0 -0
  154. {claude_code_log-1.4.0 → claude_code_log-1.5.0}/dev-docs/messages/tools/ExitPlanMode-tool_result.json +0 -0
  155. {claude_code_log-1.4.0 → claude_code_log-1.5.0}/dev-docs/messages/tools/ExitPlanMode-tool_result.jsonl +0 -0
  156. {claude_code_log-1.4.0 → claude_code_log-1.5.0}/dev-docs/messages/tools/ExitPlanMode-tool_result_error.json +0 -0
  157. {claude_code_log-1.4.0 → claude_code_log-1.5.0}/dev-docs/messages/tools/ExitPlanMode-tool_result_error.jsonl +0 -0
  158. {claude_code_log-1.4.0 → claude_code_log-1.5.0}/dev-docs/messages/tools/ExitPlanMode-tool_use.json +0 -0
  159. {claude_code_log-1.4.0 → claude_code_log-1.5.0}/dev-docs/messages/tools/ExitPlanMode-tool_use.jsonl +0 -0
  160. {claude_code_log-1.4.0 → claude_code_log-1.5.0}/dev-docs/messages/tools/Glob-tool_result.json +0 -0
  161. {claude_code_log-1.4.0 → claude_code_log-1.5.0}/dev-docs/messages/tools/Glob-tool_result.jsonl +0 -0
  162. {claude_code_log-1.4.0 → claude_code_log-1.5.0}/dev-docs/messages/tools/Glob-tool_use.json +0 -0
  163. {claude_code_log-1.4.0 → claude_code_log-1.5.0}/dev-docs/messages/tools/Glob-tool_use.jsonl +0 -0
  164. {claude_code_log-1.4.0 → claude_code_log-1.5.0}/dev-docs/messages/tools/Grep-tool_result.json +0 -0
  165. {claude_code_log-1.4.0 → claude_code_log-1.5.0}/dev-docs/messages/tools/Grep-tool_result.jsonl +0 -0
  166. {claude_code_log-1.4.0 → claude_code_log-1.5.0}/dev-docs/messages/tools/Grep-tool_use.json +0 -0
  167. {claude_code_log-1.4.0 → claude_code_log-1.5.0}/dev-docs/messages/tools/Grep-tool_use.jsonl +0 -0
  168. {claude_code_log-1.4.0 → claude_code_log-1.5.0}/dev-docs/messages/tools/KillShell-tool_result.json +0 -0
  169. {claude_code_log-1.4.0 → claude_code_log-1.5.0}/dev-docs/messages/tools/KillShell-tool_result.jsonl +0 -0
  170. {claude_code_log-1.4.0 → claude_code_log-1.5.0}/dev-docs/messages/tools/KillShell-tool_result_error.json +0 -0
  171. {claude_code_log-1.4.0 → claude_code_log-1.5.0}/dev-docs/messages/tools/KillShell-tool_result_error.jsonl +0 -0
  172. {claude_code_log-1.4.0 → claude_code_log-1.5.0}/dev-docs/messages/tools/KillShell-tool_use.json +0 -0
  173. {claude_code_log-1.4.0 → claude_code_log-1.5.0}/dev-docs/messages/tools/KillShell-tool_use.jsonl +0 -0
  174. {claude_code_log-1.4.0 → claude_code_log-1.5.0}/dev-docs/messages/tools/LS-tool_result.json +0 -0
  175. {claude_code_log-1.4.0 → claude_code_log-1.5.0}/dev-docs/messages/tools/LS-tool_result.jsonl +0 -0
  176. {claude_code_log-1.4.0 → claude_code_log-1.5.0}/dev-docs/messages/tools/LS-tool_use.json +0 -0
  177. {claude_code_log-1.4.0 → claude_code_log-1.5.0}/dev-docs/messages/tools/LS-tool_use.jsonl +0 -0
  178. {claude_code_log-1.4.0 → claude_code_log-1.5.0}/dev-docs/messages/tools/MultiEdit-tool_result.json +0 -0
  179. {claude_code_log-1.4.0 → claude_code_log-1.5.0}/dev-docs/messages/tools/MultiEdit-tool_result.jsonl +0 -0
  180. {claude_code_log-1.4.0 → claude_code_log-1.5.0}/dev-docs/messages/tools/MultiEdit-tool_result_error.json +0 -0
  181. {claude_code_log-1.4.0 → claude_code_log-1.5.0}/dev-docs/messages/tools/MultiEdit-tool_result_error.jsonl +0 -0
  182. {claude_code_log-1.4.0 → claude_code_log-1.5.0}/dev-docs/messages/tools/MultiEdit-tool_use.json +0 -0
  183. {claude_code_log-1.4.0 → claude_code_log-1.5.0}/dev-docs/messages/tools/MultiEdit-tool_use.jsonl +0 -0
  184. {claude_code_log-1.4.0 → claude_code_log-1.5.0}/dev-docs/messages/tools/Read-tool_result.json +0 -0
  185. {claude_code_log-1.4.0 → claude_code_log-1.5.0}/dev-docs/messages/tools/Read-tool_result.jsonl +0 -0
  186. {claude_code_log-1.4.0 → claude_code_log-1.5.0}/dev-docs/messages/tools/Read-tool_result_error.json +0 -0
  187. {claude_code_log-1.4.0 → claude_code_log-1.5.0}/dev-docs/messages/tools/Read-tool_result_error.jsonl +0 -0
  188. {claude_code_log-1.4.0 → claude_code_log-1.5.0}/dev-docs/messages/tools/Read-tool_use.json +0 -0
  189. {claude_code_log-1.4.0 → claude_code_log-1.5.0}/dev-docs/messages/tools/Read-tool_use.jsonl +0 -0
  190. {claude_code_log-1.4.0 → claude_code_log-1.5.0}/dev-docs/messages/tools/Task-tool_result.json +0 -0
  191. {claude_code_log-1.4.0 → claude_code_log-1.5.0}/dev-docs/messages/tools/Task-tool_result.jsonl +0 -0
  192. {claude_code_log-1.4.0 → claude_code_log-1.5.0}/dev-docs/messages/tools/Task-tool_use.json +0 -0
  193. {claude_code_log-1.4.0 → claude_code_log-1.5.0}/dev-docs/messages/tools/Task-tool_use.jsonl +0 -0
  194. {claude_code_log-1.4.0 → claude_code_log-1.5.0}/dev-docs/messages/tools/TodoWrite-tool_result.json +0 -0
  195. {claude_code_log-1.4.0 → claude_code_log-1.5.0}/dev-docs/messages/tools/TodoWrite-tool_result.jsonl +0 -0
  196. {claude_code_log-1.4.0 → claude_code_log-1.5.0}/dev-docs/messages/tools/TodoWrite-tool_use.json +0 -0
  197. {claude_code_log-1.4.0 → claude_code_log-1.5.0}/dev-docs/messages/tools/TodoWrite-tool_use.jsonl +0 -0
  198. {claude_code_log-1.4.0 → claude_code_log-1.5.0}/dev-docs/messages/tools/WebFetch-tool_result.json +0 -0
  199. {claude_code_log-1.4.0 → claude_code_log-1.5.0}/dev-docs/messages/tools/WebFetch-tool_result.jsonl +0 -0
  200. {claude_code_log-1.4.0 → claude_code_log-1.5.0}/dev-docs/messages/tools/WebFetch-tool_use.json +0 -0
  201. {claude_code_log-1.4.0 → claude_code_log-1.5.0}/dev-docs/messages/tools/WebFetch-tool_use.jsonl +0 -0
  202. {claude_code_log-1.4.0 → claude_code_log-1.5.0}/dev-docs/messages/tools/WebSearch-tool_result.json +0 -0
  203. {claude_code_log-1.4.0 → claude_code_log-1.5.0}/dev-docs/messages/tools/WebSearch-tool_result.jsonl +0 -0
  204. {claude_code_log-1.4.0 → claude_code_log-1.5.0}/dev-docs/messages/tools/WebSearch-tool_use.json +0 -0
  205. {claude_code_log-1.4.0 → claude_code_log-1.5.0}/dev-docs/messages/tools/WebSearch-tool_use.jsonl +0 -0
  206. {claude_code_log-1.4.0 → claude_code_log-1.5.0}/dev-docs/messages/tools/Write-tool_result.json +0 -0
  207. {claude_code_log-1.4.0 → claude_code_log-1.5.0}/dev-docs/messages/tools/Write-tool_result.jsonl +0 -0
  208. {claude_code_log-1.4.0 → claude_code_log-1.5.0}/dev-docs/messages/tools/Write-tool_result_error.json +0 -0
  209. {claude_code_log-1.4.0 → claude_code_log-1.5.0}/dev-docs/messages/tools/Write-tool_result_error.jsonl +0 -0
  210. {claude_code_log-1.4.0 → claude_code_log-1.5.0}/dev-docs/messages/tools/Write-tool_use.json +0 -0
  211. {claude_code_log-1.4.0 → claude_code_log-1.5.0}/dev-docs/messages/tools/Write-tool_use.jsonl +0 -0
  212. {claude_code_log-1.4.0 → claude_code_log-1.5.0}/dev-docs/messages/tools/exit_plan_mode-tool_result.json +0 -0
  213. {claude_code_log-1.4.0 → claude_code_log-1.5.0}/dev-docs/messages/tools/exit_plan_mode-tool_result.jsonl +0 -0
  214. {claude_code_log-1.4.0 → claude_code_log-1.5.0}/dev-docs/messages/tools/exit_plan_mode-tool_use.json +0 -0
  215. {claude_code_log-1.4.0 → claude_code_log-1.5.0}/dev-docs/messages/tools/exit_plan_mode-tool_use.jsonl +0 -0
  216. {claude_code_log-1.4.0 → claude_code_log-1.5.0}/dev-docs/messages/user/bash_input.json +0 -0
  217. {claude_code_log-1.4.0 → claude_code_log-1.5.0}/dev-docs/messages/user/bash_input.jsonl +0 -0
  218. {claude_code_log-1.4.0 → claude_code_log-1.5.0}/dev-docs/messages/user/bash_output.json +0 -0
  219. {claude_code_log-1.4.0 → claude_code_log-1.5.0}/dev-docs/messages/user/bash_output.jsonl +0 -0
  220. {claude_code_log-1.4.0 → claude_code_log-1.5.0}/dev-docs/messages/user/command_output.json +0 -0
  221. {claude_code_log-1.4.0 → claude_code_log-1.5.0}/dev-docs/messages/user/command_output.jsonl +0 -0
  222. {claude_code_log-1.4.0 → claude_code_log-1.5.0}/dev-docs/messages/user/image.json +0 -0
  223. {claude_code_log-1.4.0 → claude_code_log-1.5.0}/dev-docs/messages/user/image.jsonl +0 -0
  224. {claude_code_log-1.4.0 → claude_code_log-1.5.0}/dev-docs/messages/user/user.json +0 -0
  225. {claude_code_log-1.4.0 → claude_code_log-1.5.0}/dev-docs/messages/user/user.jsonl +0 -0
  226. {claude_code_log-1.4.0 → claude_code_log-1.5.0}/dev-docs/messages/user/user_command.json +0 -0
  227. {claude_code_log-1.4.0 → claude_code_log-1.5.0}/dev-docs/messages/user/user_command.jsonl +0 -0
  228. {claude_code_log-1.4.0 → claude_code_log-1.5.0}/dev-docs/messages/user/user_sidechain.json +0 -0
  229. {claude_code_log-1.4.0 → claude_code_log-1.5.0}/dev-docs/messages/user/user_sidechain.jsonl +0 -0
  230. {claude_code_log-1.4.0 → claude_code_log-1.5.0}/dev-docs/messages/user/user_slash_command.json +0 -0
  231. {claude_code_log-1.4.0 → claude_code_log-1.5.0}/dev-docs/messages/user/user_slash_command.jsonl +0 -0
  232. {claude_code_log-1.4.0 → claude_code_log-1.5.0}/dev-docs/teammates.md +0 -0
  233. {claude_code_log-1.4.0 → claude_code_log-1.5.0}/stubs/pygments/__init__.pyi +0 -0
  234. {claude_code_log-1.4.0 → claude_code_log-1.5.0}/stubs/pygments/formatter.pyi +0 -0
  235. {claude_code_log-1.4.0 → claude_code_log-1.5.0}/stubs/pygments/formatters/__init__.pyi +0 -0
  236. {claude_code_log-1.4.0 → claude_code_log-1.5.0}/stubs/pygments/lexer.pyi +0 -0
  237. {claude_code_log-1.4.0 → claude_code_log-1.5.0}/stubs/pygments/lexers/__init__.pyi +0 -0
  238. {claude_code_log-1.4.0 → claude_code_log-1.5.0}/stubs/pygments/util.pyi +0 -0
  239. {claude_code_log-1.4.0 → claude_code_log-1.5.0}/work/obsidian-friendly-output.md +0 -0
  240. {claude_code_log-1.4.0 → claude_code_log-1.5.0}/work/rendering-next.md +0 -0
  241. {claude_code_log-1.4.0 → claude_code_log-1.5.0}/work/session-state-propagation.md +0 -0
  242. {claude_code_log-1.4.0 → claude_code_log-1.5.0}/work/tool-renderer-plugins.md +0 -0
@@ -0,0 +1,117 @@
1
+ name: CI
2
+
3
+ on:
4
+ push:
5
+ branches: [ main ]
6
+ pull_request:
7
+ branches: [ main ]
8
+
9
+ jobs:
10
+ test:
11
+ runs-on: ${{ matrix.os }}
12
+ strategy:
13
+ matrix:
14
+ os: ["ubuntu-latest", "windows-latest"]
15
+ python-version: ["3.10", "3.11", "3.12", "3.13", "3.14"]
16
+ include:
17
+ - os: "ubuntu-latest"
18
+ python-version: "3.12"
19
+ is-primary: true
20
+
21
+ env:
22
+ # Collect coverage only on the primary job: it is the only one that
23
+ # uploads the reports and feeds the PR comment, while --cov costs
24
+ # ~65% extra pytest wall time (worst on Windows). Windows-only
25
+ # covered lines measured at 5 (diffing covered lines per file,
26
+ # Windows vs the primary artifact): the SIGUSR1 no-op return and the
27
+ # drive-letter branch of convert_project_path_to_claude_dir, both in
28
+ # cli.py. Those were never in the published report (non-primary
29
+ # coverage was computed and discarded), so this changes nothing.
30
+ COV: ${{ matrix.is-primary && '--cov=claude_code_log --cov-report=xml --cov-report=html --cov-report=term' || '' }}
31
+ COV_APPEND: ${{ matrix.is-primary && '--cov=claude_code_log --cov-append --cov-report=xml --cov-report=html --cov-report=term' || '' }}
32
+
33
+ steps:
34
+ - uses: actions/checkout@v4
35
+ with:
36
+ # Full history so test/test_commit_linkifier.py's
37
+ # TestIntegrationLocalRepo can resolve SHAs older than the
38
+ # current tip (`git branch -r --contains` reads local refs
39
+ # only and needs the commits in the object DB).
40
+ fetch-depth: 0
41
+
42
+ - name: Move TEMP to the workspace disk (Windows)
43
+ # On windows runners TMP/TEMP default to the C: OS disk, whose
44
+ # throttled I/O dominates this tmp-heavy suite; RUNNER_TEMP lives on
45
+ # the same disk as the workspace (D:). Measured via A/B experiment
46
+ # (run 28715118956): this redirect alone took the unit selection from
47
+ # 555s mean to 150s — parity with ubuntu (~161s). Defender is NOT the
48
+ # culprit: real-time protection is already off on current images
49
+ # (exclusions/disable measured as no-ops), and a ReFS dev drive
50
+ # (~296s) was dominated by this redirect. Details: work/win-io.md
51
+ if: runner.os == 'Windows'
52
+ shell: pwsh
53
+ run: |
54
+ "TMP=$env:RUNNER_TEMP" >> $env:GITHUB_ENV
55
+ "TEMP=$env:RUNNER_TEMP" >> $env:GITHUB_ENV
56
+
57
+ - name: Install uv
58
+ uses: astral-sh/setup-uv@v4
59
+ with:
60
+ enable-cache: true
61
+
62
+ - name: Set up Python ${{ matrix.python-version }}
63
+ run: uv python install ${{ matrix.python-version }}
64
+
65
+ - name: Install dependencies
66
+ run: uv sync --all-extras --dev && uv run playwright install chromium
67
+
68
+ - name: Run unit tests (coverage on primary only)
69
+ # -p no:playwright: unit tests don't use the browser fixtures; skipping the
70
+ # plugin avoids a ~1s playwright import per xdist worker, which Windows pays
71
+ # per spawned worker. See work/xdist-import-cost.md.
72
+ run: uv run pytest -p no:playwright -m "not (tui or browser or benchmark)" ${{ env.COV }}
73
+
74
+ - name: Run TUI tests (coverage append on primary only)
75
+ run: uv run pytest -m tui ${{ env.COV_APPEND }}
76
+
77
+ - name: Run browser tests (coverage append on primary only)
78
+ run: uv run pytest -m browser ${{ env.COV_APPEND }}
79
+
80
+ - name: Run benchmark tests with coverage append (primary only)
81
+ if: matrix.is-primary
82
+ env:
83
+ CLAUDE_CODE_LOG_DEBUG_TIMING: "1"
84
+ run: uv run pytest -m benchmark ${{ env.COV_APPEND }} -v
85
+
86
+ - name: Upload coverage HTML report as artifact
87
+ uses: actions/upload-artifact@v4
88
+ if: matrix.is-primary
89
+ with:
90
+ name: coverage-report-html
91
+ path: htmlcov/
92
+ retention-days: 30
93
+
94
+ - name: Upload coverage XML as artifact
95
+ uses: actions/upload-artifact@v4
96
+ if: matrix.is-primary
97
+ with:
98
+ name: coverage-report-xml
99
+ path: coverage.xml
100
+ retention-days: 30
101
+
102
+ - name: Coverage comment (PR only)
103
+ if: github.event_name == 'pull_request' && matrix.is-primary
104
+ uses: py-cov-action/python-coverage-comment-action@v3
105
+ with:
106
+ GITHUB_TOKEN: ${{ github.token }}
107
+ MINIMUM_GREEN: 75
108
+ MINIMUM_ORANGE: 60
109
+
110
+ - name: Run linting
111
+ run: uv run ruff check
112
+
113
+ - name: Run formatting check
114
+ run: uv run ruff format --check
115
+
116
+ - name: Run type checking
117
+ run: uv run pyright
@@ -6,6 +6,66 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
6
6
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
7
 
8
8
 
9
+ ## [1.5.0] - 2026-07-09
10
+
11
+ ### Changed
12
+
13
+ - **Filter <local-command-caveat>-wrapped caveat messages (Claude Code ≥ ~2.1) (#269)**
14
+ - **Fix perpetual re-regeneration of projects with up-to-date caches (#268)**
15
+ - **Fix ty possibly-missing-attribute warnings in tests (#267)**
16
+ - **Speed up cache freshness checks on large archives (#266)**
17
+ - **ci: move TMP/TEMP to the workspace disk on Windows runners**
18
+ - **perf(cov): use sys.monitoring coverage core (COVERAGE_CORE=sysmon equivalent)**
19
+ - **ci: collect coverage only on the primary matrix job**
20
+ - **Improve Artifact tool look&feel: label-linked result, lean tool use (#262) (#263)**
21
+ - **Remap dangling parent refs when dedup drops a distinct DAG node (#259) (#261)**
22
+ - **Render the Artifact tool (deploy HTML/MD page → claude.ai URL) (#257) (#260)**
23
+ - **Handle the full variety of Workflow invocation shapes (#174 follow-up) (#258)**
24
+ - **Regenerate a --no-cache directory when a source .jsonl grows (#254) (#256)**
25
+ - **Regenerate single-file output when its source grows; fix success-message accuracy (#221 follow-up) (#253)**
26
+ - **Surface the model each agent ran on, once per agent context (#246) (#252)**
27
+ - **feat: use nicer glyphs for fold/unfold controls (#247) (#255)**
28
+ - **Escape raw HTML in assistant/tool content to fix XSS (#245)**
29
+ - **Warn that --output / --format are ignored under --tui (#220) (#241)**
30
+ - **Speed up the SQLite metadata-cache build (connection reuse + synchronous=NORMAL) (#251)**
31
+ - **Cut pytest-xdist per-worker import cost on Windows unit runs (#250)**
32
+ - **Speed up integration-test fixtures on Windows (shrink copied transcripts; skip symlink tests when unavailable) (#249)**
33
+ - **Stream output to stdout: support -o - / /dev/stdout (#223 part 2) (#239)**
34
+ - **Keep fork points visible when their host message is detail-filtered (#233 follow-up) (#240)**
35
+ - **Explicit --output: always regenerate, infer format from suffix, fix stdout hang (#221, #222, #223 part 1) (#237)**
36
+ - **Connect fork-point and branch headers to their anchor message (#238)**
37
+ - **Anchor fork points whose node is an unrendered system message (#236)**
38
+ - **Render Read/Write of any Markdown file as Markdown, not Pygments (#232) (#234)**
39
+ - **Generic tool results: render structured content items (#227) (#235)**
40
+ - **Nested agent hierarchies: visual layer (#213, part 2 — WIP) (#219)**
41
+ - **Render nested sub-agent hierarchies (#213) (#218)**
42
+ - **Build(deps): bump pygments from 2.19.2 to 2.20.0 (#228)**
43
+ - **Build(deps): bump requests from 2.32.5 to 2.33.0 (#229)**
44
+ - **Build(deps-dev): bump pytest from 9.0.1 to 9.0.3 (#230)**
45
+ - **Build(deps): bump idna from 3.11 to 3.15 (#231)**
46
+ - **Side-channel prompt rendering + params-fold UX and accessibility (#174 follow-up) (#217)**
47
+ - **Polish the workflow run tree + dev-docs sync (#174 follow-up) (#215)**
48
+ - **Hybrid JSON/Markdown rendering for tool params tables (#216)**
49
+ - **Linearize assistant-continuation tool-flow forks instead of branching (#214)**
50
+ - **Enrich fork-point and branch nav labels with message summaries (#179 follow-up) (#212)**
51
+ - **Render dynamic-workflow run tree on the nested DOM (#174 PR3) (#210)**
52
+ - **Keep recaps visible at all detail levels; add --no-recaps (#179) (#209)**
53
+ - **Dedupe "unrecognized message type" warnings per file (#211)**
54
+ - **work/: delete the completed converter/renderer simplification plans (#208)**
55
+ - **Flag auto-memory interactions: 🧠 titles + memory filter (#192) (#204)**
56
+ - **Phase 3: single-axis collapse — delete pre-render _filter_by_detail (#206)**
57
+ - **Render Workflow tool-input (JS) + async-result JSON bodies (#174 PR2) (#205)**
58
+ - **Phase 2 of ghosting epic: detail-filter ghosts in place; drop _reindex_filtered_context (#194)**
59
+ - **Parse dynamic-Workflow runs into WorkflowRun models (#174 PR1) (#203)**
60
+ - **Bump mistune from 3.1.4 to 3.2.1 (#202)**
61
+ - **Bump gitpython from 3.1.45 to 3.1.50 (#200)**
62
+ - **Exclude docs/ from ty type-checking (#201)**
63
+ - **Render message tree as nested DOM (PR0 for #174) (#191)**
64
+ - **Bump urllib3 from 2.5.0 to 2.7.0 (#199)**
65
+ - **Ghost-aware ctx.messages; skill-fold uses None slots (ghosting epic Phase 1) (#193)**
66
+ - **Merge pull request #198 from daaain/fix-linux-release**
67
+
68
+
9
69
  ## [1.4.0] - 2026-06-03
10
70
 
11
71
  ### Changed
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: claude-code-log
3
- Version: 1.4.0
3
+ Version: 1.5.0
4
4
  Summary: Convert Claude Code transcript JSONL files to HTML
5
5
  Project-URL: Homepage, https://github.com/daaain/claude-code-log
6
6
  Project-URL: Issues, https://github.com/daaain/claude-code-log/issues
@@ -10,7 +10,7 @@ import zlib
10
10
  from contextlib import contextmanager
11
11
  from datetime import datetime, timezone
12
12
  from pathlib import Path
13
- from typing import Any, Dict, Generator, List, Optional
13
+ from typing import Any, Callable, Dict, Generator, List, Optional
14
14
 
15
15
  from packaging import version
16
16
  from pydantic import BaseModel
@@ -39,7 +39,6 @@ class CachedFileInfo(BaseModel):
39
39
  source_mtime: float
40
40
  cached_mtime: float
41
41
  message_count: int
42
- session_ids: list[str]
43
42
 
44
43
 
45
44
  class SessionCacheData(BaseModel):
@@ -220,6 +219,72 @@ def get_library_version() -> str:
220
219
  # ========== Cache Path Configuration ==========
221
220
 
222
221
 
222
+ def subagents_fingerprint(jsonl_path: Path) -> str:
223
+ """Fingerprint of the sidecar inputs that feed spawn discovery (#213).
224
+
225
+ Parsing a transcript also reads ``agent-*.meta.json`` sidecars (see
226
+ ``converter._subagent_meta_map``), so they must take part in cache
227
+ invalidation: a sidecar appearing after the transcript was cached
228
+ (e.g. a still-running agent spawning a child without touching the
229
+ trunk file) would otherwise go unnoticed by the mtime-only check.
230
+
231
+ The fingerprint is ``"<count>:<newest int mtime>"`` over the sidecars
232
+ of the dirs where this transcript's children can live — the sibling
233
+ ``<stem>/subagents/`` dir, plus the containing dir for agent files
234
+ (the flat layout puts children next to their parent). Sidecars are
235
+ written once at spawn time, so count+newest-mtime captures every
236
+ addition and removal. Empty string when there are none.
237
+
238
+ Deliberately narrower than ``converter._subagent_meta_map``'s scan
239
+ for TRUNK files: the parse also defensively checks the project dir
240
+ itself, but Claude Code never writes sidecars there, and
241
+ fingerprinting it would rescan a potentially large dir on every
242
+ cache read for a theoretical-only input.
243
+ """
244
+ candidates = [jsonl_path.parent / jsonl_path.stem / "subagents"]
245
+ if jsonl_path.parent.name == "subagents":
246
+ candidates.append(jsonl_path.parent)
247
+ metas: list[Path] = []
248
+ for directory in candidates:
249
+ if directory.is_dir():
250
+ metas.extend(directory.glob("agent-*.meta.json"))
251
+ if not metas:
252
+ return ""
253
+ try:
254
+ newest = max(int(p.stat().st_mtime) for p in metas)
255
+ except OSError:
256
+ # A sidecar vanished mid-scan; treat as unstable so the next
257
+ # check re-reads (an always-mismatching fingerprint is safe).
258
+ return f"{len(metas)}:unstable"
259
+ return f"{len(metas)}:{newest}"
260
+
261
+
262
+ def _cache_row_is_fresh(
263
+ row: sqlite3.Row, source_mtime: float, current_fp: Callable[[], str]
264
+ ) -> bool:
265
+ """Decide whether a cached_files row is still fresh.
266
+
267
+ Single source of truth for the freshness rule shared by
268
+ is_file_cached() and get_modified_files(). current_fp is a callable
269
+ so callers only pay for the sidecar fingerprint scan when the mtime
270
+ check passes (and get_modified_files() can plug in its
271
+ scandir-optimized variant).
272
+
273
+ Cache is valid if modification times match (within 1 second
274
+ tolerance) and the sidecar inputs of spawn discovery (#213) match
275
+ too — new agent-*.meta.json files appear without touching the
276
+ source jsonl. Pre-007 rows carry NULL: accept those only when the
277
+ file has no sidecars today (nothing to miss), so legacy caches
278
+ don't mass-invalidate while sessions WITH sidecars reparse once.
279
+ """
280
+ if abs(source_mtime - row["source_mtime"]) >= 1.0:
281
+ return False
282
+ cached_fp = row["subagents_fingerprint"]
283
+ if cached_fp is None:
284
+ return current_fp() == ""
285
+ return cached_fp == current_fp()
286
+
287
+
223
288
  def get_cache_db_path(projects_dir: Path) -> Path:
224
289
  """Get cache database path, respecting CLAUDE_CODE_LOG_CACHE_PATH env var.
225
290
 
@@ -240,6 +305,16 @@ def get_cache_db_path(projects_dir: Path) -> Path:
240
305
  # ========== Cache Manager ==========
241
306
 
242
307
 
308
+ # Process-level memo of cache DBs whose migrations have already been
309
+ # checked. CacheManager is constructed per project (the TUI project
310
+ # selector builds one per table row, on every resize), so without the
311
+ # memo every instance re-pays the migration check: a connection plus
312
+ # version-table and migration-directory reads, ~2ms each (issue #12).
313
+ # The exists() guard in _init_database covers --clear-cache deleting
314
+ # the DB file mid-process.
315
+ _migrated_db_paths: set[str] = set()
316
+
317
+
243
318
  class CacheManager:
244
319
  """SQLite-based cache manager for Claude Code Log."""
245
320
 
@@ -266,27 +341,109 @@ class CacheManager:
266
341
  else:
267
342
  self.db_path = get_cache_db_path(project_path.parent)
268
343
 
344
+ # When inside a batch() scope this holds the single shared connection
345
+ # reused by every _get_connection() call; None means the default
346
+ # open-a-connection-per-call behaviour. Set before _init_database() so
347
+ # the field always exists before any _get_connection() could run, even
348
+ # if migrations are ever routed through it.
349
+ self._shared_conn: Optional[sqlite3.Connection] = None
350
+
269
351
  # Initialise database and ensure project exists
270
352
  self._init_database()
271
353
  self._project_id: Optional[int] = None
272
354
  self._ensure_project_exists()
273
355
 
274
- @contextmanager
275
- def _get_connection(self) -> Generator[sqlite3.Connection, None, None]:
276
- """Get a database connection with proper settings."""
277
- conn = sqlite3.connect(self.db_path, timeout=30.0)
356
+ def _configure_connection(self, conn: sqlite3.Connection) -> None:
357
+ """Apply the standard pragmas/row factory to a fresh connection."""
278
358
  conn.row_factory = sqlite3.Row
279
359
  conn.execute("PRAGMA foreign_keys = ON")
280
360
  conn.execute("PRAGMA journal_mode = WAL")
361
+ # synchronous=NORMAL is the recommended pairing for WAL: it keeps
362
+ # durability across application crashes (only a power/OS crash can lose
363
+ # the last committed transaction) while skipping an fsync on every
364
+ # commit. The cache is fully regenerable from the JSONL source, so that
365
+ # residual risk is acceptable.
366
+ conn.execute("PRAGMA synchronous = NORMAL")
367
+
368
+ def _open_configured_connection(self) -> sqlite3.Connection:
369
+ """Open a connection and apply pragmas, closing it if setup fails.
370
+
371
+ If a PRAGMA in ``_configure_connection`` raises, the just-opened
372
+ handle is closed before re-raising so it can't leak and lock the
373
+ .db/.db-wal/.db-shm files — the exact failure mode the connection
374
+ lifecycle elsewhere is careful to avoid (Windows WinError 32).
375
+ """
376
+ conn = sqlite3.connect(self.db_path, timeout=30.0)
377
+ try:
378
+ self._configure_connection(conn)
379
+ except BaseException:
380
+ conn.close()
381
+ raise
382
+ return conn
383
+
384
+ @contextmanager
385
+ def _get_connection(self) -> Generator[sqlite3.Connection, None, None]:
386
+ """Get a database connection with proper settings.
387
+
388
+ Inside a ``batch()`` scope this yields the shared connection without
389
+ closing it (the batch owns its lifecycle). Otherwise it opens a fresh
390
+ connection and closes it on exit — the default, Windows-safe behaviour
391
+ (no lingering file handle on the .db/.db-wal/.db-shm files).
392
+ """
393
+ if self._shared_conn is not None:
394
+ yield self._shared_conn
395
+ return
396
+
397
+ conn = self._open_configured_connection()
281
398
  try:
282
399
  yield conn
283
400
  finally:
284
401
  conn.close()
285
402
 
403
+ @contextmanager
404
+ def batch(self) -> Generator[None, None, None]:
405
+ """Reuse a single connection for every operation within the scope.
406
+
407
+ A full project build issues ~190 ``_get_connection`` calls; opening a
408
+ fresh SQLite connection each time dominates cache-build cost. Wrapping
409
+ the build in ``with cache_manager.batch():`` collapses those to one
410
+ connection.
411
+
412
+ Lifecycle guarantees (the risky part — see the integrity tests):
413
+ - The shared connection is **always closed on scope exit**, including
414
+ when the body raises (the ``finally``). This releases the file lock
415
+ on the .db/.db-wal/.db-shm files *before* any caller tears down a
416
+ TemporaryDirectory or runs ``clear_cache``/rmtree — critical on
417
+ Windows, which refuses to delete open files (WinError 32).
418
+ - Outside a batch, behaviour is unchanged (connection-per-call).
419
+ - Nesting is a no-op: an inner ``batch()`` reuses the outer connection
420
+ and does NOT close it, so the converter loop can't double-open or
421
+ close a connection still in use by an enclosing scope.
422
+ """
423
+ if self._shared_conn is not None:
424
+ # Already batching — reuse the existing shared connection and leave
425
+ # its lifecycle to the outermost batch().
426
+ yield
427
+ return
428
+
429
+ # Open+configure first; only publish to _shared_conn once setup has
430
+ # succeeded, so a failed PRAGMA never leaves a half-initialised (and
431
+ # now-closed) handle assigned for other calls to reuse.
432
+ conn = self._open_configured_connection()
433
+ self._shared_conn = conn
434
+ try:
435
+ yield
436
+ finally:
437
+ self._shared_conn = None
438
+ conn.close()
439
+
286
440
  def _init_database(self) -> None:
287
441
  """Create schema if needed using migration runner."""
288
- # Run any pending migrations
442
+ key = str(self.db_path)
443
+ if key in _migrated_db_paths and self.db_path.exists():
444
+ return
289
445
  run_migrations(self.db_path)
446
+ _migrated_db_paths.add(key)
290
447
 
291
448
  def _ensure_project_exists(self) -> None:
292
449
  """Ensure project record exists and get its ID."""
@@ -471,18 +628,19 @@ class CacheManager:
471
628
 
472
629
  with self._get_connection() as conn:
473
630
  row = conn.execute(
474
- "SELECT source_mtime FROM cached_files WHERE project_id = ? AND file_name = ?",
631
+ "SELECT source_mtime, subagents_fingerprint FROM cached_files"
632
+ " WHERE project_id = ? AND file_name = ?",
475
633
  (self._project_id, jsonl_path.name),
476
634
  ).fetchone()
477
635
 
478
636
  if not row:
479
637
  return False
480
638
 
481
- source_mtime = jsonl_path.stat().st_mtime
482
- cached_mtime = row["source_mtime"]
483
-
484
- # Cache is valid if modification times match (within 1 second tolerance)
485
- return abs(source_mtime - cached_mtime) < 1.0
639
+ return _cache_row_is_fresh(
640
+ row,
641
+ jsonl_path.stat().st_mtime,
642
+ lambda: subagents_fingerprint(jsonl_path),
643
+ )
486
644
 
487
645
  def load_cached_entries(self, jsonl_path: Path) -> Optional[List[TranscriptEntry]]:
488
646
  """Load cached transcript entries for a JSONL file."""
@@ -564,14 +722,27 @@ class CacheManager:
564
722
  return [self._deserialize_entry(row) for row in rows]
565
723
 
566
724
  def save_cached_entries(
567
- self, jsonl_path: Path, entries: List[TranscriptEntry]
725
+ self,
726
+ jsonl_path: Path,
727
+ entries: List[TranscriptEntry],
728
+ subagents_fp: Optional[str] = None,
568
729
  ) -> None:
569
- """Save parsed transcript entries to cache."""
730
+ """Save parsed transcript entries to cache.
731
+
732
+ ``subagents_fp`` is the sidecar fingerprint AS OF THE PARSE —
733
+ callers that scanned sidecars should pass the value captured
734
+ before the scan, so a sidecar landing mid-parse mismatches on the
735
+ next read and forces a reparse (over-invalidation; computing it
736
+ here at save time would instead validate a parse that never saw
737
+ the late sidecar). Falls back to computing now when omitted.
738
+ """
570
739
  if self._project_id is None:
571
740
  return
572
741
 
573
742
  source_mtime = jsonl_path.stat().st_mtime
574
743
  cached_mtime = datetime.now().timestamp()
744
+ if subagents_fp is None:
745
+ subagents_fp = subagents_fingerprint(jsonl_path)
575
746
 
576
747
  with self._get_connection() as conn:
577
748
  # Insert or update file record
@@ -579,13 +750,15 @@ class CacheManager:
579
750
  conn.execute(
580
751
  """
581
752
  INSERT INTO cached_files
582
- (project_id, file_name, file_path, source_mtime, cached_mtime, message_count)
583
- VALUES (?, ?, ?, ?, ?, ?)
753
+ (project_id, file_name, file_path, source_mtime, cached_mtime,
754
+ message_count, subagents_fingerprint)
755
+ VALUES (?, ?, ?, ?, ?, ?, ?)
584
756
  ON CONFLICT(project_id, file_name) DO UPDATE SET
585
757
  file_path = excluded.file_path,
586
758
  source_mtime = excluded.source_mtime,
587
759
  cached_mtime = excluded.cached_mtime,
588
- message_count = excluded.message_count
760
+ message_count = excluded.message_count,
761
+ subagents_fingerprint = excluded.subagents_fingerprint
589
762
  """,
590
763
  (
591
764
  self._project_id,
@@ -594,6 +767,7 @@ class CacheManager:
594
767
  source_mtime,
595
768
  cached_mtime,
596
769
  len(entries),
770
+ subagents_fp,
597
771
  ),
598
772
  )
599
773
 
@@ -738,21 +912,82 @@ class CacheManager:
738
912
  if self._project_id is None:
739
913
  return []
740
914
 
915
+ with self._get_connection() as conn:
916
+ return self._get_working_directories(conn)
917
+
918
+ def _get_working_directories(self, conn: sqlite3.Connection) -> List[str]:
919
+ """get_working_directories() on an already-open connection."""
920
+ rows = conn.execute(
921
+ "SELECT DISTINCT cwd FROM sessions WHERE project_id = ? AND cwd IS NOT NULL",
922
+ (self._project_id,),
923
+ ).fetchall()
924
+ return [row["cwd"] for row in rows]
925
+
926
+ def get_modified_files(self, jsonl_files: List[Path]) -> List[Path]:
927
+ """Get list of JSONL files that need to be reprocessed.
928
+
929
+ Batched equivalent of calling is_file_cached() per file: one SQL
930
+ query fetches every cached row (one connection open instead of
931
+ one per file), and one scandir per parent directory rules out
932
+ subagent-sidecar fingerprints, replacing the per-file query +
933
+ directory probes that dominated freshness-check time on large
934
+ archives (issue #12).
935
+ """
936
+ if not jsonl_files:
937
+ return []
938
+ if self._project_id is None:
939
+ return list(jsonl_files)
940
+
741
941
  with self._get_connection() as conn:
742
942
  rows = conn.execute(
743
- "SELECT DISTINCT cwd FROM sessions WHERE project_id = ? AND cwd IS NOT NULL",
943
+ "SELECT file_name, source_mtime, subagents_fingerprint"
944
+ " FROM cached_files WHERE project_id = ?",
744
945
  (self._project_id,),
745
946
  ).fetchall()
947
+ cached = {row["file_name"]: row for row in rows}
948
+
949
+ # A session file's sidecars live under <parent>/<stem>/subagents,
950
+ # so one scandir of the parent tells us which files can have a
951
+ # non-empty fingerprint without any per-file stat probes.
952
+ subdir_names: Dict[Path, set[str]] = {}
953
+
954
+ def current_fp(jsonl_file: Path) -> str:
955
+ parent = jsonl_file.parent
956
+ if parent.name == "subagents":
957
+ # Agent file in a subagents dir: its sidecars sit next to
958
+ # it, so the full fingerprint scan is required.
959
+ return subagents_fingerprint(jsonl_file)
960
+ names = subdir_names.get(parent)
961
+ if names is None:
962
+ try:
963
+ with os.scandir(parent) as entries:
964
+ names = {e.name for e in entries if e.is_dir()}
965
+ except OSError:
966
+ names = set[str]()
967
+ subdir_names[parent] = names
968
+ return subagents_fingerprint(jsonl_file) if jsonl_file.stem in names else ""
969
+
970
+ modified: List[Path] = []
971
+ for jsonl_file in jsonl_files:
972
+ row = cached.get(jsonl_file.name)
973
+ if row is None:
974
+ modified.append(jsonl_file)
975
+ continue
976
+
977
+ try:
978
+ source_mtime = jsonl_file.stat().st_mtime
979
+ except OSError:
980
+ # Missing file: same outcome as is_file_cached()'s
981
+ # exists() check returning False.
982
+ modified.append(jsonl_file)
983
+ continue
984
+
985
+ if not _cache_row_is_fresh(
986
+ row, source_mtime, lambda file=jsonl_file: current_fp(file)
987
+ ):
988
+ modified.append(jsonl_file)
746
989
 
747
- return [row["cwd"] for row in rows]
748
-
749
- def get_modified_files(self, jsonl_files: List[Path]) -> List[Path]:
750
- """Get list of JSONL files that need to be reprocessed."""
751
- return [
752
- jsonl_file
753
- for jsonl_file in jsonl_files
754
- if not self.is_file_cached(jsonl_file)
755
- ]
990
+ return modified
756
991
 
757
992
  def get_cached_project_data(self) -> Optional[ProjectCache]:
758
993
  """Get the cached project data if available."""
@@ -775,19 +1010,11 @@ class CacheManager:
775
1010
 
776
1011
  cached_files: Dict[str, CachedFileInfo] = {}
777
1012
  for row in file_rows:
778
- # Get session IDs for this file from messages
779
- session_rows = conn.execute(
780
- "SELECT DISTINCT session_id FROM messages WHERE file_id = ? AND session_id IS NOT NULL",
781
- (row["id"],),
782
- ).fetchall()
783
- session_ids = [r["session_id"] for r in session_rows]
784
-
785
1013
  cached_files[row["file_name"]] = CachedFileInfo(
786
1014
  file_path=row["file_path"],
787
1015
  source_mtime=row["source_mtime"],
788
1016
  cached_mtime=row["cached_mtime"],
789
1017
  message_count=row["message_count"],
790
- session_ids=session_ids,
791
1018
  )
792
1019
 
793
1020
  # Get sessions
@@ -813,6 +1040,10 @@ class CacheManager:
813
1040
  team_name=row["team_name"] if "team_name" in row.keys() else None,
814
1041
  )
815
1042
 
1043
+ # On the same connection: calling the public method here
1044
+ # would open a second connection per call.
1045
+ working_directories = self._get_working_directories(conn)
1046
+
816
1047
  return ProjectCache(
817
1048
  version=project_row["version"],
818
1049
  cache_created=project_row["cache_created"],
@@ -825,7 +1056,7 @@ class CacheManager:
825
1056
  total_cache_creation_tokens=project_row["total_cache_creation_tokens"],
826
1057
  total_cache_read_tokens=project_row["total_cache_read_tokens"],
827
1058
  sessions=sessions,
828
- working_directories=self.get_working_directories(),
1059
+ working_directories=working_directories,
829
1060
  earliest_timestamp=project_row["earliest_timestamp"],
830
1061
  latest_timestamp=project_row["latest_timestamp"],
831
1062
  )
@@ -855,6 +1086,11 @@ class CacheManager:
855
1086
  # JSONLs are re-ingested, so the project index keeps showing
856
1087
  # the old session-id title until the cache is rebuilt.
857
1088
  "1.2.0": "1.3.0",
1089
+ # 1.5.0 filters <local-command-caveat>-wrapped caveat messages
1090
+ # (Claude Code ≥ ~2.1) from session previews: caches built
1091
+ # earlier have the caveat text baked into first_user_message
1092
+ # for affected sessions.
1093
+ "1.4.0": "1.5.0",
858
1094
  }
859
1095
 
860
1096
  cache_ver = version.parse(cache_version)