cc-transcript 14.14.0__tar.gz → 14.16.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 (162) hide show
  1. {cc_transcript-14.14.0 → cc_transcript-14.16.0}/PKG-INFO +1 -1
  2. {cc_transcript-14.14.0 → cc_transcript-14.16.0}/cc_transcript/decision_store.py +104 -26
  3. {cc_transcript-14.14.0 → cc_transcript-14.16.0}/cc_transcript/filterspec.py +2 -1
  4. {cc_transcript-14.14.0 → cc_transcript-14.16.0}/cc_transcript/query.py +64 -9
  5. {cc_transcript-14.14.0 → cc_transcript-14.16.0}/pyproject.toml +1 -1
  6. {cc_transcript-14.14.0 → cc_transcript-14.16.0}/rust/crates/core/src/filter.rs +1 -1
  7. {cc_transcript-14.14.0 → cc_transcript-14.16.0}/rust/crates/core/src/literals/protocol.rs +1 -1
  8. {cc_transcript-14.14.0 → cc_transcript-14.16.0}/rust/crates/core/src/protocol.rs +17 -0
  9. {cc_transcript-14.14.0 → cc_transcript-14.16.0}/Cargo.lock +0 -0
  10. {cc_transcript-14.14.0 → cc_transcript-14.16.0}/Cargo.toml +0 -0
  11. {cc_transcript-14.14.0 → cc_transcript-14.16.0}/LICENSE +0 -0
  12. {cc_transcript-14.14.0 → cc_transcript-14.16.0}/README.md +0 -0
  13. {cc_transcript-14.14.0 → cc_transcript-14.16.0}/cc_transcript/__init__.py +0 -0
  14. {cc_transcript-14.14.0 → cc_transcript-14.16.0}/cc_transcript/__init__.pyi +0 -0
  15. {cc_transcript-14.14.0 → cc_transcript-14.16.0}/cc_transcript/_native.pyi +0 -0
  16. {cc_transcript-14.14.0 → cc_transcript-14.16.0}/cc_transcript/activity.py +0 -0
  17. {cc_transcript-14.14.0 → cc_transcript-14.16.0}/cc_transcript/activity_probe.py +0 -0
  18. {cc_transcript-14.14.0 → cc_transcript-14.16.0}/cc_transcript/builders.py +0 -0
  19. {cc_transcript-14.14.0 → cc_transcript-14.16.0}/cc_transcript/codex.py +0 -0
  20. {cc_transcript-14.14.0 → cc_transcript-14.16.0}/cc_transcript/command.py +0 -0
  21. {cc_transcript-14.14.0 → cc_transcript-14.16.0}/cc_transcript/context.py +0 -0
  22. {cc_transcript-14.14.0 → cc_transcript-14.16.0}/cc_transcript/corrections.py +0 -0
  23. {cc_transcript-14.14.0 → cc_transcript-14.16.0}/cc_transcript/cost.py +0 -0
  24. {cc_transcript-14.14.0 → cc_transcript-14.16.0}/cc_transcript/decisions.py +0 -0
  25. {cc_transcript-14.14.0 → cc_transcript-14.16.0}/cc_transcript/discovery.py +0 -0
  26. {cc_transcript-14.14.0 → cc_transcript-14.16.0}/cc_transcript/disktruth.py +0 -0
  27. {cc_transcript-14.14.0 → cc_transcript-14.16.0}/cc_transcript/evidence.py +0 -0
  28. {cc_transcript-14.14.0 → cc_transcript-14.16.0}/cc_transcript/extract/__init__.py +0 -0
  29. {cc_transcript-14.14.0 → cc_transcript-14.16.0}/cc_transcript/extract/correct.py +0 -0
  30. {cc_transcript-14.14.0 → cc_transcript-14.16.0}/cc_transcript/facts.py +0 -0
  31. {cc_transcript-14.14.0 → cc_transcript-14.16.0}/cc_transcript/heartbeats.py +0 -0
  32. {cc_transcript-14.14.0 → cc_transcript-14.16.0}/cc_transcript/ids.py +0 -0
  33. {cc_transcript-14.14.0 → cc_transcript-14.16.0}/cc_transcript/judge/__init__.py +0 -0
  34. {cc_transcript-14.14.0 → cc_transcript-14.16.0}/cc_transcript/judge/llm.py +0 -0
  35. {cc_transcript-14.14.0 → cc_transcript-14.16.0}/cc_transcript/judge/similar.py +0 -0
  36. {cc_transcript-14.14.0 → cc_transcript-14.16.0}/cc_transcript/judge/verdicts.py +0 -0
  37. {cc_transcript-14.14.0 → cc_transcript-14.16.0}/cc_transcript/ledger.py +0 -0
  38. {cc_transcript-14.14.0 → cc_transcript-14.16.0}/cc_transcript/literals.py +0 -0
  39. {cc_transcript-14.14.0 → cc_transcript-14.16.0}/cc_transcript/mining/__init__.py +0 -0
  40. {cc_transcript-14.14.0 → cc_transcript-14.16.0}/cc_transcript/mining/candidates.py +0 -0
  41. {cc_transcript-14.14.0 → cc_transcript-14.16.0}/cc_transcript/mining/confidence.py +0 -0
  42. {cc_transcript-14.14.0 → cc_transcript-14.16.0}/cc_transcript/mining/engine.py +0 -0
  43. {cc_transcript-14.14.0 → cc_transcript-14.16.0}/cc_transcript/mining/formats.py +0 -0
  44. {cc_transcript-14.14.0 → cc_transcript-14.16.0}/cc_transcript/mining/sampling.py +0 -0
  45. {cc_transcript-14.14.0 → cc_transcript-14.16.0}/cc_transcript/mining/signals.py +0 -0
  46. {cc_transcript-14.14.0 → cc_transcript-14.16.0}/cc_transcript/mining/sourcekind.py +0 -0
  47. {cc_transcript-14.14.0 → cc_transcript-14.16.0}/cc_transcript/mining/spec.py +0 -0
  48. {cc_transcript-14.14.0 → cc_transcript-14.16.0}/cc_transcript/mining/store.py +0 -0
  49. {cc_transcript-14.14.0 → cc_transcript-14.16.0}/cc_transcript/models.py +0 -0
  50. {cc_transcript-14.14.0 → cc_transcript-14.16.0}/cc_transcript/nlp.py +0 -0
  51. {cc_transcript-14.14.0 → cc_transcript-14.16.0}/cc_transcript/notifications.py +0 -0
  52. {cc_transcript-14.14.0 → cc_transcript-14.16.0}/cc_transcript/parser.py +0 -0
  53. {cc_transcript-14.14.0 → cc_transcript-14.16.0}/cc_transcript/py.typed +0 -0
  54. {cc_transcript-14.14.0 → cc_transcript-14.16.0}/cc_transcript/render.py +0 -0
  55. {cc_transcript-14.14.0 → cc_transcript-14.16.0}/cc_transcript/sentiment/__init__.py +0 -0
  56. {cc_transcript-14.14.0 → cc_transcript-14.16.0}/cc_transcript/sentiment/buckets.py +0 -0
  57. {cc_transcript-14.14.0 → cc_transcript-14.16.0}/cc_transcript/sentiment/data/afinn-en-165.tsv +0 -0
  58. {cc_transcript-14.14.0 → cc_transcript-14.16.0}/cc_transcript/sentiment/data/domain_overrides.tsv +0 -0
  59. {cc_transcript-14.14.0 → cc_transcript-14.16.0}/cc_transcript/sentiment/data/en-ewt.udpipe +0 -0
  60. {cc_transcript-14.14.0 → cc_transcript-14.16.0}/cc_transcript/sentiment/engine.py +0 -0
  61. {cc_transcript-14.14.0 → cc_transcript-14.16.0}/cc_transcript/sentiment/lexicon.py +0 -0
  62. {cc_transcript-14.14.0 → cc_transcript-14.16.0}/cc_transcript/sentiment/scorespec.py +0 -0
  63. {cc_transcript-14.14.0 → cc_transcript-14.16.0}/cc_transcript/synthetic.py +0 -0
  64. {cc_transcript-14.14.0 → cc_transcript-14.16.0}/cc_transcript/tools.py +0 -0
  65. {cc_transcript-14.14.0 → cc_transcript-14.16.0}/cc_transcript/watch.py +0 -0
  66. {cc_transcript-14.14.0 → cc_transcript-14.16.0}/rust/crates/cli/Cargo.toml +0 -0
  67. {cc_transcript-14.14.0 → cc_transcript-14.16.0}/rust/crates/cli/src/commands/blame.rs +0 -0
  68. {cc_transcript-14.14.0 → cc_transcript-14.16.0}/rust/crates/cli/src/commands/corrections.rs +0 -0
  69. {cc_transcript-14.14.0 → cc_transcript-14.16.0}/rust/crates/cli/src/commands/digest.rs +0 -0
  70. {cc_transcript-14.14.0 → cc_transcript-14.16.0}/rust/crates/cli/src/commands/facts.rs +0 -0
  71. {cc_transcript-14.14.0 → cc_transcript-14.16.0}/rust/crates/cli/src/commands/grep.rs +0 -0
  72. {cc_transcript-14.14.0 → cc_transcript-14.16.0}/rust/crates/cli/src/commands/list.rs +0 -0
  73. {cc_transcript-14.14.0 → cc_transcript-14.16.0}/rust/crates/cli/src/commands/mod.rs +0 -0
  74. {cc_transcript-14.14.0 → cc_transcript-14.16.0}/rust/crates/cli/src/commands/scratchpad.rs +0 -0
  75. {cc_transcript-14.14.0 → cc_transcript-14.16.0}/rust/crates/cli/src/commands/show.rs +0 -0
  76. {cc_transcript-14.14.0 → cc_transcript-14.16.0}/rust/crates/cli/src/commands/slice.rs +0 -0
  77. {cc_transcript-14.14.0 → cc_transcript-14.16.0}/rust/crates/cli/src/commands/stats.rs +0 -0
  78. {cc_transcript-14.14.0 → cc_transcript-14.16.0}/rust/crates/cli/src/commands/watch.rs +0 -0
  79. {cc_transcript-14.14.0 → cc_transcript-14.16.0}/rust/crates/cli/src/lib.rs +0 -0
  80. {cc_transcript-14.14.0 → cc_transcript-14.16.0}/rust/crates/cli/src/main.rs +0 -0
  81. {cc_transcript-14.14.0 → cc_transcript-14.16.0}/rust/crates/cli/src/output.rs +0 -0
  82. {cc_transcript-14.14.0 → cc_transcript-14.16.0}/rust/crates/cli/src/target.rs +0 -0
  83. {cc_transcript-14.14.0 → cc_transcript-14.16.0}/rust/crates/cli/src/timearg.rs +0 -0
  84. {cc_transcript-14.14.0 → cc_transcript-14.16.0}/rust/crates/core/Cargo.toml +0 -0
  85. {cc_transcript-14.14.0 → cc_transcript-14.16.0}/rust/crates/core/benches/parse.rs +0 -0
  86. {cc_transcript-14.14.0 → cc_transcript-14.16.0}/rust/crates/core/benches/render.rs +0 -0
  87. {cc_transcript-14.14.0 → cc_transcript-14.16.0}/rust/crates/core/benches/toolcall.rs +0 -0
  88. {cc_transcript-14.14.0 → cc_transcript-14.16.0}/rust/crates/core/src/activity.rs +0 -0
  89. {cc_transcript-14.14.0 → cc_transcript-14.16.0}/rust/crates/core/src/actor.rs +0 -0
  90. {cc_transcript-14.14.0 → cc_transcript-14.16.0}/rust/crates/core/src/blame.rs +0 -0
  91. {cc_transcript-14.14.0 → cc_transcript-14.16.0}/rust/crates/core/src/buckets.rs +0 -0
  92. {cc_transcript-14.14.0 → cc_transcript-14.16.0}/rust/crates/core/src/codex/discovery.rs +0 -0
  93. {cc_transcript-14.14.0 → cc_transcript-14.16.0}/rust/crates/core/src/codex/lower.rs +0 -0
  94. {cc_transcript-14.14.0 → cc_transcript-14.16.0}/rust/crates/core/src/codex/mod.rs +0 -0
  95. {cc_transcript-14.14.0 → cc_transcript-14.16.0}/rust/crates/core/src/codex/parse.rs +0 -0
  96. {cc_transcript-14.14.0 → cc_transcript-14.16.0}/rust/crates/core/src/codex/probe.rs +0 -0
  97. {cc_transcript-14.14.0 → cc_transcript-14.16.0}/rust/crates/core/src/codex/protocol.rs +0 -0
  98. {cc_transcript-14.14.0 → cc_transcript-14.16.0}/rust/crates/core/src/codex/types.rs +0 -0
  99. {cc_transcript-14.14.0 → cc_transcript-14.16.0}/rust/crates/core/src/command.rs +0 -0
  100. {cc_transcript-14.14.0 → cc_transcript-14.16.0}/rust/crates/core/src/context.rs +0 -0
  101. {cc_transcript-14.14.0 → cc_transcript-14.16.0}/rust/crates/core/src/corrections.rs +0 -0
  102. {cc_transcript-14.14.0 → cc_transcript-14.16.0}/rust/crates/core/src/cost.rs +0 -0
  103. {cc_transcript-14.14.0 → cc_transcript-14.16.0}/rust/crates/core/src/discovery.rs +0 -0
  104. {cc_transcript-14.14.0 → cc_transcript-14.16.0}/rust/crates/core/src/facts.rs +0 -0
  105. {cc_transcript-14.14.0 → cc_transcript-14.16.0}/rust/crates/core/src/feedback.rs +0 -0
  106. {cc_transcript-14.14.0 → cc_transcript-14.16.0}/rust/crates/core/src/gateway.rs +0 -0
  107. {cc_transcript-14.14.0 → cc_transcript-14.16.0}/rust/crates/core/src/ids.rs +0 -0
  108. {cc_transcript-14.14.0 → cc_transcript-14.16.0}/rust/crates/core/src/judge.rs +0 -0
  109. {cc_transcript-14.14.0 → cc_transcript-14.16.0}/rust/crates/core/src/lib.rs +0 -0
  110. {cc_transcript-14.14.0 → cc_transcript-14.16.0}/rust/crates/core/src/literals/command.rs +0 -0
  111. {cc_transcript-14.14.0 → cc_transcript-14.16.0}/rust/crates/core/src/literals/corrections.rs +0 -0
  112. {cc_transcript-14.14.0 → cc_transcript-14.16.0}/rust/crates/core/src/literals/feedback.rs +0 -0
  113. {cc_transcript-14.14.0 → cc_transcript-14.16.0}/rust/crates/core/src/literals/mining.rs +0 -0
  114. {cc_transcript-14.14.0 → cc_transcript-14.16.0}/rust/crates/core/src/literals/mod.rs +0 -0
  115. {cc_transcript-14.14.0 → cc_transcript-14.16.0}/rust/crates/core/src/notifications.rs +0 -0
  116. {cc_transcript-14.14.0 → cc_transcript-14.16.0}/rust/crates/core/src/parse.rs +0 -0
  117. {cc_transcript-14.14.0 → cc_transcript-14.16.0}/rust/crates/core/src/pystr.rs +0 -0
  118. {cc_transcript-14.14.0 → cc_transcript-14.16.0}/rust/crates/core/src/query.rs +0 -0
  119. {cc_transcript-14.14.0 → cc_transcript-14.16.0}/rust/crates/core/src/render.rs +0 -0
  120. {cc_transcript-14.14.0 → cc_transcript-14.16.0}/rust/crates/core/src/rng.rs +0 -0
  121. {cc_transcript-14.14.0 → cc_transcript-14.16.0}/rust/crates/core/src/schema.rs +0 -0
  122. {cc_transcript-14.14.0 → cc_transcript-14.16.0}/rust/crates/core/src/sqlite.rs +0 -0
  123. {cc_transcript-14.14.0 → cc_transcript-14.16.0}/rust/crates/core/src/toolcall.rs +0 -0
  124. {cc_transcript-14.14.0 → cc_transcript-14.16.0}/rust/crates/core/src/types.rs +0 -0
  125. {cc_transcript-14.14.0 → cc_transcript-14.16.0}/rust/crates/core/src/value.rs +0 -0
  126. {cc_transcript-14.14.0 → cc_transcript-14.16.0}/rust/crates/core/src/watch.rs +0 -0
  127. {cc_transcript-14.14.0 → cc_transcript-14.16.0}/rust/crates/py/Cargo.toml +0 -0
  128. {cc_transcript-14.14.0 → cc_transcript-14.16.0}/rust/crates/py/build.rs +0 -0
  129. {cc_transcript-14.14.0 → cc_transcript-14.16.0}/rust/crates/py/data/command_prefix_pins.tsv +0 -0
  130. {cc_transcript-14.14.0 → cc_transcript-14.16.0}/rust/crates/py/src/actor_bridge.rs +0 -0
  131. {cc_transcript-14.14.0 → cc_transcript-14.16.0}/rust/crates/py/src/bin/stub_gen.rs +0 -0
  132. {cc_transcript-14.14.0 → cc_transcript-14.16.0}/rust/crates/py/src/codex.rs +0 -0
  133. {cc_transcript-14.14.0 → cc_transcript-14.16.0}/rust/crates/py/src/command.rs +0 -0
  134. {cc_transcript-14.14.0 → cc_transcript-14.16.0}/rust/crates/py/src/context.rs +0 -0
  135. {cc_transcript-14.14.0 → cc_transcript-14.16.0}/rust/crates/py/src/corrections.rs +0 -0
  136. {cc_transcript-14.14.0 → cc_transcript-14.16.0}/rust/crates/py/src/discovery.rs +0 -0
  137. {cc_transcript-14.14.0 → cc_transcript-14.16.0}/rust/crates/py/src/feedback.rs +0 -0
  138. {cc_transcript-14.14.0 → cc_transcript-14.16.0}/rust/crates/py/src/judge.rs +0 -0
  139. {cc_transcript-14.14.0 → cc_transcript-14.16.0}/rust/crates/py/src/lexicon.rs +0 -0
  140. {cc_transcript-14.14.0 → cc_transcript-14.16.0}/rust/crates/py/src/lib.rs +0 -0
  141. {cc_transcript-14.14.0 → cc_transcript-14.16.0}/rust/crates/py/src/mining.rs +0 -0
  142. {cc_transcript-14.14.0 → cc_transcript-14.16.0}/rust/crates/py/src/nlp.rs +0 -0
  143. {cc_transcript-14.14.0 → cc_transcript-14.16.0}/rust/crates/py/src/python.rs +0 -0
  144. {cc_transcript-14.14.0 → cc_transcript-14.16.0}/rust/crates/py/src/render.rs +0 -0
  145. {cc_transcript-14.14.0 → cc_transcript-14.16.0}/rust/crates/py/src/score.rs +0 -0
  146. {cc_transcript-14.14.0 → cc_transcript-14.16.0}/rust/crates/py/src/sqlite.rs +0 -0
  147. {cc_transcript-14.14.0 → cc_transcript-14.16.0}/rust/crates/py/src/toolcall.rs +0 -0
  148. {cc_transcript-14.14.0 → cc_transcript-14.16.0}/rust/crates/py/src/views/attachment.rs +0 -0
  149. {cc_transcript-14.14.0 → cc_transcript-14.16.0}/rust/crates/py/src/views/blocks.rs +0 -0
  150. {cc_transcript-14.14.0 → cc_transcript-14.16.0}/rust/crates/py/src/views/command.rs +0 -0
  151. {cc_transcript-14.14.0 → cc_transcript-14.16.0}/rust/crates/py/src/views/convert.rs +0 -0
  152. {cc_transcript-14.14.0 → cc_transcript-14.16.0}/rust/crates/py/src/views/dunder.rs +0 -0
  153. {cc_transcript-14.14.0 → cc_transcript-14.16.0}/rust/crates/py/src/views/events.rs +0 -0
  154. {cc_transcript-14.14.0 → cc_transcript-14.16.0}/rust/crates/py/src/views/meta.rs +0 -0
  155. {cc_transcript-14.14.0 → cc_transcript-14.16.0}/rust/crates/py/src/views/mod.rs +0 -0
  156. {cc_transcript-14.14.0 → cc_transcript-14.16.0}/rust/crates/py/src/views/print.rs +0 -0
  157. {cc_transcript-14.14.0 → cc_transcript-14.16.0}/rust/crates/py/src/views/store.rs +0 -0
  158. {cc_transcript-14.14.0 → cc_transcript-14.16.0}/rust/crates/py/src/views/system.rs +0 -0
  159. {cc_transcript-14.14.0 → cc_transcript-14.16.0}/rust/crates/py/src/views/toolcall.rs +0 -0
  160. {cc_transcript-14.14.0 → cc_transcript-14.16.0}/rust/crates/py/src/views/toolresult.rs +0 -0
  161. {cc_transcript-14.14.0 → cc_transcript-14.16.0}/rust/crates/py/src/views/transcript.rs +0 -0
  162. {cc_transcript-14.14.0 → cc_transcript-14.16.0}/rust/crates/py/src/watch.rs +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: cc-transcript
3
- Version: 14.14.0
3
+ Version: 14.16.0
4
4
  Classifier: Development Status :: 3 - Alpha
5
5
  Classifier: Environment :: Console
6
6
  Classifier: Intended Audience :: Developers
@@ -1,13 +1,23 @@
1
1
  from __future__ import annotations
2
2
 
3
+ import errno
4
+ import fcntl
3
5
  import hashlib
6
+ import logging
4
7
  import sqlite3
8
+ import time
9
+ from collections.abc import Iterator
10
+ from contextlib import contextmanager
5
11
  from pathlib import Path
6
12
 
13
+ logger = logging.getLogger(__name__)
14
+
7
15
  SCHEMA_COMPONENT = "cc-review-decisions-v1"
8
16
  SCHEMA_VERSION = 1
9
17
  EXPECTED_DDL_FINGERPRINT = "6ae938038f3420cdd4a00189b678fb399d60bb7647d009acb0fa9cc4a653040f"
10
18
  EXPECTED_OBJECT_FINGERPRINT = "a993521f1ae402d85545d9cd841b58c7e9ba755babba32c7d59cc3a97ee17af9"
19
+ _ARCHIVE_LOCK_TIMEOUT_SECONDS = 2.0
20
+ _ARCHIVE_LOCK_RETRY_SECONDS = 0.01
11
21
 
12
22
  _SCHEMA_DDL_ACTIONS = frozenset(
13
23
  {
@@ -146,6 +156,66 @@ def _authorize_exact_schema(
146
156
  return sqlite3.SQLITE_OK
147
157
 
148
158
 
159
+ @contextmanager
160
+ def _archive_lock(db_path: Path) -> Iterator[None]:
161
+ lock_path = db_path.with_name(f"{db_path.name}.archive.lock")
162
+ with lock_path.open("a+b") as lock:
163
+ deadline = time.monotonic() + _ARCHIVE_LOCK_TIMEOUT_SECONDS
164
+ while True:
165
+ try:
166
+ fcntl.flock(lock, fcntl.LOCK_EX | fcntl.LOCK_NB)
167
+ break
168
+ except OSError as error:
169
+ if error.errno not in (errno.EACCES, errno.EAGAIN):
170
+ raise
171
+ remaining = deadline - time.monotonic()
172
+ if remaining <= 0:
173
+ raise TimeoutError(
174
+ f"timed out acquiring decisions archive lock {lock_path} "
175
+ f"after {_ARCHIVE_LOCK_TIMEOUT_SECONDS:g} seconds"
176
+ ) from error
177
+ time.sleep(min(_ARCHIVE_LOCK_RETRY_SECONDS, remaining))
178
+ try:
179
+ yield
180
+ finally:
181
+ fcntl.flock(lock, fcntl.LOCK_UN)
182
+
183
+
184
+ def _archive_incompatible_database(db_path: Path, found_version: int) -> None:
185
+ checkpoint = sqlite3.connect(db_path, autocommit=True)
186
+ try:
187
+ try:
188
+ checkpoint.execute("PRAGMA wal_checkpoint(TRUNCATE)")
189
+ except sqlite3.Error:
190
+ pass
191
+ finally:
192
+ checkpoint.close()
193
+
194
+ timestamp_ns = time.time_ns()
195
+ seconds = timestamp_ns // 1_000_000_000
196
+ microseconds = timestamp_ns // 1_000 % 1_000_000
197
+ archive_stem = (
198
+ f"{db_path.name}.v{found_version}."
199
+ f"{time.strftime('%Y%m%d-%H%M%S', time.localtime(seconds))}-{microseconds:06d}"
200
+ )
201
+ archive_path = db_path.with_name(f"{archive_stem}.bak")
202
+ collision = 0
203
+ while archive_path.exists():
204
+ collision += 1
205
+ archive_path = db_path.with_name(f"{archive_stem}.{collision}.bak")
206
+ db_path.rename(archive_path)
207
+ for suffix in ("-wal", "-shm"):
208
+ sidecar = db_path.with_name(f"{db_path.name}{suffix}")
209
+ sidecar.unlink(missing_ok=True)
210
+ logger.warning(
211
+ "Archived incompatible decisions database %s with schema version %s (expected %s) to %s",
212
+ db_path,
213
+ found_version,
214
+ SCHEMA_VERSION,
215
+ archive_path,
216
+ )
217
+
218
+
149
219
  def open_decisions_sqlite(path: Path | None) -> sqlite3.Connection:
150
220
  if ddl_fingerprint() != EXPECTED_DDL_FINGERPRINT:
151
221
  raise RuntimeError(
@@ -153,32 +223,40 @@ def open_decisions_sqlite(path: Path | None) -> sqlite3.Connection:
153
223
  )
154
224
  db_path = path or Path.home() / ".cc-transcript" / "decisions.db"
155
225
  db_path.parent.mkdir(parents=True, exist_ok=True)
156
- conn = sqlite3.connect(db_path, autocommit=True)
157
- conn.row_factory = sqlite3.Row
158
- conn.execute("PRAGMA busy_timeout = 2000")
159
- committed = False
160
226
  created = False
161
- try:
162
- conn.execute("BEGIN IMMEDIATE")
163
- version = conn.execute("PRAGMA user_version").fetchone()[0]
164
- app_objects = conn.execute(
165
- "SELECT count(*) FROM sqlite_schema "
166
- "WHERE type IN ('table', 'index', 'trigger', 'view') "
167
- "AND lower(substr(name, 1, 7)) <> 'sqlite_'"
168
- ).fetchone()[0]
169
- created = version == 0 and app_objects == 0
170
- create_schema(conn) if created else verify_schema(conn)
171
- conn.execute("COMMIT")
172
- committed = True
173
- finally:
174
- if not committed:
175
- if conn.in_transaction:
176
- conn.execute("ROLLBACK")
227
+ with _archive_lock(db_path):
228
+ while True:
229
+ conn = sqlite3.connect(db_path, autocommit=True)
230
+ committed = False
231
+ try:
232
+ conn.row_factory = sqlite3.Row
233
+ conn.execute("PRAGMA busy_timeout = 2000")
234
+ conn.execute("BEGIN IMMEDIATE")
235
+ version = conn.execute("PRAGMA user_version").fetchone()[0]
236
+ has_objects = conn.execute("SELECT EXISTS(SELECT 1 FROM sqlite_master)").fetchone()[0]
237
+ created = version == 0 and not has_objects
238
+ if version != SCHEMA_VERSION and not created:
239
+ conn.execute("ROLLBACK")
240
+ else:
241
+ create_schema(conn) if created else verify_schema(conn)
242
+ conn.execute("COMMIT")
243
+ committed = True
244
+ except BaseException:
245
+ if conn.in_transaction:
246
+ conn.execute("ROLLBACK")
247
+ conn.close()
248
+ raise
249
+ if committed:
250
+ break
251
+ conn.close()
252
+ _archive_incompatible_database(db_path, version)
253
+ try:
254
+ if created:
255
+ db_path.chmod(0o600)
256
+ if (journal_mode := conn.execute("PRAGMA journal_mode = WAL").fetchone()[0]) != "wal":
257
+ raise RuntimeError(f"enable decisions WAL: mode {journal_mode!r}")
258
+ conn.set_authorizer(_authorize_exact_schema)
259
+ except BaseException:
177
260
  conn.close()
178
- if created:
179
- db_path.chmod(0o600)
180
- if (journal_mode := conn.execute("PRAGMA journal_mode = WAL").fetchone()[0]) != "wal":
181
- conn.close()
182
- raise RuntimeError(f"enable decisions WAL: mode {journal_mode!r}")
183
- conn.set_authorizer(_authorize_exact_schema)
261
+ raise
184
262
  return conn
@@ -73,7 +73,8 @@ STRUCTURAL_GROUPS: tuple[tuple[str, str], ...] = (
73
73
  # Start-anchored so a mid-text mention is not a banner.
74
74
  # Dialect-hardened for Rust-regex parity — see cc-notes doc "rust-regex-dialect-parity".
75
75
  AGENT_INJECTION_GROUPS: tuple[tuple[str, str], ...] = (
76
- ("xml_tags_extra", r"\A\s*<(?:teammate-message|scheduled-task)(?:[\s/>]|$)"),
76
+ ("xml_tags_extra", r"\A\s*<(?:teammate-message|scheduled-task|agent-message|cross-session-message)(?:[\s/>]|$)"),
77
+ ("relay_preamble", r"\A\s*Another Claude sess(?-i:[Ii])on sent a message:"),
77
78
  ("augment_agent", r"\A\s*# Augment Agent(?:\s|$)"),
78
79
  ("role_reminder", r"\A\s*\[Role Rem(?-i:[Ii])nder(?:[\s:\]]|$)"),
79
80
  )
@@ -11,6 +11,7 @@ predicates compose over progressively narrower windows.
11
11
  from __future__ import annotations
12
12
 
13
13
  import re
14
+ import threading
14
15
  from collections.abc import Mapping
15
16
  from dataclasses import dataclass
16
17
  from fnmatch import fnmatch
@@ -46,6 +47,31 @@ if TYPE_CHECKING:
46
47
  from cc_transcript.models import TranscriptEvent
47
48
 
48
49
 
50
+ DEEP_LIFT_BUDGET = 64 * 1024 * 1024
51
+
52
+ DEEP_LIFTS: dict[LiftKey, tuple[LiftStamp, DeepSession]] = {}
53
+ """Lifted sidechain transcripts, keyed by tree position and stamped with the file's ``(size, mtime_ns)``.
54
+
55
+ :meth:`Session.walk` re-reads every node of a tree in the same order, so evicting the
56
+ least-recently-used entry thrashes on any tree larger than the cache — each walk drops
57
+ exactly what the next one asks for first. Admission stops at :data:`DEEP_LIFT_BUDGET`
58
+ source bytes instead: what is held stays held, and a tree that outgrows the budget
59
+ reparses only its tail. A sidechain that grew replaces its own entry, budget or not.
60
+ """
61
+
62
+ DEEP_LIFT_GUARD = threading.Lock()
63
+
64
+ type LiftStamp = tuple[int, int]
65
+ type LiftKey = tuple[Path, int, ToolUseId | None]
66
+
67
+
68
+ def hold_lift(key: LiftKey, stamp: LiftStamp, deep: DeepSession) -> DeepSession:
69
+ with DEEP_LIFT_GUARD:
70
+ if key in DEEP_LIFTS or sum(size for (size, _), _ in DEEP_LIFTS.values()) < DEEP_LIFT_BUDGET:
71
+ DEEP_LIFTS[key] = (stamp, deep)
72
+ return deep
73
+
74
+
49
75
  def is_failure(use: ToolUse) -> bool:
50
76
  return use.result is not None and use.result.is_error
51
77
 
@@ -236,13 +262,18 @@ class ToolCallQuery:
236
262
  return bool(self.items)
237
263
 
238
264
 
239
- @dataclass(frozen=True, slots=True)
265
+ @dataclass(frozen=True)
240
266
  class Session:
241
267
  """An immutable windowed view of a session's turns.
242
268
 
243
269
  Every slicing operation returns another :class:`Session`; turns at a
244
270
  window boundary are trimmed copies, so mid-turn slices stay event-precise.
245
271
 
272
+ Unslotted, unlike its neighbours: the window is immutable, so every
273
+ derivation over it — :attr:`tool_calls`, :meth:`commands`,
274
+ :meth:`command_lines` — memoizes per instance, and a predicate that
275
+ queries one window repeatedly pays each derivation once.
276
+
246
277
  Attributes:
247
278
  turns: The turns in the window.
248
279
  path: The transcript file the session was loaded from, when known —
@@ -301,7 +332,7 @@ class Session:
301
332
  """Every event in the window, in order."""
302
333
  return tuple(event for turn in self.turns for event in turn.events)
303
334
 
304
- @property
335
+ @cached_property
305
336
  def tool_calls(self) -> ToolCallQuery:
306
337
  """The window's tool calls as a chainable query."""
307
338
  return ToolCallQuery(tuple(use for turn in self.turns for use in turn.tool_uses))
@@ -344,6 +375,10 @@ class Session:
344
375
  cycles terminate. An unreadable transcript is skipped but its children
345
376
  are still walked; a structurally malformed line raises, as
346
377
  :attr:`subagents` does.
378
+
379
+ Each reached transcript is parsed and lifted once per ``(size, mtime)``
380
+ stamp and memoized across walks, so a predicate that walks repeatedly —
381
+ or a resident process that re-walks per event — reparses only what grew.
347
382
  """
348
383
  return deep_sessions(self)
349
384
 
@@ -489,13 +524,21 @@ class Session:
489
524
 
490
525
  def commands(self) -> tuple[str, ...]:
491
526
  """The shell command strings of the window's Bash calls."""
492
- return tuple(call.command for use in self.tool_calls.named("Bash") if isinstance(call := use.call, BashCall))
527
+ return self._commands
493
528
 
494
529
  def command_lines(self) -> tuple[CommandLine, ...]:
495
530
  """The window's Bash commands parsed into :class:`~cc_transcript.command.CommandLine` objects."""
531
+ return self._command_lines
532
+
533
+ @cached_property
534
+ def _commands(self) -> tuple[str, ...]:
535
+ return tuple(call.command for use in self.tool_calls.named("Bash") if isinstance(call := use.call, BashCall))
536
+
537
+ @cached_property
538
+ def _command_lines(self) -> tuple[CommandLine, ...]:
496
539
  from cc_transcript.command import parse_command_line
497
540
 
498
- return tuple(parse_command_line(command) for command in self.commands())
541
+ return tuple(parse_command_line(command) for command in self._commands)
499
542
 
500
543
  def __len__(self) -> int:
501
544
  return sum(len(turn.events) for turn in self.turns)
@@ -659,11 +702,8 @@ def visit_transcript(path: Path, depth: int, spawned_by: ToolUseId | None, seen:
659
702
  yield from descend_sidechains(path, depth + 1, seen)
660
703
 
661
704
 
662
- def load_deep_session(path: Path, depth: int, spawned_by: ToolUseId | None) -> DeepSession | None:
663
- try:
664
- transcript = parse(path)
665
- except OSError:
666
- return None
705
+ def lift_deep_session(path: Path, depth: int, spawned_by: ToolUseId | None) -> DeepSession:
706
+ transcript = parse(path)
667
707
  session_id = session_id_of(transcript.events) or SessionId(path.stem)
668
708
  return DeepSession(
669
709
  session=Session.from_activity(SessionActivity.from_events(session_id, transcript.events), path=path),
@@ -672,3 +712,18 @@ def load_deep_session(path: Path, depth: int, spawned_by: ToolUseId | None) -> D
672
712
  depth=depth,
673
713
  spawned_by=spawned_by,
674
714
  )
715
+
716
+
717
+ def load_deep_session(path: Path, depth: int, spawned_by: ToolUseId | None) -> DeepSession | None:
718
+ try:
719
+ stat = path.stat()
720
+ except OSError:
721
+ return None
722
+ stamp = (stat.st_size, stat.st_mtime_ns)
723
+ key = (path, depth, spawned_by)
724
+ if (held := DEEP_LIFTS.get(key)) is not None and held[0] == stamp:
725
+ return held[1]
726
+ try:
727
+ return hold_lift(key, stamp, lift_deep_session(path, depth, spawned_by))
728
+ except OSError:
729
+ return None
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "cc-transcript"
3
- version = "14.14.0" # informational — releases stamp the version from the git tag
3
+ version = "14.16.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 crate::value::{field, field_bool, field_str};
11
11
  /// bench reuses: `--signal` keeps conversational turns minus junk, `--no-junk`
12
12
  /// (`builders.NOISE_SPEC` in Python) drops structural noise. The one source of
13
13
  /// truth; `tests/test_show_specs.py` pins Python's `NOISE_SPEC` to `NOISE_SPEC_JSON`.
14
- pub const SIGNAL_SPEC_JSON: &str = r#"{"clauses":[{"predicate":{"kind":"KindIs","kinds":["assistant","user"]},"action":"drop","applies_to":[],"negate":true,"label":null},{"predicate":{"kind":"TextMatchesAny","groups":[["xml_tags","<(?:system[_-](?:instruction|reminder)|local-command-(?:stdout|stderr|caveat)|command-(?:name|message|args)|task-notification|persisted-output|output-file)\\b"],["local_command_caveat","Caveat: The messages below were generated by the user while running local commands\\."],["remaining_tasks_ack","REMAINING_TASKS_ACKNOWLEDGED"],["angle_token","<<[a-z][a-z0-9-]*>>"],["skill_base_dir","^Base directory for this skill:"],["hash_banner","(?:###\\s+[\\w][\\w \\-]{0,30}\\s+){3,}###"],["xml_tags_extra","\\A\\s*<(?:teammate-message|scheduled-task)(?:[\\s/>]|$)"],["augment_agent","\\A\\s*# Augment Agent(?:\\s|$)"],["role_reminder","\\A\\s*\\[Role Rem(?-i:[Ii])nder(?:[\\s:\\]]|$)"],["command_echo","\\A\\s*<bash-(?:input|stdout|stderr)\\b"]],"ignore_case":true},"action":"drop","applies_to":["user"],"negate":false,"label":null},{"predicate":{"kind":"ModelIs","models":["<synthetic>"]},"action":"drop","applies_to":["assistant"],"negate":false,"label":null},{"predicate":{"kind":"MetaFlag","flag":"is_sidechain"},"action":"drop","applies_to":[],"negate":false,"label":null},{"predicate":{"kind":"TextEmpty","consider_tool_use":false},"action":"drop","applies_to":["user"],"negate":false,"label":null},{"predicate":{"kind":"TextEmpty","consider_tool_use":true},"action":"drop","applies_to":["assistant"],"negate":false,"label":null}]}"#;
14
+ pub const SIGNAL_SPEC_JSON: &str = r#"{"clauses":[{"predicate":{"kind":"KindIs","kinds":["assistant","user"]},"action":"drop","applies_to":[],"negate":true,"label":null},{"predicate":{"kind":"TextMatchesAny","groups":[["xml_tags","<(?:system[_-](?:instruction|reminder)|local-command-(?:stdout|stderr|caveat)|command-(?:name|message|args)|task-notification|persisted-output|output-file)\\b"],["local_command_caveat","Caveat: The messages below were generated by the user while running local commands\\."],["remaining_tasks_ack","REMAINING_TASKS_ACKNOWLEDGED"],["angle_token","<<[a-z][a-z0-9-]*>>"],["skill_base_dir","^Base directory for this skill:"],["hash_banner","(?:###\\s+[\\w][\\w \\-]{0,30}\\s+){3,}###"],["xml_tags_extra","\\A\\s*<(?:teammate-message|scheduled-task|agent-message|cross-session-message)(?:[\\s/>]|$)"],["relay_preamble","\\A\\s*Another Claude sess(?-i:[Ii])on sent a message:"],["augment_agent","\\A\\s*# Augment Agent(?:\\s|$)"],["role_reminder","\\A\\s*\\[Role Rem(?-i:[Ii])nder(?:[\\s:\\]]|$)"],["command_echo","\\A\\s*<bash-(?:input|stdout|stderr)\\b"]],"ignore_case":true},"action":"drop","applies_to":["user"],"negate":false,"label":null},{"predicate":{"kind":"ModelIs","models":["<synthetic>"]},"action":"drop","applies_to":["assistant"],"negate":false,"label":null},{"predicate":{"kind":"MetaFlag","flag":"is_sidechain"},"action":"drop","applies_to":[],"negate":false,"label":null},{"predicate":{"kind":"TextEmpty","consider_tool_use":false},"action":"drop","applies_to":["user"],"negate":false,"label":null},{"predicate":{"kind":"TextEmpty","consider_tool_use":true},"action":"drop","applies_to":["assistant"],"negate":false,"label":null}]}"#;
15
15
  pub const NOISE_SPEC_JSON: &str = r#"{"clauses":[{"predicate":{"kind":"TextMatchesAny","groups":[["xml_tags","<(?:system[_-](?:instruction|reminder)|local-command-(?:stdout|stderr|caveat)|command-(?:name|message|args)|task-notification|persisted-output|output-file)\\b"],["local_command_caveat","Caveat: The messages below were generated by the user while running local commands\\."],["remaining_tasks_ack","REMAINING_TASKS_ACKNOWLEDGED"],["angle_token","<<[a-z][a-z0-9-]*>>"],["skill_base_dir","^Base directory for this skill:"],["hash_banner","(?:###\\s+[\\w][\\w \\-]{0,30}\\s+){3,}###"]],"ignore_case":true},"action":"drop","applies_to":["user"],"negate":false,"label":null}]}"#;
16
16
 
17
17
  pub static SIGNAL_SPEC: Lazy<CompiledSpec> =
@@ -10,7 +10,7 @@ pub const USER_SAID_TRAILER: &str = "Note: The user's next message";
10
10
  pub const ANSWERED_PREFIX: &str = "Your questions have been answered: ";
11
11
  pub const ANSWERED_TRAILER: &str = ". You can now continue with these answers in mind.";
12
12
  pub const INTERRUPT_MARKER_PATTERN: &str = "(?:^\\s*\\[Request (?-i:[Ii])nterrupted by user)";
13
- pub const AGENT_INJECTION_PATTERN: &str = "(?:\\A\\s*<(?:teammate-message|scheduled-task)(?:[\\s/>]|$))|(?:\\A\\s*# Augment Agent(?:\\s|$))|(?:\\A\\s*\\[Role Rem(?-i:[Ii])nder(?:[\\s:\\]]|$))";
13
+ pub const AGENT_INJECTION_PATTERN: &str = "(?:\\A\\s*<(?:teammate-message|scheduled-task|agent-message|cross-session-message)(?:[\\s/>]|$))|(?:\\A\\s*Another Claude sess(?-i:[Ii])on sent a message:)|(?:\\A\\s*# Augment Agent(?:\\s|$))|(?:\\A\\s*\\[Role Rem(?-i:[Ii])nder(?:[\\s:\\]]|$))";
14
14
  pub const TASK_NOTIFICATION_MARKER: &str = "<task-notification>";
15
15
  pub const TOOL_USE_ID_PREFIX: &str = "<tool-use-id>";
16
16
  pub const TOOL_USE_ID_SUFFIX: &str = "</tool-use-id>";
@@ -167,6 +167,15 @@ mod tests {
167
167
  assert!(is_agent_injection(
168
168
  "<scheduled-task id='7'>run the suite</scheduled-task>"
169
169
  ));
170
+ assert!(is_agent_injection(
171
+ "Another Claude session sent a message: <teammate-message teammate_id=\"x\" color=\"orange\">\nhello"
172
+ ));
173
+ assert!(is_agent_injection(
174
+ "<agent-message from='x'>rebase onto main"
175
+ ));
176
+ assert!(is_agent_injection(
177
+ "<cross-session-message from='x'>the build is green"
178
+ ));
170
179
  assert!(is_agent_injection("[Role Reminder: You are a Coordinator."));
171
180
  assert!(is_agent_injection("# Augment Agent\nyou have these tools"));
172
181
  // Leading whitespace before the marker is tolerated.
@@ -190,6 +199,14 @@ mod tests {
190
199
  assert!(!is_agent_injection(
191
200
  "we discussed the [Role Reminder] banner mid-sentence"
192
201
  ));
202
+ assert!(!is_agent_injection(
203
+ "the log says Another Claude session sent a message: further down"
204
+ ));
205
+ // The "i" of "session" is ASCII-pinned: the dotless-I (U+0131) Python re folds must
206
+ // never match on either backend.
207
+ assert!(!is_agent_injection(
208
+ "Another Claude sess\u{131}on sent a message: dotless"
209
+ ));
193
210
  // A combining mark (U+0301) after the tag name is not a portable word boundary; and
194
211
  // dotless-I must not fold to ASCII "i" — Python re once matched both, Rust never did.
195
212
  assert!(!is_agent_injection("<teammate-message\u{0301}>"));
File without changes