cc-transcript 9.0.0__tar.gz → 9.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 (68) hide show
  1. {cc_transcript-9.0.0 → cc_transcript-9.1.0}/PKG-INFO +14 -14
  2. {cc_transcript-9.0.0 → cc_transcript-9.1.0}/README.md +13 -13
  3. {cc_transcript-9.0.0 → cc_transcript-9.1.0}/cc_transcript/__init__.py +2 -0
  4. {cc_transcript-9.0.0 → cc_transcript-9.1.0}/cc_transcript/judge/llm.py +2 -1
  5. cc_transcript-9.1.0/cc_transcript/notifications.py +115 -0
  6. {cc_transcript-9.0.0 → cc_transcript-9.1.0}/cc_transcript/query.py +6 -0
  7. {cc_transcript-9.0.0 → cc_transcript-9.1.0}/pyproject.toml +1 -1
  8. {cc_transcript-9.0.0 → cc_transcript-9.1.0}/Cargo.lock +0 -0
  9. {cc_transcript-9.0.0 → cc_transcript-9.1.0}/Cargo.toml +0 -0
  10. {cc_transcript-9.0.0 → cc_transcript-9.1.0}/LICENSE +0 -0
  11. {cc_transcript-9.0.0 → cc_transcript-9.1.0}/cc_transcript/__main__.py +0 -0
  12. {cc_transcript-9.0.0 → cc_transcript-9.1.0}/cc_transcript/_parser_rs.pyi +0 -0
  13. {cc_transcript-9.0.0 → cc_transcript-9.1.0}/cc_transcript/activity.py +0 -0
  14. {cc_transcript-9.0.0 → cc_transcript-9.1.0}/cc_transcript/backend.py +0 -0
  15. {cc_transcript-9.0.0 → cc_transcript-9.1.0}/cc_transcript/builders.py +0 -0
  16. {cc_transcript-9.0.0 → cc_transcript-9.1.0}/cc_transcript/cli.py +0 -0
  17. {cc_transcript-9.0.0 → cc_transcript-9.1.0}/cc_transcript/command.py +0 -0
  18. {cc_transcript-9.0.0 → cc_transcript-9.1.0}/cc_transcript/context.py +0 -0
  19. {cc_transcript-9.0.0 → cc_transcript-9.1.0}/cc_transcript/corrections.py +0 -0
  20. {cc_transcript-9.0.0 → cc_transcript-9.1.0}/cc_transcript/corrections_cli.py +0 -0
  21. {cc_transcript-9.0.0 → cc_transcript-9.1.0}/cc_transcript/cost.py +0 -0
  22. {cc_transcript-9.0.0 → cc_transcript-9.1.0}/cc_transcript/decisions.py +0 -0
  23. {cc_transcript-9.0.0 → cc_transcript-9.1.0}/cc_transcript/discovery.py +0 -0
  24. {cc_transcript-9.0.0 → cc_transcript-9.1.0}/cc_transcript/disktruth.py +0 -0
  25. {cc_transcript-9.0.0 → cc_transcript-9.1.0}/cc_transcript/evidence.py +0 -0
  26. {cc_transcript-9.0.0 → cc_transcript-9.1.0}/cc_transcript/extract/__init__.py +0 -0
  27. {cc_transcript-9.0.0 → cc_transcript-9.1.0}/cc_transcript/extract/correct.py +0 -0
  28. {cc_transcript-9.0.0 → cc_transcript-9.1.0}/cc_transcript/facts.py +0 -0
  29. {cc_transcript-9.0.0 → cc_transcript-9.1.0}/cc_transcript/filterspec.py +0 -0
  30. {cc_transcript-9.0.0 → cc_transcript-9.1.0}/cc_transcript/ids.py +0 -0
  31. {cc_transcript-9.0.0 → cc_transcript-9.1.0}/cc_transcript/judge/__init__.py +0 -0
  32. {cc_transcript-9.0.0 → cc_transcript-9.1.0}/cc_transcript/judge/similar.py +0 -0
  33. {cc_transcript-9.0.0 → cc_transcript-9.1.0}/cc_transcript/judge/verdicts.py +0 -0
  34. {cc_transcript-9.0.0 → cc_transcript-9.1.0}/cc_transcript/mining/__init__.py +0 -0
  35. {cc_transcript-9.0.0 → cc_transcript-9.1.0}/cc_transcript/mining/candidates.py +0 -0
  36. {cc_transcript-9.0.0 → cc_transcript-9.1.0}/cc_transcript/mining/confidence.py +0 -0
  37. {cc_transcript-9.0.0 → cc_transcript-9.1.0}/cc_transcript/mining/engine.py +0 -0
  38. {cc_transcript-9.0.0 → cc_transcript-9.1.0}/cc_transcript/mining/filterspec.py +0 -0
  39. {cc_transcript-9.0.0 → cc_transcript-9.1.0}/cc_transcript/mining/formats.py +0 -0
  40. {cc_transcript-9.0.0 → cc_transcript-9.1.0}/cc_transcript/mining/signals.py +0 -0
  41. {cc_transcript-9.0.0 → cc_transcript-9.1.0}/cc_transcript/mining/sourcekind.py +0 -0
  42. {cc_transcript-9.0.0 → cc_transcript-9.1.0}/cc_transcript/mining/spec.py +0 -0
  43. {cc_transcript-9.0.0 → cc_transcript-9.1.0}/cc_transcript/mining/store.py +0 -0
  44. {cc_transcript-9.0.0 → cc_transcript-9.1.0}/cc_transcript/models.py +0 -0
  45. {cc_transcript-9.0.0 → cc_transcript-9.1.0}/cc_transcript/parser.py +0 -0
  46. {cc_transcript-9.0.0 → cc_transcript-9.1.0}/cc_transcript/py.typed +0 -0
  47. {cc_transcript-9.0.0 → cc_transcript-9.1.0}/cc_transcript/render.py +0 -0
  48. {cc_transcript-9.0.0 → cc_transcript-9.1.0}/cc_transcript/rust.py +0 -0
  49. {cc_transcript-9.0.0 → cc_transcript-9.1.0}/cc_transcript/sentiment/__init__.py +0 -0
  50. {cc_transcript-9.0.0 → cc_transcript-9.1.0}/cc_transcript/sentiment/buckets.py +0 -0
  51. {cc_transcript-9.0.0 → cc_transcript-9.1.0}/cc_transcript/sentiment/engine.py +0 -0
  52. {cc_transcript-9.0.0 → cc_transcript-9.1.0}/cc_transcript/sentiment/lexicon.py +0 -0
  53. {cc_transcript-9.0.0 → cc_transcript-9.1.0}/cc_transcript/sentiment/scorespec.py +0 -0
  54. {cc_transcript-9.0.0 → cc_transcript-9.1.0}/cc_transcript/store.py +0 -0
  55. {cc_transcript-9.0.0 → cc_transcript-9.1.0}/cc_transcript/tools.py +0 -0
  56. {cc_transcript-9.0.0 → cc_transcript-9.1.0}/rust/Cargo.toml +0 -0
  57. {cc_transcript-9.0.0 → cc_transcript-9.1.0}/rust/data/afinn-en-165.tsv +0 -0
  58. {cc_transcript-9.0.0 → cc_transcript-9.1.0}/rust/data/domain_overrides.tsv +0 -0
  59. {cc_transcript-9.0.0 → cc_transcript-9.1.0}/rust/src/command.rs +0 -0
  60. {cc_transcript-9.0.0 → cc_transcript-9.1.0}/rust/src/event.rs +0 -0
  61. {cc_transcript-9.0.0 → cc_transcript-9.1.0}/rust/src/filter.rs +0 -0
  62. {cc_transcript-9.0.0 → cc_transcript-9.1.0}/rust/src/lexicon.rs +0 -0
  63. {cc_transcript-9.0.0 → cc_transcript-9.1.0}/rust/src/lib.rs +0 -0
  64. {cc_transcript-9.0.0 → cc_transcript-9.1.0}/rust/src/mining.rs +0 -0
  65. {cc_transcript-9.0.0 → cc_transcript-9.1.0}/rust/src/model.rs +0 -0
  66. {cc_transcript-9.0.0 → cc_transcript-9.1.0}/rust/src/protocol.rs +0 -0
  67. {cc_transcript-9.0.0 → cc_transcript-9.1.0}/rust/src/score.rs +0 -0
  68. {cc_transcript-9.0.0 → cc_transcript-9.1.0}/rust/src/value.rs +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: cc-transcript
3
- Version: 9.0.0
3
+ Version: 9.1.0
4
4
  Classifier: Development Status :: 3 - Alpha
5
5
  Classifier: Environment :: Console
6
6
  Classifier: Intended Audience :: Developers
@@ -47,7 +47,7 @@ Project-URL: Repository, https://github.com/yasyf/cc-transcript
47
47
 
48
48
  [![CI](https://github.com/yasyf/cc-transcript/actions/workflows/ci.yml/badge.svg)](https://github.com/yasyf/cc-transcript/actions/workflows/ci.yml)
49
49
  [![PyPI](https://img.shields.io/pypi/v/cc-transcript.svg)](https://pypi.org/project/cc-transcript/)
50
- [![License: PolyForm Noncommercial](https://img.shields.io/badge/license-PolyForm--Noncommercial--1.0.0-blue)](https://github.com/yasyf/cc-transcript/blob/main/LICENSE)
50
+ [![License PolyForm Noncommercial](https://img.shields.io/badge/license-PolyForm--Noncommercial--1.0.0-blue)](https://github.com/yasyf/cc-transcript/blob/main/LICENSE)
51
51
 
52
52
  ## Get started
53
53
 
@@ -55,9 +55,9 @@ Project-URL: Repository, https://github.com/yasyf/cc-transcript
55
55
  uvx cc-transcript list
56
56
  ```
57
57
 
58
- Every session is already on disk under `~/.claude/projects` `list` finds them newest first, and `stats` collapses one into a screenful:
58
+ Every session is already on disk under `~/.claude/projects`. `list` finds them newest first, and `stats` collapses one into a screenful:
59
59
 
60
- <img src="https://github.com/yasyf/cc-transcript/raw/main/docs/assets/demo.png" alt="Terminal running 'uvx cc-transcript stats' — a 1,023-event session summarized as counts, kinds, models, and tool names" width="700">
60
+ <img src="https://github.com/yasyf/cc-transcript/raw/main/docs/assets/demo.png" alt="Terminal running 'uvx cc-transcript stats' — one session summarized as counts, kinds, models, and tool names" width="700">
61
61
 
62
62
  Driving with an agent? Paste this:
63
63
 
@@ -66,7 +66,7 @@ Driving with an agent? Paste this:
66
66
  /plugin install cc-transcript@cc-transcript
67
67
  ```
68
68
 
69
- The plugin's skill teaches Claude to answer "what did I ask you yesterday?" from its own history, funneling through the CLI instead of reading raw JSONL.
69
+ The plugin's skill teaches Claude to answer "what did I ask you yesterday" from its own history, funneling through the CLI instead of reading raw JSONL.
70
70
 
71
71
  <details>
72
72
  <summary>No Claude Code? Point any agent at the CLI</summary>
@@ -92,7 +92,7 @@ Yesterday's session is a megabyte of JSONL, and pasting it into a chat blows the
92
92
  uvx cc-transcript show --signal --tail 20 ~/.claude/projects/<project>/<session>.jsonl
93
93
  ```
94
94
 
95
- `--signal` keeps only the substantive user and assistant turns one line per event, a few hundred tokens for the whole exchange. With the plugin installed, skip the command: ask Claude directly and its skill runs this funnel for you.
95
+ `--signal` keeps only the substantive user and assistant turns. One line per event, a few hundred tokens for the whole exchange. With the plugin installed, skip the command and ask Claude directly; its skill runs this funnel for you.
96
96
 
97
97
  ### Find the session where you fixed that bug
98
98
 
@@ -102,7 +102,7 @@ The fix happened weeks ago, in one of a few hundred transcripts. Search them all
102
102
  uvx cc-transcript grep "TranscriptExpiredError" --project cc-transcript
103
103
  ```
104
104
 
105
- Hits print under their transcript's path, each line carrying its raw event index feed an index back into `show --range` to re-read the conversation around the fix. `grep` searches your newest 50 transcripts by default; `--all` takes it to every session on disk, and a no-match run exits 1 so it scripts cleanly.
105
+ Hits print under their transcript's path, each line carrying its raw event index; feed an index back into `show --range` to re-read the conversation around the fix. `grep` searches your newest 50 transcripts by default; `--all` takes it to every session on disk, and a no-match run exits 1 so it scripts cleanly.
106
106
 
107
107
  ### Debug a token blowup from transcript evidence
108
108
 
@@ -112,16 +112,16 @@ A session burned through the context window and you want the culprit, not a gues
112
112
  uvx cc-transcript stats --per-file --project myapp
113
113
  ```
114
114
 
115
- Each block reports text, thinking, and tool-io bytes plus per-tool call counts the blowup stands out as an outsized `tool io` line. Then `grep --tool <name> --with-result` pins it to the exact calls.
115
+ Each block reports text, thinking, and tool-io bytes plus per-tool call counts; the blowup stands out as an outsized `tool io` line. Then `grep --tool <name> --with-result` pins it to the exact calls.
116
116
 
117
117
  ## More in the docs
118
118
 
119
- - **The Python library** raw JSONL bytes to typed events, a composed filter, and a score in a dozen lines — [getting started](https://yasyf.github.io/cc-transcript/docs/getting-started/index.html)
120
- - **Filtering events** — composable clauses, specs, and the ready-made `NOISE_SPEC` — [builder catalog](https://yasyf.github.io/cc-transcript/docs/guide/filtering-events.html)
121
- - **Sentiment scoring** — bucket conversations and score them around any inference engine — [score specs](https://yasyf.github.io/cc-transcript/docs/guide/scoring-sentiment.html)
122
- - **Feedback mining** detectors, confidence calibration, and LLM verdict passes over your corpus — [mine your sessions](https://yasyf.github.io/cc-transcript/docs/guide/mining-feedback.html)
123
- - **Two backends, one protocol** a Rust fast path and a Python reference, parity-asserted against each other — [how parity works](https://yasyf.github.io/cc-transcript/docs/guide/backends-and-parity.html)
124
- - **API reference** the complete typed surface, from `TranscriptEvent` to `SessionActivity` — [reference](https://yasyf.github.io/cc-transcript/reference/index.html)
119
+ - [The Python library](https://yasyf.github.io/cc-transcript/docs/getting-started/index.html) turns raw JSONL bytes into typed events, a composed filter, and a score in a dozen lines.
120
+ - [Filtering events](https://yasyf.github.io/cc-transcript/docs/guide/filtering-events.html) covers composable clauses, specs, and the ready-made `NOISE_SPEC`.
121
+ - [Sentiment scoring](https://yasyf.github.io/cc-transcript/docs/guide/scoring-sentiment.html) buckets conversations and scores them around any inference engine.
122
+ - [Feedback mining](https://yasyf.github.io/cc-transcript/docs/guide/mining-feedback.html) runs detectors, confidence calibration, and LLM verdict passes over your corpus.
123
+ - [Two backends, one protocol](https://yasyf.github.io/cc-transcript/docs/guide/backends-and-parity.html) pairs a Rust fast path with a Python reference, parity-asserted against each other.
124
+ - [API reference](https://yasyf.github.io/cc-transcript/reference/index.html) documents the complete typed surface, from `TranscriptEvent` to `SessionActivity`.
125
125
 
126
126
  Read the [docs](https://yasyf.github.io/cc-transcript/) for the full guide. Licensed under [PolyForm Noncommercial 1.0.0](https://github.com/yasyf/cc-transcript/blob/main/LICENSE).
127
127
 
@@ -4,7 +4,7 @@
4
4
 
5
5
  [![CI](https://github.com/yasyf/cc-transcript/actions/workflows/ci.yml/badge.svg)](https://github.com/yasyf/cc-transcript/actions/workflows/ci.yml)
6
6
  [![PyPI](https://img.shields.io/pypi/v/cc-transcript.svg)](https://pypi.org/project/cc-transcript/)
7
- [![License: PolyForm Noncommercial](https://img.shields.io/badge/license-PolyForm--Noncommercial--1.0.0-blue)](https://github.com/yasyf/cc-transcript/blob/main/LICENSE)
7
+ [![License PolyForm Noncommercial](https://img.shields.io/badge/license-PolyForm--Noncommercial--1.0.0-blue)](https://github.com/yasyf/cc-transcript/blob/main/LICENSE)
8
8
 
9
9
  ## Get started
10
10
 
@@ -12,9 +12,9 @@
12
12
  uvx cc-transcript list
13
13
  ```
14
14
 
15
- Every session is already on disk under `~/.claude/projects` `list` finds them newest first, and `stats` collapses one into a screenful:
15
+ Every session is already on disk under `~/.claude/projects`. `list` finds them newest first, and `stats` collapses one into a screenful:
16
16
 
17
- <img src="https://github.com/yasyf/cc-transcript/raw/main/docs/assets/demo.png" alt="Terminal running 'uvx cc-transcript stats' — a 1,023-event session summarized as counts, kinds, models, and tool names" width="700">
17
+ <img src="https://github.com/yasyf/cc-transcript/raw/main/docs/assets/demo.png" alt="Terminal running 'uvx cc-transcript stats' — one session summarized as counts, kinds, models, and tool names" width="700">
18
18
 
19
19
  Driving with an agent? Paste this:
20
20
 
@@ -23,7 +23,7 @@ Driving with an agent? Paste this:
23
23
  /plugin install cc-transcript@cc-transcript
24
24
  ```
25
25
 
26
- The plugin's skill teaches Claude to answer "what did I ask you yesterday?" from its own history, funneling through the CLI instead of reading raw JSONL.
26
+ The plugin's skill teaches Claude to answer "what did I ask you yesterday" from its own history, funneling through the CLI instead of reading raw JSONL.
27
27
 
28
28
  <details>
29
29
  <summary>No Claude Code? Point any agent at the CLI</summary>
@@ -49,7 +49,7 @@ Yesterday's session is a megabyte of JSONL, and pasting it into a chat blows the
49
49
  uvx cc-transcript show --signal --tail 20 ~/.claude/projects/<project>/<session>.jsonl
50
50
  ```
51
51
 
52
- `--signal` keeps only the substantive user and assistant turns one line per event, a few hundred tokens for the whole exchange. With the plugin installed, skip the command: ask Claude directly and its skill runs this funnel for you.
52
+ `--signal` keeps only the substantive user and assistant turns. One line per event, a few hundred tokens for the whole exchange. With the plugin installed, skip the command and ask Claude directly; its skill runs this funnel for you.
53
53
 
54
54
  ### Find the session where you fixed that bug
55
55
 
@@ -59,7 +59,7 @@ The fix happened weeks ago, in one of a few hundred transcripts. Search them all
59
59
  uvx cc-transcript grep "TranscriptExpiredError" --project cc-transcript
60
60
  ```
61
61
 
62
- Hits print under their transcript's path, each line carrying its raw event index feed an index back into `show --range` to re-read the conversation around the fix. `grep` searches your newest 50 transcripts by default; `--all` takes it to every session on disk, and a no-match run exits 1 so it scripts cleanly.
62
+ Hits print under their transcript's path, each line carrying its raw event index; feed an index back into `show --range` to re-read the conversation around the fix. `grep` searches your newest 50 transcripts by default; `--all` takes it to every session on disk, and a no-match run exits 1 so it scripts cleanly.
63
63
 
64
64
  ### Debug a token blowup from transcript evidence
65
65
 
@@ -69,15 +69,15 @@ A session burned through the context window and you want the culprit, not a gues
69
69
  uvx cc-transcript stats --per-file --project myapp
70
70
  ```
71
71
 
72
- Each block reports text, thinking, and tool-io bytes plus per-tool call counts the blowup stands out as an outsized `tool io` line. Then `grep --tool <name> --with-result` pins it to the exact calls.
72
+ Each block reports text, thinking, and tool-io bytes plus per-tool call counts; the blowup stands out as an outsized `tool io` line. Then `grep --tool <name> --with-result` pins it to the exact calls.
73
73
 
74
74
  ## More in the docs
75
75
 
76
- - **The Python library** raw JSONL bytes to typed events, a composed filter, and a score in a dozen lines — [getting started](https://yasyf.github.io/cc-transcript/docs/getting-started/index.html)
77
- - **Filtering events** — composable clauses, specs, and the ready-made `NOISE_SPEC` — [builder catalog](https://yasyf.github.io/cc-transcript/docs/guide/filtering-events.html)
78
- - **Sentiment scoring** — bucket conversations and score them around any inference engine — [score specs](https://yasyf.github.io/cc-transcript/docs/guide/scoring-sentiment.html)
79
- - **Feedback mining** detectors, confidence calibration, and LLM verdict passes over your corpus — [mine your sessions](https://yasyf.github.io/cc-transcript/docs/guide/mining-feedback.html)
80
- - **Two backends, one protocol** a Rust fast path and a Python reference, parity-asserted against each other — [how parity works](https://yasyf.github.io/cc-transcript/docs/guide/backends-and-parity.html)
81
- - **API reference** the complete typed surface, from `TranscriptEvent` to `SessionActivity` — [reference](https://yasyf.github.io/cc-transcript/reference/index.html)
76
+ - [The Python library](https://yasyf.github.io/cc-transcript/docs/getting-started/index.html) turns raw JSONL bytes into typed events, a composed filter, and a score in a dozen lines.
77
+ - [Filtering events](https://yasyf.github.io/cc-transcript/docs/guide/filtering-events.html) covers composable clauses, specs, and the ready-made `NOISE_SPEC`.
78
+ - [Sentiment scoring](https://yasyf.github.io/cc-transcript/docs/guide/scoring-sentiment.html) buckets conversations and scores them around any inference engine.
79
+ - [Feedback mining](https://yasyf.github.io/cc-transcript/docs/guide/mining-feedback.html) runs detectors, confidence calibration, and LLM verdict passes over your corpus.
80
+ - [Two backends, one protocol](https://yasyf.github.io/cc-transcript/docs/guide/backends-and-parity.html) pairs a Rust fast path with a Python reference, parity-asserted against each other.
81
+ - [API reference](https://yasyf.github.io/cc-transcript/reference/index.html) documents the complete typed surface, from `TranscriptEvent` to `SessionActivity`.
82
82
 
83
83
  Read the [docs](https://yasyf.github.io/cc-transcript/) for the full guide. Licensed under [PolyForm Noncommercial 1.0.0](https://github.com/yasyf/cc-transcript/blob/main/LICENSE).
@@ -158,6 +158,7 @@ EXPORTS: dict[str, str] = {
158
158
  "capture_window",
159
159
  ),
160
160
  "cc_transcript.render": ("Budget", "render_session", "render_tool_call", "render_turn"),
161
+ "cc_transcript.notifications": ("Notifications",),
161
162
  "cc_transcript.query": ("FileRef", "Session", "SubagentIndex", "SubagentSession", "ToolCallQuery"),
162
163
  "cc_transcript.decisions": ("DECISIONS_DDL", "Action", "Decision", "DecisionLog"),
163
164
  "cc_transcript.disktruth": (
@@ -554,6 +555,7 @@ if TYPE_CHECKING:
554
555
  from cc_transcript.models import (
555
556
  tool_uses as tool_uses,
556
557
  )
558
+ from cc_transcript.notifications import Notifications as Notifications
557
559
  from cc_transcript.parser import (
558
560
  TranscriptParser as TranscriptParser,
559
561
  )
@@ -11,6 +11,7 @@ needs no extra installed.
11
11
 
12
12
  from __future__ import annotations
13
13
 
14
+ import json
14
15
  from functools import cache
15
16
  from typing import TYPE_CHECKING
16
17
 
@@ -69,7 +70,7 @@ def structured_judge[M: BaseModel](
69
70
  async def judge(prompt: str) -> M:
70
71
  try:
71
72
  return await extract(prompt, response_model, backend=default_backend(), model=tier, timeout=timeout)
72
- except (BackendCallError, BackendUnavailable, TimeoutError, ValidationError) as error:
73
+ except (BackendCallError, BackendUnavailable, TimeoutError, ValidationError, json.JSONDecodeError) as error:
73
74
  raise JudgeError(str(error)) from error
74
75
 
75
76
  return judge
@@ -0,0 +1,115 @@
1
+ """The harness notification-delivery queue, replayed from a transcript.
2
+
3
+ Claude Code enqueues background-task notifications (and queued user commands)
4
+ into a FIFO, then drains it as each item reaches the agent, recording every
5
+ ``enqueue``/``dequeue``/``remove``/``popAll`` as a ``queue-operation`` audit
6
+ entry. :class:`Notifications` replays that audit trail over a session's events
7
+ to model which notifications are still queued, which were delivered, and which
8
+ ever passed through — so a hook can ask whether a given background tool call has
9
+ already been reported to the agent.
10
+ """
11
+
12
+ from __future__ import annotations
13
+
14
+ from dataclasses import dataclass
15
+ from typing import TYPE_CHECKING
16
+
17
+ from cc_transcript.models import OtherEvent, UserEvent
18
+
19
+ if TYPE_CHECKING:
20
+ from collections.abc import Sequence
21
+
22
+ from cc_transcript.models import TranscriptEvent
23
+
24
+ NOTIFICATION_MARKER = "<task-notification>"
25
+
26
+
27
+ def tool_use_marker(tool_use_id: str) -> str:
28
+ return f"<tool-use-id>{tool_use_id}</tool-use-id>"
29
+
30
+
31
+ def delivered_text(event: TranscriptEvent) -> str | None:
32
+ match event:
33
+ case UserEvent(text=text) if NOTIFICATION_MARKER in text:
34
+ return text
35
+ case OtherEvent(type="attachment", raw=raw) if (attachment := raw.get("attachment") or {}).get(
36
+ "type"
37
+ ) == "queued_command":
38
+ return str(attachment.get("prompt", ""))
39
+ case _:
40
+ return None
41
+
42
+
43
+ def replay_queue(events: Sequence[TranscriptEvent]) -> tuple[tuple[str, ...], tuple[str, ...]]:
44
+ queued: list[str] = []
45
+ enqueued: list[str] = []
46
+ for event in events:
47
+ if not (isinstance(event, OtherEvent) and event.type == "queue-operation"):
48
+ continue
49
+ match event.raw.get("operation"):
50
+ case "enqueue":
51
+ enqueued.append(content := str(event.raw.get("content", "")))
52
+ queued.append(content)
53
+ case "dequeue" | "remove" if queued:
54
+ queued.pop(0)
55
+ case "popAll":
56
+ content = str(event.raw.get("content", ""))
57
+ queued = [item for item in queued if item not in content]
58
+ return tuple(queued), tuple(enqueued)
59
+
60
+
61
+ @dataclass(frozen=True, slots=True)
62
+ class Notifications:
63
+ """The modeled state of a session's harness notification-delivery queue.
64
+
65
+ Reconstructed by replaying the transcript's ``queue-operation`` audit
66
+ records in order: each ``enqueue`` appends its content to a FIFO, each
67
+ ``dequeue``/``remove`` drops the head, and each ``popAll`` subtracts every
68
+ queued item whose text is contained in the operation's content — a queued
69
+ user command drains without taking an unrelated notification with it.
70
+
71
+ Attributes:
72
+ queued: The enqueue contents still in the modeled FIFO, undelivered.
73
+ delivered: The notification texts that actually reached the agent —
74
+ user turns carrying a ``<task-notification>`` plus every queued
75
+ command replayed to the model.
76
+ enqueued: Every enqueue content ever observed, in order.
77
+
78
+ Example:
79
+ >>> session.notifications.completed("toolu_01XVXcp6yKvn2xbmPxdf1a3z")
80
+ True
81
+ """
82
+
83
+ queued: tuple[str, ...]
84
+ delivered: tuple[str, ...]
85
+ enqueued: tuple[str, ...]
86
+
87
+ @classmethod
88
+ def from_events(cls, events: Sequence[TranscriptEvent]) -> Notifications:
89
+ """Replays the notification queue over ``events``, in order."""
90
+ queued, enqueued = replay_queue(events)
91
+ return cls(
92
+ queued=queued,
93
+ delivered=tuple(text for event in events if (text := delivered_text(event)) is not None),
94
+ enqueued=enqueued,
95
+ )
96
+
97
+ def completed(self, tool_use_id: str) -> bool:
98
+ """Whether the tool call's notification has reached the agent.
99
+
100
+ True when the notification was delivered, or when it was enqueued at
101
+ some point yet no longer sits undelivered in the queue.
102
+ """
103
+ marker = tool_use_marker(tool_use_id)
104
+ return any(marker in text for text in self.delivered) or (
105
+ any(marker in text for text in self.enqueued) and not any(marker in text for text in self.queued)
106
+ )
107
+
108
+ def pending(self, tool_use_id: str) -> bool:
109
+ """Whether the tool call's notification is still queued for delivery."""
110
+ return any(tool_use_marker(tool_use_id) in text for text in self.queued)
111
+
112
+ @property
113
+ def has_pending(self) -> bool:
114
+ """Whether any queued item is an undelivered task notification."""
115
+ return any(NOTIFICATION_MARKER in text for text in self.queued)
@@ -22,6 +22,7 @@ from cc_transcript.discovery import TranscriptExpiredError, find_transcript, sub
22
22
  from cc_transcript.filterspec import event_meta, session_id_of
23
23
  from cc_transcript.ids import SessionId
24
24
  from cc_transcript.models import AssistantEvent, SystemEvent, ToolResultBlock, UserEvent
25
+ from cc_transcript.notifications import Notifications
25
26
  from cc_transcript.parser import parse_events_async, parse_events_from_bytes
26
27
  from cc_transcript.tools import BashCall, SkillCall, TaskCall, file_path_of, hunks_of, tool_name_matches
27
28
 
@@ -288,6 +289,11 @@ class Session:
288
289
  """The window's tool calls as a chainable query."""
289
290
  return ToolCallQuery(tuple(use for turn in self.turns for use in turn.tool_uses))
290
291
 
292
+ @property
293
+ def notifications(self) -> Notifications:
294
+ """The harness notification-delivery queue replayed over the window's events."""
295
+ return Notifications.from_events(self.events)
296
+
291
297
  @property
292
298
  def subagents(self) -> SubagentIndex:
293
299
  """The window's Task dispatches whose sidechain transcripts exist on disk."""
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "cc-transcript"
3
- version = "9.0.0"
3
+ version = "9.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"
File without changes
File without changes
File without changes