cc-transcript 14.18.0__tar.gz → 14.20.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.
Potentially problematic release.
This version of cc-transcript might be problematic. Click here for more details.
- {cc_transcript-14.18.0 → cc_transcript-14.20.0}/PKG-INFO +1 -1
- {cc_transcript-14.18.0 → cc_transcript-14.20.0}/cc_transcript/_native.pyi +5 -0
- {cc_transcript-14.18.0 → cc_transcript-14.20.0}/cc_transcript/activity.py +4 -2
- {cc_transcript-14.18.0 → cc_transcript-14.20.0}/cc_transcript/query.py +304 -55
- {cc_transcript-14.18.0 → cc_transcript-14.20.0}/pyproject.toml +1 -1
- {cc_transcript-14.18.0 → cc_transcript-14.20.0}/rust/crates/core/src/context.rs +13 -4
- {cc_transcript-14.18.0 → cc_transcript-14.20.0}/rust/crates/core/src/parse.rs +1 -0
- {cc_transcript-14.18.0 → cc_transcript-14.20.0}/rust/crates/core/src/render.rs +56 -17
- {cc_transcript-14.18.0 → cc_transcript-14.20.0}/rust/crates/core/src/types.rs +1 -0
- {cc_transcript-14.18.0 → cc_transcript-14.20.0}/rust/crates/py/src/views/attachment.rs +9 -1
- {cc_transcript-14.18.0 → cc_transcript-14.20.0}/Cargo.lock +0 -0
- {cc_transcript-14.18.0 → cc_transcript-14.20.0}/Cargo.toml +0 -0
- {cc_transcript-14.18.0 → cc_transcript-14.20.0}/LICENSE +0 -0
- {cc_transcript-14.18.0 → cc_transcript-14.20.0}/README.md +0 -0
- {cc_transcript-14.18.0 → cc_transcript-14.20.0}/cc_transcript/__init__.py +0 -0
- {cc_transcript-14.18.0 → cc_transcript-14.20.0}/cc_transcript/__init__.pyi +0 -0
- {cc_transcript-14.18.0 → cc_transcript-14.20.0}/cc_transcript/activity_probe.py +0 -0
- {cc_transcript-14.18.0 → cc_transcript-14.20.0}/cc_transcript/builders.py +0 -0
- {cc_transcript-14.18.0 → cc_transcript-14.20.0}/cc_transcript/codex.py +0 -0
- {cc_transcript-14.18.0 → cc_transcript-14.20.0}/cc_transcript/command.py +0 -0
- {cc_transcript-14.18.0 → cc_transcript-14.20.0}/cc_transcript/context.py +0 -0
- {cc_transcript-14.18.0 → cc_transcript-14.20.0}/cc_transcript/corrections.py +0 -0
- {cc_transcript-14.18.0 → cc_transcript-14.20.0}/cc_transcript/cost.py +0 -0
- {cc_transcript-14.18.0 → cc_transcript-14.20.0}/cc_transcript/decision_store.py +0 -0
- {cc_transcript-14.18.0 → cc_transcript-14.20.0}/cc_transcript/decisions.py +0 -0
- {cc_transcript-14.18.0 → cc_transcript-14.20.0}/cc_transcript/discovery.py +0 -0
- {cc_transcript-14.18.0 → cc_transcript-14.20.0}/cc_transcript/disktruth.py +0 -0
- {cc_transcript-14.18.0 → cc_transcript-14.20.0}/cc_transcript/evidence.py +0 -0
- {cc_transcript-14.18.0 → cc_transcript-14.20.0}/cc_transcript/extract/__init__.py +0 -0
- {cc_transcript-14.18.0 → cc_transcript-14.20.0}/cc_transcript/extract/correct.py +0 -0
- {cc_transcript-14.18.0 → cc_transcript-14.20.0}/cc_transcript/facts.py +0 -0
- {cc_transcript-14.18.0 → cc_transcript-14.20.0}/cc_transcript/filterspec.py +0 -0
- {cc_transcript-14.18.0 → cc_transcript-14.20.0}/cc_transcript/heartbeats.py +0 -0
- {cc_transcript-14.18.0 → cc_transcript-14.20.0}/cc_transcript/ids.py +0 -0
- {cc_transcript-14.18.0 → cc_transcript-14.20.0}/cc_transcript/judge/__init__.py +0 -0
- {cc_transcript-14.18.0 → cc_transcript-14.20.0}/cc_transcript/judge/llm.py +0 -0
- {cc_transcript-14.18.0 → cc_transcript-14.20.0}/cc_transcript/judge/similar.py +0 -0
- {cc_transcript-14.18.0 → cc_transcript-14.20.0}/cc_transcript/judge/verdicts.py +0 -0
- {cc_transcript-14.18.0 → cc_transcript-14.20.0}/cc_transcript/ledger.py +0 -0
- {cc_transcript-14.18.0 → cc_transcript-14.20.0}/cc_transcript/literals.py +0 -0
- {cc_transcript-14.18.0 → cc_transcript-14.20.0}/cc_transcript/mining/__init__.py +0 -0
- {cc_transcript-14.18.0 → cc_transcript-14.20.0}/cc_transcript/mining/candidates.py +0 -0
- {cc_transcript-14.18.0 → cc_transcript-14.20.0}/cc_transcript/mining/confidence.py +0 -0
- {cc_transcript-14.18.0 → cc_transcript-14.20.0}/cc_transcript/mining/engine.py +0 -0
- {cc_transcript-14.18.0 → cc_transcript-14.20.0}/cc_transcript/mining/formats.py +0 -0
- {cc_transcript-14.18.0 → cc_transcript-14.20.0}/cc_transcript/mining/sampling.py +0 -0
- {cc_transcript-14.18.0 → cc_transcript-14.20.0}/cc_transcript/mining/signals.py +0 -0
- {cc_transcript-14.18.0 → cc_transcript-14.20.0}/cc_transcript/mining/sourcekind.py +0 -0
- {cc_transcript-14.18.0 → cc_transcript-14.20.0}/cc_transcript/mining/spec.py +0 -0
- {cc_transcript-14.18.0 → cc_transcript-14.20.0}/cc_transcript/mining/store.py +0 -0
- {cc_transcript-14.18.0 → cc_transcript-14.20.0}/cc_transcript/models.py +0 -0
- {cc_transcript-14.18.0 → cc_transcript-14.20.0}/cc_transcript/nlp.py +0 -0
- {cc_transcript-14.18.0 → cc_transcript-14.20.0}/cc_transcript/notifications.py +0 -0
- {cc_transcript-14.18.0 → cc_transcript-14.20.0}/cc_transcript/parser.py +0 -0
- {cc_transcript-14.18.0 → cc_transcript-14.20.0}/cc_transcript/py.typed +0 -0
- {cc_transcript-14.18.0 → cc_transcript-14.20.0}/cc_transcript/render.py +0 -0
- {cc_transcript-14.18.0 → cc_transcript-14.20.0}/cc_transcript/sentiment/__init__.py +0 -0
- {cc_transcript-14.18.0 → cc_transcript-14.20.0}/cc_transcript/sentiment/buckets.py +0 -0
- {cc_transcript-14.18.0 → cc_transcript-14.20.0}/cc_transcript/sentiment/data/afinn-en-165.tsv +0 -0
- {cc_transcript-14.18.0 → cc_transcript-14.20.0}/cc_transcript/sentiment/data/domain_overrides.tsv +0 -0
- {cc_transcript-14.18.0 → cc_transcript-14.20.0}/cc_transcript/sentiment/data/en-ewt.udpipe +0 -0
- {cc_transcript-14.18.0 → cc_transcript-14.20.0}/cc_transcript/sentiment/engine.py +0 -0
- {cc_transcript-14.18.0 → cc_transcript-14.20.0}/cc_transcript/sentiment/lexicon.py +0 -0
- {cc_transcript-14.18.0 → cc_transcript-14.20.0}/cc_transcript/sentiment/scorespec.py +0 -0
- {cc_transcript-14.18.0 → cc_transcript-14.20.0}/cc_transcript/synthetic.py +0 -0
- {cc_transcript-14.18.0 → cc_transcript-14.20.0}/cc_transcript/tools.py +0 -0
- {cc_transcript-14.18.0 → cc_transcript-14.20.0}/cc_transcript/watch.py +0 -0
- {cc_transcript-14.18.0 → cc_transcript-14.20.0}/rust/crates/cli/Cargo.toml +0 -0
- {cc_transcript-14.18.0 → cc_transcript-14.20.0}/rust/crates/cli/src/commands/blame.rs +0 -0
- {cc_transcript-14.18.0 → cc_transcript-14.20.0}/rust/crates/cli/src/commands/corrections.rs +0 -0
- {cc_transcript-14.18.0 → cc_transcript-14.20.0}/rust/crates/cli/src/commands/digest.rs +0 -0
- {cc_transcript-14.18.0 → cc_transcript-14.20.0}/rust/crates/cli/src/commands/facts.rs +0 -0
- {cc_transcript-14.18.0 → cc_transcript-14.20.0}/rust/crates/cli/src/commands/grep.rs +0 -0
- {cc_transcript-14.18.0 → cc_transcript-14.20.0}/rust/crates/cli/src/commands/list.rs +0 -0
- {cc_transcript-14.18.0 → cc_transcript-14.20.0}/rust/crates/cli/src/commands/mod.rs +0 -0
- {cc_transcript-14.18.0 → cc_transcript-14.20.0}/rust/crates/cli/src/commands/scratchpad.rs +0 -0
- {cc_transcript-14.18.0 → cc_transcript-14.20.0}/rust/crates/cli/src/commands/show.rs +0 -0
- {cc_transcript-14.18.0 → cc_transcript-14.20.0}/rust/crates/cli/src/commands/slice.rs +0 -0
- {cc_transcript-14.18.0 → cc_transcript-14.20.0}/rust/crates/cli/src/commands/stats.rs +0 -0
- {cc_transcript-14.18.0 → cc_transcript-14.20.0}/rust/crates/cli/src/commands/watch.rs +0 -0
- {cc_transcript-14.18.0 → cc_transcript-14.20.0}/rust/crates/cli/src/lib.rs +0 -0
- {cc_transcript-14.18.0 → cc_transcript-14.20.0}/rust/crates/cli/src/main.rs +0 -0
- {cc_transcript-14.18.0 → cc_transcript-14.20.0}/rust/crates/cli/src/output.rs +0 -0
- {cc_transcript-14.18.0 → cc_transcript-14.20.0}/rust/crates/cli/src/target.rs +0 -0
- {cc_transcript-14.18.0 → cc_transcript-14.20.0}/rust/crates/cli/src/timearg.rs +0 -0
- {cc_transcript-14.18.0 → cc_transcript-14.20.0}/rust/crates/core/Cargo.toml +0 -0
- {cc_transcript-14.18.0 → cc_transcript-14.20.0}/rust/crates/core/benches/parse.rs +0 -0
- {cc_transcript-14.18.0 → cc_transcript-14.20.0}/rust/crates/core/benches/render.rs +0 -0
- {cc_transcript-14.18.0 → cc_transcript-14.20.0}/rust/crates/core/benches/toolcall.rs +0 -0
- {cc_transcript-14.18.0 → cc_transcript-14.20.0}/rust/crates/core/src/activity.rs +0 -0
- {cc_transcript-14.18.0 → cc_transcript-14.20.0}/rust/crates/core/src/actor.rs +0 -0
- {cc_transcript-14.18.0 → cc_transcript-14.20.0}/rust/crates/core/src/blame.rs +0 -0
- {cc_transcript-14.18.0 → cc_transcript-14.20.0}/rust/crates/core/src/buckets.rs +0 -0
- {cc_transcript-14.18.0 → cc_transcript-14.20.0}/rust/crates/core/src/codex/discovery.rs +0 -0
- {cc_transcript-14.18.0 → cc_transcript-14.20.0}/rust/crates/core/src/codex/lower.rs +0 -0
- {cc_transcript-14.18.0 → cc_transcript-14.20.0}/rust/crates/core/src/codex/mod.rs +0 -0
- {cc_transcript-14.18.0 → cc_transcript-14.20.0}/rust/crates/core/src/codex/parse.rs +0 -0
- {cc_transcript-14.18.0 → cc_transcript-14.20.0}/rust/crates/core/src/codex/probe.rs +0 -0
- {cc_transcript-14.18.0 → cc_transcript-14.20.0}/rust/crates/core/src/codex/protocol.rs +0 -0
- {cc_transcript-14.18.0 → cc_transcript-14.20.0}/rust/crates/core/src/codex/types.rs +0 -0
- {cc_transcript-14.18.0 → cc_transcript-14.20.0}/rust/crates/core/src/command.rs +0 -0
- {cc_transcript-14.18.0 → cc_transcript-14.20.0}/rust/crates/core/src/corrections.rs +0 -0
- {cc_transcript-14.18.0 → cc_transcript-14.20.0}/rust/crates/core/src/cost.rs +0 -0
- {cc_transcript-14.18.0 → cc_transcript-14.20.0}/rust/crates/core/src/discovery.rs +0 -0
- {cc_transcript-14.18.0 → cc_transcript-14.20.0}/rust/crates/core/src/facts.rs +0 -0
- {cc_transcript-14.18.0 → cc_transcript-14.20.0}/rust/crates/core/src/feedback.rs +0 -0
- {cc_transcript-14.18.0 → cc_transcript-14.20.0}/rust/crates/core/src/filter.rs +0 -0
- {cc_transcript-14.18.0 → cc_transcript-14.20.0}/rust/crates/core/src/gateway.rs +0 -0
- {cc_transcript-14.18.0 → cc_transcript-14.20.0}/rust/crates/core/src/ids.rs +0 -0
- {cc_transcript-14.18.0 → cc_transcript-14.20.0}/rust/crates/core/src/judge.rs +0 -0
- {cc_transcript-14.18.0 → cc_transcript-14.20.0}/rust/crates/core/src/lib.rs +0 -0
- {cc_transcript-14.18.0 → cc_transcript-14.20.0}/rust/crates/core/src/literals/command.rs +0 -0
- {cc_transcript-14.18.0 → cc_transcript-14.20.0}/rust/crates/core/src/literals/corrections.rs +0 -0
- {cc_transcript-14.18.0 → cc_transcript-14.20.0}/rust/crates/core/src/literals/feedback.rs +0 -0
- {cc_transcript-14.18.0 → cc_transcript-14.20.0}/rust/crates/core/src/literals/mining.rs +0 -0
- {cc_transcript-14.18.0 → cc_transcript-14.20.0}/rust/crates/core/src/literals/mod.rs +0 -0
- {cc_transcript-14.18.0 → cc_transcript-14.20.0}/rust/crates/core/src/literals/protocol.rs +0 -0
- {cc_transcript-14.18.0 → cc_transcript-14.20.0}/rust/crates/core/src/notifications.rs +0 -0
- {cc_transcript-14.18.0 → cc_transcript-14.20.0}/rust/crates/core/src/protocol.rs +0 -0
- {cc_transcript-14.18.0 → cc_transcript-14.20.0}/rust/crates/core/src/pystr.rs +0 -0
- {cc_transcript-14.18.0 → cc_transcript-14.20.0}/rust/crates/core/src/query.rs +0 -0
- {cc_transcript-14.18.0 → cc_transcript-14.20.0}/rust/crates/core/src/rng.rs +0 -0
- {cc_transcript-14.18.0 → cc_transcript-14.20.0}/rust/crates/core/src/schema.rs +0 -0
- {cc_transcript-14.18.0 → cc_transcript-14.20.0}/rust/crates/core/src/sqlite.rs +0 -0
- {cc_transcript-14.18.0 → cc_transcript-14.20.0}/rust/crates/core/src/toolcall.rs +0 -0
- {cc_transcript-14.18.0 → cc_transcript-14.20.0}/rust/crates/core/src/value.rs +0 -0
- {cc_transcript-14.18.0 → cc_transcript-14.20.0}/rust/crates/core/src/watch.rs +0 -0
- {cc_transcript-14.18.0 → cc_transcript-14.20.0}/rust/crates/py/Cargo.toml +0 -0
- {cc_transcript-14.18.0 → cc_transcript-14.20.0}/rust/crates/py/build.rs +0 -0
- {cc_transcript-14.18.0 → cc_transcript-14.20.0}/rust/crates/py/data/command_prefix_pins.tsv +0 -0
- {cc_transcript-14.18.0 → cc_transcript-14.20.0}/rust/crates/py/src/actor_bridge.rs +0 -0
- {cc_transcript-14.18.0 → cc_transcript-14.20.0}/rust/crates/py/src/bin/stub_gen.rs +0 -0
- {cc_transcript-14.18.0 → cc_transcript-14.20.0}/rust/crates/py/src/codex.rs +0 -0
- {cc_transcript-14.18.0 → cc_transcript-14.20.0}/rust/crates/py/src/command.rs +0 -0
- {cc_transcript-14.18.0 → cc_transcript-14.20.0}/rust/crates/py/src/context.rs +0 -0
- {cc_transcript-14.18.0 → cc_transcript-14.20.0}/rust/crates/py/src/corrections.rs +0 -0
- {cc_transcript-14.18.0 → cc_transcript-14.20.0}/rust/crates/py/src/discovery.rs +0 -0
- {cc_transcript-14.18.0 → cc_transcript-14.20.0}/rust/crates/py/src/feedback.rs +0 -0
- {cc_transcript-14.18.0 → cc_transcript-14.20.0}/rust/crates/py/src/judge.rs +0 -0
- {cc_transcript-14.18.0 → cc_transcript-14.20.0}/rust/crates/py/src/lexicon.rs +0 -0
- {cc_transcript-14.18.0 → cc_transcript-14.20.0}/rust/crates/py/src/lib.rs +0 -0
- {cc_transcript-14.18.0 → cc_transcript-14.20.0}/rust/crates/py/src/mining.rs +0 -0
- {cc_transcript-14.18.0 → cc_transcript-14.20.0}/rust/crates/py/src/nlp.rs +0 -0
- {cc_transcript-14.18.0 → cc_transcript-14.20.0}/rust/crates/py/src/python.rs +0 -0
- {cc_transcript-14.18.0 → cc_transcript-14.20.0}/rust/crates/py/src/render.rs +0 -0
- {cc_transcript-14.18.0 → cc_transcript-14.20.0}/rust/crates/py/src/score.rs +0 -0
- {cc_transcript-14.18.0 → cc_transcript-14.20.0}/rust/crates/py/src/sqlite.rs +0 -0
- {cc_transcript-14.18.0 → cc_transcript-14.20.0}/rust/crates/py/src/toolcall.rs +0 -0
- {cc_transcript-14.18.0 → cc_transcript-14.20.0}/rust/crates/py/src/views/blocks.rs +0 -0
- {cc_transcript-14.18.0 → cc_transcript-14.20.0}/rust/crates/py/src/views/command.rs +0 -0
- {cc_transcript-14.18.0 → cc_transcript-14.20.0}/rust/crates/py/src/views/convert.rs +0 -0
- {cc_transcript-14.18.0 → cc_transcript-14.20.0}/rust/crates/py/src/views/dunder.rs +0 -0
- {cc_transcript-14.18.0 → cc_transcript-14.20.0}/rust/crates/py/src/views/events.rs +0 -0
- {cc_transcript-14.18.0 → cc_transcript-14.20.0}/rust/crates/py/src/views/meta.rs +0 -0
- {cc_transcript-14.18.0 → cc_transcript-14.20.0}/rust/crates/py/src/views/mod.rs +0 -0
- {cc_transcript-14.18.0 → cc_transcript-14.20.0}/rust/crates/py/src/views/print.rs +0 -0
- {cc_transcript-14.18.0 → cc_transcript-14.20.0}/rust/crates/py/src/views/store.rs +0 -0
- {cc_transcript-14.18.0 → cc_transcript-14.20.0}/rust/crates/py/src/views/system.rs +0 -0
- {cc_transcript-14.18.0 → cc_transcript-14.20.0}/rust/crates/py/src/views/toolcall.rs +0 -0
- {cc_transcript-14.18.0 → cc_transcript-14.20.0}/rust/crates/py/src/views/toolresult.rs +0 -0
- {cc_transcript-14.18.0 → cc_transcript-14.20.0}/rust/crates/py/src/views/transcript.rs +0 -0
- {cc_transcript-14.18.0 → cc_transcript-14.20.0}/rust/crates/py/src/watch.rs +0 -0
|
@@ -1498,12 +1498,17 @@ class QueuedCommand:
|
|
|
1498
1498
|
plain-string prompt (e.g. an image-paste payload).
|
|
1499
1499
|
command_mode: How the command was queued, e.g. ``prompt`` or
|
|
1500
1500
|
``task-notification``, or None.
|
|
1501
|
+
origin: Who sent a ``prompt``-mode command: ``human`` for the user,
|
|
1502
|
+
``peer`` for another agent or session, ``channel`` for an MCP
|
|
1503
|
+
channel event; None when the transcript does not record it.
|
|
1501
1504
|
"""
|
|
1502
1505
|
__match_args__: typing.ClassVar[tuple[str, ...]]
|
|
1503
1506
|
@property
|
|
1504
1507
|
def prompt(self) -> typing.Optional[builtins.str]: ...
|
|
1505
1508
|
@property
|
|
1506
1509
|
def command_mode(self) -> typing.Optional[builtins.str]: ...
|
|
1510
|
+
@property
|
|
1511
|
+
def origin(self) -> typing.Optional[builtins.str]: ...
|
|
1507
1512
|
|
|
1508
1513
|
@typing.final
|
|
1509
1514
|
class ReadCall(ToolCallBase):
|
|
@@ -285,8 +285,10 @@ class ActivityLift:
|
|
|
285
285
|
re-derives the session.
|
|
286
286
|
|
|
287
287
|
Feed only the events appended since the previous call: nothing checks
|
|
288
|
-
provenance, so events fed twice lift twice.
|
|
289
|
-
|
|
288
|
+
provenance, so events fed twice lift twice. The contract is append-only:
|
|
289
|
+
a cursor's transcript is assumed never rewritten behind the events it has
|
|
290
|
+
been fed, and a transcript rewritten in place, rather than appended to,
|
|
291
|
+
needs a new cursor. Claude Code and Codex transcripts satisfy it.
|
|
290
292
|
|
|
291
293
|
The classifier is read once, at construction, and must be deterministic
|
|
292
294
|
and event-only: its answer for an event has to stay the same as later
|
|
@@ -10,21 +10,23 @@ predicates compose over progressively narrower windows.
|
|
|
10
10
|
|
|
11
11
|
from __future__ import annotations
|
|
12
12
|
|
|
13
|
+
import os
|
|
13
14
|
import re
|
|
14
15
|
import threading
|
|
15
16
|
from collections import OrderedDict
|
|
16
17
|
from collections.abc import Mapping
|
|
17
|
-
from dataclasses import dataclass, field
|
|
18
|
+
from dataclasses import dataclass, field, replace
|
|
18
19
|
from fnmatch import fnmatch
|
|
19
20
|
from functools import cached_property
|
|
20
21
|
from pathlib import PurePath
|
|
22
|
+
from time import monotonic
|
|
21
23
|
from typing import TYPE_CHECKING, ClassVar
|
|
22
24
|
|
|
23
|
-
from cc_transcript.activity import SessionActivity, Turn, event_stamps, native_user_classifier
|
|
25
|
+
from cc_transcript.activity import ActivityLift, SessionActivity, Turn, event_stamps, native_user_classifier
|
|
24
26
|
from cc_transcript.discovery import TranscriptExpiredError, resolve, subagent_paths, subagent_transcripts
|
|
25
27
|
from cc_transcript.filterspec import event_meta, session_id_of
|
|
26
28
|
from cc_transcript.ids import SessionId, ToolUseId
|
|
27
|
-
from cc_transcript.models import AssistantEvent, SystemEvent, ToolResultBlock, UserEvent
|
|
29
|
+
from cc_transcript.models import AssistantEvent, AttachmentEvent, QueuedCommand, SystemEvent, ToolResultBlock, UserEvent
|
|
28
30
|
from cc_transcript.notifications import Notifications
|
|
29
31
|
from cc_transcript.parser import parse
|
|
30
32
|
from cc_transcript.tools import (
|
|
@@ -45,45 +47,79 @@ if TYPE_CHECKING:
|
|
|
45
47
|
from cc_transcript.activity import ToolUse, UserClassifier
|
|
46
48
|
from cc_transcript.command import CommandLine
|
|
47
49
|
from cc_transcript.ids import EventUuid
|
|
48
|
-
from cc_transcript.models import TranscriptEvent
|
|
50
|
+
from cc_transcript.models import Transcript, TranscriptEvent
|
|
49
51
|
|
|
50
52
|
|
|
51
53
|
DEEP_LIFT_BUDGET = 1024 * 1024 * 1024
|
|
54
|
+
DEEP_LIFT_FENCE = 64
|
|
55
|
+
IDLE_SECONDS_BEFORE_RELEASE = 900
|
|
52
56
|
|
|
53
|
-
type LiftStamp = tuple[int, int, int]
|
|
57
|
+
type LiftStamp = tuple[int, int, int, int]
|
|
54
58
|
type LiftKey = tuple[Path, int, ToolUseId | None]
|
|
55
59
|
|
|
56
60
|
|
|
61
|
+
def stamp_of(stat: os.stat_result) -> LiftStamp:
|
|
62
|
+
return (stat.st_size, stat.st_mtime_ns, stat.st_ctime_ns, stat.st_ino)
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
@dataclass(frozen=True, slots=True)
|
|
66
|
+
class DeepLift:
|
|
67
|
+
stamp: LiftStamp
|
|
68
|
+
deep: DeepSession
|
|
69
|
+
lift: ActivityLift
|
|
70
|
+
consumed: int
|
|
71
|
+
fence: bytes
|
|
72
|
+
found: SessionId | None
|
|
73
|
+
provider: str | None
|
|
74
|
+
grown_at: float
|
|
75
|
+
|
|
76
|
+
|
|
57
77
|
@dataclass(slots=True)
|
|
58
78
|
class LiftCache:
|
|
59
|
-
held: OrderedDict[LiftKey,
|
|
79
|
+
held: OrderedDict[LiftKey, DeepLift] = field(default_factory=OrderedDict)
|
|
60
80
|
size: int = 0
|
|
61
81
|
guard: threading.Lock = field(default_factory=threading.Lock)
|
|
62
82
|
|
|
63
|
-
def get(self, key: LiftKey, stamp: LiftStamp) ->
|
|
83
|
+
def get(self, key: LiftKey, stamp: LiftStamp) -> DeepLift | None:
|
|
64
84
|
with self.guard:
|
|
65
85
|
match self.held.get(key):
|
|
66
|
-
case
|
|
67
|
-
return None
|
|
68
|
-
case (held_stamp, deep) if held_stamp == stamp:
|
|
86
|
+
case DeepLift(stamp=held_stamp) as lift if held_stamp == stamp:
|
|
69
87
|
self.held.move_to_end(key)
|
|
70
|
-
return
|
|
88
|
+
return lift
|
|
71
89
|
case _:
|
|
72
|
-
self.drop(key)
|
|
73
90
|
return None
|
|
74
91
|
|
|
75
|
-
def
|
|
92
|
+
def idle_release(self, key: LiftKey, stamp: LiftStamp) -> None:
|
|
93
|
+
"""Releases ``key``'s cursor once its file has gone :data:`IDLE_SECONDS_BEFORE_RELEASE` unwritten.
|
|
94
|
+
|
|
95
|
+
A held cursor amortizes the next growth of a live sidechain; a finished one earns
|
|
96
|
+
nothing. Expiry is elapsed inactivity, not a hit count, so a burst of predicate
|
|
97
|
+
traffic over a cursor that just grew never releases it — the clock is read from the
|
|
98
|
+
entry's last growth. Any growth replaces the entry, resetting ``grown_at``.
|
|
99
|
+
"""
|
|
100
|
+
with self.guard:
|
|
101
|
+
match self.held.get(key):
|
|
102
|
+
case DeepLift(stamp=held_stamp, grown_at=at) if held_stamp == stamp:
|
|
103
|
+
if monotonic() - at >= IDLE_SECONDS_BEFORE_RELEASE:
|
|
104
|
+
self.drop(key)
|
|
105
|
+
|
|
106
|
+
def take(self, key: LiftKey) -> DeepLift | None:
|
|
107
|
+
with self.guard:
|
|
108
|
+
return self.drop(key)
|
|
109
|
+
|
|
110
|
+
def put(self, key: LiftKey, lift: DeepLift) -> DeepLift:
|
|
76
111
|
with self.guard:
|
|
77
112
|
self.drop(key)
|
|
78
|
-
self.held[key] =
|
|
79
|
-
self.size += stamp[0]
|
|
113
|
+
self.held[key] = lift
|
|
114
|
+
self.size += lift.stamp[0]
|
|
80
115
|
while self.size > DEEP_LIFT_BUDGET:
|
|
81
116
|
self.drop(next(iter(self.held)))
|
|
82
|
-
return
|
|
117
|
+
return lift
|
|
83
118
|
|
|
84
|
-
def drop(self, key: LiftKey) -> None:
|
|
85
|
-
if (
|
|
86
|
-
self.size -=
|
|
119
|
+
def drop(self, key: LiftKey) -> DeepLift | None:
|
|
120
|
+
if (lift := self.held.pop(key, None)) is not None:
|
|
121
|
+
self.size -= lift.stamp[0]
|
|
122
|
+
return lift
|
|
87
123
|
|
|
88
124
|
def clear(self) -> None:
|
|
89
125
|
with self.guard:
|
|
@@ -95,14 +131,51 @@ class LiftCache:
|
|
|
95
131
|
|
|
96
132
|
|
|
97
133
|
DEEP_LIFTS = LiftCache()
|
|
98
|
-
"""Lifted sidechain transcripts, keyed by tree position and stamped
|
|
134
|
+
"""Lifted sidechain transcripts, keyed by tree position and stamped ``(size, mtime_ns, ctime_ns, inode)``.
|
|
99
135
|
|
|
100
136
|
A least-recently-used hold bounded at :data:`DEEP_LIFT_BUDGET` source bytes. One resident
|
|
101
137
|
process serves many lead sessions, so the tree walked most recently stays held while an
|
|
102
|
-
idle lead's sidechains age out
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
138
|
+
idle lead's sidechains age out. A lifted session costs about 2.3 times its source bytes in
|
|
139
|
+
resident memory, so the default holds roughly 2.3 GiB. A single tree larger than the budget
|
|
140
|
+
reparses the part that no longer fits on every walk.
|
|
141
|
+
|
|
142
|
+
Each entry keeps the :class:`~cc_transcript.activity.ActivityLift` cursor behind its lift and
|
|
143
|
+
the byte offset it has consumed, which always ends on a newline. A Claude transcript that has
|
|
144
|
+
only grown — same inode, larger, its last held bytes unchanged — is extended by its appended
|
|
145
|
+
lines alone: the tail past the last newline is parsed for the session but never fed to the
|
|
146
|
+
cursor. Only Claude is extended incrementally, because its parse is line-compositional; a
|
|
147
|
+
Codex rollout lowers against whole-session context (echo sets, the model timeline), so its
|
|
148
|
+
provider is pinned on the cold parse and any growth cold-relifts, as does a growth whose
|
|
149
|
+
appended chunk parses as a different provider than the file was pinned to. An empty file pins
|
|
150
|
+
no provider until a line lands, extending while its appended lines parse as Claude and
|
|
151
|
+
cold-relifting the moment a Codex line completes. A shrink, a replaced or rewritten file, a
|
|
152
|
+
short read, or a session id that surfaces only in the appended lines lifts the file afresh.
|
|
153
|
+
|
|
154
|
+
The contract is append-only: a held transcript is assumed never rewritten behind its consumed
|
|
155
|
+
offset. Growth is recognized by the same inode, a larger size, and the last
|
|
156
|
+
:data:`DEEP_LIFT_FENCE` consumed bytes still in place, with the descriptor's stat rechecked
|
|
157
|
+
around the read; a shrink, a replaced inode, a same-size rewrite, or a fence mismatch triggers
|
|
158
|
+
a cold relift. An in-place rewrite of earlier bytes that keeps the fence and grows the file is
|
|
159
|
+
not detected and is folded in as growth. Claude Code and Codex transcripts satisfy the contract.
|
|
160
|
+
Every read validates the open descriptor's stat after reading against the stat that routed it
|
|
161
|
+
— a replacement between the routing stat and the read moves the inode and ctime — so a growth
|
|
162
|
+
that reads spliced bytes falls to a cold relift, and a cold read whose descriptor moved under
|
|
163
|
+
it answers the request but is published to no cache — not here, not :data:`SIDECHAIN_INPUTS`,
|
|
164
|
+
not :data:`UNREADABLE` — and every publication is keyed by the descriptor stamp the read
|
|
165
|
+
verified rather than the routing stat. The cursor is mutable, so a stamp miss takes the
|
|
166
|
+
entry out of the hold before extending it and puts the grown entry back: a concurrent reader
|
|
167
|
+
never sees a cursor mid-extension, and two threads that miss together each produce a correct
|
|
168
|
+
entry, the later put replacing the earlier. A dropped entry releases its cursor with it.
|
|
169
|
+
|
|
170
|
+
:meth:`Session.walk` holds every transcript it reaches; the ``has_*`` predicates hold only a
|
|
171
|
+
transcript they have had to lift twice, since a sidechain that changed once is a running
|
|
172
|
+
subagent whose next growth would otherwise cost a whole relift, while a finished one is
|
|
173
|
+
lifted once and its :class:`PredicateInputs` kept instead. A held cursor is released once its
|
|
174
|
+
file has gone :data:`IDLE_SECONDS_BEFORE_RELEASE` unwritten — elapsed inactivity since its
|
|
175
|
+
last growth, not a count of walks, so aggregate predicate traffic from many readers never
|
|
176
|
+
expires a cursor that just grew — so resident cursors track the set of sidechains still being
|
|
177
|
+
written rather than every one that ever grew; a released file that grows again pays one relift
|
|
178
|
+
and is re-admitted. :data:`DEEP_LIFT_BUDGET` stays the upper bound over that live set.
|
|
106
179
|
"""
|
|
107
180
|
|
|
108
181
|
|
|
@@ -131,6 +204,37 @@ class PredicateInputsCache:
|
|
|
131
204
|
self.held.popitem(last=False)
|
|
132
205
|
return inputs
|
|
133
206
|
|
|
207
|
+
def holds(self, path: Path) -> bool:
|
|
208
|
+
with self.guard:
|
|
209
|
+
return path in self.held
|
|
210
|
+
|
|
211
|
+
def clear(self) -> None:
|
|
212
|
+
with self.guard:
|
|
213
|
+
self.held.clear()
|
|
214
|
+
|
|
215
|
+
def __len__(self) -> int:
|
|
216
|
+
return len(self.held)
|
|
217
|
+
|
|
218
|
+
|
|
219
|
+
@dataclass(slots=True)
|
|
220
|
+
class StampSet:
|
|
221
|
+
held: OrderedDict[Path, LiftStamp] = field(default_factory=OrderedDict)
|
|
222
|
+
guard: threading.Lock = field(default_factory=threading.Lock)
|
|
223
|
+
|
|
224
|
+
def has(self, path: Path, stamp: LiftStamp) -> bool:
|
|
225
|
+
with self.guard:
|
|
226
|
+
if self.held.get(path) != stamp:
|
|
227
|
+
return False
|
|
228
|
+
self.held.move_to_end(path)
|
|
229
|
+
return True
|
|
230
|
+
|
|
231
|
+
def put(self, path: Path, stamp: LiftStamp) -> None:
|
|
232
|
+
with self.guard:
|
|
233
|
+
self.held[path] = stamp
|
|
234
|
+
self.held.move_to_end(path)
|
|
235
|
+
while len(self.held) > SIDECHAIN_INPUTS_LIMIT:
|
|
236
|
+
self.held.popitem(last=False)
|
|
237
|
+
|
|
134
238
|
def clear(self) -> None:
|
|
135
239
|
with self.guard:
|
|
136
240
|
self.held.clear()
|
|
@@ -149,6 +253,21 @@ process re-answering the same predicates per event neither reparses nor holds th
|
|
|
149
253
|
"""
|
|
150
254
|
|
|
151
255
|
|
|
256
|
+
UNREADABLE = StampSet()
|
|
257
|
+
"""Transcripts whose typed parse failed, keyed by path and stamped like :data:`DEEP_LIFTS`.
|
|
258
|
+
|
|
259
|
+
A sidechain carrying one line the typed parser rejects — schema drift Claude Code has not
|
|
260
|
+
caught up to — cannot be lifted, so a walk skips it. Without this a resident process re-read
|
|
261
|
+
and re-parsed the whole file on every walk forever. Only a deterministic parse failure
|
|
262
|
+
(:class:`UnparseableTranscript`) is held, never a transient filesystem error, which must
|
|
263
|
+
retry. The failure is held by full stamp — the stamp of the descriptor the rejected bytes were
|
|
264
|
+
read from, never the stat that routed the read, and only when that read settled — so an
|
|
265
|
+
unchanged bad file is skipped at stamp-check cost, any change — a growth that completes the
|
|
266
|
+
line, a rewrite — clears the miss and retries, and a file that was swapped under the read is
|
|
267
|
+
never held against the file the path names next time.
|
|
268
|
+
"""
|
|
269
|
+
|
|
270
|
+
|
|
152
271
|
SIDECHAIN_LISTINGS: OrderedDict[Path, tuple[tuple[int, int], tuple[tuple[Path, bool], ...]]] = OrderedDict()
|
|
153
272
|
"""Each non-empty ``subagents`` listing and whether each file is a symlink, stamped ``(inode, mtime_ns)``.
|
|
154
273
|
|
|
@@ -465,13 +584,16 @@ class Session:
|
|
|
465
584
|
never this session itself. A resolved-path seen-set (seeded with
|
|
466
585
|
:attr:`path`) dedupes: the first occurrence of a path wins, so a
|
|
467
586
|
tree-discovered sidechain outranks an equal attachment, and symlink
|
|
468
|
-
cycles terminate. An unreadable transcript is skipped
|
|
469
|
-
are still walked
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
or a resident process that re-walks per event —
|
|
587
|
+
cycles terminate. An unreadable or unparseable transcript is skipped
|
|
588
|
+
but its children are still walked.
|
|
589
|
+
|
|
590
|
+
Each reached transcript is parsed and lifted once per ``(size, mtime,
|
|
591
|
+
ctime, inode)`` stamp and memoized across walks, and one that has only
|
|
592
|
+
grown is extended by its appended lines, so a predicate that walks
|
|
593
|
+
repeatedly — or a resident process that re-walks per event — parses
|
|
594
|
+
only what was appended. Transcripts are assumed append-only: a
|
|
595
|
+
rewrite of earlier bytes that keeps the file's consumed tail and
|
|
596
|
+
grows it is read as growth, as :data:`DEEP_LIFTS` documents.
|
|
475
597
|
"""
|
|
476
598
|
return deep_sessions(self)
|
|
477
599
|
|
|
@@ -520,6 +642,16 @@ class Session:
|
|
|
520
642
|
"""The window's last ``n`` events."""
|
|
521
643
|
return windowed(self, max(len(self) - n, 0), len(self))
|
|
522
644
|
|
|
645
|
+
def recent_messages(self, n: int) -> Session:
|
|
646
|
+
"""The window reaching back over its last ``n`` messages, for positive ``n``.
|
|
647
|
+
|
|
648
|
+
A message is a user or assistant event or a queued command. The hook,
|
|
649
|
+
reminder, system and mode events between messages ride along without
|
|
650
|
+
counting, so harness noise never crowds the conversation out of the window.
|
|
651
|
+
"""
|
|
652
|
+
messages = [index for index, event in enumerate(self.events) if is_message(event)]
|
|
653
|
+
return windowed(self, messages[-n] if n <= len(messages) else 0, len(self))
|
|
654
|
+
|
|
523
655
|
@property
|
|
524
656
|
def current_turn(self) -> Session:
|
|
525
657
|
"""The one-turn view of the window's last turn."""
|
|
@@ -578,9 +710,11 @@ class Session:
|
|
|
578
710
|
def deep_inputs(self) -> Iterator[PredicateInputs]:
|
|
579
711
|
"""This window's :class:`PredicateInputs`, then those of every transcript :meth:`walk` reaches.
|
|
580
712
|
|
|
581
|
-
The walk order and dedupe match :meth:`walk`, but
|
|
582
|
-
one's inputs are held per ``(size, mtime, inode)`` stamp,
|
|
583
|
-
once its stamp moves
|
|
713
|
+
The walk order and dedupe match :meth:`walk`, but a reached transcript stays lifted only
|
|
714
|
+
once it has changed: each one's inputs are held per ``(size, mtime, ctime, inode)`` stamp,
|
|
715
|
+
a transcript is lifted again only once its stamp moves, and from then on one that has only
|
|
716
|
+
grown is extended by its appended lines under the append-only contract :data:`DEEP_LIFTS`
|
|
717
|
+
documents. Prefer it to :meth:`walk` for any predicate these inputs can answer.
|
|
584
718
|
"""
|
|
585
719
|
yield self.predicate_inputs
|
|
586
720
|
for path, depth, spawned_by in reachable_transcripts(self):
|
|
@@ -836,6 +970,12 @@ class DeepView:
|
|
|
836
970
|
return iter(self.sessions)
|
|
837
971
|
|
|
838
972
|
|
|
973
|
+
def is_message(event: TranscriptEvent) -> bool:
|
|
974
|
+
return isinstance(event, UserEvent | AssistantEvent) or (
|
|
975
|
+
isinstance(event, AttachmentEvent) and isinstance(event.detail, QueuedCommand)
|
|
976
|
+
)
|
|
977
|
+
|
|
978
|
+
|
|
839
979
|
def windowed(session: Session, start: int, stop: int) -> Session:
|
|
840
980
|
turns: list[Turn] = []
|
|
841
981
|
base = 0
|
|
@@ -906,17 +1046,18 @@ def sidechain_listing(parent: Path) -> tuple[tuple[Path, Path], ...]:
|
|
|
906
1046
|
|
|
907
1047
|
def load_predicate_inputs(path: Path, depth: int, spawned_by: ToolUseId | None) -> PredicateInputs | None:
|
|
908
1048
|
try:
|
|
909
|
-
|
|
1049
|
+
stamp = stamp_of(path.stat())
|
|
910
1050
|
except OSError:
|
|
911
1051
|
return None
|
|
912
|
-
stamp = (stat.st_size, stat.st_mtime_ns, stat.st_ino)
|
|
913
1052
|
if (held := SIDECHAIN_INPUTS.get(path, stamp)) is not None:
|
|
1053
|
+
DEEP_LIFTS.idle_release((path, depth, spawned_by), stamp)
|
|
914
1054
|
return held
|
|
915
1055
|
try:
|
|
916
|
-
deep =
|
|
1056
|
+
deep, read, settled = deep_session_at(path, depth, spawned_by, stamp, hold=SIDECHAIN_INPUTS.holds(path))
|
|
917
1057
|
except OSError:
|
|
918
1058
|
return None
|
|
919
|
-
|
|
1059
|
+
inputs = PredicateInputs.of(deep.session)
|
|
1060
|
+
return SIDECHAIN_INPUTS.put(path, read, inputs) if settled else inputs
|
|
920
1061
|
|
|
921
1062
|
|
|
922
1063
|
def deep_sessions(root: Session) -> Iterator[DeepSession]:
|
|
@@ -925,28 +1066,136 @@ def deep_sessions(root: Session) -> Iterator[DeepSession]:
|
|
|
925
1066
|
yield deep
|
|
926
1067
|
|
|
927
1068
|
|
|
928
|
-
def lift_deep_session(path: Path, depth: int, spawned_by: ToolUseId | None) -> DeepSession:
|
|
929
|
-
transcript = parse(path)
|
|
930
|
-
session_id = session_id_of(transcript.events) or SessionId(path.stem)
|
|
931
|
-
return DeepSession(
|
|
932
|
-
session=Session.from_activity(SessionActivity.from_events(session_id, transcript.events), path=path),
|
|
933
|
-
path=path,
|
|
934
|
-
provider=transcript.provider,
|
|
935
|
-
depth=depth,
|
|
936
|
-
spawned_by=spawned_by,
|
|
937
|
-
)
|
|
938
|
-
|
|
939
|
-
|
|
940
1069
|
def load_deep_session(path: Path, depth: int, spawned_by: ToolUseId | None) -> DeepSession | None:
|
|
941
1070
|
try:
|
|
942
|
-
|
|
1071
|
+
return deep_session_at(path, depth, spawned_by, stamp_of(path.stat()), hold=True)[0]
|
|
943
1072
|
except OSError:
|
|
944
1073
|
return None
|
|
945
|
-
|
|
1074
|
+
|
|
1075
|
+
|
|
1076
|
+
def deep_session_at(
|
|
1077
|
+
path: Path, depth: int, spawned_by: ToolUseId | None, stamp: LiftStamp, *, hold: bool
|
|
1078
|
+
) -> tuple[DeepSession, LiftStamp, bool]:
|
|
946
1079
|
key = (path, depth, spawned_by)
|
|
947
1080
|
if (held := DEEP_LIFTS.get(key, stamp)) is not None:
|
|
948
|
-
return held
|
|
1081
|
+
return held.deep, stamp, True
|
|
1082
|
+
if UNREADABLE.has(path, stamp):
|
|
1083
|
+
raise UnparseableTranscript(path, stamp, True)
|
|
1084
|
+
stale = DEEP_LIFTS.take(key)
|
|
949
1085
|
try:
|
|
950
|
-
|
|
951
|
-
|
|
1086
|
+
if stale is not None and (grown := grown_lift(stale, path, stamp)) is not None:
|
|
1087
|
+
return DEEP_LIFTS.put(key, grown).deep, stamp, True
|
|
1088
|
+
lifted, settled = lift_deep_session(path, depth, spawned_by)
|
|
1089
|
+
except UnparseableTranscript as failure:
|
|
1090
|
+
if failure.settled:
|
|
1091
|
+
UNREADABLE.put(path, failure.stamp)
|
|
1092
|
+
raise
|
|
1093
|
+
if settled and (hold or stale is not None):
|
|
1094
|
+
DEEP_LIFTS.put(key, lifted)
|
|
1095
|
+
return lifted.deep, lifted.stamp, settled
|
|
1096
|
+
|
|
1097
|
+
|
|
1098
|
+
def lift_deep_session(path: Path, depth: int, spawned_by: ToolUseId | None) -> tuple[DeepLift, bool]:
|
|
1099
|
+
with path.open("rb") as handle:
|
|
1100
|
+
stamp = stamp_of(before := os.fstat(handle.fileno()))
|
|
1101
|
+
raw = handle.read(before.st_size)
|
|
1102
|
+
settled = stamp_of(os.fstat(handle.fileno())) == stamp
|
|
1103
|
+
events = list((transcript := parsed(path, raw, stamp, settled)).events)
|
|
1104
|
+
session_id = (found := session_id_of(events)) or SessionId(path.stem)
|
|
1105
|
+
cut = raw.rfind(b"\n") + 1
|
|
1106
|
+
committed = len(events) if cut == len(raw) else len(events) - len(parsed(path, raw[cut:], stamp, settled).events)
|
|
1107
|
+
activity = (lift := ActivityLift(session_id)).extend(events[:committed])
|
|
1108
|
+
return DeepLift(
|
|
1109
|
+
stamp=stamp,
|
|
1110
|
+
deep=DeepSession(
|
|
1111
|
+
session=Session.from_activity(
|
|
1112
|
+
activity if committed == len(events) else SessionActivity.from_events(session_id, events), path=path
|
|
1113
|
+
),
|
|
1114
|
+
path=path,
|
|
1115
|
+
provider=transcript.provider,
|
|
1116
|
+
depth=depth,
|
|
1117
|
+
spawned_by=spawned_by,
|
|
1118
|
+
),
|
|
1119
|
+
lift=lift,
|
|
1120
|
+
consumed=cut,
|
|
1121
|
+
fence=raw[max(cut - DEEP_LIFT_FENCE, 0) : cut],
|
|
1122
|
+
found=found,
|
|
1123
|
+
provider=transcript.provider if events else None,
|
|
1124
|
+
grown_at=monotonic(),
|
|
1125
|
+
), settled
|
|
1126
|
+
|
|
1127
|
+
|
|
1128
|
+
def grown_lift(held: DeepLift, path: Path, stamp: LiftStamp) -> DeepLift | None:
|
|
1129
|
+
if (
|
|
1130
|
+
held.provider not in (None, "claude")
|
|
1131
|
+
or stamp[3] != held.stamp[3]
|
|
1132
|
+
or stamp[0] <= held.stamp[0]
|
|
1133
|
+
or stamp[0] < held.consumed
|
|
1134
|
+
):
|
|
952
1135
|
return None
|
|
1136
|
+
if (read := appended_bytes(path, held.consumed - len(held.fence), stamp[0], stamp)) is None or not read.startswith(
|
|
1137
|
+
held.fence
|
|
1138
|
+
):
|
|
1139
|
+
return None
|
|
1140
|
+
appended = read[len(held.fence) :]
|
|
1141
|
+
cut = appended.rfind(b"\n") + 1
|
|
1142
|
+
if (committed_chunk := parsed(path, appended[:cut], stamp, True)).provider != "claude":
|
|
1143
|
+
return None
|
|
1144
|
+
if cut != len(appended) and (tail_chunk := parsed(path, appended[cut:], stamp, True)).provider != "claude":
|
|
1145
|
+
return None
|
|
1146
|
+
committed = list(committed_chunk.events)
|
|
1147
|
+
tail = [] if cut == len(appended) else list(tail_chunk.events)
|
|
1148
|
+
found = held.found or session_id_of([*committed, *tail])
|
|
1149
|
+
if (found or SessionId(path.stem)) != held.lift.session_id:
|
|
1150
|
+
return None
|
|
1151
|
+
activity = held.lift.extend(committed)
|
|
1152
|
+
session = Session.from_activity(
|
|
1153
|
+
activity
|
|
1154
|
+
if not tail
|
|
1155
|
+
else SessionActivity.from_events(
|
|
1156
|
+
held.lift.session_id, [*(event for turn in activity.turns for event in turn.events), *tail]
|
|
1157
|
+
),
|
|
1158
|
+
path=path,
|
|
1159
|
+
)
|
|
1160
|
+
return DeepLift(
|
|
1161
|
+
stamp=stamp,
|
|
1162
|
+
deep=replace(held.deep, session=session),
|
|
1163
|
+
lift=held.lift,
|
|
1164
|
+
consumed=held.consumed + cut,
|
|
1165
|
+
fence=(held.fence + appended[:cut])[-DEEP_LIFT_FENCE:],
|
|
1166
|
+
found=found,
|
|
1167
|
+
provider=held.provider or ("claude" if committed or tail else None),
|
|
1168
|
+
grown_at=monotonic(),
|
|
1169
|
+
)
|
|
1170
|
+
|
|
1171
|
+
|
|
1172
|
+
def appended_bytes(path: Path, start: int, stop: int, stamp: LiftStamp) -> bytes | None:
|
|
1173
|
+
with path.open("rb") as handle:
|
|
1174
|
+
handle.seek(start)
|
|
1175
|
+
read = handle.read(stop - start)
|
|
1176
|
+
settled = stamp_of(os.fstat(handle.fileno())) == stamp
|
|
1177
|
+
return read if len(read) == stop - start and settled else None
|
|
1178
|
+
|
|
1179
|
+
|
|
1180
|
+
class UnparseableTranscript(OSError):
|
|
1181
|
+
"""A transcript a typed parse rejected deterministically, not a filesystem failure.
|
|
1182
|
+
|
|
1183
|
+
Raised only for a line the parser cannot type — schema drift — so a walk can hold the
|
|
1184
|
+
failure by stamp, where a transient ``OSError`` (EACCES, EIO, an ENOENT race) must
|
|
1185
|
+
retry. An ``OSError`` subclass so both still skip the transcript rather than raise.
|
|
1186
|
+
Carries the stamp of the descriptor the rejected bytes were read from and whether that
|
|
1187
|
+
read settled, so the failure is held under the bytes actually parsed, never under the
|
|
1188
|
+
stat that routed the read, and never when the file moved under it.
|
|
1189
|
+
"""
|
|
1190
|
+
|
|
1191
|
+
def __init__(self, path: Path, stamp: LiftStamp, settled: bool) -> None:
|
|
1192
|
+
super().__init__(f"unreadable transcript: {path}")
|
|
1193
|
+
self.stamp = stamp
|
|
1194
|
+
self.settled = settled
|
|
1195
|
+
|
|
1196
|
+
|
|
1197
|
+
def parsed(path: Path, raw: bytes, stamp: LiftStamp, settled: bool) -> Transcript:
|
|
1198
|
+
try:
|
|
1199
|
+
return parse(raw)
|
|
1200
|
+
except (KeyError, ValueError):
|
|
1201
|
+
raise UnparseableTranscript(path, stamp, settled) from None
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "cc-transcript"
|
|
3
|
-
version = "14.
|
|
3
|
+
version = "14.20.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"
|
|
@@ -11,7 +11,7 @@ use sonic_rs::{JsonContainerTrait, JsonValueTrait, Value};
|
|
|
11
11
|
use crate::activity::{LiftedSession, ToolUse, Turn};
|
|
12
12
|
use crate::ids::{encode_string, tool_digest, EventRef};
|
|
13
13
|
use crate::parse::parse_questions;
|
|
14
|
-
use crate::render::{clip, render_tool_call, render_turn, Budget};
|
|
14
|
+
use crate::render::{clip, human_queued_prompt, render_tool_call, render_turn, Budget};
|
|
15
15
|
use crate::toolcall::{parse_tool_call, parse_tool_result, ToolResult};
|
|
16
16
|
use crate::types::{ContentBlock, Entry, Question, ToolUseBlock};
|
|
17
17
|
use crate::value::{field, field_bool, field_str};
|
|
@@ -19,7 +19,7 @@ use crate::value::{field, field_bool, field_str};
|
|
|
19
19
|
pub const SCHEMA: &str = "cc-transcript.context/2";
|
|
20
20
|
pub const PREVIEW_SCHEMA: &str = "cc-transcript.preview/1";
|
|
21
21
|
pub const SUMMARY_LABEL: &str = "[summary fidelity — transcript unavailable]";
|
|
22
|
-
const ASK_USER_QUESTION: &str = "AskUserQuestion";
|
|
22
|
+
pub(crate) const ASK_USER_QUESTION: &str = "AskUserQuestion";
|
|
23
23
|
|
|
24
24
|
/// One structured preview part of a turn (context.py `Preview`): typed text, a tool
|
|
25
25
|
/// call reduced to name + digest + summary, or an AskUserQuestion round with its
|
|
@@ -549,8 +549,17 @@ fn build_previews(turn: &Turn, budget: &Budget) -> Vec<Preview> {
|
|
|
549
549
|
}
|
|
550
550
|
let mut uses = turn.tool_uses.iter();
|
|
551
551
|
for &event in &turn.events {
|
|
552
|
-
let
|
|
553
|
-
|
|
552
|
+
let assistant = match event {
|
|
553
|
+
Entry::Assistant(assistant) => assistant,
|
|
554
|
+
Entry::Attachment(attachment) => {
|
|
555
|
+
if let Some(prompt) = human_queued_prompt(&attachment.detail) {
|
|
556
|
+
parts.push(Preview::Text {
|
|
557
|
+
text: clip(prompt, budget.turn_chars),
|
|
558
|
+
});
|
|
559
|
+
}
|
|
560
|
+
continue;
|
|
561
|
+
}
|
|
562
|
+
_ => continue,
|
|
554
563
|
};
|
|
555
564
|
for block in &assistant.blocks {
|
|
556
565
|
match block {
|
|
@@ -478,6 +478,7 @@ fn parse_attachment_detail(data: &Value) -> AttachmentDetail {
|
|
|
478
478
|
Some("queued_command") => AttachmentDetail::QueuedCommand(QueuedCommand {
|
|
479
479
|
prompt: opt_str(att, "prompt"),
|
|
480
480
|
command_mode: opt_str(att, "commandMode"),
|
|
481
|
+
origin: field(att, "origin").and_then(|origin| opt_str(origin, "kind")),
|
|
481
482
|
}),
|
|
482
483
|
Some("deferred_tools_delta") => AttachmentDetail::DeferredToolsDelta(DeferredToolsDelta {
|
|
483
484
|
added_names: str_array(att, "addedNames"),
|