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