cc-transcript 14.16.1__tar.gz → 14.18.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {cc_transcript-14.16.1 → cc_transcript-14.18.0}/PKG-INFO +1 -1
- {cc_transcript-14.16.1 → cc_transcript-14.18.0}/cc_transcript/__init__.py +2 -0
- {cc_transcript-14.16.1 → cc_transcript-14.18.0}/cc_transcript/__init__.pyi +4 -0
- {cc_transcript-14.16.1 → cc_transcript-14.18.0}/cc_transcript/_native.pyi +3 -0
- {cc_transcript-14.16.1 → cc_transcript-14.18.0}/cc_transcript/activity.py +174 -60
- {cc_transcript-14.16.1 → cc_transcript-14.18.0}/cc_transcript/query.py +221 -32
- {cc_transcript-14.16.1 → cc_transcript-14.18.0}/pyproject.toml +1 -1
- {cc_transcript-14.16.1 → cc_transcript-14.18.0}/rust/crates/core/benches/parse.rs +10 -1
- {cc_transcript-14.16.1 → cc_transcript-14.18.0}/rust/crates/core/src/activity.rs +90 -7
- {cc_transcript-14.16.1 → cc_transcript-14.18.0}/rust/crates/py/src/python.rs +68 -22
- {cc_transcript-14.16.1 → cc_transcript-14.18.0}/Cargo.lock +0 -0
- {cc_transcript-14.16.1 → cc_transcript-14.18.0}/Cargo.toml +0 -0
- {cc_transcript-14.16.1 → cc_transcript-14.18.0}/LICENSE +0 -0
- {cc_transcript-14.16.1 → cc_transcript-14.18.0}/README.md +0 -0
- {cc_transcript-14.16.1 → cc_transcript-14.18.0}/cc_transcript/activity_probe.py +0 -0
- {cc_transcript-14.16.1 → cc_transcript-14.18.0}/cc_transcript/builders.py +0 -0
- {cc_transcript-14.16.1 → cc_transcript-14.18.0}/cc_transcript/codex.py +0 -0
- {cc_transcript-14.16.1 → cc_transcript-14.18.0}/cc_transcript/command.py +0 -0
- {cc_transcript-14.16.1 → cc_transcript-14.18.0}/cc_transcript/context.py +0 -0
- {cc_transcript-14.16.1 → cc_transcript-14.18.0}/cc_transcript/corrections.py +0 -0
- {cc_transcript-14.16.1 → cc_transcript-14.18.0}/cc_transcript/cost.py +0 -0
- {cc_transcript-14.16.1 → cc_transcript-14.18.0}/cc_transcript/decision_store.py +0 -0
- {cc_transcript-14.16.1 → cc_transcript-14.18.0}/cc_transcript/decisions.py +0 -0
- {cc_transcript-14.16.1 → cc_transcript-14.18.0}/cc_transcript/discovery.py +0 -0
- {cc_transcript-14.16.1 → cc_transcript-14.18.0}/cc_transcript/disktruth.py +0 -0
- {cc_transcript-14.16.1 → cc_transcript-14.18.0}/cc_transcript/evidence.py +0 -0
- {cc_transcript-14.16.1 → cc_transcript-14.18.0}/cc_transcript/extract/__init__.py +0 -0
- {cc_transcript-14.16.1 → cc_transcript-14.18.0}/cc_transcript/extract/correct.py +0 -0
- {cc_transcript-14.16.1 → cc_transcript-14.18.0}/cc_transcript/facts.py +0 -0
- {cc_transcript-14.16.1 → cc_transcript-14.18.0}/cc_transcript/filterspec.py +0 -0
- {cc_transcript-14.16.1 → cc_transcript-14.18.0}/cc_transcript/heartbeats.py +0 -0
- {cc_transcript-14.16.1 → cc_transcript-14.18.0}/cc_transcript/ids.py +0 -0
- {cc_transcript-14.16.1 → cc_transcript-14.18.0}/cc_transcript/judge/__init__.py +0 -0
- {cc_transcript-14.16.1 → cc_transcript-14.18.0}/cc_transcript/judge/llm.py +0 -0
- {cc_transcript-14.16.1 → cc_transcript-14.18.0}/cc_transcript/judge/similar.py +0 -0
- {cc_transcript-14.16.1 → cc_transcript-14.18.0}/cc_transcript/judge/verdicts.py +0 -0
- {cc_transcript-14.16.1 → cc_transcript-14.18.0}/cc_transcript/ledger.py +0 -0
- {cc_transcript-14.16.1 → cc_transcript-14.18.0}/cc_transcript/literals.py +0 -0
- {cc_transcript-14.16.1 → cc_transcript-14.18.0}/cc_transcript/mining/__init__.py +0 -0
- {cc_transcript-14.16.1 → cc_transcript-14.18.0}/cc_transcript/mining/candidates.py +0 -0
- {cc_transcript-14.16.1 → cc_transcript-14.18.0}/cc_transcript/mining/confidence.py +0 -0
- {cc_transcript-14.16.1 → cc_transcript-14.18.0}/cc_transcript/mining/engine.py +0 -0
- {cc_transcript-14.16.1 → cc_transcript-14.18.0}/cc_transcript/mining/formats.py +0 -0
- {cc_transcript-14.16.1 → cc_transcript-14.18.0}/cc_transcript/mining/sampling.py +0 -0
- {cc_transcript-14.16.1 → cc_transcript-14.18.0}/cc_transcript/mining/signals.py +0 -0
- {cc_transcript-14.16.1 → cc_transcript-14.18.0}/cc_transcript/mining/sourcekind.py +0 -0
- {cc_transcript-14.16.1 → cc_transcript-14.18.0}/cc_transcript/mining/spec.py +0 -0
- {cc_transcript-14.16.1 → cc_transcript-14.18.0}/cc_transcript/mining/store.py +0 -0
- {cc_transcript-14.16.1 → cc_transcript-14.18.0}/cc_transcript/models.py +0 -0
- {cc_transcript-14.16.1 → cc_transcript-14.18.0}/cc_transcript/nlp.py +0 -0
- {cc_transcript-14.16.1 → cc_transcript-14.18.0}/cc_transcript/notifications.py +0 -0
- {cc_transcript-14.16.1 → cc_transcript-14.18.0}/cc_transcript/parser.py +0 -0
- {cc_transcript-14.16.1 → cc_transcript-14.18.0}/cc_transcript/py.typed +0 -0
- {cc_transcript-14.16.1 → cc_transcript-14.18.0}/cc_transcript/render.py +0 -0
- {cc_transcript-14.16.1 → cc_transcript-14.18.0}/cc_transcript/sentiment/__init__.py +0 -0
- {cc_transcript-14.16.1 → cc_transcript-14.18.0}/cc_transcript/sentiment/buckets.py +0 -0
- {cc_transcript-14.16.1 → cc_transcript-14.18.0}/cc_transcript/sentiment/data/afinn-en-165.tsv +0 -0
- {cc_transcript-14.16.1 → cc_transcript-14.18.0}/cc_transcript/sentiment/data/domain_overrides.tsv +0 -0
- {cc_transcript-14.16.1 → cc_transcript-14.18.0}/cc_transcript/sentiment/data/en-ewt.udpipe +0 -0
- {cc_transcript-14.16.1 → cc_transcript-14.18.0}/cc_transcript/sentiment/engine.py +0 -0
- {cc_transcript-14.16.1 → cc_transcript-14.18.0}/cc_transcript/sentiment/lexicon.py +0 -0
- {cc_transcript-14.16.1 → cc_transcript-14.18.0}/cc_transcript/sentiment/scorespec.py +0 -0
- {cc_transcript-14.16.1 → cc_transcript-14.18.0}/cc_transcript/synthetic.py +0 -0
- {cc_transcript-14.16.1 → cc_transcript-14.18.0}/cc_transcript/tools.py +0 -0
- {cc_transcript-14.16.1 → cc_transcript-14.18.0}/cc_transcript/watch.py +0 -0
- {cc_transcript-14.16.1 → cc_transcript-14.18.0}/rust/crates/cli/Cargo.toml +0 -0
- {cc_transcript-14.16.1 → cc_transcript-14.18.0}/rust/crates/cli/src/commands/blame.rs +0 -0
- {cc_transcript-14.16.1 → cc_transcript-14.18.0}/rust/crates/cli/src/commands/corrections.rs +0 -0
- {cc_transcript-14.16.1 → cc_transcript-14.18.0}/rust/crates/cli/src/commands/digest.rs +0 -0
- {cc_transcript-14.16.1 → cc_transcript-14.18.0}/rust/crates/cli/src/commands/facts.rs +0 -0
- {cc_transcript-14.16.1 → cc_transcript-14.18.0}/rust/crates/cli/src/commands/grep.rs +0 -0
- {cc_transcript-14.16.1 → cc_transcript-14.18.0}/rust/crates/cli/src/commands/list.rs +0 -0
- {cc_transcript-14.16.1 → cc_transcript-14.18.0}/rust/crates/cli/src/commands/mod.rs +0 -0
- {cc_transcript-14.16.1 → cc_transcript-14.18.0}/rust/crates/cli/src/commands/scratchpad.rs +0 -0
- {cc_transcript-14.16.1 → cc_transcript-14.18.0}/rust/crates/cli/src/commands/show.rs +0 -0
- {cc_transcript-14.16.1 → cc_transcript-14.18.0}/rust/crates/cli/src/commands/slice.rs +0 -0
- {cc_transcript-14.16.1 → cc_transcript-14.18.0}/rust/crates/cli/src/commands/stats.rs +0 -0
- {cc_transcript-14.16.1 → cc_transcript-14.18.0}/rust/crates/cli/src/commands/watch.rs +0 -0
- {cc_transcript-14.16.1 → cc_transcript-14.18.0}/rust/crates/cli/src/lib.rs +0 -0
- {cc_transcript-14.16.1 → cc_transcript-14.18.0}/rust/crates/cli/src/main.rs +0 -0
- {cc_transcript-14.16.1 → cc_transcript-14.18.0}/rust/crates/cli/src/output.rs +0 -0
- {cc_transcript-14.16.1 → cc_transcript-14.18.0}/rust/crates/cli/src/target.rs +0 -0
- {cc_transcript-14.16.1 → cc_transcript-14.18.0}/rust/crates/cli/src/timearg.rs +0 -0
- {cc_transcript-14.16.1 → cc_transcript-14.18.0}/rust/crates/core/Cargo.toml +0 -0
- {cc_transcript-14.16.1 → cc_transcript-14.18.0}/rust/crates/core/benches/render.rs +0 -0
- {cc_transcript-14.16.1 → cc_transcript-14.18.0}/rust/crates/core/benches/toolcall.rs +0 -0
- {cc_transcript-14.16.1 → cc_transcript-14.18.0}/rust/crates/core/src/actor.rs +0 -0
- {cc_transcript-14.16.1 → cc_transcript-14.18.0}/rust/crates/core/src/blame.rs +0 -0
- {cc_transcript-14.16.1 → cc_transcript-14.18.0}/rust/crates/core/src/buckets.rs +0 -0
- {cc_transcript-14.16.1 → cc_transcript-14.18.0}/rust/crates/core/src/codex/discovery.rs +0 -0
- {cc_transcript-14.16.1 → cc_transcript-14.18.0}/rust/crates/core/src/codex/lower.rs +0 -0
- {cc_transcript-14.16.1 → cc_transcript-14.18.0}/rust/crates/core/src/codex/mod.rs +0 -0
- {cc_transcript-14.16.1 → cc_transcript-14.18.0}/rust/crates/core/src/codex/parse.rs +0 -0
- {cc_transcript-14.16.1 → cc_transcript-14.18.0}/rust/crates/core/src/codex/probe.rs +0 -0
- {cc_transcript-14.16.1 → cc_transcript-14.18.0}/rust/crates/core/src/codex/protocol.rs +0 -0
- {cc_transcript-14.16.1 → cc_transcript-14.18.0}/rust/crates/core/src/codex/types.rs +0 -0
- {cc_transcript-14.16.1 → cc_transcript-14.18.0}/rust/crates/core/src/command.rs +0 -0
- {cc_transcript-14.16.1 → cc_transcript-14.18.0}/rust/crates/core/src/context.rs +0 -0
- {cc_transcript-14.16.1 → cc_transcript-14.18.0}/rust/crates/core/src/corrections.rs +0 -0
- {cc_transcript-14.16.1 → cc_transcript-14.18.0}/rust/crates/core/src/cost.rs +0 -0
- {cc_transcript-14.16.1 → cc_transcript-14.18.0}/rust/crates/core/src/discovery.rs +0 -0
- {cc_transcript-14.16.1 → cc_transcript-14.18.0}/rust/crates/core/src/facts.rs +0 -0
- {cc_transcript-14.16.1 → cc_transcript-14.18.0}/rust/crates/core/src/feedback.rs +0 -0
- {cc_transcript-14.16.1 → cc_transcript-14.18.0}/rust/crates/core/src/filter.rs +0 -0
- {cc_transcript-14.16.1 → cc_transcript-14.18.0}/rust/crates/core/src/gateway.rs +0 -0
- {cc_transcript-14.16.1 → cc_transcript-14.18.0}/rust/crates/core/src/ids.rs +0 -0
- {cc_transcript-14.16.1 → cc_transcript-14.18.0}/rust/crates/core/src/judge.rs +0 -0
- {cc_transcript-14.16.1 → cc_transcript-14.18.0}/rust/crates/core/src/lib.rs +0 -0
- {cc_transcript-14.16.1 → cc_transcript-14.18.0}/rust/crates/core/src/literals/command.rs +0 -0
- {cc_transcript-14.16.1 → cc_transcript-14.18.0}/rust/crates/core/src/literals/corrections.rs +0 -0
- {cc_transcript-14.16.1 → cc_transcript-14.18.0}/rust/crates/core/src/literals/feedback.rs +0 -0
- {cc_transcript-14.16.1 → cc_transcript-14.18.0}/rust/crates/core/src/literals/mining.rs +0 -0
- {cc_transcript-14.16.1 → cc_transcript-14.18.0}/rust/crates/core/src/literals/mod.rs +0 -0
- {cc_transcript-14.16.1 → cc_transcript-14.18.0}/rust/crates/core/src/literals/protocol.rs +0 -0
- {cc_transcript-14.16.1 → cc_transcript-14.18.0}/rust/crates/core/src/notifications.rs +0 -0
- {cc_transcript-14.16.1 → cc_transcript-14.18.0}/rust/crates/core/src/parse.rs +0 -0
- {cc_transcript-14.16.1 → cc_transcript-14.18.0}/rust/crates/core/src/protocol.rs +0 -0
- {cc_transcript-14.16.1 → cc_transcript-14.18.0}/rust/crates/core/src/pystr.rs +0 -0
- {cc_transcript-14.16.1 → cc_transcript-14.18.0}/rust/crates/core/src/query.rs +0 -0
- {cc_transcript-14.16.1 → cc_transcript-14.18.0}/rust/crates/core/src/render.rs +0 -0
- {cc_transcript-14.16.1 → cc_transcript-14.18.0}/rust/crates/core/src/rng.rs +0 -0
- {cc_transcript-14.16.1 → cc_transcript-14.18.0}/rust/crates/core/src/schema.rs +0 -0
- {cc_transcript-14.16.1 → cc_transcript-14.18.0}/rust/crates/core/src/sqlite.rs +0 -0
- {cc_transcript-14.16.1 → cc_transcript-14.18.0}/rust/crates/core/src/toolcall.rs +0 -0
- {cc_transcript-14.16.1 → cc_transcript-14.18.0}/rust/crates/core/src/types.rs +0 -0
- {cc_transcript-14.16.1 → cc_transcript-14.18.0}/rust/crates/core/src/value.rs +0 -0
- {cc_transcript-14.16.1 → cc_transcript-14.18.0}/rust/crates/core/src/watch.rs +0 -0
- {cc_transcript-14.16.1 → cc_transcript-14.18.0}/rust/crates/py/Cargo.toml +0 -0
- {cc_transcript-14.16.1 → cc_transcript-14.18.0}/rust/crates/py/build.rs +0 -0
- {cc_transcript-14.16.1 → cc_transcript-14.18.0}/rust/crates/py/data/command_prefix_pins.tsv +0 -0
- {cc_transcript-14.16.1 → cc_transcript-14.18.0}/rust/crates/py/src/actor_bridge.rs +0 -0
- {cc_transcript-14.16.1 → cc_transcript-14.18.0}/rust/crates/py/src/bin/stub_gen.rs +0 -0
- {cc_transcript-14.16.1 → cc_transcript-14.18.0}/rust/crates/py/src/codex.rs +0 -0
- {cc_transcript-14.16.1 → cc_transcript-14.18.0}/rust/crates/py/src/command.rs +0 -0
- {cc_transcript-14.16.1 → cc_transcript-14.18.0}/rust/crates/py/src/context.rs +0 -0
- {cc_transcript-14.16.1 → cc_transcript-14.18.0}/rust/crates/py/src/corrections.rs +0 -0
- {cc_transcript-14.16.1 → cc_transcript-14.18.0}/rust/crates/py/src/discovery.rs +0 -0
- {cc_transcript-14.16.1 → cc_transcript-14.18.0}/rust/crates/py/src/feedback.rs +0 -0
- {cc_transcript-14.16.1 → cc_transcript-14.18.0}/rust/crates/py/src/judge.rs +0 -0
- {cc_transcript-14.16.1 → cc_transcript-14.18.0}/rust/crates/py/src/lexicon.rs +0 -0
- {cc_transcript-14.16.1 → cc_transcript-14.18.0}/rust/crates/py/src/lib.rs +0 -0
- {cc_transcript-14.16.1 → cc_transcript-14.18.0}/rust/crates/py/src/mining.rs +0 -0
- {cc_transcript-14.16.1 → cc_transcript-14.18.0}/rust/crates/py/src/nlp.rs +0 -0
- {cc_transcript-14.16.1 → cc_transcript-14.18.0}/rust/crates/py/src/render.rs +0 -0
- {cc_transcript-14.16.1 → cc_transcript-14.18.0}/rust/crates/py/src/score.rs +0 -0
- {cc_transcript-14.16.1 → cc_transcript-14.18.0}/rust/crates/py/src/sqlite.rs +0 -0
- {cc_transcript-14.16.1 → cc_transcript-14.18.0}/rust/crates/py/src/toolcall.rs +0 -0
- {cc_transcript-14.16.1 → cc_transcript-14.18.0}/rust/crates/py/src/views/attachment.rs +0 -0
- {cc_transcript-14.16.1 → cc_transcript-14.18.0}/rust/crates/py/src/views/blocks.rs +0 -0
- {cc_transcript-14.16.1 → cc_transcript-14.18.0}/rust/crates/py/src/views/command.rs +0 -0
- {cc_transcript-14.16.1 → cc_transcript-14.18.0}/rust/crates/py/src/views/convert.rs +0 -0
- {cc_transcript-14.16.1 → cc_transcript-14.18.0}/rust/crates/py/src/views/dunder.rs +0 -0
- {cc_transcript-14.16.1 → cc_transcript-14.18.0}/rust/crates/py/src/views/events.rs +0 -0
- {cc_transcript-14.16.1 → cc_transcript-14.18.0}/rust/crates/py/src/views/meta.rs +0 -0
- {cc_transcript-14.16.1 → cc_transcript-14.18.0}/rust/crates/py/src/views/mod.rs +0 -0
- {cc_transcript-14.16.1 → cc_transcript-14.18.0}/rust/crates/py/src/views/print.rs +0 -0
- {cc_transcript-14.16.1 → cc_transcript-14.18.0}/rust/crates/py/src/views/store.rs +0 -0
- {cc_transcript-14.16.1 → cc_transcript-14.18.0}/rust/crates/py/src/views/system.rs +0 -0
- {cc_transcript-14.16.1 → cc_transcript-14.18.0}/rust/crates/py/src/views/toolcall.rs +0 -0
- {cc_transcript-14.16.1 → cc_transcript-14.18.0}/rust/crates/py/src/views/toolresult.rs +0 -0
- {cc_transcript-14.16.1 → cc_transcript-14.18.0}/rust/crates/py/src/views/transcript.rs +0 -0
- {cc_transcript-14.16.1 → cc_transcript-14.18.0}/rust/crates/py/src/watch.rs +0 -0
|
@@ -197,6 +197,7 @@ EXPORTS: dict[str, str] = {
|
|
|
197
197
|
"Lifecycle",
|
|
198
198
|
),
|
|
199
199
|
"cc_transcript.activity": (
|
|
200
|
+
"ActivityLift",
|
|
200
201
|
"Edit",
|
|
201
202
|
"SessionActivity",
|
|
202
203
|
"ToolUse",
|
|
@@ -240,6 +241,7 @@ EXPORTS: dict[str, str] = {
|
|
|
240
241
|
"DeepSession",
|
|
241
242
|
"DeepView",
|
|
242
243
|
"FileRef",
|
|
244
|
+
"PredicateInputs",
|
|
243
245
|
"Session",
|
|
244
246
|
"SubagentIndex",
|
|
245
247
|
"SubagentSession",
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
# isort: off
|
|
3
3
|
|
|
4
4
|
from cc_transcript.activity import (
|
|
5
|
+
ActivityLift as ActivityLift,
|
|
5
6
|
Edit as Edit,
|
|
6
7
|
SessionActivity as SessionActivity,
|
|
7
8
|
ToolUse as ToolUse,
|
|
@@ -207,6 +208,7 @@ from cc_transcript.query import (
|
|
|
207
208
|
DeepSession as DeepSession,
|
|
208
209
|
DeepView as DeepView,
|
|
209
210
|
FileRef as FileRef,
|
|
211
|
+
PredicateInputs as PredicateInputs,
|
|
210
212
|
Session as Session,
|
|
211
213
|
SubagentIndex as SubagentIndex,
|
|
212
214
|
SubagentSession as SubagentSession,
|
|
@@ -304,6 +306,7 @@ EXPORTS: dict[str, str]
|
|
|
304
306
|
__all__ = [
|
|
305
307
|
"ASSISTANTS",
|
|
306
308
|
"Action",
|
|
309
|
+
"ActivityLift",
|
|
307
310
|
"ApiError",
|
|
308
311
|
"ApplyPatchCall",
|
|
309
312
|
"AskUserQuestionResult",
|
|
@@ -390,6 +393,7 @@ __all__ = [
|
|
|
390
393
|
"PRICING",
|
|
391
394
|
"PatchEdit",
|
|
392
395
|
"Plugin",
|
|
396
|
+
"PredicateInputs",
|
|
393
397
|
"PreservedMessages",
|
|
394
398
|
"PreservedSegment",
|
|
395
399
|
"PrintMessage",
|
|
@@ -108,6 +108,7 @@ __all__ = [
|
|
|
108
108
|
"activity_hunk_overlap",
|
|
109
109
|
"activity_lift",
|
|
110
110
|
"activity_lift_from_events",
|
|
111
|
+
"activity_lift_tail",
|
|
111
112
|
"activity_overlap_between",
|
|
112
113
|
"activity_parse_show_hunks",
|
|
113
114
|
"bucket_events_from_events",
|
|
@@ -2295,6 +2296,8 @@ def activity_lift(path: builtins.str, max_events: builtins.int) -> dict[str, typ
|
|
|
2295
2296
|
|
|
2296
2297
|
def activity_lift_from_events(events: list[models.TranscriptEvent], opener_flags: typing.Optional[typing.Sequence[builtins.bool]] = None) -> list[dict[str, typing.Any]]: ...
|
|
2297
2298
|
|
|
2299
|
+
def activity_lift_tail(events: list[models.TranscriptEvent], opener_flags: typing.Optional[typing.Sequence[builtins.bool]] = None, open_turn: builtins.bool = False) -> dict[str, typing.Any]: ...
|
|
2300
|
+
|
|
2298
2301
|
def activity_overlap_between(incorrect: typing.Sequence[tuple[builtins.str, builtins.str]], correction: typing.Sequence[tuple[builtins.str, builtins.str]]) -> builtins.float: ...
|
|
2299
2302
|
|
|
2300
2303
|
def activity_parse_show_hunks(diff: builtins.str) -> builtins.list[tuple[builtins.str, builtins.str]]: ...
|
|
@@ -13,8 +13,8 @@ products.
|
|
|
13
13
|
from __future__ import annotations
|
|
14
14
|
|
|
15
15
|
from collections.abc import Callable
|
|
16
|
-
from dataclasses import dataclass
|
|
17
|
-
from typing import TYPE_CHECKING
|
|
16
|
+
from dataclasses import dataclass, replace
|
|
17
|
+
from typing import TYPE_CHECKING, Any
|
|
18
18
|
|
|
19
19
|
from cc_transcript import _native
|
|
20
20
|
from cc_transcript.discovery import TranscriptExpiredError, resolve
|
|
@@ -25,7 +25,7 @@ from cc_transcript.parser import parse
|
|
|
25
25
|
from cc_transcript.tools import edits_of, parse_tool_call, parse_tool_result
|
|
26
26
|
|
|
27
27
|
if TYPE_CHECKING:
|
|
28
|
-
from collections.abc import Sequence
|
|
28
|
+
from collections.abc import Mapping, Sequence
|
|
29
29
|
from datetime import datetime
|
|
30
30
|
from pathlib import Path
|
|
31
31
|
|
|
@@ -181,64 +181,10 @@ class SessionActivity:
|
|
|
181
181
|
turn; everything else folds into the current one. Events before the
|
|
182
182
|
first qualifying prompt form turn 0 with prompt ``""``.
|
|
183
183
|
"""
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
if user_classifier is native_user_classifier
|
|
188
|
-
else [bool(user_classifier(event)) if isinstance(event, UserEvent) else False for event in evs]
|
|
184
|
+
return cls(
|
|
185
|
+
session_id=session_id,
|
|
186
|
+
turns=ActivityLift(session_id, user_classifier=user_classifier).extend(events).turns,
|
|
189
187
|
)
|
|
190
|
-
tool_blocks = {
|
|
191
|
-
event_idx: tuple(block for block in event.blocks if isinstance(block, ToolUseBlock))
|
|
192
|
-
for event_idx, event in enumerate(evs)
|
|
193
|
-
if isinstance(event, AssistantEvent)
|
|
194
|
-
}
|
|
195
|
-
tool_block_positions: dict[int, int] = {}
|
|
196
|
-
turns: list[Turn] = []
|
|
197
|
-
for index, skeleton in enumerate(_native.activity_lift_from_events(evs, opener_flags)):
|
|
198
|
-
tool_uses: list[ToolUse] = []
|
|
199
|
-
for use in skeleton["tool_uses"]:
|
|
200
|
-
event_idx = use["event_idx"]
|
|
201
|
-
event = evs[event_idx]
|
|
202
|
-
block_position = tool_block_positions.get(event_idx, 0)
|
|
203
|
-
block = tool_blocks[event_idx][block_position]
|
|
204
|
-
tool_block_positions[event_idx] = block_position + 1
|
|
205
|
-
result_event_idx = use["result_event_idx"]
|
|
206
|
-
if result_event_idx is None:
|
|
207
|
-
result = None
|
|
208
|
-
result_ts = None
|
|
209
|
-
else:
|
|
210
|
-
result_event = evs[result_event_idx]
|
|
211
|
-
result = next(
|
|
212
|
-
candidate
|
|
213
|
-
for candidate in reversed(result_event.blocks)
|
|
214
|
-
if isinstance(candidate, ToolResultBlock) and candidate.tool_use_id == use["tool_use_id"]
|
|
215
|
-
)
|
|
216
|
-
result_ts = result_event.meta.timestamp
|
|
217
|
-
call = parse_tool_call(block.name, block.input, on_error="other")
|
|
218
|
-
tool_uses.append(
|
|
219
|
-
ToolUse(
|
|
220
|
-
ref=EventRef(session_id, event.meta.uuid, block.id),
|
|
221
|
-
call=call,
|
|
222
|
-
result=result,
|
|
223
|
-
result_ts=result_ts,
|
|
224
|
-
edits=edits_of(call),
|
|
225
|
-
turn_index=index,
|
|
226
|
-
ts=event.meta.timestamp,
|
|
227
|
-
)
|
|
228
|
-
)
|
|
229
|
-
started_idx = skeleton["started_idx"]
|
|
230
|
-
ended_idx = skeleton["ended_idx"]
|
|
231
|
-
turns.append(
|
|
232
|
-
Turn(
|
|
233
|
-
index=index,
|
|
234
|
-
prompt=skeleton["prompt"],
|
|
235
|
-
started_at=None if started_idx is None else evs[started_idx].meta.timestamp,
|
|
236
|
-
ended_at=None if ended_idx is None else evs[ended_idx].meta.timestamp,
|
|
237
|
-
events=tuple(evs[skeleton["start"] : skeleton["end"]]),
|
|
238
|
-
tool_uses=tuple(tool_uses),
|
|
239
|
-
)
|
|
240
|
-
)
|
|
241
|
-
return cls(session_id=session_id, turns=tuple(turns))
|
|
242
188
|
|
|
243
189
|
@classmethod
|
|
244
190
|
def from_session(
|
|
@@ -326,6 +272,174 @@ class SessionActivity:
|
|
|
326
272
|
return tuple(same_turn + later)
|
|
327
273
|
|
|
328
274
|
|
|
275
|
+
class ActivityLift:
|
|
276
|
+
"""A session's lift that grows with its transcript.
|
|
277
|
+
|
|
278
|
+
Each :meth:`extend` folds the events appended since the previous call into
|
|
279
|
+
the activity lifted so far: closed turns are kept, the open turn grows by
|
|
280
|
+
the appended events alone, and a tool use whose result arrives in the tail
|
|
281
|
+
is re-paired wherever it sits. The result always equals
|
|
282
|
+
:meth:`SessionActivity.from_events` over every event fed so far. A step
|
|
283
|
+
costs the appended events, the tool uses they re-pair, and copies of the
|
|
284
|
+
turn tuple and of the open turn's event and tool-use tuples; it never
|
|
285
|
+
re-derives the session.
|
|
286
|
+
|
|
287
|
+
Feed only the events appended since the previous call: nothing checks
|
|
288
|
+
provenance, so events fed twice lift twice. A transcript rewritten in
|
|
289
|
+
place, rather than appended to, needs a new cursor.
|
|
290
|
+
|
|
291
|
+
The classifier is read once, at construction, and must be deterministic
|
|
292
|
+
and event-only: its answer for an event has to stay the same as later
|
|
293
|
+
events are appended, since turns lifted under it are never revisited.
|
|
294
|
+
|
|
295
|
+
Example:
|
|
296
|
+
>>> lift = ActivityLift(session_id)
|
|
297
|
+
>>> activity = lift.extend(events)
|
|
298
|
+
>>> lift.extend(appended) == SessionActivity.from_events(session_id, [*events, *appended])
|
|
299
|
+
True
|
|
300
|
+
"""
|
|
301
|
+
|
|
302
|
+
__slots__ = ("_activity", "_results", "_session_id", "_user_classifier", "_uses")
|
|
303
|
+
|
|
304
|
+
def __init__(self, session_id: SessionId, *, user_classifier: UserClassifier = native_user_classifier) -> None:
|
|
305
|
+
self._session_id = session_id
|
|
306
|
+
self._user_classifier = user_classifier
|
|
307
|
+
self._activity = SessionActivity(session_id=session_id, turns=())
|
|
308
|
+
self._uses: dict[ToolUseId, tuple[tuple[int, int], ...]] = {}
|
|
309
|
+
self._results: dict[ToolUseId, UserEvent] = {}
|
|
310
|
+
|
|
311
|
+
@property
|
|
312
|
+
def session_id(self) -> SessionId:
|
|
313
|
+
"""The session being lifted."""
|
|
314
|
+
return self._session_id
|
|
315
|
+
|
|
316
|
+
@property
|
|
317
|
+
def user_classifier(self) -> UserClassifier:
|
|
318
|
+
"""Decides which user events open turns."""
|
|
319
|
+
return self._user_classifier
|
|
320
|
+
|
|
321
|
+
@property
|
|
322
|
+
def activity(self) -> SessionActivity:
|
|
323
|
+
"""Everything lifted so far."""
|
|
324
|
+
return self._activity
|
|
325
|
+
|
|
326
|
+
def extend(self, events: Sequence[TranscriptEvent]) -> SessionActivity:
|
|
327
|
+
"""Lifts ``events``, appended after everything fed so far, into :attr:`activity`."""
|
|
328
|
+
tail = list(events)
|
|
329
|
+
if not tail:
|
|
330
|
+
return self._activity
|
|
331
|
+
opener_flags = (
|
|
332
|
+
None
|
|
333
|
+
if self._user_classifier is native_user_classifier
|
|
334
|
+
else [bool(self._user_classifier(event)) if isinstance(event, UserEvent) else False for event in tail]
|
|
335
|
+
)
|
|
336
|
+
turns = list(self._activity.turns)
|
|
337
|
+
lifted = _native.activity_lift_tail(tail, opener_flags, open_turn=bool(turns))
|
|
338
|
+
first = len(turns) - 1 if lifted["continued"] else len(turns)
|
|
339
|
+
grown = [
|
|
340
|
+
lift_turn(self._session_id, first + offset, skeleton, tail, self._results)
|
|
341
|
+
for offset, skeleton in enumerate(lifted["turns"])
|
|
342
|
+
]
|
|
343
|
+
if lifted["continued"]:
|
|
344
|
+
turns[-1] = continued(turns[-1], grown[0])
|
|
345
|
+
turns.extend(grown[1:])
|
|
346
|
+
else:
|
|
347
|
+
turns.extend(grown)
|
|
348
|
+
repairs: dict[int, dict[int, UserEvent]] = {}
|
|
349
|
+
for tool_use_id, result_idx in lifted["results"]:
|
|
350
|
+
for turn_index, position in self._uses.get(tool_use_id, ()):
|
|
351
|
+
repairs.setdefault(turn_index, {})[position] = tail[result_idx]
|
|
352
|
+
self._results[tool_use_id] = tail[result_idx]
|
|
353
|
+
for turn_index, at in repairs.items():
|
|
354
|
+
turns[turn_index] = repaired(turns[turn_index], at)
|
|
355
|
+
for turn in grown:
|
|
356
|
+
base = len(turns[turn.index].tool_uses) - len(turn.tool_uses)
|
|
357
|
+
for position, use in enumerate(turn.tool_uses, base):
|
|
358
|
+
self._uses[use.ref.tool_use_id] = (*self._uses.get(use.ref.tool_use_id, ()), (turn.index, position))
|
|
359
|
+
self._activity = SessionActivity(session_id=self._session_id, turns=tuple(turns))
|
|
360
|
+
return self._activity
|
|
361
|
+
|
|
362
|
+
|
|
363
|
+
def lift_turn(
|
|
364
|
+
session_id: SessionId,
|
|
365
|
+
index: int,
|
|
366
|
+
skeleton: dict[str, Any],
|
|
367
|
+
evs: Sequence[TranscriptEvent],
|
|
368
|
+
prior_results: Mapping[ToolUseId, UserEvent],
|
|
369
|
+
) -> Turn:
|
|
370
|
+
events = tuple(evs[skeleton["start"] : skeleton["end"]])
|
|
371
|
+
calls = [
|
|
372
|
+
(event, block)
|
|
373
|
+
for event in events
|
|
374
|
+
if isinstance(event, AssistantEvent)
|
|
375
|
+
for block in event.blocks
|
|
376
|
+
if isinstance(block, ToolUseBlock)
|
|
377
|
+
]
|
|
378
|
+
started_idx = skeleton["started_idx"]
|
|
379
|
+
ended_idx = skeleton["ended_idx"]
|
|
380
|
+
return Turn(
|
|
381
|
+
index=index,
|
|
382
|
+
prompt=skeleton["prompt"],
|
|
383
|
+
started_at=None if started_idx is None else evs[started_idx].meta.timestamp,
|
|
384
|
+
ended_at=None if ended_idx is None else evs[ended_idx].meta.timestamp,
|
|
385
|
+
events=events,
|
|
386
|
+
tool_uses=tuple(
|
|
387
|
+
lift_tool_use(
|
|
388
|
+
session_id,
|
|
389
|
+
index,
|
|
390
|
+
event,
|
|
391
|
+
block,
|
|
392
|
+
prior_results.get(block.id) if (result_idx := use["result_event_idx"]) is None else evs[result_idx],
|
|
393
|
+
)
|
|
394
|
+
for (event, block), use in zip(calls, skeleton["tool_uses"], strict=True)
|
|
395
|
+
),
|
|
396
|
+
)
|
|
397
|
+
|
|
398
|
+
|
|
399
|
+
def lift_tool_use(
|
|
400
|
+
session_id: SessionId, turn_index: int, event: AssistantEvent, block: ToolUseBlock, result_event: UserEvent | None
|
|
401
|
+
) -> ToolUse:
|
|
402
|
+
call = parse_tool_call(block.name, block.input, on_error="other")
|
|
403
|
+
return ToolUse(
|
|
404
|
+
ref=EventRef(session_id, event.meta.uuid, block.id),
|
|
405
|
+
call=call,
|
|
406
|
+
result=None if result_event is None else result_block(result_event, block.id),
|
|
407
|
+
result_ts=None if result_event is None else result_event.meta.timestamp,
|
|
408
|
+
edits=edits_of(call),
|
|
409
|
+
turn_index=turn_index,
|
|
410
|
+
ts=event.meta.timestamp,
|
|
411
|
+
)
|
|
412
|
+
|
|
413
|
+
|
|
414
|
+
def result_block(event: UserEvent, tool_use_id: ToolUseId) -> ToolResultBlock:
|
|
415
|
+
return next(
|
|
416
|
+
candidate
|
|
417
|
+
for candidate in reversed(event.blocks)
|
|
418
|
+
if isinstance(candidate, ToolResultBlock) and candidate.tool_use_id == tool_use_id
|
|
419
|
+
)
|
|
420
|
+
|
|
421
|
+
|
|
422
|
+
def continued(turn: Turn, tail: Turn) -> Turn:
|
|
423
|
+
return replace(
|
|
424
|
+
turn,
|
|
425
|
+
started_at=tail.started_at if turn.started_at is None else turn.started_at,
|
|
426
|
+
ended_at=turn.ended_at if tail.ended_at is None else tail.ended_at,
|
|
427
|
+
events=turn.events + tail.events,
|
|
428
|
+
tool_uses=turn.tool_uses + tail.tool_uses,
|
|
429
|
+
)
|
|
430
|
+
|
|
431
|
+
|
|
432
|
+
def repaired(turn: Turn, at: Mapping[int, UserEvent]) -> Turn:
|
|
433
|
+
uses = list(turn.tool_uses)
|
|
434
|
+
for position, event in at.items():
|
|
435
|
+
uses[position] = replace(
|
|
436
|
+
uses[position],
|
|
437
|
+
result=result_block(event, uses[position].ref.tool_use_id),
|
|
438
|
+
result_ts=event.meta.timestamp,
|
|
439
|
+
)
|
|
440
|
+
return replace(turn, tool_uses=tuple(uses))
|
|
441
|
+
|
|
442
|
+
|
|
329
443
|
def hunk_overlap(a: Hunk, b: Hunk) -> float:
|
|
330
444
|
"""The fraction of ``a.new``'s non-empty lines present in ``b.old``.
|
|
331
445
|
|
|
@@ -106,6 +106,61 @@ no longer fits on every walk.
|
|
|
106
106
|
"""
|
|
107
107
|
|
|
108
108
|
|
|
109
|
+
SIDECHAIN_INPUTS_LIMIT = 16384
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
@dataclass(slots=True)
|
|
113
|
+
class PredicateInputsCache:
|
|
114
|
+
held: OrderedDict[Path, tuple[LiftStamp, PredicateInputs]] = field(default_factory=OrderedDict)
|
|
115
|
+
guard: threading.Lock = field(default_factory=threading.Lock)
|
|
116
|
+
|
|
117
|
+
def get(self, path: Path, stamp: LiftStamp) -> PredicateInputs | None:
|
|
118
|
+
with self.guard:
|
|
119
|
+
match self.held.get(path):
|
|
120
|
+
case (held_stamp, inputs) if held_stamp == stamp:
|
|
121
|
+
self.held.move_to_end(path)
|
|
122
|
+
return inputs
|
|
123
|
+
case _:
|
|
124
|
+
return None
|
|
125
|
+
|
|
126
|
+
def put(self, path: Path, stamp: LiftStamp, inputs: PredicateInputs) -> PredicateInputs:
|
|
127
|
+
with self.guard:
|
|
128
|
+
self.held[path] = (stamp, inputs)
|
|
129
|
+
self.held.move_to_end(path)
|
|
130
|
+
while len(self.held) > SIDECHAIN_INPUTS_LIMIT:
|
|
131
|
+
self.held.popitem(last=False)
|
|
132
|
+
return inputs
|
|
133
|
+
|
|
134
|
+
def clear(self) -> None:
|
|
135
|
+
with self.guard:
|
|
136
|
+
self.held.clear()
|
|
137
|
+
|
|
138
|
+
def __len__(self) -> int:
|
|
139
|
+
return len(self.held)
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
SIDECHAIN_INPUTS = PredicateInputsCache()
|
|
143
|
+
"""Each sidechain's :class:`PredicateInputs`, keyed by path and stamped like :data:`DEEP_LIFTS`.
|
|
144
|
+
|
|
145
|
+
A lifted session costs about 2.3 times its source bytes, so a tree of a thousand sidechains
|
|
146
|
+
cannot stay lifted; its :class:`PredicateInputs` are a few kilobytes each. A predicate lifts a
|
|
147
|
+
sidechain only when its stamp moved, keeps those inputs, and lets the lift go, so a resident
|
|
148
|
+
process re-answering the same predicates per event neither reparses nor holds the tree.
|
|
149
|
+
"""
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
SIDECHAIN_LISTINGS: OrderedDict[Path, tuple[tuple[int, int], tuple[tuple[Path, bool], ...]]] = OrderedDict()
|
|
153
|
+
"""Each non-empty ``subagents`` listing and whether each file is a symlink, stamped ``(inode, mtime_ns)``.
|
|
154
|
+
|
|
155
|
+
Adding, removing, renaming, or replacing a sidechain moves the directory's mtime, so an unchanged
|
|
156
|
+
stamp proves the listing current. A walk then skips the directory read and resolves the directory
|
|
157
|
+
once rather than every file; only a symlinked sidechain is resolved on every walk. An empty listing
|
|
158
|
+
is never held, since the native lister reports an unreadable directory as empty.
|
|
159
|
+
"""
|
|
160
|
+
|
|
161
|
+
SIDECHAIN_LISTINGS_GUARD = threading.Lock()
|
|
162
|
+
|
|
163
|
+
|
|
109
164
|
def is_failure(use: ToolUse) -> bool:
|
|
110
165
|
return use.result is not None and use.result.is_error
|
|
111
166
|
|
|
@@ -256,8 +311,12 @@ class ToolCallQuery:
|
|
|
256
311
|
callable predicate, or a value compared for equality.
|
|
257
312
|
"""
|
|
258
313
|
return self.where(
|
|
259
|
-
lambda use:
|
|
260
|
-
|
|
314
|
+
lambda use: (
|
|
315
|
+
isinstance(use.call.raw, Mapping)
|
|
316
|
+
and all(
|
|
317
|
+
key in use.call.raw and input_rule_matches(rule, use.call.raw[key]) for key, rule in rules.items()
|
|
318
|
+
)
|
|
319
|
+
)
|
|
261
320
|
)
|
|
262
321
|
|
|
263
322
|
def count(self) -> int:
|
|
@@ -488,9 +547,7 @@ class Session:
|
|
|
488
547
|
|
|
489
548
|
def has_tool(self, name: str, *, subagents: bool = True) -> bool:
|
|
490
549
|
"""Whether any call in the window matches the pipe spec ``name``."""
|
|
491
|
-
return self.
|
|
492
|
-
subagents and any(deep.session.has_tool(name, subagents=False) for deep in self.walk())
|
|
493
|
-
)
|
|
550
|
+
return any_inputs(self, lambda inputs: inputs.has_tool(name), subagents=subagents)
|
|
494
551
|
|
|
495
552
|
def has_command(self, *argv: str, subagents: bool = True) -> bool:
|
|
496
553
|
"""Whether any Bash command in the window runs ``argv``.
|
|
@@ -499,28 +556,36 @@ class Session:
|
|
|
499
556
|
unwrapped argv, so ``has_command("git", "push")`` matches
|
|
500
557
|
``sudo git push -f`` and ``cd x && git push`` but not ``echo "git push"``.
|
|
501
558
|
"""
|
|
502
|
-
return
|
|
503
|
-
subagents and any(deep.session.has_command(*argv, subagents=False) for deep in self.walk())
|
|
504
|
-
)
|
|
559
|
+
return any_inputs(self, lambda inputs: inputs.has_command(argv), subagents=subagents)
|
|
505
560
|
|
|
506
561
|
def has_edit_to(self, *globs: str, subagents: bool = True) -> bool:
|
|
507
562
|
"""Whether any edit-shaped call in the window targets a file matching any glob."""
|
|
508
|
-
return
|
|
509
|
-
subagents and any(deep.session.has_edit_to(*globs, subagents=False) for deep in self.walk())
|
|
510
|
-
)
|
|
563
|
+
return any_inputs(self, lambda inputs: inputs.has_edit_to(globs), subagents=subagents)
|
|
511
564
|
|
|
512
565
|
def has_read(self, pattern: str, *, subagents: bool = True) -> bool:
|
|
513
566
|
"""Whether any Read in the window targets a path containing ``pattern``."""
|
|
514
|
-
return
|
|
515
|
-
subagents and any(deep.session.has_read(pattern, subagents=False) for deep in self.walk())
|
|
516
|
-
)
|
|
567
|
+
return any_inputs(self, lambda inputs: inputs.has_read(pattern), subagents=subagents)
|
|
517
568
|
|
|
518
569
|
def has_skill(self, *names: str, subagents: bool = True) -> bool:
|
|
519
570
|
"""Whether any Skill invocation in the window names one of ``names``."""
|
|
520
|
-
return
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
571
|
+
return any_inputs(self, lambda inputs: inputs.has_skill(names), subagents=subagents)
|
|
572
|
+
|
|
573
|
+
@cached_property
|
|
574
|
+
def predicate_inputs(self) -> PredicateInputs:
|
|
575
|
+
"""What the ``has_*`` predicates read from this window."""
|
|
576
|
+
return PredicateInputs.of(self)
|
|
577
|
+
|
|
578
|
+
def deep_inputs(self) -> Iterator[PredicateInputs]:
|
|
579
|
+
"""This window's :class:`PredicateInputs`, then those of every transcript :meth:`walk` reaches.
|
|
580
|
+
|
|
581
|
+
The walk order and dedupe match :meth:`walk`, but no reached transcript stays lifted: each
|
|
582
|
+
one's inputs are held per ``(size, mtime, inode)`` stamp, and a transcript is reparsed only
|
|
583
|
+
once its stamp moves. Prefer it to :meth:`walk` for any predicate these inputs can answer.
|
|
584
|
+
"""
|
|
585
|
+
yield self.predicate_inputs
|
|
586
|
+
for path, depth, spawned_by in reachable_transcripts(self):
|
|
587
|
+
if (inputs := load_predicate_inputs(path, depth, spawned_by)) is not None:
|
|
588
|
+
yield inputs
|
|
524
589
|
|
|
525
590
|
def has_override(self, token: str, *, invalidated_by: Sequence[str] = ("Edit", "Write")) -> bool:
|
|
526
591
|
"""Whether ``token`` appears in the window without a later invalidating call.
|
|
@@ -570,9 +635,7 @@ class Session:
|
|
|
570
635
|
|
|
571
636
|
@cached_property
|
|
572
637
|
def _command_lines(self) -> tuple[CommandLine, ...]:
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
return tuple(parse_command_line(command) for command in self._commands)
|
|
638
|
+
return self.predicate_inputs.command_lines
|
|
576
639
|
|
|
577
640
|
def __len__(self) -> int:
|
|
578
641
|
return sum(len(turn.events) for turn in self.turns)
|
|
@@ -653,6 +716,83 @@ class DeepSession:
|
|
|
653
716
|
spawned_by: ToolUseId | None
|
|
654
717
|
|
|
655
718
|
|
|
719
|
+
@dataclass(frozen=True) # non-slots: the cached_property below needs __dict__
|
|
720
|
+
class PredicateInputs:
|
|
721
|
+
"""The slice of one transcript window that the ``has_*`` predicates read.
|
|
722
|
+
|
|
723
|
+
Small enough to hold for every transcript in a tree of thousands, where a lifted
|
|
724
|
+
:class:`Session` is not. Calls whose result errored are left out, as
|
|
725
|
+
:class:`ToolCallQuery` hides them by default. Answers that do not depend on the MCP
|
|
726
|
+
tool registry are memoized, up to :attr:`MAX_ANSWERS` per instance.
|
|
727
|
+
|
|
728
|
+
Attributes:
|
|
729
|
+
calls: Each call's tool name and the file paths it targets, in order.
|
|
730
|
+
commands: The command string of every Bash call.
|
|
731
|
+
edited_files: The files modified by edit-shaped calls, one entry per edited file.
|
|
732
|
+
skills: The skill named by every Skill call.
|
|
733
|
+
"""
|
|
734
|
+
|
|
735
|
+
calls: tuple[tuple[str, tuple[str, ...]], ...]
|
|
736
|
+
commands: tuple[str, ...]
|
|
737
|
+
edited_files: tuple[FileRef, ...]
|
|
738
|
+
skills: tuple[str, ...]
|
|
739
|
+
answers: dict[tuple[str, object], bool] = field(default_factory=dict, compare=False, repr=False)
|
|
740
|
+
|
|
741
|
+
MAX_ANSWERS: ClassVar[int] = 256
|
|
742
|
+
|
|
743
|
+
@classmethod
|
|
744
|
+
def of(cls, session: Session) -> PredicateInputs:
|
|
745
|
+
"""Extracts the predicate inputs from ``session``'s window."""
|
|
746
|
+
calls = session.tool_calls
|
|
747
|
+
return cls(
|
|
748
|
+
calls=tuple((use.call.name, tuple(file_paths_of(use.call))) for use in calls.items),
|
|
749
|
+
commands=session.commands(),
|
|
750
|
+
edited_files=session.edited_files,
|
|
751
|
+
skills=tuple(call.skill for use in calls.named("Skill") if isinstance(call := use.call, SkillCall)),
|
|
752
|
+
)
|
|
753
|
+
|
|
754
|
+
@cached_property
|
|
755
|
+
def command_lines(self) -> tuple[CommandLine, ...]:
|
|
756
|
+
""":attr:`commands` parsed into :class:`~cc_transcript.command.CommandLine` objects."""
|
|
757
|
+
from cc_transcript.command import parse_command_line
|
|
758
|
+
|
|
759
|
+
return tuple(parse_command_line(command) for command in self.commands)
|
|
760
|
+
|
|
761
|
+
@cached_property
|
|
762
|
+
def tool_names(self) -> frozenset[str]:
|
|
763
|
+
"""The distinct names in :attr:`calls`."""
|
|
764
|
+
return frozenset(name for name, _ in self.calls)
|
|
765
|
+
|
|
766
|
+
def files(self, spec: str) -> tuple[FileRef, ...]:
|
|
767
|
+
"""The files targeted by calls matching the pipe spec ``spec``, one entry per path."""
|
|
768
|
+
matching = {name for name in self.tool_names if tool_name_matches(name, spec)}
|
|
769
|
+
return tuple(FileRef(path) for name, paths in self.calls if name in matching for path in paths)
|
|
770
|
+
|
|
771
|
+
def answer(self, query: tuple[str, object], compute: Callable[[], bool]) -> bool:
|
|
772
|
+
if (known := self.answers.get(query)) is None:
|
|
773
|
+
if len(self.answers) >= self.MAX_ANSWERS:
|
|
774
|
+
self.answers.clear()
|
|
775
|
+
known = self.answers[query] = compute()
|
|
776
|
+
return known
|
|
777
|
+
|
|
778
|
+
def has_tool(self, name: str) -> bool:
|
|
779
|
+
return any(tool_name_matches(tool, name) for tool in self.tool_names)
|
|
780
|
+
|
|
781
|
+
def has_command(self, argv: tuple[str, ...]) -> bool:
|
|
782
|
+
return self.answer(
|
|
783
|
+
("command", argv), lambda: any(cmd.runs(*argv) for line in self.command_lines for cmd in line)
|
|
784
|
+
)
|
|
785
|
+
|
|
786
|
+
def has_edit_to(self, globs: tuple[str, ...]) -> bool:
|
|
787
|
+
return self.answer(("edit", globs), lambda: any(file.matches(*globs) for file in self.edited_files))
|
|
788
|
+
|
|
789
|
+
def has_read(self, pattern: str) -> bool:
|
|
790
|
+
return any(pattern in str(file) for file in self.files("Read"))
|
|
791
|
+
|
|
792
|
+
def has_skill(self, names: tuple[str, ...]) -> bool:
|
|
793
|
+
return self.answer(("skill", names), lambda: any(skill in names for skill in self.skills))
|
|
794
|
+
|
|
795
|
+
|
|
656
796
|
@dataclass(frozen=True) # non-slots: the cached_property below needs __dict__
|
|
657
797
|
class DeepView:
|
|
658
798
|
"""The recursive union of a session and every transcript reachable from it.
|
|
@@ -714,26 +854,75 @@ def sidechain_sessions(path: Path | None) -> tuple[Session, ...]:
|
|
|
714
854
|
return tuple(Session.from_path(entry) for entry in subagent_paths(path))
|
|
715
855
|
|
|
716
856
|
|
|
717
|
-
def
|
|
857
|
+
def any_inputs(session: Session, predicate: Callable[[PredicateInputs], bool], *, subagents: bool) -> bool:
|
|
858
|
+
return any(map(predicate, session.deep_inputs() if subagents else (session.predicate_inputs,)))
|
|
859
|
+
|
|
860
|
+
|
|
861
|
+
def reachable_transcripts(root: Session) -> Iterator[tuple[Path, int, ToolUseId | None]]:
|
|
718
862
|
seen: set[Path] = {root.path.resolve()} if root.path is not None else set()
|
|
719
863
|
if root.path is not None:
|
|
720
|
-
yield from
|
|
864
|
+
yield from reachable_sidechains(root.path, 1, seen)
|
|
721
865
|
for attachment in root.attachments:
|
|
722
|
-
yield from
|
|
866
|
+
yield from reachable_from(attachment, attachment.resolve(), 1, None, seen)
|
|
723
867
|
|
|
724
868
|
|
|
725
|
-
def
|
|
726
|
-
for child in
|
|
727
|
-
yield from
|
|
869
|
+
def reachable_sidechains(parent: Path, depth: int, seen: set[Path]) -> Iterator[tuple[Path, int, ToolUseId | None]]:
|
|
870
|
+
for child, resolved in sidechain_listing(parent):
|
|
871
|
+
yield from reachable_from(child, resolved, depth, ToolUseId(child.stem.removeprefix("agent-")), seen)
|
|
728
872
|
|
|
729
873
|
|
|
730
|
-
def
|
|
731
|
-
|
|
874
|
+
def reachable_from(
|
|
875
|
+
path: Path, resolved: Path, depth: int, spawned_by: ToolUseId | None, seen: set[Path]
|
|
876
|
+
) -> Iterator[tuple[Path, int, ToolUseId | None]]:
|
|
877
|
+
if resolved in seen:
|
|
732
878
|
return
|
|
733
879
|
seen.add(resolved)
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
880
|
+
yield path, depth, spawned_by
|
|
881
|
+
yield from reachable_sidechains(path, depth + 1, seen)
|
|
882
|
+
|
|
883
|
+
|
|
884
|
+
def sidechain_listing(parent: Path) -> tuple[tuple[Path, Path], ...]:
|
|
885
|
+
directory = parent.parent / parent.stem / "subagents"
|
|
886
|
+
try:
|
|
887
|
+
stat = directory.stat()
|
|
888
|
+
except OSError:
|
|
889
|
+
return ()
|
|
890
|
+
stamp = (stat.st_ino, stat.st_mtime_ns)
|
|
891
|
+
with SIDECHAIN_LISTINGS_GUARD:
|
|
892
|
+
held = SIDECHAIN_LISTINGS.get(directory)
|
|
893
|
+
if held is not None and held[0] == stamp:
|
|
894
|
+
children = held[1]
|
|
895
|
+
else:
|
|
896
|
+
children = tuple((child, child.is_symlink()) for child in subagent_paths(parent))
|
|
897
|
+
if children:
|
|
898
|
+
with SIDECHAIN_LISTINGS_GUARD:
|
|
899
|
+
SIDECHAIN_LISTINGS[directory] = (stamp, children)
|
|
900
|
+
SIDECHAIN_LISTINGS.move_to_end(directory)
|
|
901
|
+
while len(SIDECHAIN_LISTINGS) > SIDECHAIN_INPUTS_LIMIT:
|
|
902
|
+
SIDECHAIN_LISTINGS.popitem(last=False)
|
|
903
|
+
real = directory.resolve()
|
|
904
|
+
return tuple((child, child.resolve() if link else real / child.name) for child, link in children)
|
|
905
|
+
|
|
906
|
+
|
|
907
|
+
def load_predicate_inputs(path: Path, depth: int, spawned_by: ToolUseId | None) -> PredicateInputs | None:
|
|
908
|
+
try:
|
|
909
|
+
stat = path.stat()
|
|
910
|
+
except OSError:
|
|
911
|
+
return None
|
|
912
|
+
stamp = (stat.st_size, stat.st_mtime_ns, stat.st_ino)
|
|
913
|
+
if (held := SIDECHAIN_INPUTS.get(path, stamp)) is not None:
|
|
914
|
+
return held
|
|
915
|
+
try:
|
|
916
|
+
deep = DEEP_LIFTS.get((path, depth, spawned_by), stamp) or lift_deep_session(path, depth, spawned_by)
|
|
917
|
+
except OSError:
|
|
918
|
+
return None
|
|
919
|
+
return SIDECHAIN_INPUTS.put(path, stamp, PredicateInputs.of(deep.session))
|
|
920
|
+
|
|
921
|
+
|
|
922
|
+
def deep_sessions(root: Session) -> Iterator[DeepSession]:
|
|
923
|
+
for path, depth, spawned_by in reachable_transcripts(root):
|
|
924
|
+
if (deep := load_deep_session(path, depth, spawned_by)) is not None:
|
|
925
|
+
yield deep
|
|
737
926
|
|
|
738
927
|
|
|
739
928
|
def lift_deep_session(path: Path, depth: int, spawned_by: ToolUseId | None) -> DeepSession:
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "cc-transcript"
|
|
3
|
-
version = "14.
|
|
3
|
+
version = "14.18.0" # informational — releases stamp the version from the git tag
|
|
4
4
|
description = "Grep every Claude Code session you've ever run."
|
|
5
5
|
readme = "README.md"
|
|
6
6
|
license = "PolyForm-Noncommercial-1.0.0"
|