cc-transcript 10.4.0__tar.gz → 10.5.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.5.0}/PKG-INFO +1 -1
  2. {cc_transcript-10.4.0 → cc_transcript-10.5.0}/cc_transcript/activity.py +7 -2
  3. {cc_transcript-10.4.0 → cc_transcript-10.5.0}/cc_transcript/filterspec.py +5 -0
  4. {cc_transcript-10.4.0 → cc_transcript-10.5.0}/cc_transcript/models.py +4 -0
  5. {cc_transcript-10.4.0 → cc_transcript-10.5.0}/cc_transcript/parser.py +2 -1
  6. {cc_transcript-10.4.0 → cc_transcript-10.5.0}/pyproject.toml +1 -1
  7. {cc_transcript-10.4.0 → cc_transcript-10.5.0}/rust/src/activity.rs +25 -4
  8. {cc_transcript-10.4.0 → cc_transcript-10.5.0}/rust/src/event.rs +3 -1
  9. {cc_transcript-10.4.0 → cc_transcript-10.5.0}/rust/src/protocol.rs +37 -0
  10. {cc_transcript-10.4.0 → cc_transcript-10.5.0}/rust/src/types.rs +5 -1
  11. {cc_transcript-10.4.0 → cc_transcript-10.5.0}/Cargo.lock +0 -0
  12. {cc_transcript-10.4.0 → cc_transcript-10.5.0}/Cargo.toml +0 -0
  13. {cc_transcript-10.4.0 → cc_transcript-10.5.0}/LICENSE +0 -0
  14. {cc_transcript-10.4.0 → cc_transcript-10.5.0}/README.md +0 -0
  15. {cc_transcript-10.4.0 → cc_transcript-10.5.0}/cc_transcript/__init__.py +0 -0
  16. {cc_transcript-10.4.0 → cc_transcript-10.5.0}/cc_transcript/__main__.py +0 -0
  17. {cc_transcript-10.4.0 → cc_transcript-10.5.0}/cc_transcript/_parser_rs.pyi +0 -0
  18. {cc_transcript-10.4.0 → cc_transcript-10.5.0}/cc_transcript/activity_probe.py +0 -0
  19. {cc_transcript-10.4.0 → cc_transcript-10.5.0}/cc_transcript/backend.py +0 -0
  20. {cc_transcript-10.4.0 → cc_transcript-10.5.0}/cc_transcript/builders.py +0 -0
  21. {cc_transcript-10.4.0 → cc_transcript-10.5.0}/cc_transcript/cli.py +0 -0
  22. {cc_transcript-10.4.0 → cc_transcript-10.5.0}/cc_transcript/command.py +0 -0
  23. {cc_transcript-10.4.0 → cc_transcript-10.5.0}/cc_transcript/context.py +0 -0
  24. {cc_transcript-10.4.0 → cc_transcript-10.5.0}/cc_transcript/corrections.py +0 -0
  25. {cc_transcript-10.4.0 → cc_transcript-10.5.0}/cc_transcript/corrections_cli.py +0 -0
  26. {cc_transcript-10.4.0 → cc_transcript-10.5.0}/cc_transcript/cost.py +0 -0
  27. {cc_transcript-10.4.0 → cc_transcript-10.5.0}/cc_transcript/decisions.py +0 -0
  28. {cc_transcript-10.4.0 → cc_transcript-10.5.0}/cc_transcript/discovery.py +0 -0
  29. {cc_transcript-10.4.0 → cc_transcript-10.5.0}/cc_transcript/disktruth.py +0 -0
  30. {cc_transcript-10.4.0 → cc_transcript-10.5.0}/cc_transcript/evidence.py +0 -0
  31. {cc_transcript-10.4.0 → cc_transcript-10.5.0}/cc_transcript/extract/__init__.py +0 -0
  32. {cc_transcript-10.4.0 → cc_transcript-10.5.0}/cc_transcript/extract/correct.py +0 -0
  33. {cc_transcript-10.4.0 → cc_transcript-10.5.0}/cc_transcript/facts.py +0 -0
  34. {cc_transcript-10.4.0 → cc_transcript-10.5.0}/cc_transcript/ids.py +0 -0
  35. {cc_transcript-10.4.0 → cc_transcript-10.5.0}/cc_transcript/judge/__init__.py +0 -0
  36. {cc_transcript-10.4.0 → cc_transcript-10.5.0}/cc_transcript/judge/llm.py +0 -0
  37. {cc_transcript-10.4.0 → cc_transcript-10.5.0}/cc_transcript/judge/similar.py +0 -0
  38. {cc_transcript-10.4.0 → cc_transcript-10.5.0}/cc_transcript/judge/verdicts.py +0 -0
  39. {cc_transcript-10.4.0 → cc_transcript-10.5.0}/cc_transcript/mining/__init__.py +0 -0
  40. {cc_transcript-10.4.0 → cc_transcript-10.5.0}/cc_transcript/mining/candidates.py +0 -0
  41. {cc_transcript-10.4.0 → cc_transcript-10.5.0}/cc_transcript/mining/confidence.py +0 -0
  42. {cc_transcript-10.4.0 → cc_transcript-10.5.0}/cc_transcript/mining/engine.py +0 -0
  43. {cc_transcript-10.4.0 → cc_transcript-10.5.0}/cc_transcript/mining/filterspec.py +0 -0
  44. {cc_transcript-10.4.0 → cc_transcript-10.5.0}/cc_transcript/mining/formats.py +0 -0
  45. {cc_transcript-10.4.0 → cc_transcript-10.5.0}/cc_transcript/mining/signals.py +0 -0
  46. {cc_transcript-10.4.0 → cc_transcript-10.5.0}/cc_transcript/mining/sourcekind.py +0 -0
  47. {cc_transcript-10.4.0 → cc_transcript-10.5.0}/cc_transcript/mining/spec.py +0 -0
  48. {cc_transcript-10.4.0 → cc_transcript-10.5.0}/cc_transcript/mining/store.py +0 -0
  49. {cc_transcript-10.4.0 → cc_transcript-10.5.0}/cc_transcript/notifications.py +0 -0
  50. {cc_transcript-10.4.0 → cc_transcript-10.5.0}/cc_transcript/py.typed +0 -0
  51. {cc_transcript-10.4.0 → cc_transcript-10.5.0}/cc_transcript/query.py +0 -0
  52. {cc_transcript-10.4.0 → cc_transcript-10.5.0}/cc_transcript/render.py +0 -0
  53. {cc_transcript-10.4.0 → cc_transcript-10.5.0}/cc_transcript/rust.py +0 -0
  54. {cc_transcript-10.4.0 → cc_transcript-10.5.0}/cc_transcript/sentiment/__init__.py +0 -0
  55. {cc_transcript-10.4.0 → cc_transcript-10.5.0}/cc_transcript/sentiment/buckets.py +0 -0
  56. {cc_transcript-10.4.0 → cc_transcript-10.5.0}/cc_transcript/sentiment/engine.py +0 -0
  57. {cc_transcript-10.4.0 → cc_transcript-10.5.0}/cc_transcript/sentiment/lexicon.py +0 -0
  58. {cc_transcript-10.4.0 → cc_transcript-10.5.0}/cc_transcript/sentiment/scorespec.py +0 -0
  59. {cc_transcript-10.4.0 → cc_transcript-10.5.0}/cc_transcript/store.py +0 -0
  60. {cc_transcript-10.4.0 → cc_transcript-10.5.0}/cc_transcript/tools.py +0 -0
  61. {cc_transcript-10.4.0 → cc_transcript-10.5.0}/rust/Cargo.toml +0 -0
  62. {cc_transcript-10.4.0 → cc_transcript-10.5.0}/rust/build.rs +0 -0
  63. {cc_transcript-10.4.0 → cc_transcript-10.5.0}/rust/data/afinn-en-165.tsv +0 -0
  64. {cc_transcript-10.4.0 → cc_transcript-10.5.0}/rust/data/domain_overrides.tsv +0 -0
  65. {cc_transcript-10.4.0 → cc_transcript-10.5.0}/rust/src/command.rs +0 -0
  66. {cc_transcript-10.4.0 → cc_transcript-10.5.0}/rust/src/filter.rs +0 -0
  67. {cc_transcript-10.4.0 → cc_transcript-10.5.0}/rust/src/lexicon.rs +0 -0
  68. {cc_transcript-10.4.0 → cc_transcript-10.5.0}/rust/src/lib.rs +0 -0
  69. {cc_transcript-10.4.0 → cc_transcript-10.5.0}/rust/src/mining.rs +0 -0
  70. {cc_transcript-10.4.0 → cc_transcript-10.5.0}/rust/src/model.rs +0 -0
  71. {cc_transcript-10.4.0 → cc_transcript-10.5.0}/rust/src/parse.rs +0 -0
  72. {cc_transcript-10.4.0 → cc_transcript-10.5.0}/rust/src/python.rs +0 -0
  73. {cc_transcript-10.4.0 → cc_transcript-10.5.0}/rust/src/score.rs +0 -0
  74. {cc_transcript-10.4.0 → cc_transcript-10.5.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.5.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
 
@@ -419,6 +419,10 @@ def is_bare_interrupt_marker(text: str) -> bool:
419
419
  return (marker := interrupt_marker(text)) is not None and not text.strip()[len(marker.strip()) :].strip()
420
420
 
421
421
 
422
+ def is_agent_injection(text: str) -> bool:
423
+ return AGENT_INJECTION_RE.search(text) is not None
424
+
425
+
422
426
  def normalize_bare(text: str, strip_trailing: str = TRAILING_PUNCT) -> str:
423
427
  return text.strip().rstrip(strip_trailing).strip().lower()
424
428
 
@@ -551,5 +555,6 @@ def predicate_to_dict(predicate: Predicate) -> dict[str, Any]:
551
555
 
552
556
 
553
557
  STRUCTURAL_NOISE_RE = compile_groups(STRUCTURAL_NOISE_GROUPS, True)
558
+ AGENT_INJECTION_RE = compile_groups(AGENT_INJECTION_GROUPS, True)
554
559
  INTERRUPT_MARKER_RE = compile_groups(INTERRUPT_MARKER_GROUPS, True)
555
560
  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.5.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,12 @@ 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)`.
18
+ const AGENT_INJECTION_PATTERN: &str =
19
+ r"(?:<(?:teammate-message|scheduled-task)\b)|(?:^# Augment Agent\b)|(?:^\s*\[Role Reminder\b)";
20
+
15
21
  /// The one interrupt-marker regex (filterspec.py INTERRUPT_MARKER_RE): the pattern
16
22
  /// compiled case-insensitively, anchored at the start of the haystack with leading
17
23
  /// whitespace tolerated. Shared with mining's structural fallback so both uses
@@ -20,6 +26,19 @@ pub static INTERRUPT_MARKER_RE: Lazy<Regex> = Lazy::new(|| {
20
26
  Regex::new(&format!("(?i){INTERRUPT_MARKER_PATTERN}")).expect("interrupt regex")
21
27
  });
22
28
 
29
+ /// The agent-injection regex (filterspec.py AGENT_INJECTION_RE): the group
30
+ /// alternation compiled case-insensitively, matched with search semantics
31
+ /// (filterspec.py is_agent_injection uses .search()).
32
+ pub static AGENT_INJECTION_RE: Lazy<Regex> = Lazy::new(|| {
33
+ Regex::new(&format!("(?i){AGENT_INJECTION_PATTERN}")).expect("agent injection regex")
34
+ });
35
+
36
+ /// is_agent_injection (filterspec.py is_agent_injection): whether ``text`` is an
37
+ /// agent-injected relay banner rather than an authored prompt.
38
+ pub fn is_agent_injection(text: &str) -> bool {
39
+ AGENT_INJECTION_RE.is_match(text)
40
+ }
41
+
23
42
  /// embedded_user_text (filterspec.py embedded_user_text): the verbatim instruction
24
43
  /// wrapped between the USER_SAID markers, or None.
25
44
  pub fn embedded_user_text(content: &str) -> Option<String> {
@@ -91,4 +110,22 @@ mod tests {
91
110
  assert!(is_bare_interrupt_marker("[Request interrupted by user]"));
92
111
  assert!(!is_bare_interrupt_marker("[Request interrupted by user] no do it differently"));
93
112
  }
113
+
114
+ #[test]
115
+ fn agent_injection_matches_relay_banners() {
116
+ assert!(is_agent_injection("<teammate-message from='r'>rebase</teammate-message>"));
117
+ assert!(is_agent_injection("<scheduled-task id='7'>run the suite</scheduled-task>"));
118
+ assert!(is_agent_injection("[Role Reminder: You are a Coordinator."));
119
+ assert!(is_agent_injection("# Augment Agent\nyou have these tools"));
120
+ // search() is unanchored: a mid-text teammate tag still matches.
121
+ assert!(is_agent_injection("as noted in the <teammate-message> above"));
122
+ }
123
+
124
+ #[test]
125
+ fn agent_injection_rejects_prose_and_head_anchored_mid_text() {
126
+ assert!(!is_agent_injection("remind me what the teammate coordinator does"));
127
+ // the head-anchored role-reminder group does not match mid-text.
128
+ assert!(!is_agent_injection("we discussed the [Role Reminder] banner mid-sentence"));
129
+ assert!(!is_agent_injection(""));
130
+ }
94
131
  }
@@ -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