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.
- {cc_transcript-9.0.0 → cc_transcript-9.1.0}/PKG-INFO +14 -14
- {cc_transcript-9.0.0 → cc_transcript-9.1.0}/README.md +13 -13
- {cc_transcript-9.0.0 → cc_transcript-9.1.0}/cc_transcript/__init__.py +2 -0
- {cc_transcript-9.0.0 → cc_transcript-9.1.0}/cc_transcript/judge/llm.py +2 -1
- cc_transcript-9.1.0/cc_transcript/notifications.py +115 -0
- {cc_transcript-9.0.0 → cc_transcript-9.1.0}/cc_transcript/query.py +6 -0
- {cc_transcript-9.0.0 → cc_transcript-9.1.0}/pyproject.toml +1 -1
- {cc_transcript-9.0.0 → cc_transcript-9.1.0}/Cargo.lock +0 -0
- {cc_transcript-9.0.0 → cc_transcript-9.1.0}/Cargo.toml +0 -0
- {cc_transcript-9.0.0 → cc_transcript-9.1.0}/LICENSE +0 -0
- {cc_transcript-9.0.0 → cc_transcript-9.1.0}/cc_transcript/__main__.py +0 -0
- {cc_transcript-9.0.0 → cc_transcript-9.1.0}/cc_transcript/_parser_rs.pyi +0 -0
- {cc_transcript-9.0.0 → cc_transcript-9.1.0}/cc_transcript/activity.py +0 -0
- {cc_transcript-9.0.0 → cc_transcript-9.1.0}/cc_transcript/backend.py +0 -0
- {cc_transcript-9.0.0 → cc_transcript-9.1.0}/cc_transcript/builders.py +0 -0
- {cc_transcript-9.0.0 → cc_transcript-9.1.0}/cc_transcript/cli.py +0 -0
- {cc_transcript-9.0.0 → cc_transcript-9.1.0}/cc_transcript/command.py +0 -0
- {cc_transcript-9.0.0 → cc_transcript-9.1.0}/cc_transcript/context.py +0 -0
- {cc_transcript-9.0.0 → cc_transcript-9.1.0}/cc_transcript/corrections.py +0 -0
- {cc_transcript-9.0.0 → cc_transcript-9.1.0}/cc_transcript/corrections_cli.py +0 -0
- {cc_transcript-9.0.0 → cc_transcript-9.1.0}/cc_transcript/cost.py +0 -0
- {cc_transcript-9.0.0 → cc_transcript-9.1.0}/cc_transcript/decisions.py +0 -0
- {cc_transcript-9.0.0 → cc_transcript-9.1.0}/cc_transcript/discovery.py +0 -0
- {cc_transcript-9.0.0 → cc_transcript-9.1.0}/cc_transcript/disktruth.py +0 -0
- {cc_transcript-9.0.0 → cc_transcript-9.1.0}/cc_transcript/evidence.py +0 -0
- {cc_transcript-9.0.0 → cc_transcript-9.1.0}/cc_transcript/extract/__init__.py +0 -0
- {cc_transcript-9.0.0 → cc_transcript-9.1.0}/cc_transcript/extract/correct.py +0 -0
- {cc_transcript-9.0.0 → cc_transcript-9.1.0}/cc_transcript/facts.py +0 -0
- {cc_transcript-9.0.0 → cc_transcript-9.1.0}/cc_transcript/filterspec.py +0 -0
- {cc_transcript-9.0.0 → cc_transcript-9.1.0}/cc_transcript/ids.py +0 -0
- {cc_transcript-9.0.0 → cc_transcript-9.1.0}/cc_transcript/judge/__init__.py +0 -0
- {cc_transcript-9.0.0 → cc_transcript-9.1.0}/cc_transcript/judge/similar.py +0 -0
- {cc_transcript-9.0.0 → cc_transcript-9.1.0}/cc_transcript/judge/verdicts.py +0 -0
- {cc_transcript-9.0.0 → cc_transcript-9.1.0}/cc_transcript/mining/__init__.py +0 -0
- {cc_transcript-9.0.0 → cc_transcript-9.1.0}/cc_transcript/mining/candidates.py +0 -0
- {cc_transcript-9.0.0 → cc_transcript-9.1.0}/cc_transcript/mining/confidence.py +0 -0
- {cc_transcript-9.0.0 → cc_transcript-9.1.0}/cc_transcript/mining/engine.py +0 -0
- {cc_transcript-9.0.0 → cc_transcript-9.1.0}/cc_transcript/mining/filterspec.py +0 -0
- {cc_transcript-9.0.0 → cc_transcript-9.1.0}/cc_transcript/mining/formats.py +0 -0
- {cc_transcript-9.0.0 → cc_transcript-9.1.0}/cc_transcript/mining/signals.py +0 -0
- {cc_transcript-9.0.0 → cc_transcript-9.1.0}/cc_transcript/mining/sourcekind.py +0 -0
- {cc_transcript-9.0.0 → cc_transcript-9.1.0}/cc_transcript/mining/spec.py +0 -0
- {cc_transcript-9.0.0 → cc_transcript-9.1.0}/cc_transcript/mining/store.py +0 -0
- {cc_transcript-9.0.0 → cc_transcript-9.1.0}/cc_transcript/models.py +0 -0
- {cc_transcript-9.0.0 → cc_transcript-9.1.0}/cc_transcript/parser.py +0 -0
- {cc_transcript-9.0.0 → cc_transcript-9.1.0}/cc_transcript/py.typed +0 -0
- {cc_transcript-9.0.0 → cc_transcript-9.1.0}/cc_transcript/render.py +0 -0
- {cc_transcript-9.0.0 → cc_transcript-9.1.0}/cc_transcript/rust.py +0 -0
- {cc_transcript-9.0.0 → cc_transcript-9.1.0}/cc_transcript/sentiment/__init__.py +0 -0
- {cc_transcript-9.0.0 → cc_transcript-9.1.0}/cc_transcript/sentiment/buckets.py +0 -0
- {cc_transcript-9.0.0 → cc_transcript-9.1.0}/cc_transcript/sentiment/engine.py +0 -0
- {cc_transcript-9.0.0 → cc_transcript-9.1.0}/cc_transcript/sentiment/lexicon.py +0 -0
- {cc_transcript-9.0.0 → cc_transcript-9.1.0}/cc_transcript/sentiment/scorespec.py +0 -0
- {cc_transcript-9.0.0 → cc_transcript-9.1.0}/cc_transcript/store.py +0 -0
- {cc_transcript-9.0.0 → cc_transcript-9.1.0}/cc_transcript/tools.py +0 -0
- {cc_transcript-9.0.0 → cc_transcript-9.1.0}/rust/Cargo.toml +0 -0
- {cc_transcript-9.0.0 → cc_transcript-9.1.0}/rust/data/afinn-en-165.tsv +0 -0
- {cc_transcript-9.0.0 → cc_transcript-9.1.0}/rust/data/domain_overrides.tsv +0 -0
- {cc_transcript-9.0.0 → cc_transcript-9.1.0}/rust/src/command.rs +0 -0
- {cc_transcript-9.0.0 → cc_transcript-9.1.0}/rust/src/event.rs +0 -0
- {cc_transcript-9.0.0 → cc_transcript-9.1.0}/rust/src/filter.rs +0 -0
- {cc_transcript-9.0.0 → cc_transcript-9.1.0}/rust/src/lexicon.rs +0 -0
- {cc_transcript-9.0.0 → cc_transcript-9.1.0}/rust/src/lib.rs +0 -0
- {cc_transcript-9.0.0 → cc_transcript-9.1.0}/rust/src/mining.rs +0 -0
- {cc_transcript-9.0.0 → cc_transcript-9.1.0}/rust/src/model.rs +0 -0
- {cc_transcript-9.0.0 → cc_transcript-9.1.0}/rust/src/protocol.rs +0 -0
- {cc_transcript-9.0.0 → cc_transcript-9.1.0}/rust/src/score.rs +0 -0
- {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.
|
|
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
|
[](https://github.com/yasyf/cc-transcript/actions/workflows/ci.yml)
|
|
49
49
|
[](https://pypi.org/project/cc-transcript/)
|
|
50
|
-
[](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
|
|
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' —
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
-
-
|
|
120
|
-
-
|
|
121
|
-
-
|
|
122
|
-
-
|
|
123
|
-
-
|
|
124
|
-
-
|
|
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
|
[](https://github.com/yasyf/cc-transcript/actions/workflows/ci.yml)
|
|
6
6
|
[](https://pypi.org/project/cc-transcript/)
|
|
7
|
-
[](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
|
|
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' —
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
-
-
|
|
77
|
-
-
|
|
78
|
-
-
|
|
79
|
-
-
|
|
80
|
-
-
|
|
81
|
-
-
|
|
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."""
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|