cc-transcript 9.1.0__tar.gz → 10.1.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 (77) hide show
  1. {cc_transcript-9.1.0 → cc_transcript-10.1.0}/Cargo.lock +132 -13
  2. {cc_transcript-9.1.0 → cc_transcript-10.1.0}/PKG-INFO +1 -1
  3. {cc_transcript-9.1.0 → cc_transcript-10.1.0}/cc_transcript/_parser_rs.pyi +14 -0
  4. cc_transcript-10.1.0/cc_transcript/activity_probe.py +223 -0
  5. {cc_transcript-9.1.0 → cc_transcript-10.1.0}/pyproject.toml +1 -1
  6. cc_transcript-10.1.0/rust/Cargo.toml +42 -0
  7. cc_transcript-10.1.0/rust/build.rs +20 -0
  8. cc_transcript-10.1.0/rust/src/activity.rs +695 -0
  9. cc_transcript-10.1.0/rust/src/event.rs +288 -0
  10. {cc_transcript-9.1.0 → cc_transcript-10.1.0}/rust/src/filter.rs +41 -44
  11. cc_transcript-10.1.0/rust/src/lib.rs +22 -0
  12. {cc_transcript-9.1.0 → cc_transcript-10.1.0}/rust/src/mining.rs +132 -252
  13. cc_transcript-10.1.0/rust/src/parse.rs +628 -0
  14. {cc_transcript-9.1.0 → cc_transcript-10.1.0}/rust/src/protocol.rs +9 -9
  15. cc_transcript-9.1.0/rust/src/lib.rs → cc_transcript-10.1.0/rust/src/python.rs +57 -45
  16. cc_transcript-10.1.0/rust/src/types.rs +331 -0
  17. cc_transcript-10.1.0/rust/src/value.rs +19 -0
  18. cc_transcript-9.1.0/rust/Cargo.toml +0 -29
  19. cc_transcript-9.1.0/rust/src/event.rs +0 -459
  20. cc_transcript-9.1.0/rust/src/value.rs +0 -44
  21. {cc_transcript-9.1.0 → cc_transcript-10.1.0}/Cargo.toml +0 -0
  22. {cc_transcript-9.1.0 → cc_transcript-10.1.0}/LICENSE +0 -0
  23. {cc_transcript-9.1.0 → cc_transcript-10.1.0}/README.md +0 -0
  24. {cc_transcript-9.1.0 → cc_transcript-10.1.0}/cc_transcript/__init__.py +0 -0
  25. {cc_transcript-9.1.0 → cc_transcript-10.1.0}/cc_transcript/__main__.py +0 -0
  26. {cc_transcript-9.1.0 → cc_transcript-10.1.0}/cc_transcript/activity.py +0 -0
  27. {cc_transcript-9.1.0 → cc_transcript-10.1.0}/cc_transcript/backend.py +0 -0
  28. {cc_transcript-9.1.0 → cc_transcript-10.1.0}/cc_transcript/builders.py +0 -0
  29. {cc_transcript-9.1.0 → cc_transcript-10.1.0}/cc_transcript/cli.py +0 -0
  30. {cc_transcript-9.1.0 → cc_transcript-10.1.0}/cc_transcript/command.py +0 -0
  31. {cc_transcript-9.1.0 → cc_transcript-10.1.0}/cc_transcript/context.py +0 -0
  32. {cc_transcript-9.1.0 → cc_transcript-10.1.0}/cc_transcript/corrections.py +0 -0
  33. {cc_transcript-9.1.0 → cc_transcript-10.1.0}/cc_transcript/corrections_cli.py +0 -0
  34. {cc_transcript-9.1.0 → cc_transcript-10.1.0}/cc_transcript/cost.py +0 -0
  35. {cc_transcript-9.1.0 → cc_transcript-10.1.0}/cc_transcript/decisions.py +0 -0
  36. {cc_transcript-9.1.0 → cc_transcript-10.1.0}/cc_transcript/discovery.py +0 -0
  37. {cc_transcript-9.1.0 → cc_transcript-10.1.0}/cc_transcript/disktruth.py +0 -0
  38. {cc_transcript-9.1.0 → cc_transcript-10.1.0}/cc_transcript/evidence.py +0 -0
  39. {cc_transcript-9.1.0 → cc_transcript-10.1.0}/cc_transcript/extract/__init__.py +0 -0
  40. {cc_transcript-9.1.0 → cc_transcript-10.1.0}/cc_transcript/extract/correct.py +0 -0
  41. {cc_transcript-9.1.0 → cc_transcript-10.1.0}/cc_transcript/facts.py +0 -0
  42. {cc_transcript-9.1.0 → cc_transcript-10.1.0}/cc_transcript/filterspec.py +0 -0
  43. {cc_transcript-9.1.0 → cc_transcript-10.1.0}/cc_transcript/ids.py +0 -0
  44. {cc_transcript-9.1.0 → cc_transcript-10.1.0}/cc_transcript/judge/__init__.py +0 -0
  45. {cc_transcript-9.1.0 → cc_transcript-10.1.0}/cc_transcript/judge/llm.py +0 -0
  46. {cc_transcript-9.1.0 → cc_transcript-10.1.0}/cc_transcript/judge/similar.py +0 -0
  47. {cc_transcript-9.1.0 → cc_transcript-10.1.0}/cc_transcript/judge/verdicts.py +0 -0
  48. {cc_transcript-9.1.0 → cc_transcript-10.1.0}/cc_transcript/mining/__init__.py +0 -0
  49. {cc_transcript-9.1.0 → cc_transcript-10.1.0}/cc_transcript/mining/candidates.py +0 -0
  50. {cc_transcript-9.1.0 → cc_transcript-10.1.0}/cc_transcript/mining/confidence.py +0 -0
  51. {cc_transcript-9.1.0 → cc_transcript-10.1.0}/cc_transcript/mining/engine.py +0 -0
  52. {cc_transcript-9.1.0 → cc_transcript-10.1.0}/cc_transcript/mining/filterspec.py +0 -0
  53. {cc_transcript-9.1.0 → cc_transcript-10.1.0}/cc_transcript/mining/formats.py +0 -0
  54. {cc_transcript-9.1.0 → cc_transcript-10.1.0}/cc_transcript/mining/signals.py +0 -0
  55. {cc_transcript-9.1.0 → cc_transcript-10.1.0}/cc_transcript/mining/sourcekind.py +0 -0
  56. {cc_transcript-9.1.0 → cc_transcript-10.1.0}/cc_transcript/mining/spec.py +0 -0
  57. {cc_transcript-9.1.0 → cc_transcript-10.1.0}/cc_transcript/mining/store.py +0 -0
  58. {cc_transcript-9.1.0 → cc_transcript-10.1.0}/cc_transcript/models.py +0 -0
  59. {cc_transcript-9.1.0 → cc_transcript-10.1.0}/cc_transcript/notifications.py +0 -0
  60. {cc_transcript-9.1.0 → cc_transcript-10.1.0}/cc_transcript/parser.py +0 -0
  61. {cc_transcript-9.1.0 → cc_transcript-10.1.0}/cc_transcript/py.typed +0 -0
  62. {cc_transcript-9.1.0 → cc_transcript-10.1.0}/cc_transcript/query.py +0 -0
  63. {cc_transcript-9.1.0 → cc_transcript-10.1.0}/cc_transcript/render.py +0 -0
  64. {cc_transcript-9.1.0 → cc_transcript-10.1.0}/cc_transcript/rust.py +0 -0
  65. {cc_transcript-9.1.0 → cc_transcript-10.1.0}/cc_transcript/sentiment/__init__.py +0 -0
  66. {cc_transcript-9.1.0 → cc_transcript-10.1.0}/cc_transcript/sentiment/buckets.py +0 -0
  67. {cc_transcript-9.1.0 → cc_transcript-10.1.0}/cc_transcript/sentiment/engine.py +0 -0
  68. {cc_transcript-9.1.0 → cc_transcript-10.1.0}/cc_transcript/sentiment/lexicon.py +0 -0
  69. {cc_transcript-9.1.0 → cc_transcript-10.1.0}/cc_transcript/sentiment/scorespec.py +0 -0
  70. {cc_transcript-9.1.0 → cc_transcript-10.1.0}/cc_transcript/store.py +0 -0
  71. {cc_transcript-9.1.0 → cc_transcript-10.1.0}/cc_transcript/tools.py +0 -0
  72. {cc_transcript-9.1.0 → cc_transcript-10.1.0}/rust/data/afinn-en-165.tsv +0 -0
  73. {cc_transcript-9.1.0 → cc_transcript-10.1.0}/rust/data/domain_overrides.tsv +0 -0
  74. {cc_transcript-9.1.0 → cc_transcript-10.1.0}/rust/src/command.rs +0 -0
  75. {cc_transcript-9.1.0 → cc_transcript-10.1.0}/rust/src/lexicon.rs +0 -0
  76. {cc_transcript-9.1.0 → cc_transcript-10.1.0}/rust/src/model.rs +0 -0
  77. {cc_transcript-9.1.0 → cc_transcript-10.1.0}/rust/src/score.rs +0 -0
@@ -42,6 +42,12 @@ version = "0.22.1"
42
42
  source = "registry+https://github.com/rust-lang/crates.io-index"
43
43
  checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6"
44
44
 
45
+ [[package]]
46
+ name = "bitflags"
47
+ version = "2.13.0"
48
+ source = "registry+https://github.com/rust-lang/crates.io-index"
49
+ checksum = "b4388bee8683e3d04af747c73422af53102d2bd24d9eadb6cbc100baef4b43f8"
50
+
45
51
  [[package]]
46
52
  name = "bumpalo"
47
53
  version = "3.20.3"
@@ -66,7 +72,7 @@ dependencies = [
66
72
 
67
73
  [[package]]
68
74
  name = "cc_transcript_parser"
69
- version = "0.8.0"
75
+ version = "0.9.0"
70
76
  dependencies = [
71
77
  "chrono",
72
78
  "crossbeam-channel",
@@ -75,15 +81,24 @@ dependencies = [
75
81
  "num_cpus",
76
82
  "once_cell",
77
83
  "pyo3",
84
+ "pyo3-build-config",
78
85
  "rayon",
79
86
  "regex",
80
- "serde",
81
87
  "sonic-rs",
82
88
  "tree-sitter",
83
89
  "tree-sitter-bash",
84
90
  "udpipe-rs",
85
91
  ]
86
92
 
93
+ [[package]]
94
+ name = "cc_transcript_swift"
95
+ version = "0.9.0"
96
+ dependencies = [
97
+ "cc_transcript_parser",
98
+ "swift-bridge",
99
+ "swift-bridge-build",
100
+ ]
101
+
87
102
  [[package]]
88
103
  name = "cfg-if"
89
104
  version = "1.0.4"
@@ -175,6 +190,22 @@ version = "1.0.2"
175
190
  source = "registry+https://github.com/rust-lang/crates.io-index"
176
191
  checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f"
177
192
 
193
+ [[package]]
194
+ name = "errno"
195
+ version = "0.3.14"
196
+ source = "registry+https://github.com/rust-lang/crates.io-index"
197
+ checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb"
198
+ dependencies = [
199
+ "libc",
200
+ "windows-sys 0.52.0",
201
+ ]
202
+
203
+ [[package]]
204
+ name = "fastrand"
205
+ version = "2.4.1"
206
+ source = "registry+https://github.com/rust-lang/crates.io-index"
207
+ checksum = "9f1f227452a390804cdb637b74a86990f2a7d7ba4b7d5693aac9b4dd6defd8d6"
208
+
178
209
  [[package]]
179
210
  name = "faststr"
180
211
  version = "0.2.34"
@@ -327,6 +358,12 @@ dependencies = [
327
358
  "libc",
328
359
  ]
329
360
 
361
+ [[package]]
362
+ name = "linux-raw-sys"
363
+ version = "0.12.1"
364
+ source = "registry+https://github.com/rust-lang/crates.io-index"
365
+ checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53"
366
+
330
367
  [[package]]
331
368
  name = "log"
332
369
  version = "0.4.32"
@@ -366,7 +403,7 @@ checksum = "4568f25ccbd45ab5d5603dc34318c1ec56b117531781260002151b8530a9f931"
366
403
  dependencies = [
367
404
  "proc-macro2",
368
405
  "quote",
369
- "syn",
406
+ "syn 2.0.117",
370
407
  ]
371
408
 
372
409
  [[package]]
@@ -444,7 +481,7 @@ checksum = "7347867d0a7e1208d93b46767be83e2b8f978c3dad35f775ac8d8847551d6fe1"
444
481
  dependencies = [
445
482
  "proc-macro2",
446
483
  "quote",
447
- "syn",
484
+ "syn 2.0.117",
448
485
  ]
449
486
 
450
487
  [[package]]
@@ -490,7 +527,7 @@ dependencies = [
490
527
  "proc-macro2",
491
528
  "pyo3-macros-backend",
492
529
  "quote",
493
- "syn",
530
+ "syn 2.0.117",
494
531
  ]
495
532
 
496
533
  [[package]]
@@ -503,7 +540,7 @@ dependencies = [
503
540
  "proc-macro2",
504
541
  "pyo3-build-config",
505
542
  "quote",
506
- "syn",
543
+ "syn 2.0.117",
507
544
  ]
508
545
 
509
546
  [[package]]
@@ -578,7 +615,7 @@ checksum = "b7186006dcb21920990093f30e3dea63b7d6e977bf1256be20c3563a5db070da"
578
615
  dependencies = [
579
616
  "proc-macro2",
580
617
  "quote",
581
- "syn",
618
+ "syn 2.0.117",
582
619
  ]
583
620
 
584
621
  [[package]]
@@ -656,7 +693,20 @@ checksum = "5d2ed0b54125315fb36bd021e82d314d1c126548f871634b483f46b31d13cac6"
656
693
  dependencies = [
657
694
  "proc-macro2",
658
695
  "quote",
659
- "syn",
696
+ "syn 2.0.117",
697
+ ]
698
+
699
+ [[package]]
700
+ name = "rustix"
701
+ version = "1.1.4"
702
+ source = "registry+https://github.com/rust-lang/crates.io-index"
703
+ checksum = "b6fe4565b9518b83ef4f91bb47ce29620ca828bd32cb7e408f0062e9930ba190"
704
+ dependencies = [
705
+ "bitflags",
706
+ "errno",
707
+ "libc",
708
+ "linux-raw-sys",
709
+ "windows-sys 0.52.0",
660
710
  ]
661
711
 
662
712
  [[package]]
@@ -727,7 +777,7 @@ checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79"
727
777
  dependencies = [
728
778
  "proc-macro2",
729
779
  "quote",
730
- "syn",
780
+ "syn 2.0.117",
731
781
  ]
732
782
 
733
783
  [[package]]
@@ -819,6 +869,62 @@ version = "2.6.1"
819
869
  source = "registry+https://github.com/rust-lang/crates.io-index"
820
870
  checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292"
821
871
 
872
+ [[package]]
873
+ name = "swift-bridge"
874
+ version = "0.1.59"
875
+ source = "registry+https://github.com/rust-lang/crates.io-index"
876
+ checksum = "384ed39ea10f1cefabb197b7d8e67f0034b15a94ccbb1038b8e020da59bfb0be"
877
+ dependencies = [
878
+ "swift-bridge-build",
879
+ "swift-bridge-macro",
880
+ ]
881
+
882
+ [[package]]
883
+ name = "swift-bridge-build"
884
+ version = "0.1.59"
885
+ source = "registry+https://github.com/rust-lang/crates.io-index"
886
+ checksum = "71b36df21e7f8a8b5eeb718d2e71f9cfc308477bfb705981cca705de9767dcb7"
887
+ dependencies = [
888
+ "proc-macro2",
889
+ "swift-bridge-ir",
890
+ "syn 1.0.109",
891
+ "tempfile",
892
+ ]
893
+
894
+ [[package]]
895
+ name = "swift-bridge-ir"
896
+ version = "0.1.59"
897
+ source = "registry+https://github.com/rust-lang/crates.io-index"
898
+ checksum = "c73bd16155df50708b92306945656e57d62d321290a7db490f299f709fb31c83"
899
+ dependencies = [
900
+ "proc-macro2",
901
+ "quote",
902
+ "syn 1.0.109",
903
+ ]
904
+
905
+ [[package]]
906
+ name = "swift-bridge-macro"
907
+ version = "0.1.59"
908
+ source = "registry+https://github.com/rust-lang/crates.io-index"
909
+ checksum = "a1a13dc0dc875d85341dec5b5344a7d713f20eb5650b71086b27d09a6ece272f"
910
+ dependencies = [
911
+ "proc-macro2",
912
+ "quote",
913
+ "swift-bridge-ir",
914
+ "syn 1.0.109",
915
+ ]
916
+
917
+ [[package]]
918
+ name = "syn"
919
+ version = "1.0.109"
920
+ source = "registry+https://github.com/rust-lang/crates.io-index"
921
+ checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237"
922
+ dependencies = [
923
+ "proc-macro2",
924
+ "quote",
925
+ "unicode-ident",
926
+ ]
927
+
822
928
  [[package]]
823
929
  name = "syn"
824
930
  version = "2.0.117"
@@ -836,6 +942,19 @@ version = "0.13.5"
836
942
  source = "registry+https://github.com/rust-lang/crates.io-index"
837
943
  checksum = "adb6935a6f5c20170eeceb1a3835a49e12e19d792f6dd344ccc76a985ca5a6ca"
838
944
 
945
+ [[package]]
946
+ name = "tempfile"
947
+ version = "3.27.0"
948
+ source = "registry+https://github.com/rust-lang/crates.io-index"
949
+ checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd"
950
+ dependencies = [
951
+ "fastrand",
952
+ "getrandom 0.3.4",
953
+ "once_cell",
954
+ "rustix",
955
+ "windows-sys 0.52.0",
956
+ ]
957
+
839
958
  [[package]]
840
959
  name = "thiserror"
841
960
  version = "1.0.69"
@@ -862,7 +981,7 @@ checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1"
862
981
  dependencies = [
863
982
  "proc-macro2",
864
983
  "quote",
865
- "syn",
984
+ "syn 2.0.117",
866
985
  ]
867
986
 
868
987
  [[package]]
@@ -873,7 +992,7 @@ checksum = "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5"
873
992
  dependencies = [
874
993
  "proc-macro2",
875
994
  "quote",
876
- "syn",
995
+ "syn 2.0.117",
877
996
  ]
878
997
 
879
998
  [[package]]
@@ -1041,7 +1160,7 @@ dependencies = [
1041
1160
  "bumpalo",
1042
1161
  "proc-macro2",
1043
1162
  "quote",
1044
- "syn",
1163
+ "syn 2.0.117",
1045
1164
  "wasm-bindgen-shared",
1046
1165
  ]
1047
1166
 
@@ -1225,7 +1344,7 @@ checksum = "0b631b19d36a892ab55420c92dbc83ccd79274f25be714855d3074aa71cab639"
1225
1344
  dependencies = [
1226
1345
  "proc-macro2",
1227
1346
  "quote",
1228
- "syn",
1347
+ "syn 2.0.117",
1229
1348
  ]
1230
1349
 
1231
1350
  [[package]]
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: cc-transcript
3
- Version: 9.1.0
3
+ Version: 10.1.0
4
4
  Classifier: Development Status :: 3 - Alpha
5
5
  Classifier: Environment :: Console
6
6
  Classifier: Intended Audience :: Developers
@@ -1,5 +1,7 @@
1
1
  from __future__ import annotations
2
2
 
3
+ from typing import Any
4
+
3
5
  from cc_transcript.models import PrintResult, TranscriptEvent
4
6
 
5
7
  class ParseStream:
@@ -41,3 +43,15 @@ def score_post_process(spec_json: str, buckets: list[list[str]], raw: list[int],
41
43
 
42
44
  def command_prefixes(commands: list[str], /) -> list[list[str]]:
43
45
  """Permission-style prefixes per command line, parsed in parallel off the GIL."""
46
+
47
+ def mine_signals(raw: bytes, spec_json: str, /) -> list[dict[str, Any]]:
48
+ """Parses raw transcript bytes and mines signal dicts per the portable mining spec."""
49
+
50
+ def session_activity_probe(
51
+ path: str, waiting_tools: list[str] | None = ..., human_facing_tools: list[str] | None = ..., /
52
+ ) -> dict[str, Any]:
53
+ """Parses the transcript at ``path`` and returns the session-activity verdict dict.
54
+
55
+ The dict carries ``is_waiting: bool``, ``mid_tool: bool``, ``last_event_epoch: int | None``,
56
+ and ``pending: list[dict]`` with ``tool_use_id``/``name``/``kind`` per contributing call.
57
+ """
@@ -0,0 +1,223 @@
1
+ """Dual-backend session-activity probe — captain-hook's ``is_waiting`` oracle over one transcript.
2
+
3
+ :func:`rust_probe_backend` resolves the Rust extension when built, else None to fall back
4
+ to the Python reference. :func:`session_activity_probe` is the public dual-backend entry:
5
+ it reads a ``.jsonl`` transcript, runs the oracle over its parsed events, and returns a
6
+ :class:`SessionActivityProbe`; :func:`probe_events` is the Python reference the Rust probe
7
+ stays identical to. The verdict spans undelivered task notifications (the
8
+ :class:`~cc_transcript.notifications.Notifications` queue replay), ephemeral waits
9
+ (waiting tools, backgrounded Agent/Task/Bash, subagentless Agent/Task) over the current
10
+ turn, and pending async launches (async Agent/Task, live Workflow) session-wide — a
11
+ launch counts as completed only once its notification was delivered or drained, never
12
+ when merely enqueued — and flags unanswered non-human-facing tool calls in the current
13
+ turn as mid-tool. Compact-summary user lines do not open turns, deliberately leading
14
+ captain-hook, which still lets compaction close the turn.
15
+ """
16
+
17
+ from __future__ import annotations
18
+
19
+ import os
20
+ from dataclasses import dataclass
21
+ from typing import TYPE_CHECKING, Literal
22
+
23
+ from cc_transcript.activity import native_user_classifier
24
+ from cc_transcript.models import (
25
+ AssistantEvent,
26
+ SystemEvent,
27
+ ToolResultBlock,
28
+ ToolUseBlock,
29
+ UserEvent,
30
+ )
31
+ from cc_transcript.notifications import Notifications
32
+ from cc_transcript.parser import parse_events_from_bytes
33
+
34
+ if TYPE_CHECKING:
35
+ from collections.abc import Sequence
36
+ from pathlib import Path
37
+ from types import ModuleType
38
+
39
+ from cc_transcript.models import TranscriptEvent
40
+
41
+ DEFAULT_WAITING_TOOLS = frozenset({"Monitor", "ScheduleWakeup", "SendMessage", "TeamCreate"})
42
+ DEFAULT_HUMAN_FACING_TOOLS = frozenset({"AskUserQuestion", "ExitPlanMode"})
43
+
44
+ PendingKind = Literal[
45
+ "waiting_tool",
46
+ "background",
47
+ "subagentless_task",
48
+ "pending_async_task",
49
+ "pending_async_workflow",
50
+ "mid_tool",
51
+ ]
52
+ """How a tool call contributes to the session-activity verdict."""
53
+
54
+
55
+ @dataclass(frozen=True, slots=True)
56
+ class PendingItem:
57
+ """One tool call contributing to the session-activity verdict.
58
+
59
+ Attributes:
60
+ tool_use_id: The contributing call's tool-use id.
61
+ name: The tool's name exactly as invoked.
62
+ kind: How the call contributes to the verdict.
63
+ """
64
+
65
+ tool_use_id: str | None
66
+ name: str
67
+ kind: PendingKind
68
+
69
+
70
+ @dataclass(frozen=True, slots=True)
71
+ class SessionActivityProbe:
72
+ """The session-activity verdict over one transcript.
73
+
74
+ Attributes:
75
+ is_waiting: Whether the session waits on ephemeral or pending async work.
76
+ mid_tool: Whether the current turn has an unanswered non-human-facing call.
77
+ pending: The contributing calls, in document order, deduped by tool-use id.
78
+ last_event_epoch: The latest envelope timestamp as epoch seconds, or None.
79
+ """
80
+
81
+ is_waiting: bool
82
+ mid_tool: bool
83
+ pending: tuple[PendingItem, ...]
84
+ last_event_epoch: int | None
85
+
86
+
87
+ def ephemeral_wait(block: ToolUseBlock, waiting_tools: frozenset[str]) -> PendingKind | None:
88
+ if block.name in waiting_tools:
89
+ return "waiting_tool"
90
+ match block.name:
91
+ case "Agent" | "Task" | "Bash" if block.input.get("run_in_background") is True:
92
+ return "background"
93
+ case "Agent" | "Task" if not isinstance(block.input.get("subagent_type"), str):
94
+ return "subagentless_task"
95
+ return None
96
+
97
+
98
+ def pending_async(
99
+ block: ToolUseBlock, result: ToolResultBlock | None, notifications: Notifications
100
+ ) -> PendingKind | None:
101
+ match block.name:
102
+ case "Agent" | "Task" if result is not None and result.is_async and not notifications.completed(block.id):
103
+ return "pending_async_task"
104
+ case "Workflow" if not notifications.completed(block.id):
105
+ return "pending_async_workflow"
106
+ return None
107
+
108
+
109
+ def probe_events(
110
+ events: Sequence[TranscriptEvent],
111
+ *,
112
+ waiting_tools: frozenset[str] = DEFAULT_WAITING_TOOLS,
113
+ human_facing_tools: frozenset[str] = DEFAULT_HUMAN_FACING_TOOLS,
114
+ ) -> SessionActivityProbe:
115
+ """The Python reference oracle over parsed events, twin of the Rust probe.
116
+
117
+ An undelivered task notification alone sets ``is_waiting`` with no pending
118
+ item — a resumed session's orphan has no launch to point at.
119
+ """
120
+ results = {
121
+ block.tool_use_id: block
122
+ for event in events
123
+ if isinstance(event, UserEvent | AssistantEvent)
124
+ for block in event.blocks
125
+ if isinstance(block, ToolResultBlock)
126
+ }
127
+ notifications = Notifications.from_events(events)
128
+ turn_start = next(
129
+ (
130
+ index
131
+ for index in reversed(range(len(events)))
132
+ if isinstance(event := events[index], UserEvent)
133
+ and native_user_classifier(event)
134
+ and not event.meta.is_compact_summary
135
+ ),
136
+ 0,
137
+ )
138
+
139
+ is_waiting = notifications.has_pending
140
+ mid_tool = False
141
+ pending: list[PendingItem] = []
142
+ seen: set[str] = set()
143
+ for index, event in enumerate(events):
144
+ if not isinstance(event, UserEvent | AssistantEvent):
145
+ continue
146
+ for block in event.blocks:
147
+ if not isinstance(block, ToolUseBlock):
148
+ continue
149
+ result = results.get(block.id)
150
+ if result is not None and result.is_error:
151
+ continue
152
+ in_current_turn = index >= turn_start
153
+ waiting_kind = (ephemeral_wait(block, waiting_tools) if in_current_turn else None) or pending_async(
154
+ block, result, notifications
155
+ )
156
+ unmatched = in_current_turn and result is None and block.name not in human_facing_tools
157
+ is_waiting = is_waiting or waiting_kind is not None
158
+ mid_tool = mid_tool or unmatched
159
+ kind = waiting_kind or ("mid_tool" if unmatched else None)
160
+ if kind is None or block.id in seen:
161
+ continue
162
+ seen.add(block.id)
163
+ pending.append(PendingItem(tool_use_id=block.id, name=block.name, kind=kind))
164
+ return SessionActivityProbe(
165
+ is_waiting=is_waiting,
166
+ mid_tool=mid_tool,
167
+ pending=tuple(pending),
168
+ last_event_epoch=max(
169
+ (
170
+ int(event.meta.timestamp.timestamp())
171
+ for event in events
172
+ if isinstance(event, UserEvent | AssistantEvent | SystemEvent)
173
+ ),
174
+ default=None,
175
+ ),
176
+ )
177
+
178
+
179
+ def rust_probe_backend() -> ModuleType | None:
180
+ """The Rust probe when the extension is built with it; else None → the Python reference."""
181
+ if os.environ.get("CC_TRANSCRIPT_DISABLE_RUST"):
182
+ return None
183
+ try:
184
+ from cc_transcript import _parser_rs
185
+ except ImportError:
186
+ return None
187
+ return _parser_rs if hasattr(_parser_rs, "session_activity_probe") else None
188
+
189
+
190
+ def session_activity_probe(
191
+ path: Path,
192
+ *,
193
+ waiting_tools: frozenset[str] = DEFAULT_WAITING_TOOLS,
194
+ human_facing_tools: frozenset[str] = DEFAULT_HUMAN_FACING_TOOLS,
195
+ ) -> SessionActivityProbe:
196
+ """Probes the transcript at ``path`` for session activity via the active backend.
197
+
198
+ The Rust backend reads and parses the file inside the extension in one pass; the
199
+ Python reference parses via :func:`~cc_transcript.parser.parse_events_from_bytes`
200
+ and runs :func:`probe_events`. Both yield identical verdicts.
201
+
202
+ Args:
203
+ path: The ``.jsonl`` transcript to probe.
204
+ waiting_tools: Tool names whose calls in the current turn mark the session waiting.
205
+ human_facing_tools: Tool names whose unanswered calls are the user's move, never mid-tool.
206
+
207
+ Returns:
208
+ The session-activity verdict.
209
+ """
210
+ rust = rust_probe_backend()
211
+ if rust is None:
212
+ return probe_events(
213
+ parse_events_from_bytes(path.read_bytes()),
214
+ waiting_tools=waiting_tools,
215
+ human_facing_tools=human_facing_tools,
216
+ )
217
+ payload = rust.session_activity_probe(str(path), sorted(waiting_tools), sorted(human_facing_tools))
218
+ return SessionActivityProbe(
219
+ is_waiting=payload["is_waiting"],
220
+ mid_tool=payload["mid_tool"],
221
+ pending=tuple(PendingItem(**item) for item in payload["pending"]),
222
+ last_event_epoch=payload["last_event_epoch"],
223
+ )
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "cc-transcript"
3
- version = "9.1.0"
3
+ version = "10.1.0"
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"
@@ -0,0 +1,42 @@
1
+ [package]
2
+ name = "cc_transcript_parser"
3
+ version = "0.9.0"
4
+ edition = "2021"
5
+
6
+ [lib]
7
+ name = "_parser_rs"
8
+ crate-type = ["cdylib", "rlib"]
9
+
10
+ [features]
11
+ default = ["python"]
12
+ python = [
13
+ "dep:pyo3",
14
+ "dep:rayon",
15
+ "dep:crossbeam-channel",
16
+ "dep:num_cpus",
17
+ "dep:udpipe-rs",
18
+ "dep:dirs",
19
+ "dep:tree-sitter",
20
+ "dep:tree-sitter-bash",
21
+ ]
22
+ extension-module = ["python", "pyo3/extension-module"]
23
+
24
+ [dependencies]
25
+ pyo3 = { version = "0.28", features = ["abi3-py313", "chrono"], optional = true }
26
+ sonic-rs = "0.5"
27
+ chrono = { version = "0.4", default-features = false, features = ["std"] }
28
+ rayon = { version = "1.10", optional = true }
29
+ crossbeam-channel = { version = "0.5", optional = true }
30
+ num_cpus = { version = "1", optional = true }
31
+ memchr = "2.7"
32
+ once_cell = "1"
33
+ regex = "1"
34
+ udpipe-rs = { version = "0.2", optional = true }
35
+ dirs = { version = "5", optional = true }
36
+ # Grammar-pinned to the Python wheel: tree_sitter_bash 0.25.1 both sides (same
37
+ # bash grammar), tree-sitter core 0.26 matches the wheel's tree-sitter 0.26.0.
38
+ tree-sitter = { version = "0.26", optional = true }
39
+ tree-sitter-bash = { version = "0.25", optional = true }
40
+
41
+ [build-dependencies]
42
+ pyo3-build-config = { version = "0.28", features = ["resolve-config"] }
@@ -0,0 +1,20 @@
1
+ // Local `cargo test` on the default (python) feature links a shared libpython
2
+ // whose install name is `@rpath/libpython3.X.dylib`. The cargo test harness
3
+ // carries no rpath to the interpreter's lib dir, so the binary aborts at load.
4
+ // Emit that rpath, derived from the same interpreter pyo3 linked. Gated off when
5
+ // `extension-module` is active, so the shipped wheel (which links no libpython
6
+ // and resolves symbols from the host interpreter) keeps its exact build flags.
7
+ fn main() {
8
+ let python = std::env::var_os("CARGO_FEATURE_PYTHON").is_some();
9
+ let extension_module = std::env::var_os("CARGO_FEATURE_EXTENSION_MODULE").is_some();
10
+ if !python || extension_module {
11
+ return;
12
+ }
13
+ println!("cargo:rerun-if-env-changed=PYO3_PYTHON");
14
+ println!("cargo:rerun-if-env-changed=PYO3_CONFIG_FILE");
15
+ let lib_dir = pyo3_build_config::get()
16
+ .lib_dir
17
+ .as_ref()
18
+ .expect("pyo3 linked a shared libpython but reported no lib_dir for the test rpath");
19
+ println!("cargo:rustc-link-arg=-Wl,-rpath,{lib_dir}");
20
+ }