cc-transcript 14.17.0__tar.gz → 14.18.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 (162) hide show
  1. {cc_transcript-14.17.0 → cc_transcript-14.18.0}/PKG-INFO +1 -1
  2. {cc_transcript-14.17.0 → cc_transcript-14.18.0}/cc_transcript/__init__.py +1 -0
  3. {cc_transcript-14.17.0 → cc_transcript-14.18.0}/cc_transcript/__init__.pyi +2 -0
  4. {cc_transcript-14.17.0 → cc_transcript-14.18.0}/cc_transcript/_native.pyi +3 -0
  5. {cc_transcript-14.17.0 → cc_transcript-14.18.0}/cc_transcript/activity.py +174 -60
  6. {cc_transcript-14.17.0 → cc_transcript-14.18.0}/pyproject.toml +1 -1
  7. {cc_transcript-14.17.0 → cc_transcript-14.18.0}/rust/crates/core/benches/parse.rs +10 -1
  8. {cc_transcript-14.17.0 → cc_transcript-14.18.0}/rust/crates/core/src/activity.rs +90 -7
  9. {cc_transcript-14.17.0 → cc_transcript-14.18.0}/rust/crates/py/src/python.rs +68 -22
  10. {cc_transcript-14.17.0 → cc_transcript-14.18.0}/Cargo.lock +0 -0
  11. {cc_transcript-14.17.0 → cc_transcript-14.18.0}/Cargo.toml +0 -0
  12. {cc_transcript-14.17.0 → cc_transcript-14.18.0}/LICENSE +0 -0
  13. {cc_transcript-14.17.0 → cc_transcript-14.18.0}/README.md +0 -0
  14. {cc_transcript-14.17.0 → cc_transcript-14.18.0}/cc_transcript/activity_probe.py +0 -0
  15. {cc_transcript-14.17.0 → cc_transcript-14.18.0}/cc_transcript/builders.py +0 -0
  16. {cc_transcript-14.17.0 → cc_transcript-14.18.0}/cc_transcript/codex.py +0 -0
  17. {cc_transcript-14.17.0 → cc_transcript-14.18.0}/cc_transcript/command.py +0 -0
  18. {cc_transcript-14.17.0 → cc_transcript-14.18.0}/cc_transcript/context.py +0 -0
  19. {cc_transcript-14.17.0 → cc_transcript-14.18.0}/cc_transcript/corrections.py +0 -0
  20. {cc_transcript-14.17.0 → cc_transcript-14.18.0}/cc_transcript/cost.py +0 -0
  21. {cc_transcript-14.17.0 → cc_transcript-14.18.0}/cc_transcript/decision_store.py +0 -0
  22. {cc_transcript-14.17.0 → cc_transcript-14.18.0}/cc_transcript/decisions.py +0 -0
  23. {cc_transcript-14.17.0 → cc_transcript-14.18.0}/cc_transcript/discovery.py +0 -0
  24. {cc_transcript-14.17.0 → cc_transcript-14.18.0}/cc_transcript/disktruth.py +0 -0
  25. {cc_transcript-14.17.0 → cc_transcript-14.18.0}/cc_transcript/evidence.py +0 -0
  26. {cc_transcript-14.17.0 → cc_transcript-14.18.0}/cc_transcript/extract/__init__.py +0 -0
  27. {cc_transcript-14.17.0 → cc_transcript-14.18.0}/cc_transcript/extract/correct.py +0 -0
  28. {cc_transcript-14.17.0 → cc_transcript-14.18.0}/cc_transcript/facts.py +0 -0
  29. {cc_transcript-14.17.0 → cc_transcript-14.18.0}/cc_transcript/filterspec.py +0 -0
  30. {cc_transcript-14.17.0 → cc_transcript-14.18.0}/cc_transcript/heartbeats.py +0 -0
  31. {cc_transcript-14.17.0 → cc_transcript-14.18.0}/cc_transcript/ids.py +0 -0
  32. {cc_transcript-14.17.0 → cc_transcript-14.18.0}/cc_transcript/judge/__init__.py +0 -0
  33. {cc_transcript-14.17.0 → cc_transcript-14.18.0}/cc_transcript/judge/llm.py +0 -0
  34. {cc_transcript-14.17.0 → cc_transcript-14.18.0}/cc_transcript/judge/similar.py +0 -0
  35. {cc_transcript-14.17.0 → cc_transcript-14.18.0}/cc_transcript/judge/verdicts.py +0 -0
  36. {cc_transcript-14.17.0 → cc_transcript-14.18.0}/cc_transcript/ledger.py +0 -0
  37. {cc_transcript-14.17.0 → cc_transcript-14.18.0}/cc_transcript/literals.py +0 -0
  38. {cc_transcript-14.17.0 → cc_transcript-14.18.0}/cc_transcript/mining/__init__.py +0 -0
  39. {cc_transcript-14.17.0 → cc_transcript-14.18.0}/cc_transcript/mining/candidates.py +0 -0
  40. {cc_transcript-14.17.0 → cc_transcript-14.18.0}/cc_transcript/mining/confidence.py +0 -0
  41. {cc_transcript-14.17.0 → cc_transcript-14.18.0}/cc_transcript/mining/engine.py +0 -0
  42. {cc_transcript-14.17.0 → cc_transcript-14.18.0}/cc_transcript/mining/formats.py +0 -0
  43. {cc_transcript-14.17.0 → cc_transcript-14.18.0}/cc_transcript/mining/sampling.py +0 -0
  44. {cc_transcript-14.17.0 → cc_transcript-14.18.0}/cc_transcript/mining/signals.py +0 -0
  45. {cc_transcript-14.17.0 → cc_transcript-14.18.0}/cc_transcript/mining/sourcekind.py +0 -0
  46. {cc_transcript-14.17.0 → cc_transcript-14.18.0}/cc_transcript/mining/spec.py +0 -0
  47. {cc_transcript-14.17.0 → cc_transcript-14.18.0}/cc_transcript/mining/store.py +0 -0
  48. {cc_transcript-14.17.0 → cc_transcript-14.18.0}/cc_transcript/models.py +0 -0
  49. {cc_transcript-14.17.0 → cc_transcript-14.18.0}/cc_transcript/nlp.py +0 -0
  50. {cc_transcript-14.17.0 → cc_transcript-14.18.0}/cc_transcript/notifications.py +0 -0
  51. {cc_transcript-14.17.0 → cc_transcript-14.18.0}/cc_transcript/parser.py +0 -0
  52. {cc_transcript-14.17.0 → cc_transcript-14.18.0}/cc_transcript/py.typed +0 -0
  53. {cc_transcript-14.17.0 → cc_transcript-14.18.0}/cc_transcript/query.py +0 -0
  54. {cc_transcript-14.17.0 → cc_transcript-14.18.0}/cc_transcript/render.py +0 -0
  55. {cc_transcript-14.17.0 → cc_transcript-14.18.0}/cc_transcript/sentiment/__init__.py +0 -0
  56. {cc_transcript-14.17.0 → cc_transcript-14.18.0}/cc_transcript/sentiment/buckets.py +0 -0
  57. {cc_transcript-14.17.0 → cc_transcript-14.18.0}/cc_transcript/sentiment/data/afinn-en-165.tsv +0 -0
  58. {cc_transcript-14.17.0 → cc_transcript-14.18.0}/cc_transcript/sentiment/data/domain_overrides.tsv +0 -0
  59. {cc_transcript-14.17.0 → cc_transcript-14.18.0}/cc_transcript/sentiment/data/en-ewt.udpipe +0 -0
  60. {cc_transcript-14.17.0 → cc_transcript-14.18.0}/cc_transcript/sentiment/engine.py +0 -0
  61. {cc_transcript-14.17.0 → cc_transcript-14.18.0}/cc_transcript/sentiment/lexicon.py +0 -0
  62. {cc_transcript-14.17.0 → cc_transcript-14.18.0}/cc_transcript/sentiment/scorespec.py +0 -0
  63. {cc_transcript-14.17.0 → cc_transcript-14.18.0}/cc_transcript/synthetic.py +0 -0
  64. {cc_transcript-14.17.0 → cc_transcript-14.18.0}/cc_transcript/tools.py +0 -0
  65. {cc_transcript-14.17.0 → cc_transcript-14.18.0}/cc_transcript/watch.py +0 -0
  66. {cc_transcript-14.17.0 → cc_transcript-14.18.0}/rust/crates/cli/Cargo.toml +0 -0
  67. {cc_transcript-14.17.0 → cc_transcript-14.18.0}/rust/crates/cli/src/commands/blame.rs +0 -0
  68. {cc_transcript-14.17.0 → cc_transcript-14.18.0}/rust/crates/cli/src/commands/corrections.rs +0 -0
  69. {cc_transcript-14.17.0 → cc_transcript-14.18.0}/rust/crates/cli/src/commands/digest.rs +0 -0
  70. {cc_transcript-14.17.0 → cc_transcript-14.18.0}/rust/crates/cli/src/commands/facts.rs +0 -0
  71. {cc_transcript-14.17.0 → cc_transcript-14.18.0}/rust/crates/cli/src/commands/grep.rs +0 -0
  72. {cc_transcript-14.17.0 → cc_transcript-14.18.0}/rust/crates/cli/src/commands/list.rs +0 -0
  73. {cc_transcript-14.17.0 → cc_transcript-14.18.0}/rust/crates/cli/src/commands/mod.rs +0 -0
  74. {cc_transcript-14.17.0 → cc_transcript-14.18.0}/rust/crates/cli/src/commands/scratchpad.rs +0 -0
  75. {cc_transcript-14.17.0 → cc_transcript-14.18.0}/rust/crates/cli/src/commands/show.rs +0 -0
  76. {cc_transcript-14.17.0 → cc_transcript-14.18.0}/rust/crates/cli/src/commands/slice.rs +0 -0
  77. {cc_transcript-14.17.0 → cc_transcript-14.18.0}/rust/crates/cli/src/commands/stats.rs +0 -0
  78. {cc_transcript-14.17.0 → cc_transcript-14.18.0}/rust/crates/cli/src/commands/watch.rs +0 -0
  79. {cc_transcript-14.17.0 → cc_transcript-14.18.0}/rust/crates/cli/src/lib.rs +0 -0
  80. {cc_transcript-14.17.0 → cc_transcript-14.18.0}/rust/crates/cli/src/main.rs +0 -0
  81. {cc_transcript-14.17.0 → cc_transcript-14.18.0}/rust/crates/cli/src/output.rs +0 -0
  82. {cc_transcript-14.17.0 → cc_transcript-14.18.0}/rust/crates/cli/src/target.rs +0 -0
  83. {cc_transcript-14.17.0 → cc_transcript-14.18.0}/rust/crates/cli/src/timearg.rs +0 -0
  84. {cc_transcript-14.17.0 → cc_transcript-14.18.0}/rust/crates/core/Cargo.toml +0 -0
  85. {cc_transcript-14.17.0 → cc_transcript-14.18.0}/rust/crates/core/benches/render.rs +0 -0
  86. {cc_transcript-14.17.0 → cc_transcript-14.18.0}/rust/crates/core/benches/toolcall.rs +0 -0
  87. {cc_transcript-14.17.0 → cc_transcript-14.18.0}/rust/crates/core/src/actor.rs +0 -0
  88. {cc_transcript-14.17.0 → cc_transcript-14.18.0}/rust/crates/core/src/blame.rs +0 -0
  89. {cc_transcript-14.17.0 → cc_transcript-14.18.0}/rust/crates/core/src/buckets.rs +0 -0
  90. {cc_transcript-14.17.0 → cc_transcript-14.18.0}/rust/crates/core/src/codex/discovery.rs +0 -0
  91. {cc_transcript-14.17.0 → cc_transcript-14.18.0}/rust/crates/core/src/codex/lower.rs +0 -0
  92. {cc_transcript-14.17.0 → cc_transcript-14.18.0}/rust/crates/core/src/codex/mod.rs +0 -0
  93. {cc_transcript-14.17.0 → cc_transcript-14.18.0}/rust/crates/core/src/codex/parse.rs +0 -0
  94. {cc_transcript-14.17.0 → cc_transcript-14.18.0}/rust/crates/core/src/codex/probe.rs +0 -0
  95. {cc_transcript-14.17.0 → cc_transcript-14.18.0}/rust/crates/core/src/codex/protocol.rs +0 -0
  96. {cc_transcript-14.17.0 → cc_transcript-14.18.0}/rust/crates/core/src/codex/types.rs +0 -0
  97. {cc_transcript-14.17.0 → cc_transcript-14.18.0}/rust/crates/core/src/command.rs +0 -0
  98. {cc_transcript-14.17.0 → cc_transcript-14.18.0}/rust/crates/core/src/context.rs +0 -0
  99. {cc_transcript-14.17.0 → cc_transcript-14.18.0}/rust/crates/core/src/corrections.rs +0 -0
  100. {cc_transcript-14.17.0 → cc_transcript-14.18.0}/rust/crates/core/src/cost.rs +0 -0
  101. {cc_transcript-14.17.0 → cc_transcript-14.18.0}/rust/crates/core/src/discovery.rs +0 -0
  102. {cc_transcript-14.17.0 → cc_transcript-14.18.0}/rust/crates/core/src/facts.rs +0 -0
  103. {cc_transcript-14.17.0 → cc_transcript-14.18.0}/rust/crates/core/src/feedback.rs +0 -0
  104. {cc_transcript-14.17.0 → cc_transcript-14.18.0}/rust/crates/core/src/filter.rs +0 -0
  105. {cc_transcript-14.17.0 → cc_transcript-14.18.0}/rust/crates/core/src/gateway.rs +0 -0
  106. {cc_transcript-14.17.0 → cc_transcript-14.18.0}/rust/crates/core/src/ids.rs +0 -0
  107. {cc_transcript-14.17.0 → cc_transcript-14.18.0}/rust/crates/core/src/judge.rs +0 -0
  108. {cc_transcript-14.17.0 → cc_transcript-14.18.0}/rust/crates/core/src/lib.rs +0 -0
  109. {cc_transcript-14.17.0 → cc_transcript-14.18.0}/rust/crates/core/src/literals/command.rs +0 -0
  110. {cc_transcript-14.17.0 → cc_transcript-14.18.0}/rust/crates/core/src/literals/corrections.rs +0 -0
  111. {cc_transcript-14.17.0 → cc_transcript-14.18.0}/rust/crates/core/src/literals/feedback.rs +0 -0
  112. {cc_transcript-14.17.0 → cc_transcript-14.18.0}/rust/crates/core/src/literals/mining.rs +0 -0
  113. {cc_transcript-14.17.0 → cc_transcript-14.18.0}/rust/crates/core/src/literals/mod.rs +0 -0
  114. {cc_transcript-14.17.0 → cc_transcript-14.18.0}/rust/crates/core/src/literals/protocol.rs +0 -0
  115. {cc_transcript-14.17.0 → cc_transcript-14.18.0}/rust/crates/core/src/notifications.rs +0 -0
  116. {cc_transcript-14.17.0 → cc_transcript-14.18.0}/rust/crates/core/src/parse.rs +0 -0
  117. {cc_transcript-14.17.0 → cc_transcript-14.18.0}/rust/crates/core/src/protocol.rs +0 -0
  118. {cc_transcript-14.17.0 → cc_transcript-14.18.0}/rust/crates/core/src/pystr.rs +0 -0
  119. {cc_transcript-14.17.0 → cc_transcript-14.18.0}/rust/crates/core/src/query.rs +0 -0
  120. {cc_transcript-14.17.0 → cc_transcript-14.18.0}/rust/crates/core/src/render.rs +0 -0
  121. {cc_transcript-14.17.0 → cc_transcript-14.18.0}/rust/crates/core/src/rng.rs +0 -0
  122. {cc_transcript-14.17.0 → cc_transcript-14.18.0}/rust/crates/core/src/schema.rs +0 -0
  123. {cc_transcript-14.17.0 → cc_transcript-14.18.0}/rust/crates/core/src/sqlite.rs +0 -0
  124. {cc_transcript-14.17.0 → cc_transcript-14.18.0}/rust/crates/core/src/toolcall.rs +0 -0
  125. {cc_transcript-14.17.0 → cc_transcript-14.18.0}/rust/crates/core/src/types.rs +0 -0
  126. {cc_transcript-14.17.0 → cc_transcript-14.18.0}/rust/crates/core/src/value.rs +0 -0
  127. {cc_transcript-14.17.0 → cc_transcript-14.18.0}/rust/crates/core/src/watch.rs +0 -0
  128. {cc_transcript-14.17.0 → cc_transcript-14.18.0}/rust/crates/py/Cargo.toml +0 -0
  129. {cc_transcript-14.17.0 → cc_transcript-14.18.0}/rust/crates/py/build.rs +0 -0
  130. {cc_transcript-14.17.0 → cc_transcript-14.18.0}/rust/crates/py/data/command_prefix_pins.tsv +0 -0
  131. {cc_transcript-14.17.0 → cc_transcript-14.18.0}/rust/crates/py/src/actor_bridge.rs +0 -0
  132. {cc_transcript-14.17.0 → cc_transcript-14.18.0}/rust/crates/py/src/bin/stub_gen.rs +0 -0
  133. {cc_transcript-14.17.0 → cc_transcript-14.18.0}/rust/crates/py/src/codex.rs +0 -0
  134. {cc_transcript-14.17.0 → cc_transcript-14.18.0}/rust/crates/py/src/command.rs +0 -0
  135. {cc_transcript-14.17.0 → cc_transcript-14.18.0}/rust/crates/py/src/context.rs +0 -0
  136. {cc_transcript-14.17.0 → cc_transcript-14.18.0}/rust/crates/py/src/corrections.rs +0 -0
  137. {cc_transcript-14.17.0 → cc_transcript-14.18.0}/rust/crates/py/src/discovery.rs +0 -0
  138. {cc_transcript-14.17.0 → cc_transcript-14.18.0}/rust/crates/py/src/feedback.rs +0 -0
  139. {cc_transcript-14.17.0 → cc_transcript-14.18.0}/rust/crates/py/src/judge.rs +0 -0
  140. {cc_transcript-14.17.0 → cc_transcript-14.18.0}/rust/crates/py/src/lexicon.rs +0 -0
  141. {cc_transcript-14.17.0 → cc_transcript-14.18.0}/rust/crates/py/src/lib.rs +0 -0
  142. {cc_transcript-14.17.0 → cc_transcript-14.18.0}/rust/crates/py/src/mining.rs +0 -0
  143. {cc_transcript-14.17.0 → cc_transcript-14.18.0}/rust/crates/py/src/nlp.rs +0 -0
  144. {cc_transcript-14.17.0 → cc_transcript-14.18.0}/rust/crates/py/src/render.rs +0 -0
  145. {cc_transcript-14.17.0 → cc_transcript-14.18.0}/rust/crates/py/src/score.rs +0 -0
  146. {cc_transcript-14.17.0 → cc_transcript-14.18.0}/rust/crates/py/src/sqlite.rs +0 -0
  147. {cc_transcript-14.17.0 → cc_transcript-14.18.0}/rust/crates/py/src/toolcall.rs +0 -0
  148. {cc_transcript-14.17.0 → cc_transcript-14.18.0}/rust/crates/py/src/views/attachment.rs +0 -0
  149. {cc_transcript-14.17.0 → cc_transcript-14.18.0}/rust/crates/py/src/views/blocks.rs +0 -0
  150. {cc_transcript-14.17.0 → cc_transcript-14.18.0}/rust/crates/py/src/views/command.rs +0 -0
  151. {cc_transcript-14.17.0 → cc_transcript-14.18.0}/rust/crates/py/src/views/convert.rs +0 -0
  152. {cc_transcript-14.17.0 → cc_transcript-14.18.0}/rust/crates/py/src/views/dunder.rs +0 -0
  153. {cc_transcript-14.17.0 → cc_transcript-14.18.0}/rust/crates/py/src/views/events.rs +0 -0
  154. {cc_transcript-14.17.0 → cc_transcript-14.18.0}/rust/crates/py/src/views/meta.rs +0 -0
  155. {cc_transcript-14.17.0 → cc_transcript-14.18.0}/rust/crates/py/src/views/mod.rs +0 -0
  156. {cc_transcript-14.17.0 → cc_transcript-14.18.0}/rust/crates/py/src/views/print.rs +0 -0
  157. {cc_transcript-14.17.0 → cc_transcript-14.18.0}/rust/crates/py/src/views/store.rs +0 -0
  158. {cc_transcript-14.17.0 → cc_transcript-14.18.0}/rust/crates/py/src/views/system.rs +0 -0
  159. {cc_transcript-14.17.0 → cc_transcript-14.18.0}/rust/crates/py/src/views/toolcall.rs +0 -0
  160. {cc_transcript-14.17.0 → cc_transcript-14.18.0}/rust/crates/py/src/views/toolresult.rs +0 -0
  161. {cc_transcript-14.17.0 → cc_transcript-14.18.0}/rust/crates/py/src/views/transcript.rs +0 -0
  162. {cc_transcript-14.17.0 → cc_transcript-14.18.0}/rust/crates/py/src/watch.rs +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: cc-transcript
3
- Version: 14.17.0
3
+ Version: 14.18.0
4
4
  Classifier: Development Status :: 3 - Alpha
5
5
  Classifier: Environment :: Console
6
6
  Classifier: Intended Audience :: Developers
@@ -197,6 +197,7 @@ EXPORTS: dict[str, str] = {
197
197
  "Lifecycle",
198
198
  ),
199
199
  "cc_transcript.activity": (
200
+ "ActivityLift",
200
201
  "Edit",
201
202
  "SessionActivity",
202
203
  "ToolUse",
@@ -2,6 +2,7 @@
2
2
  # isort: off
3
3
 
4
4
  from cc_transcript.activity import (
5
+ ActivityLift as ActivityLift,
5
6
  Edit as Edit,
6
7
  SessionActivity as SessionActivity,
7
8
  ToolUse as ToolUse,
@@ -305,6 +306,7 @@ EXPORTS: dict[str, str]
305
306
  __all__ = [
306
307
  "ASSISTANTS",
307
308
  "Action",
309
+ "ActivityLift",
308
310
  "ApiError",
309
311
  "ApplyPatchCall",
310
312
  "AskUserQuestionResult",
@@ -108,6 +108,7 @@ __all__ = [
108
108
  "activity_hunk_overlap",
109
109
  "activity_lift",
110
110
  "activity_lift_from_events",
111
+ "activity_lift_tail",
111
112
  "activity_overlap_between",
112
113
  "activity_parse_show_hunks",
113
114
  "bucket_events_from_events",
@@ -2295,6 +2296,8 @@ def activity_lift(path: builtins.str, max_events: builtins.int) -> dict[str, typ
2295
2296
 
2296
2297
  def activity_lift_from_events(events: list[models.TranscriptEvent], opener_flags: typing.Optional[typing.Sequence[builtins.bool]] = None) -> list[dict[str, typing.Any]]: ...
2297
2298
 
2299
+ def activity_lift_tail(events: list[models.TranscriptEvent], opener_flags: typing.Optional[typing.Sequence[builtins.bool]] = None, open_turn: builtins.bool = False) -> dict[str, typing.Any]: ...
2300
+
2298
2301
  def activity_overlap_between(incorrect: typing.Sequence[tuple[builtins.str, builtins.str]], correction: typing.Sequence[tuple[builtins.str, builtins.str]]) -> builtins.float: ...
2299
2302
 
2300
2303
  def activity_parse_show_hunks(diff: builtins.str) -> builtins.list[tuple[builtins.str, builtins.str]]: ...
@@ -13,8 +13,8 @@ products.
13
13
  from __future__ import annotations
14
14
 
15
15
  from collections.abc import Callable
16
- from dataclasses import dataclass
17
- from typing import TYPE_CHECKING
16
+ from dataclasses import dataclass, replace
17
+ from typing import TYPE_CHECKING, Any
18
18
 
19
19
  from cc_transcript import _native
20
20
  from cc_transcript.discovery import TranscriptExpiredError, resolve
@@ -25,7 +25,7 @@ from cc_transcript.parser import parse
25
25
  from cc_transcript.tools import edits_of, parse_tool_call, parse_tool_result
26
26
 
27
27
  if TYPE_CHECKING:
28
- from collections.abc import Sequence
28
+ from collections.abc import Mapping, Sequence
29
29
  from datetime import datetime
30
30
  from pathlib import Path
31
31
 
@@ -181,64 +181,10 @@ class SessionActivity:
181
181
  turn; everything else folds into the current one. Events before the
182
182
  first qualifying prompt form turn 0 with prompt ``""``.
183
183
  """
184
- evs = list(events)
185
- opener_flags = (
186
- None
187
- if user_classifier is native_user_classifier
188
- else [bool(user_classifier(event)) if isinstance(event, UserEvent) else False for event in evs]
184
+ return cls(
185
+ session_id=session_id,
186
+ turns=ActivityLift(session_id, user_classifier=user_classifier).extend(events).turns,
189
187
  )
190
- tool_blocks = {
191
- event_idx: tuple(block for block in event.blocks if isinstance(block, ToolUseBlock))
192
- for event_idx, event in enumerate(evs)
193
- if isinstance(event, AssistantEvent)
194
- }
195
- tool_block_positions: dict[int, int] = {}
196
- turns: list[Turn] = []
197
- for index, skeleton in enumerate(_native.activity_lift_from_events(evs, opener_flags)):
198
- tool_uses: list[ToolUse] = []
199
- for use in skeleton["tool_uses"]:
200
- event_idx = use["event_idx"]
201
- event = evs[event_idx]
202
- block_position = tool_block_positions.get(event_idx, 0)
203
- block = tool_blocks[event_idx][block_position]
204
- tool_block_positions[event_idx] = block_position + 1
205
- result_event_idx = use["result_event_idx"]
206
- if result_event_idx is None:
207
- result = None
208
- result_ts = None
209
- else:
210
- result_event = evs[result_event_idx]
211
- result = next(
212
- candidate
213
- for candidate in reversed(result_event.blocks)
214
- if isinstance(candidate, ToolResultBlock) and candidate.tool_use_id == use["tool_use_id"]
215
- )
216
- result_ts = result_event.meta.timestamp
217
- call = parse_tool_call(block.name, block.input, on_error="other")
218
- tool_uses.append(
219
- ToolUse(
220
- ref=EventRef(session_id, event.meta.uuid, block.id),
221
- call=call,
222
- result=result,
223
- result_ts=result_ts,
224
- edits=edits_of(call),
225
- turn_index=index,
226
- ts=event.meta.timestamp,
227
- )
228
- )
229
- started_idx = skeleton["started_idx"]
230
- ended_idx = skeleton["ended_idx"]
231
- turns.append(
232
- Turn(
233
- index=index,
234
- prompt=skeleton["prompt"],
235
- started_at=None if started_idx is None else evs[started_idx].meta.timestamp,
236
- ended_at=None if ended_idx is None else evs[ended_idx].meta.timestamp,
237
- events=tuple(evs[skeleton["start"] : skeleton["end"]]),
238
- tool_uses=tuple(tool_uses),
239
- )
240
- )
241
- return cls(session_id=session_id, turns=tuple(turns))
242
188
 
243
189
  @classmethod
244
190
  def from_session(
@@ -326,6 +272,174 @@ class SessionActivity:
326
272
  return tuple(same_turn + later)
327
273
 
328
274
 
275
+ class ActivityLift:
276
+ """A session's lift that grows with its transcript.
277
+
278
+ Each :meth:`extend` folds the events appended since the previous call into
279
+ the activity lifted so far: closed turns are kept, the open turn grows by
280
+ the appended events alone, and a tool use whose result arrives in the tail
281
+ is re-paired wherever it sits. The result always equals
282
+ :meth:`SessionActivity.from_events` over every event fed so far. A step
283
+ costs the appended events, the tool uses they re-pair, and copies of the
284
+ turn tuple and of the open turn's event and tool-use tuples; it never
285
+ re-derives the session.
286
+
287
+ Feed only the events appended since the previous call: nothing checks
288
+ provenance, so events fed twice lift twice. A transcript rewritten in
289
+ place, rather than appended to, needs a new cursor.
290
+
291
+ The classifier is read once, at construction, and must be deterministic
292
+ and event-only: its answer for an event has to stay the same as later
293
+ events are appended, since turns lifted under it are never revisited.
294
+
295
+ Example:
296
+ >>> lift = ActivityLift(session_id)
297
+ >>> activity = lift.extend(events)
298
+ >>> lift.extend(appended) == SessionActivity.from_events(session_id, [*events, *appended])
299
+ True
300
+ """
301
+
302
+ __slots__ = ("_activity", "_results", "_session_id", "_user_classifier", "_uses")
303
+
304
+ def __init__(self, session_id: SessionId, *, user_classifier: UserClassifier = native_user_classifier) -> None:
305
+ self._session_id = session_id
306
+ self._user_classifier = user_classifier
307
+ self._activity = SessionActivity(session_id=session_id, turns=())
308
+ self._uses: dict[ToolUseId, tuple[tuple[int, int], ...]] = {}
309
+ self._results: dict[ToolUseId, UserEvent] = {}
310
+
311
+ @property
312
+ def session_id(self) -> SessionId:
313
+ """The session being lifted."""
314
+ return self._session_id
315
+
316
+ @property
317
+ def user_classifier(self) -> UserClassifier:
318
+ """Decides which user events open turns."""
319
+ return self._user_classifier
320
+
321
+ @property
322
+ def activity(self) -> SessionActivity:
323
+ """Everything lifted so far."""
324
+ return self._activity
325
+
326
+ def extend(self, events: Sequence[TranscriptEvent]) -> SessionActivity:
327
+ """Lifts ``events``, appended after everything fed so far, into :attr:`activity`."""
328
+ tail = list(events)
329
+ if not tail:
330
+ return self._activity
331
+ opener_flags = (
332
+ None
333
+ if self._user_classifier is native_user_classifier
334
+ else [bool(self._user_classifier(event)) if isinstance(event, UserEvent) else False for event in tail]
335
+ )
336
+ turns = list(self._activity.turns)
337
+ lifted = _native.activity_lift_tail(tail, opener_flags, open_turn=bool(turns))
338
+ first = len(turns) - 1 if lifted["continued"] else len(turns)
339
+ grown = [
340
+ lift_turn(self._session_id, first + offset, skeleton, tail, self._results)
341
+ for offset, skeleton in enumerate(lifted["turns"])
342
+ ]
343
+ if lifted["continued"]:
344
+ turns[-1] = continued(turns[-1], grown[0])
345
+ turns.extend(grown[1:])
346
+ else:
347
+ turns.extend(grown)
348
+ repairs: dict[int, dict[int, UserEvent]] = {}
349
+ for tool_use_id, result_idx in lifted["results"]:
350
+ for turn_index, position in self._uses.get(tool_use_id, ()):
351
+ repairs.setdefault(turn_index, {})[position] = tail[result_idx]
352
+ self._results[tool_use_id] = tail[result_idx]
353
+ for turn_index, at in repairs.items():
354
+ turns[turn_index] = repaired(turns[turn_index], at)
355
+ for turn in grown:
356
+ base = len(turns[turn.index].tool_uses) - len(turn.tool_uses)
357
+ for position, use in enumerate(turn.tool_uses, base):
358
+ self._uses[use.ref.tool_use_id] = (*self._uses.get(use.ref.tool_use_id, ()), (turn.index, position))
359
+ self._activity = SessionActivity(session_id=self._session_id, turns=tuple(turns))
360
+ return self._activity
361
+
362
+
363
+ def lift_turn(
364
+ session_id: SessionId,
365
+ index: int,
366
+ skeleton: dict[str, Any],
367
+ evs: Sequence[TranscriptEvent],
368
+ prior_results: Mapping[ToolUseId, UserEvent],
369
+ ) -> Turn:
370
+ events = tuple(evs[skeleton["start"] : skeleton["end"]])
371
+ calls = [
372
+ (event, block)
373
+ for event in events
374
+ if isinstance(event, AssistantEvent)
375
+ for block in event.blocks
376
+ if isinstance(block, ToolUseBlock)
377
+ ]
378
+ started_idx = skeleton["started_idx"]
379
+ ended_idx = skeleton["ended_idx"]
380
+ return Turn(
381
+ index=index,
382
+ prompt=skeleton["prompt"],
383
+ started_at=None if started_idx is None else evs[started_idx].meta.timestamp,
384
+ ended_at=None if ended_idx is None else evs[ended_idx].meta.timestamp,
385
+ events=events,
386
+ tool_uses=tuple(
387
+ lift_tool_use(
388
+ session_id,
389
+ index,
390
+ event,
391
+ block,
392
+ prior_results.get(block.id) if (result_idx := use["result_event_idx"]) is None else evs[result_idx],
393
+ )
394
+ for (event, block), use in zip(calls, skeleton["tool_uses"], strict=True)
395
+ ),
396
+ )
397
+
398
+
399
+ def lift_tool_use(
400
+ session_id: SessionId, turn_index: int, event: AssistantEvent, block: ToolUseBlock, result_event: UserEvent | None
401
+ ) -> ToolUse:
402
+ call = parse_tool_call(block.name, block.input, on_error="other")
403
+ return ToolUse(
404
+ ref=EventRef(session_id, event.meta.uuid, block.id),
405
+ call=call,
406
+ result=None if result_event is None else result_block(result_event, block.id),
407
+ result_ts=None if result_event is None else result_event.meta.timestamp,
408
+ edits=edits_of(call),
409
+ turn_index=turn_index,
410
+ ts=event.meta.timestamp,
411
+ )
412
+
413
+
414
+ def result_block(event: UserEvent, tool_use_id: ToolUseId) -> ToolResultBlock:
415
+ return next(
416
+ candidate
417
+ for candidate in reversed(event.blocks)
418
+ if isinstance(candidate, ToolResultBlock) and candidate.tool_use_id == tool_use_id
419
+ )
420
+
421
+
422
+ def continued(turn: Turn, tail: Turn) -> Turn:
423
+ return replace(
424
+ turn,
425
+ started_at=tail.started_at if turn.started_at is None else turn.started_at,
426
+ ended_at=turn.ended_at if tail.ended_at is None else tail.ended_at,
427
+ events=turn.events + tail.events,
428
+ tool_uses=turn.tool_uses + tail.tool_uses,
429
+ )
430
+
431
+
432
+ def repaired(turn: Turn, at: Mapping[int, UserEvent]) -> Turn:
433
+ uses = list(turn.tool_uses)
434
+ for position, event in at.items():
435
+ uses[position] = replace(
436
+ uses[position],
437
+ result=result_block(event, uses[position].ref.tool_use_id),
438
+ result_ts=event.meta.timestamp,
439
+ )
440
+ return replace(turn, tool_uses=tuple(uses))
441
+
442
+
329
443
  def hunk_overlap(a: Hunk, b: Hunk) -> float:
330
444
  """The fraction of ``a.new``'s non-empty lines present in ``b.old``.
331
445
 
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "cc-transcript"
3
- version = "14.17.0" # informational — releases stamp the version from the git tag
3
+ version = "14.18.0" # informational — releases stamp the version from the git tag
4
4
  description = "Grep every Claude Code session you've ever run."
5
5
  readme = "README.md"
6
6
  license = "PolyForm-Noncommercial-1.0.0"
@@ -6,9 +6,12 @@ use std::path::{Path, PathBuf};
6
6
 
7
7
  use criterion::{criterion_group, criterion_main, Criterion, Throughput};
8
8
 
9
- use cc_transcript_core::activity::{lift_session, session_activity, ActivityOpts};
9
+ use cc_transcript_core::activity::{
10
+ lift_session, lift_session_index, lift_session_index_tail, session_activity, ActivityOpts,
11
+ };
10
12
  use cc_transcript_core::filter::{compile_spec, spec_keep, SIGNAL_SPEC_JSON};
11
13
  use cc_transcript_core::parse::parse_bytes;
14
+ use cc_transcript_core::types::Entry;
12
15
 
13
16
  fn corpus_dir() -> PathBuf {
14
17
  std::env::var_os("CC_BENCH_CORPUS")
@@ -80,6 +83,12 @@ fn bench_activity(c: &mut Criterion) {
80
83
  group.bench_function("full_lift", |b| {
81
84
  b.iter(|| lift_session(&session_id, &entries))
82
85
  });
86
+ let refs: Vec<&Entry> = entries.iter().collect();
87
+ group.bench_function("lift_index", |b| b.iter(|| lift_session_index(&refs, None)));
88
+ let appended = &refs[refs.len().saturating_sub(8)..];
89
+ group.bench_function("lift_index_tail_8", |b| {
90
+ b.iter(|| lift_session_index_tail(appended, None, true))
91
+ });
83
92
  group.finish();
84
93
  }
85
94
 
@@ -500,17 +500,17 @@ struct Segment {
500
500
  end: usize,
501
501
  }
502
502
 
503
+ fn opens(entry: &Entry, index: usize, openers: Option<&[bool]>) -> bool {
504
+ matches!(entry, Entry::User(user) if openers.map_or_else(|| opens_turn(user), |flags| flags[index]))
505
+ }
506
+
503
507
  // activity.py from_events segmentation: a turn-opening user entry starts a contiguous
504
508
  // segment, else folds into the current one; `Some(openers)` replaces that per-entry decision.
505
509
  fn segments(entries: &[&Entry], openers: Option<&[bool]>) -> Vec<Segment> {
506
510
  let mut segments: Vec<Segment> = Vec::new();
507
511
  for (index, &entry) in entries.iter().enumerate() {
508
- let opens = openers.map_or_else(
509
- || matches!(entry, Entry::User(user) if opens_turn(user)),
510
- |flags| flags[index],
511
- );
512
512
  match entry {
513
- Entry::User(user) if opens => segments.push(Segment {
513
+ Entry::User(user) if opens(entry, index, openers) => segments.push(Segment {
514
514
  prompt: user.content.text(),
515
515
  start: index,
516
516
  end: index + 1,
@@ -711,7 +711,42 @@ pub fn lift_session_index<'a>(
711
711
  entries: &[&'a Entry],
712
712
  openers: Option<&[bool]>,
713
713
  ) -> Vec<TurnIndex<'a>> {
714
- let result_pos: HashMap<&'a str, usize> = entries
714
+ turn_indexes(entries, openers, &result_positions(entries))
715
+ }
716
+
717
+ /// The tail of an incremental lift (activity.py ActivityLift.extend): skeletons over the
718
+ /// appended entries alone, `continued` when the first is the rest of the prior open turn,
719
+ /// and every tool result the tail carries as `(tool_use_id, position)` for re-pairing.
720
+ #[derive(Debug, PartialEq, Eq)]
721
+ pub struct LiftTail<'a> {
722
+ pub turns: Vec<TurnIndex<'a>>,
723
+ pub continued: bool,
724
+ pub results: Vec<(&'a str, usize)>,
725
+ }
726
+
727
+ /// `lift_session_index` over the entries appended since a prior lift whose last turn
728
+ /// is open when `open_turn`. Turn boundaries never look ahead, so prior turns are final.
729
+ pub fn lift_session_index_tail<'a>(
730
+ entries: &[&'a Entry],
731
+ openers: Option<&[bool]>,
732
+ open_turn: bool,
733
+ ) -> LiftTail<'a> {
734
+ let result_pos = result_positions(entries);
735
+ let mut results: Vec<(&'a str, usize)> =
736
+ result_pos.iter().map(|(&id, &pos)| (id, pos)).collect();
737
+ results.sort_unstable_by_key(|&(_, pos)| pos);
738
+ LiftTail {
739
+ turns: turn_indexes(entries, openers, &result_pos),
740
+ continued: open_turn
741
+ && entries
742
+ .first()
743
+ .is_some_and(|&entry| !opens(entry, 0, openers)),
744
+ results,
745
+ }
746
+ }
747
+
748
+ fn result_positions<'a>(entries: &[&'a Entry]) -> HashMap<&'a str, usize> {
749
+ entries
715
750
  .iter()
716
751
  .enumerate()
717
752
  .filter_map(|(index, &entry)| match entry {
@@ -722,7 +757,14 @@ pub fn lift_session_index<'a>(
722
757
  user.tool_results()
723
758
  .map(move |block| (block.tool_use_id.as_str(), index))
724
759
  })
725
- .collect();
760
+ .collect()
761
+ }
762
+
763
+ fn turn_indexes<'a>(
764
+ entries: &[&'a Entry],
765
+ openers: Option<&[bool]>,
766
+ result_pos: &HashMap<&'a str, usize>,
767
+ ) -> Vec<TurnIndex<'a>> {
726
768
  segments(entries, openers)
727
769
  .into_iter()
728
770
  .map(|segment| {
@@ -1553,6 +1595,47 @@ mod tests {
1553
1595
  );
1554
1596
  }
1555
1597
 
1598
+ #[test]
1599
+ fn tail_continues_an_open_turn_only_through_a_non_opener() {
1600
+ let entries = vec![
1601
+ tool_use("Bash", "t1", r#"{"command":"ls"}"#),
1602
+ tool_result("t1"),
1603
+ user("next ask"),
1604
+ ];
1605
+ let refs: Vec<&Entry> = entries.iter().collect();
1606
+
1607
+ let continued = lift_session_index_tail(&refs, None, true);
1608
+ assert!(continued.continued);
1609
+ assert_eq!(continued.turns, lift_session_index(&refs, None));
1610
+ assert_eq!(continued.results, [("t1", 1)]);
1611
+
1612
+ assert!(!lift_session_index_tail(&refs, None, false).continued);
1613
+ assert!(!lift_session_index_tail(&refs[2..], None, true).continued);
1614
+ assert!(!lift_session_index_tail(&[], None, true).continued);
1615
+ assert!(lift_session_index_tail(&refs, Some(&[false, false, false]), true).continued);
1616
+ assert!(
1617
+ lift_session_index_tail(&refs, Some(&[true, false, false]), true).continued,
1618
+ "a flag on a non-user entry opens nothing, exactly as segments() ignores it"
1619
+ );
1620
+ assert!(lift_session_index_tail(&refs[2..], Some(&[false]), true).continued);
1621
+ assert!(!lift_session_index_tail(&refs[2..], Some(&[true]), true).continued);
1622
+ }
1623
+
1624
+ #[test]
1625
+ fn tail_results_keep_the_last_position_per_id_in_tail_order() {
1626
+ let entries = vec![
1627
+ tool_result("t2"),
1628
+ tool_result("t1"),
1629
+ user("interrupting ask"),
1630
+ tool_result("t2"),
1631
+ ];
1632
+ let refs: Vec<&Entry> = entries.iter().collect();
1633
+ assert_eq!(
1634
+ lift_session_index_tail(&refs, None, true).results,
1635
+ [("t1", 1), ("t2", 3)]
1636
+ );
1637
+ }
1638
+
1556
1639
  #[test]
1557
1640
  fn hunk_overlap_matches_reference() {
1558
1641
  let hunk = |old: &str, new: &str| Hunk {
@@ -15,8 +15,8 @@ use crate::views::print::print_result_view;
15
15
  use crate::views::transcript::TranscriptView;
16
16
  use crate::{command, lexicon, mining, score};
17
17
  use cc_transcript_core::activity::{
18
- hunk_overlap, lift_session, lift_session_index, overlap_between, parse_show_hunks,
19
- result_index, ActivityOpts, Hunk, SessionActivity,
18
+ hunk_overlap, lift_session, lift_session_index, lift_session_index_tail, overlap_between,
19
+ parse_show_hunks, result_index, ActivityOpts, Hunk, SessionActivity, TurnIndex,
20
20
  };
21
21
  use cc_transcript_core::buckets;
22
22
  use cc_transcript_core::command::CommandLine;
@@ -310,28 +310,73 @@ fn activity_lift_from_events<'py>(
310
310
  .collect::<PyResult<Vec<_>>>()?;
311
311
  lift_session_index(&entries, opener_flags.as_deref())
312
312
  .iter()
313
- .map(|turn| {
314
- let td = PyDict::new(py);
315
- td.set_item("prompt", &turn.prompt)?;
316
- td.set_item("start", turn.start)?;
317
- td.set_item("end", turn.end)?;
318
- td.set_item("started_idx", turn.started_idx)?;
319
- td.set_item("ended_idx", turn.ended_idx)?;
320
- let tool_uses = turn
321
- .tool_uses
313
+ .map(|turn| turn_index_dict(py, turn))
314
+ .collect()
315
+ }
316
+
317
+ // activity.ActivityLift.extend: the tail skeleton over the appended events alone, plus
318
+ // whether its first turn continues the open one and every result the tail carries.
319
+ #[pyo3_stub_gen::derive::gen_stub_pyfunction]
320
+ #[pyfunction]
321
+ #[pyo3(signature = (events, opener_flags=None, open_turn=false))]
322
+ #[gen_stub(override_return_type(type_repr = "dict[str, typing.Any]", imports = ("typing",)))]
323
+ fn activity_lift_tail<'py>(
324
+ py: Python<'py>,
325
+ #[gen_stub(override_type(type_repr = "list[cc_transcript.models.TranscriptEvent]", imports = ("cc_transcript.models",)))]
326
+ events: Vec<Bound<'py, PyAny>>,
327
+ opener_flags: Option<Vec<bool>>,
328
+ open_turn: bool,
329
+ ) -> PyResult<Bound<'py, PyDict>> {
330
+ if let Some(flags) = &opener_flags {
331
+ if flags.len() != events.len() {
332
+ return Err(PyValueError::new_err(format!(
333
+ "activity_lift_tail() got {} opener_flags for {} events",
334
+ flags.len(),
335
+ events.len()
336
+ )));
337
+ }
338
+ }
339
+ let entries = events
340
+ .iter()
341
+ .map(|event| mining::view_entry(event, "activity_lift_tail"))
342
+ .collect::<PyResult<Vec<_>>>()?;
343
+ let tail = lift_session_index_tail(&entries, opener_flags.as_deref(), open_turn);
344
+ let dict = PyDict::new(py);
345
+ dict.set_item(
346
+ "turns",
347
+ PyList::new(
348
+ py,
349
+ tail.turns
322
350
  .iter()
323
- .map(|use_| {
324
- let ud = PyDict::new(py);
325
- ud.set_item("event_idx", use_.event_idx)?;
326
- ud.set_item("tool_use_id", use_.tool_use_id)?;
327
- ud.set_item("result_event_idx", use_.result_event_idx)?;
328
- Ok(ud)
329
- })
330
- .collect::<PyResult<Vec<_>>>()?;
331
- td.set_item("tool_uses", PyList::new(py, tool_uses)?)?;
332
- Ok(td)
351
+ .map(|turn| turn_index_dict(py, turn))
352
+ .collect::<PyResult<Vec<_>>>()?,
353
+ )?,
354
+ )?;
355
+ dict.set_item("continued", tail.continued)?;
356
+ dict.set_item("results", tail.results)?;
357
+ Ok(dict)
358
+ }
359
+
360
+ fn turn_index_dict<'py>(py: Python<'py>, turn: &TurnIndex<'_>) -> PyResult<Bound<'py, PyDict>> {
361
+ let td = PyDict::new(py);
362
+ td.set_item("prompt", &turn.prompt)?;
363
+ td.set_item("start", turn.start)?;
364
+ td.set_item("end", turn.end)?;
365
+ td.set_item("started_idx", turn.started_idx)?;
366
+ td.set_item("ended_idx", turn.ended_idx)?;
367
+ let tool_uses = turn
368
+ .tool_uses
369
+ .iter()
370
+ .map(|use_| {
371
+ let ud = PyDict::new(py);
372
+ ud.set_item("event_idx", use_.event_idx)?;
373
+ ud.set_item("tool_use_id", use_.tool_use_id)?;
374
+ ud.set_item("result_event_idx", use_.result_event_idx)?;
375
+ Ok(ud)
333
376
  })
334
- .collect()
377
+ .collect::<PyResult<Vec<_>>>()?;
378
+ td.set_item("tool_uses", PyList::new(py, tool_uses)?)?;
379
+ Ok(td)
335
380
  }
336
381
 
337
382
  #[pyo3_stub_gen::derive::gen_stub_pyfunction]
@@ -1023,6 +1068,7 @@ fn _native(m: &Bound<'_, PyModule>) -> PyResult<()> {
1023
1068
  m.add_function(wrap_pyfunction!(cli_main, m)?)?;
1024
1069
  m.add_function(wrap_pyfunction!(activity_lift, m)?)?;
1025
1070
  m.add_function(wrap_pyfunction!(activity_lift_from_events, m)?)?;
1071
+ m.add_function(wrap_pyfunction!(activity_lift_tail, m)?)?;
1026
1072
  m.add_function(wrap_pyfunction!(activity_hunk_overlap, m)?)?;
1027
1073
  m.add_function(wrap_pyfunction!(activity_overlap_between, m)?)?;
1028
1074
  m.add_function(wrap_pyfunction!(activity_parse_show_hunks, m)?)?;
File without changes