cc-transcript 10.4.0__tar.gz → 10.6.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (74) hide show
  1. {cc_transcript-10.4.0 → cc_transcript-10.6.0}/PKG-INFO +1 -1
  2. {cc_transcript-10.4.0 → cc_transcript-10.6.0}/cc_transcript/activity.py +7 -2
  3. {cc_transcript-10.4.0 → cc_transcript-10.6.0}/cc_transcript/filterspec.py +12 -4
  4. {cc_transcript-10.4.0 → cc_transcript-10.6.0}/cc_transcript/models.py +4 -0
  5. {cc_transcript-10.4.0 → cc_transcript-10.6.0}/cc_transcript/parser.py +2 -1
  6. {cc_transcript-10.4.0 → cc_transcript-10.6.0}/pyproject.toml +1 -1
  7. {cc_transcript-10.4.0 → cc_transcript-10.6.0}/rust/src/activity.rs +25 -4
  8. {cc_transcript-10.4.0 → cc_transcript-10.6.0}/rust/src/event.rs +3 -1
  9. {cc_transcript-10.4.0 → cc_transcript-10.6.0}/rust/src/protocol.rs +45 -0
  10. {cc_transcript-10.4.0 → cc_transcript-10.6.0}/rust/src/types.rs +5 -1
  11. {cc_transcript-10.4.0 → cc_transcript-10.6.0}/Cargo.lock +0 -0
  12. {cc_transcript-10.4.0 → cc_transcript-10.6.0}/Cargo.toml +0 -0
  13. {cc_transcript-10.4.0 → cc_transcript-10.6.0}/LICENSE +0 -0
  14. {cc_transcript-10.4.0 → cc_transcript-10.6.0}/README.md +0 -0
  15. {cc_transcript-10.4.0 → cc_transcript-10.6.0}/cc_transcript/__init__.py +0 -0
  16. {cc_transcript-10.4.0 → cc_transcript-10.6.0}/cc_transcript/__main__.py +0 -0
  17. {cc_transcript-10.4.0 → cc_transcript-10.6.0}/cc_transcript/_parser_rs.pyi +0 -0
  18. {cc_transcript-10.4.0 → cc_transcript-10.6.0}/cc_transcript/activity_probe.py +0 -0
  19. {cc_transcript-10.4.0 → cc_transcript-10.6.0}/cc_transcript/backend.py +0 -0
  20. {cc_transcript-10.4.0 → cc_transcript-10.6.0}/cc_transcript/builders.py +0 -0
  21. {cc_transcript-10.4.0 → cc_transcript-10.6.0}/cc_transcript/cli.py +0 -0
  22. {cc_transcript-10.4.0 → cc_transcript-10.6.0}/cc_transcript/command.py +0 -0
  23. {cc_transcript-10.4.0 → cc_transcript-10.6.0}/cc_transcript/context.py +0 -0
  24. {cc_transcript-10.4.0 → cc_transcript-10.6.0}/cc_transcript/corrections.py +0 -0
  25. {cc_transcript-10.4.0 → cc_transcript-10.6.0}/cc_transcript/corrections_cli.py +0 -0
  26. {cc_transcript-10.4.0 → cc_transcript-10.6.0}/cc_transcript/cost.py +0 -0
  27. {cc_transcript-10.4.0 → cc_transcript-10.6.0}/cc_transcript/decisions.py +0 -0
  28. {cc_transcript-10.4.0 → cc_transcript-10.6.0}/cc_transcript/discovery.py +0 -0
  29. {cc_transcript-10.4.0 → cc_transcript-10.6.0}/cc_transcript/disktruth.py +0 -0
  30. {cc_transcript-10.4.0 → cc_transcript-10.6.0}/cc_transcript/evidence.py +0 -0
  31. {cc_transcript-10.4.0 → cc_transcript-10.6.0}/cc_transcript/extract/__init__.py +0 -0
  32. {cc_transcript-10.4.0 → cc_transcript-10.6.0}/cc_transcript/extract/correct.py +0 -0
  33. {cc_transcript-10.4.0 → cc_transcript-10.6.0}/cc_transcript/facts.py +0 -0
  34. {cc_transcript-10.4.0 → cc_transcript-10.6.0}/cc_transcript/ids.py +0 -0
  35. {cc_transcript-10.4.0 → cc_transcript-10.6.0}/cc_transcript/judge/__init__.py +0 -0
  36. {cc_transcript-10.4.0 → cc_transcript-10.6.0}/cc_transcript/judge/llm.py +0 -0
  37. {cc_transcript-10.4.0 → cc_transcript-10.6.0}/cc_transcript/judge/similar.py +0 -0
  38. {cc_transcript-10.4.0 → cc_transcript-10.6.0}/cc_transcript/judge/verdicts.py +0 -0
  39. {cc_transcript-10.4.0 → cc_transcript-10.6.0}/cc_transcript/mining/__init__.py +0 -0
  40. {cc_transcript-10.4.0 → cc_transcript-10.6.0}/cc_transcript/mining/candidates.py +0 -0
  41. {cc_transcript-10.4.0 → cc_transcript-10.6.0}/cc_transcript/mining/confidence.py +0 -0
  42. {cc_transcript-10.4.0 → cc_transcript-10.6.0}/cc_transcript/mining/engine.py +0 -0
  43. {cc_transcript-10.4.0 → cc_transcript-10.6.0}/cc_transcript/mining/filterspec.py +0 -0
  44. {cc_transcript-10.4.0 → cc_transcript-10.6.0}/cc_transcript/mining/formats.py +0 -0
  45. {cc_transcript-10.4.0 → cc_transcript-10.6.0}/cc_transcript/mining/signals.py +0 -0
  46. {cc_transcript-10.4.0 → cc_transcript-10.6.0}/cc_transcript/mining/sourcekind.py +0 -0
  47. {cc_transcript-10.4.0 → cc_transcript-10.6.0}/cc_transcript/mining/spec.py +0 -0
  48. {cc_transcript-10.4.0 → cc_transcript-10.6.0}/cc_transcript/mining/store.py +0 -0
  49. {cc_transcript-10.4.0 → cc_transcript-10.6.0}/cc_transcript/notifications.py +0 -0
  50. {cc_transcript-10.4.0 → cc_transcript-10.6.0}/cc_transcript/py.typed +0 -0
  51. {cc_transcript-10.4.0 → cc_transcript-10.6.0}/cc_transcript/query.py +0 -0
  52. {cc_transcript-10.4.0 → cc_transcript-10.6.0}/cc_transcript/render.py +0 -0
  53. {cc_transcript-10.4.0 → cc_transcript-10.6.0}/cc_transcript/rust.py +0 -0
  54. {cc_transcript-10.4.0 → cc_transcript-10.6.0}/cc_transcript/sentiment/__init__.py +0 -0
  55. {cc_transcript-10.4.0 → cc_transcript-10.6.0}/cc_transcript/sentiment/buckets.py +0 -0
  56. {cc_transcript-10.4.0 → cc_transcript-10.6.0}/cc_transcript/sentiment/engine.py +0 -0
  57. {cc_transcript-10.4.0 → cc_transcript-10.6.0}/cc_transcript/sentiment/lexicon.py +0 -0
  58. {cc_transcript-10.4.0 → cc_transcript-10.6.0}/cc_transcript/sentiment/scorespec.py +0 -0
  59. {cc_transcript-10.4.0 → cc_transcript-10.6.0}/cc_transcript/store.py +0 -0
  60. {cc_transcript-10.4.0 → cc_transcript-10.6.0}/cc_transcript/tools.py +0 -0
  61. {cc_transcript-10.4.0 → cc_transcript-10.6.0}/rust/Cargo.toml +0 -0
  62. {cc_transcript-10.4.0 → cc_transcript-10.6.0}/rust/build.rs +0 -0
  63. {cc_transcript-10.4.0 → cc_transcript-10.6.0}/rust/data/afinn-en-165.tsv +0 -0
  64. {cc_transcript-10.4.0 → cc_transcript-10.6.0}/rust/data/domain_overrides.tsv +0 -0
  65. {cc_transcript-10.4.0 → cc_transcript-10.6.0}/rust/src/command.rs +0 -0
  66. {cc_transcript-10.4.0 → cc_transcript-10.6.0}/rust/src/filter.rs +0 -0
  67. {cc_transcript-10.4.0 → cc_transcript-10.6.0}/rust/src/lexicon.rs +0 -0
  68. {cc_transcript-10.4.0 → cc_transcript-10.6.0}/rust/src/lib.rs +0 -0
  69. {cc_transcript-10.4.0 → cc_transcript-10.6.0}/rust/src/mining.rs +0 -0
  70. {cc_transcript-10.4.0 → cc_transcript-10.6.0}/rust/src/model.rs +0 -0
  71. {cc_transcript-10.4.0 → cc_transcript-10.6.0}/rust/src/parse.rs +0 -0
  72. {cc_transcript-10.4.0 → cc_transcript-10.6.0}/rust/src/python.rs +0 -0
  73. {cc_transcript-10.4.0 → cc_transcript-10.6.0}/rust/src/score.rs +0 -0
  74. {cc_transcript-10.4.0 → cc_transcript-10.6.0}/rust/src/value.rs +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: cc-transcript
3
- Version: 10.4.0
3
+ Version: 10.6.0
4
4
  Classifier: Development Status :: 3 - Alpha
5
5
  Classifier: Environment :: Console
6
6
  Classifier: Intended Audience :: Developers
@@ -40,10 +40,15 @@ def native_user_classifier(event: UserEvent) -> bool:
40
40
  """Whether a user event is a real prompt under native Claude Code semantics.
41
41
 
42
42
  A prompt is non-meta, non-sidechain, not a compact summary, not an
43
- interruption marker, and not tool-result-only — it must carry real text.
43
+ interruption marker, not an agent-injected relay banner, and not
44
+ tool-result-only — it must carry real text.
44
45
  """
45
46
  return not (
46
- event.meta.is_meta or event.meta.is_sidechain or event.meta.is_compact_summary or event.interrupted
47
+ event.meta.is_meta
48
+ or event.meta.is_sidechain
49
+ or event.meta.is_compact_summary
50
+ or event.interrupted
51
+ or event.is_agent_injected
47
52
  ) and bool(event.text.strip())
48
53
 
49
54
 
@@ -59,11 +59,14 @@ STRUCTURAL_GROUPS: tuple[tuple[str, str], ...] = (
59
59
  ("hash_banner", r"(?:###\s+[\w][\w \-]{0,30}\s+){3,}###"),
60
60
  )
61
61
 
62
- # Agent-injected banners (teammate messages, scheduled tasks, foreign agents).
62
+ # Agent-injected banners (teammate messages, scheduled tasks, foreign agents). Start-anchored
63
+ # so a mid-text mention is not a banner; the follower class replaces \b and Reminder's "i" is
64
+ # ASCII-pinned via (?-i:[Ii]) so Rust regex and Python re stay in parity across combining marks
65
+ # and dotted/dotless-I folding. Mirror any change in rust/src/protocol.rs AGENT_INJECTION_PATTERN.
63
66
  AGENT_INJECTION_GROUPS: tuple[tuple[str, str], ...] = (
64
- ("xml_tags_extra", r"<(?:teammate-message|scheduled-task)\b"),
65
- ("augment_agent", r"^# Augment Agent\b"),
66
- ("role_reminder", r"^\s*\[Role Reminder\b"),
67
+ ("xml_tags_extra", r"\A\s*<(?:teammate-message|scheduled-task)(?:[\s/>]|$)"),
68
+ ("augment_agent", r"\A\s*# Augment Agent(?:\s|$)"),
69
+ ("role_reminder", r"\A\s*\[Role Rem(?-i:[Ii])nder(?:[\s:\]]|$)"),
67
70
  )
68
71
 
69
72
  INTERRUPT_MARKER_GROUPS: tuple[tuple[str, str], ...] = (("interrupt", r"^\s*\[Request interrupted by user"),)
@@ -419,6 +422,10 @@ def is_bare_interrupt_marker(text: str) -> bool:
419
422
  return (marker := interrupt_marker(text)) is not None and not text.strip()[len(marker.strip()) :].strip()
420
423
 
421
424
 
425
+ def is_agent_injection(text: str) -> bool:
426
+ return AGENT_INJECTION_RE.search(text) is not None
427
+
428
+
422
429
  def normalize_bare(text: str, strip_trailing: str = TRAILING_PUNCT) -> str:
423
430
  return text.strip().rstrip(strip_trailing).strip().lower()
424
431
 
@@ -551,5 +558,6 @@ def predicate_to_dict(predicate: Predicate) -> dict[str, Any]:
551
558
 
552
559
 
553
560
  STRUCTURAL_NOISE_RE = compile_groups(STRUCTURAL_NOISE_GROUPS, True)
561
+ AGENT_INJECTION_RE = compile_groups(AGENT_INJECTION_GROUPS, True)
554
562
  INTERRUPT_MARKER_RE = compile_groups(INTERRUPT_MARKER_GROUPS, True)
555
563
  JUNK_USER_MESSAGE_RE = compile_groups(SENTIMENT_JUNK_GROUPS, True)
@@ -160,12 +160,16 @@ class UserEvent:
160
160
  text: The joined text of the turn.
161
161
  blocks: The parsed content blocks, including tool results.
162
162
  interrupted: Whether the turn is a user interruption.
163
+ is_agent_injected: Whether the turn is an agent-injected relay banner —
164
+ a teammate-message digest, scheduled-task banner, or foreign-agent
165
+ header — rather than an authored prompt.
163
166
  """
164
167
 
165
168
  meta: EntryMeta
166
169
  text: str
167
170
  blocks: tuple[ContentBlock, ...]
168
171
  interrupted: bool
172
+ is_agent_injected: bool = False
169
173
 
170
174
 
171
175
  @dataclass(frozen=True, slots=True)
@@ -11,7 +11,7 @@ import anyio.to_thread
11
11
  import orjson
12
12
 
13
13
  from cc_transcript.backend import Backend, ParsedTranscript
14
- from cc_transcript.filterspec import apply_spec, interrupt_marker
14
+ from cc_transcript.filterspec import apply_spec, interrupt_marker, is_agent_injection
15
15
  from cc_transcript.models import (
16
16
  AssistantEvent,
17
17
  CacheCreation,
@@ -158,6 +158,7 @@ def parse_event(data: Mapping[str, Any]) -> TranscriptEvent | None:
158
158
  text=text,
159
159
  blocks=blocks,
160
160
  interrupted=interrupt_marker(text) is not None,
161
+ is_agent_injected=is_agent_injection(text),
161
162
  )
162
163
  case "assistant":
163
164
  text, blocks = parse_assistant_blocks(data["message"]["content"])
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "cc-transcript"
3
- version = "10.4.0"
3
+ version = "10.6.0"
4
4
  description = "Grep every Claude Code session you've ever run."
5
5
  readme = "README.md"
6
6
  license = "PolyForm-Noncommercial-1.0.0"
@@ -73,11 +73,16 @@ pub struct SessionActivity {
73
73
  }
74
74
 
75
75
  /// Whether a user entry opens a turn (activity.py ``native_user_classifier``):
76
- /// a real prompt — non-meta, non-sidechain, not an interruption marker, with
77
- /// non-blank text. Compact-summary exclusion matches captain-hook, which
78
- /// consumes this probe since the 10.2.0 shared-classifier fix.
76
+ /// a real prompt — non-meta, non-sidechain, not an interruption marker, not an
77
+ /// agent-injected relay banner, with non-blank text. Compact-summary exclusion
78
+ /// matches captain-hook, which consumes this probe since the 10.2.0
79
+ /// shared-classifier fix.
79
80
  fn opens_turn(user: &UserEntry) -> bool {
80
- !(user.meta.is_meta || user.meta.is_sidechain || user.meta.is_compact_summary || user.interrupted())
81
+ !(user.meta.is_meta
82
+ || user.meta.is_sidechain
83
+ || user.meta.is_compact_summary
84
+ || user.interrupted()
85
+ || user.is_agent_injected())
81
86
  && !user.content.text().trim().is_empty()
82
87
  }
83
88
 
@@ -510,6 +515,22 @@ mod tests {
510
515
  assert_eq!(activity.pending[0].kind, PendingKind::Background);
511
516
  }
512
517
 
518
+ #[test]
519
+ fn agent_injected_banner_does_not_open_turn() {
520
+ let entries = vec![
521
+ user("build it"),
522
+ tool_use("Bash", "b1", r#"{"command":"make","run_in_background":true}"#),
523
+ tool_result("b1"),
524
+ user("<teammate-message from='mate'>ping</teammate-message>"),
525
+ ];
526
+ let activity = activity(&entries);
527
+ assert!(
528
+ activity.is_waiting,
529
+ "an agent-injected relay banner must not open a turn, so the background Bash stays current"
530
+ );
531
+ assert_eq!(activity.pending[0].kind, PendingKind::Background);
532
+ }
533
+
513
534
  #[test]
514
535
  fn unrelated_completion_marker_keeps_waiting() {
515
536
  let entries = vec![
@@ -12,7 +12,7 @@ use crate::model::{
12
12
  USER_EVENT_CLS,
13
13
  };
14
14
  use crate::parse::ParseError;
15
- use crate::protocol::interrupt_marker;
15
+ use crate::protocol::{interrupt_marker, is_agent_injection};
16
16
  use crate::types::{
17
17
  joined_text, ContentBlock, Entry, EntryMeta, InitInfo, ModelUsage, PrintBody, PrintMessage,
18
18
  PrintResult, Usage, UserContent,
@@ -155,11 +155,13 @@ pub fn build_event<'py>(py: Python<'py>, entry: &Entry) -> PyResult<Bound<'py, P
155
155
  Entry::User(user) => {
156
156
  let text = user.content.text();
157
157
  let interrupted = interrupt_marker(&text).is_some();
158
+ let is_agent_injected = is_agent_injection(&text);
158
159
  models_type(py, &USER_EVENT_CLS, "UserEvent")?.call1((
159
160
  build_meta(py, &user.meta)?,
160
161
  text,
161
162
  build_user_blocks(py, &user.content)?,
162
163
  interrupted,
164
+ is_agent_injected,
163
165
  ))
164
166
  }
165
167
  Entry::Assistant(assistant) => {
@@ -12,6 +12,14 @@ pub const ANSWERED_PREFIX: &str = "Your questions have been answered: ";
12
12
  pub const ANSWERED_TRAILER: &str = ". You can now continue with these answers in mind.";
13
13
  const INTERRUPT_MARKER_PATTERN: &str = r"^\s*\[Request interrupted by user";
14
14
 
15
+ // Agent-injected relay banners (filterspec.py AGENT_INJECTION_GROUPS): teammate-message
16
+ // digests, scheduled-task banners, and foreign-agent headers. The alternation mirrors
17
+ // compile_groups(AGENT_INJECTION_GROUPS): `(?:p1)|(?:p2)|(?:p3)`. Each alternative is
18
+ // start-anchored (\A\s*), the trailing \b is an explicit follower class agreeing with
19
+ // Python re across combining marks, and Reminder's "i" is ASCII-pinned via (?-i:[Ii]) so
20
+ // dotted/dotless-I case folding stays in parity.
21
+ const AGENT_INJECTION_PATTERN: &str = r"(?:\A\s*<(?:teammate-message|scheduled-task)(?:[\s/>]|$))|(?:\A\s*# Augment Agent(?:\s|$))|(?:\A\s*\[Role Rem(?-i:[Ii])nder(?:[\s:\]]|$))";
22
+
15
23
  /// The one interrupt-marker regex (filterspec.py INTERRUPT_MARKER_RE): the pattern
16
24
  /// compiled case-insensitively, anchored at the start of the haystack with leading
17
25
  /// whitespace tolerated. Shared with mining's structural fallback so both uses
@@ -20,6 +28,19 @@ pub static INTERRUPT_MARKER_RE: Lazy<Regex> = Lazy::new(|| {
20
28
  Regex::new(&format!("(?i){INTERRUPT_MARKER_PATTERN}")).expect("interrupt regex")
21
29
  });
22
30
 
31
+ /// The agent-injection regex (filterspec.py AGENT_INJECTION_RE): the group
32
+ /// alternation compiled case-insensitively, matched with search semantics
33
+ /// (filterspec.py is_agent_injection uses .search()).
34
+ pub static AGENT_INJECTION_RE: Lazy<Regex> = Lazy::new(|| {
35
+ Regex::new(&format!("(?i){AGENT_INJECTION_PATTERN}")).expect("agent injection regex")
36
+ });
37
+
38
+ /// is_agent_injection (filterspec.py is_agent_injection): whether ``text`` is an
39
+ /// agent-injected relay banner rather than an authored prompt.
40
+ pub fn is_agent_injection(text: &str) -> bool {
41
+ AGENT_INJECTION_RE.is_match(text)
42
+ }
43
+
23
44
  /// embedded_user_text (filterspec.py embedded_user_text): the verbatim instruction
24
45
  /// wrapped between the USER_SAID markers, or None.
25
46
  pub fn embedded_user_text(content: &str) -> Option<String> {
@@ -91,4 +112,28 @@ mod tests {
91
112
  assert!(is_bare_interrupt_marker("[Request interrupted by user]"));
92
113
  assert!(!is_bare_interrupt_marker("[Request interrupted by user] no do it differently"));
93
114
  }
115
+
116
+ #[test]
117
+ fn agent_injection_matches_relay_banners() {
118
+ assert!(is_agent_injection("<teammate-message from='r'>rebase</teammate-message>"));
119
+ assert!(is_agent_injection("<scheduled-task id='7'>run the suite</scheduled-task>"));
120
+ assert!(is_agent_injection("[Role Reminder: You are a Coordinator."));
121
+ assert!(is_agent_injection("# Augment Agent\nyou have these tools"));
122
+ // Leading whitespace before the marker is tolerated.
123
+ assert!(is_agent_injection(" <teammate-message from='mate'>ping</teammate-message>"));
124
+ }
125
+
126
+ #[test]
127
+ fn agent_injection_rejects_prose_and_mid_text_mentions() {
128
+ assert!(!is_agent_injection("remind me what the teammate coordinator does"));
129
+ // Start-anchored: a relay tag mentioned mid-text is authored, not injected.
130
+ assert!(!is_agent_injection("as noted in the <teammate-message> above"));
131
+ assert!(!is_agent_injection("Why did the transcript contain <teammate-message from=a>?"));
132
+ assert!(!is_agent_injection("we discussed the [Role Reminder] banner mid-sentence"));
133
+ // A combining mark (U+0301) after the tag name is not a portable word boundary; and
134
+ // dotless-I must not fold to ASCII "i" — Python re once matched both, Rust never did.
135
+ assert!(!is_agent_injection("<teammate-message\u{0301}>"));
136
+ assert!(!is_agent_injection("[Role Rem\u{0131}nder: dotless"));
137
+ assert!(!is_agent_injection(""));
138
+ }
94
139
  }
@@ -3,7 +3,7 @@ use std::collections::{HashMap, HashSet};
3
3
  use chrono::{DateTime, FixedOffset};
4
4
  use sonic_rs::Value;
5
5
 
6
- use crate::protocol::interrupt_marker;
6
+ use crate::protocol::{interrupt_marker, is_agent_injection};
7
7
 
8
8
  /// Envelope metadata shared by the conversational entry kinds (user, assistant,
9
9
  /// system). Mode and Other entries carry no envelope on disk.
@@ -116,6 +116,10 @@ impl UserEntry {
116
116
  pub fn interrupted(&self) -> bool {
117
117
  interrupt_marker(&self.content.text()).is_some()
118
118
  }
119
+
120
+ pub fn is_agent_injected(&self) -> bool {
121
+ is_agent_injection(&self.content.text())
122
+ }
119
123
  }
120
124
 
121
125
  #[derive(Debug)]
File without changes
File without changes