cc-transcript 14.4.0__tar.gz → 14.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 (155) hide show
  1. {cc_transcript-14.4.0 → cc_transcript-14.6.0}/PKG-INFO +1 -1
  2. {cc_transcript-14.4.0 → cc_transcript-14.6.0}/cc_transcript/__init__.py +1 -0
  3. {cc_transcript-14.4.0 → cc_transcript-14.6.0}/cc_transcript/__init__.pyi +2 -0
  4. {cc_transcript-14.4.0 → cc_transcript-14.6.0}/cc_transcript/_native.pyi +87 -28
  5. {cc_transcript-14.4.0 → cc_transcript-14.6.0}/cc_transcript/command.py +4 -0
  6. {cc_transcript-14.4.0 → cc_transcript-14.6.0}/cc_transcript/corrections.py +42 -21
  7. {cc_transcript-14.4.0 → cc_transcript-14.6.0}/cc_transcript/decisions.py +24 -17
  8. {cc_transcript-14.4.0 → cc_transcript-14.6.0}/cc_transcript/evidence.py +2 -2
  9. {cc_transcript-14.4.0 → cc_transcript-14.6.0}/cc_transcript/extract/correct.py +2 -2
  10. {cc_transcript-14.4.0 → cc_transcript-14.6.0}/cc_transcript/heartbeats.py +40 -20
  11. {cc_transcript-14.4.0 → cc_transcript-14.6.0}/cc_transcript/judge/similar.py +27 -26
  12. cc_transcript-14.6.0/cc_transcript/ledger.py +163 -0
  13. {cc_transcript-14.4.0 → cc_transcript-14.6.0}/cc_transcript/literals.py +7 -1
  14. {cc_transcript-14.4.0 → cc_transcript-14.6.0}/cc_transcript/mining/store.py +95 -74
  15. {cc_transcript-14.4.0 → cc_transcript-14.6.0}/cc_transcript/watch.py +23 -10
  16. {cc_transcript-14.4.0 → cc_transcript-14.6.0}/pyproject.toml +1 -1
  17. {cc_transcript-14.4.0 → cc_transcript-14.6.0}/rust/crates/cli/src/target.rs +2 -2
  18. cc_transcript-14.6.0/rust/crates/core/src/actor.rs +166 -0
  19. cc_transcript-14.6.0/rust/crates/core/src/codex/discovery.rs +192 -0
  20. cc_transcript-14.6.0/rust/crates/core/src/codex/lower.rs +1342 -0
  21. {cc_transcript-14.4.0 → cc_transcript-14.6.0}/rust/crates/core/src/codex/mod.rs +6 -0
  22. cc_transcript-14.6.0/rust/crates/core/src/codex/probe.rs +546 -0
  23. {cc_transcript-14.4.0 → cc_transcript-14.6.0}/rust/crates/core/src/command.rs +772 -39
  24. cc_transcript-14.6.0/rust/crates/core/src/gateway.rs +207 -0
  25. {cc_transcript-14.4.0 → cc_transcript-14.6.0}/rust/crates/core/src/lib.rs +3 -0
  26. {cc_transcript-14.4.0 → cc_transcript-14.6.0}/rust/crates/core/src/literals/command.rs +5 -0
  27. cc_transcript-14.6.0/rust/crates/py/src/actor_bridge.rs +135 -0
  28. {cc_transcript-14.4.0 → cc_transcript-14.6.0}/rust/crates/py/src/context.rs +2 -2
  29. cc_transcript-14.6.0/rust/crates/py/src/corrections.rs +260 -0
  30. cc_transcript-14.6.0/rust/crates/py/src/feedback.rs +405 -0
  31. {cc_transcript-14.4.0 → cc_transcript-14.6.0}/rust/crates/py/src/lib.rs +1 -0
  32. {cc_transcript-14.4.0 → cc_transcript-14.6.0}/rust/crates/py/src/python.rs +26 -16
  33. {cc_transcript-14.4.0 → cc_transcript-14.6.0}/rust/crates/py/src/render.rs +4 -4
  34. {cc_transcript-14.4.0 → cc_transcript-14.6.0}/rust/crates/py/src/views/command.rs +126 -2
  35. {cc_transcript-14.4.0 → cc_transcript-14.6.0}/rust/crates/py/src/views/mod.rs +1 -0
  36. cc_transcript-14.4.0/cc_transcript/ledger.py +0 -94
  37. cc_transcript-14.4.0/rust/crates/py/src/corrections.rs +0 -200
  38. cc_transcript-14.4.0/rust/crates/py/src/feedback.rs +0 -321
  39. {cc_transcript-14.4.0 → cc_transcript-14.6.0}/Cargo.lock +0 -0
  40. {cc_transcript-14.4.0 → cc_transcript-14.6.0}/Cargo.toml +0 -0
  41. {cc_transcript-14.4.0 → cc_transcript-14.6.0}/LICENSE +0 -0
  42. {cc_transcript-14.4.0 → cc_transcript-14.6.0}/README.md +0 -0
  43. {cc_transcript-14.4.0 → cc_transcript-14.6.0}/cc_transcript/activity.py +0 -0
  44. {cc_transcript-14.4.0 → cc_transcript-14.6.0}/cc_transcript/activity_probe.py +0 -0
  45. {cc_transcript-14.4.0 → cc_transcript-14.6.0}/cc_transcript/builders.py +0 -0
  46. {cc_transcript-14.4.0 → cc_transcript-14.6.0}/cc_transcript/context.py +0 -0
  47. {cc_transcript-14.4.0 → cc_transcript-14.6.0}/cc_transcript/cost.py +0 -0
  48. {cc_transcript-14.4.0 → cc_transcript-14.6.0}/cc_transcript/discovery.py +0 -0
  49. {cc_transcript-14.4.0 → cc_transcript-14.6.0}/cc_transcript/disktruth.py +0 -0
  50. {cc_transcript-14.4.0 → cc_transcript-14.6.0}/cc_transcript/extract/__init__.py +0 -0
  51. {cc_transcript-14.4.0 → cc_transcript-14.6.0}/cc_transcript/facts.py +0 -0
  52. {cc_transcript-14.4.0 → cc_transcript-14.6.0}/cc_transcript/filterspec.py +0 -0
  53. {cc_transcript-14.4.0 → cc_transcript-14.6.0}/cc_transcript/ids.py +0 -0
  54. {cc_transcript-14.4.0 → cc_transcript-14.6.0}/cc_transcript/judge/__init__.py +0 -0
  55. {cc_transcript-14.4.0 → cc_transcript-14.6.0}/cc_transcript/judge/llm.py +0 -0
  56. {cc_transcript-14.4.0 → cc_transcript-14.6.0}/cc_transcript/judge/verdicts.py +0 -0
  57. {cc_transcript-14.4.0 → cc_transcript-14.6.0}/cc_transcript/mining/__init__.py +0 -0
  58. {cc_transcript-14.4.0 → cc_transcript-14.6.0}/cc_transcript/mining/candidates.py +0 -0
  59. {cc_transcript-14.4.0 → cc_transcript-14.6.0}/cc_transcript/mining/confidence.py +0 -0
  60. {cc_transcript-14.4.0 → cc_transcript-14.6.0}/cc_transcript/mining/engine.py +0 -0
  61. {cc_transcript-14.4.0 → cc_transcript-14.6.0}/cc_transcript/mining/formats.py +0 -0
  62. {cc_transcript-14.4.0 → cc_transcript-14.6.0}/cc_transcript/mining/sampling.py +0 -0
  63. {cc_transcript-14.4.0 → cc_transcript-14.6.0}/cc_transcript/mining/signals.py +0 -0
  64. {cc_transcript-14.4.0 → cc_transcript-14.6.0}/cc_transcript/mining/sourcekind.py +0 -0
  65. {cc_transcript-14.4.0 → cc_transcript-14.6.0}/cc_transcript/mining/spec.py +0 -0
  66. {cc_transcript-14.4.0 → cc_transcript-14.6.0}/cc_transcript/models.py +0 -0
  67. {cc_transcript-14.4.0 → cc_transcript-14.6.0}/cc_transcript/nlp.py +0 -0
  68. {cc_transcript-14.4.0 → cc_transcript-14.6.0}/cc_transcript/notifications.py +0 -0
  69. {cc_transcript-14.4.0 → cc_transcript-14.6.0}/cc_transcript/parser.py +0 -0
  70. {cc_transcript-14.4.0 → cc_transcript-14.6.0}/cc_transcript/py.typed +0 -0
  71. {cc_transcript-14.4.0 → cc_transcript-14.6.0}/cc_transcript/query.py +0 -0
  72. {cc_transcript-14.4.0 → cc_transcript-14.6.0}/cc_transcript/render.py +0 -0
  73. {cc_transcript-14.4.0 → cc_transcript-14.6.0}/cc_transcript/sentiment/__init__.py +0 -0
  74. {cc_transcript-14.4.0 → cc_transcript-14.6.0}/cc_transcript/sentiment/buckets.py +0 -0
  75. {cc_transcript-14.4.0 → cc_transcript-14.6.0}/cc_transcript/sentiment/data/afinn-en-165.tsv +0 -0
  76. {cc_transcript-14.4.0 → cc_transcript-14.6.0}/cc_transcript/sentiment/data/domain_overrides.tsv +0 -0
  77. {cc_transcript-14.4.0 → cc_transcript-14.6.0}/cc_transcript/sentiment/data/en-ewt.udpipe +0 -0
  78. {cc_transcript-14.4.0 → cc_transcript-14.6.0}/cc_transcript/sentiment/engine.py +0 -0
  79. {cc_transcript-14.4.0 → cc_transcript-14.6.0}/cc_transcript/sentiment/lexicon.py +0 -0
  80. {cc_transcript-14.4.0 → cc_transcript-14.6.0}/cc_transcript/sentiment/scorespec.py +0 -0
  81. {cc_transcript-14.4.0 → cc_transcript-14.6.0}/cc_transcript/synthetic.py +0 -0
  82. {cc_transcript-14.4.0 → cc_transcript-14.6.0}/cc_transcript/tools.py +0 -0
  83. {cc_transcript-14.4.0 → cc_transcript-14.6.0}/rust/crates/cli/Cargo.toml +0 -0
  84. {cc_transcript-14.4.0 → cc_transcript-14.6.0}/rust/crates/cli/src/commands/corrections.rs +0 -0
  85. {cc_transcript-14.4.0 → cc_transcript-14.6.0}/rust/crates/cli/src/commands/digest.rs +0 -0
  86. {cc_transcript-14.4.0 → cc_transcript-14.6.0}/rust/crates/cli/src/commands/facts.rs +0 -0
  87. {cc_transcript-14.4.0 → cc_transcript-14.6.0}/rust/crates/cli/src/commands/grep.rs +0 -0
  88. {cc_transcript-14.4.0 → cc_transcript-14.6.0}/rust/crates/cli/src/commands/list.rs +0 -0
  89. {cc_transcript-14.4.0 → cc_transcript-14.6.0}/rust/crates/cli/src/commands/mod.rs +0 -0
  90. {cc_transcript-14.4.0 → cc_transcript-14.6.0}/rust/crates/cli/src/commands/scratchpad.rs +0 -0
  91. {cc_transcript-14.4.0 → cc_transcript-14.6.0}/rust/crates/cli/src/commands/show.rs +0 -0
  92. {cc_transcript-14.4.0 → cc_transcript-14.6.0}/rust/crates/cli/src/commands/slice.rs +0 -0
  93. {cc_transcript-14.4.0 → cc_transcript-14.6.0}/rust/crates/cli/src/commands/stats.rs +0 -0
  94. {cc_transcript-14.4.0 → cc_transcript-14.6.0}/rust/crates/cli/src/commands/watch.rs +0 -0
  95. {cc_transcript-14.4.0 → cc_transcript-14.6.0}/rust/crates/cli/src/lib.rs +0 -0
  96. {cc_transcript-14.4.0 → cc_transcript-14.6.0}/rust/crates/cli/src/main.rs +0 -0
  97. {cc_transcript-14.4.0 → cc_transcript-14.6.0}/rust/crates/cli/src/output.rs +0 -0
  98. {cc_transcript-14.4.0 → cc_transcript-14.6.0}/rust/crates/core/Cargo.toml +0 -0
  99. {cc_transcript-14.4.0 → cc_transcript-14.6.0}/rust/crates/core/benches/parse.rs +0 -0
  100. {cc_transcript-14.4.0 → cc_transcript-14.6.0}/rust/crates/core/benches/render.rs +0 -0
  101. {cc_transcript-14.4.0 → cc_transcript-14.6.0}/rust/crates/core/benches/toolcall.rs +0 -0
  102. {cc_transcript-14.4.0 → cc_transcript-14.6.0}/rust/crates/core/src/activity.rs +0 -0
  103. {cc_transcript-14.4.0 → cc_transcript-14.6.0}/rust/crates/core/src/buckets.rs +0 -0
  104. {cc_transcript-14.4.0 → cc_transcript-14.6.0}/rust/crates/core/src/codex/parse.rs +0 -0
  105. {cc_transcript-14.4.0 → cc_transcript-14.6.0}/rust/crates/core/src/codex/protocol.rs +0 -0
  106. {cc_transcript-14.4.0 → cc_transcript-14.6.0}/rust/crates/core/src/codex/types.rs +0 -0
  107. {cc_transcript-14.4.0 → cc_transcript-14.6.0}/rust/crates/core/src/context.rs +0 -0
  108. {cc_transcript-14.4.0 → cc_transcript-14.6.0}/rust/crates/core/src/corrections.rs +0 -0
  109. {cc_transcript-14.4.0 → cc_transcript-14.6.0}/rust/crates/core/src/cost.rs +0 -0
  110. {cc_transcript-14.4.0 → cc_transcript-14.6.0}/rust/crates/core/src/discovery.rs +0 -0
  111. {cc_transcript-14.4.0 → cc_transcript-14.6.0}/rust/crates/core/src/facts.rs +0 -0
  112. {cc_transcript-14.4.0 → cc_transcript-14.6.0}/rust/crates/core/src/feedback.rs +0 -0
  113. {cc_transcript-14.4.0 → cc_transcript-14.6.0}/rust/crates/core/src/filter.rs +0 -0
  114. {cc_transcript-14.4.0 → cc_transcript-14.6.0}/rust/crates/core/src/ids.rs +0 -0
  115. {cc_transcript-14.4.0 → cc_transcript-14.6.0}/rust/crates/core/src/literals/corrections.rs +0 -0
  116. {cc_transcript-14.4.0 → cc_transcript-14.6.0}/rust/crates/core/src/literals/feedback.rs +0 -0
  117. {cc_transcript-14.4.0 → cc_transcript-14.6.0}/rust/crates/core/src/literals/mining.rs +0 -0
  118. {cc_transcript-14.4.0 → cc_transcript-14.6.0}/rust/crates/core/src/literals/mod.rs +0 -0
  119. {cc_transcript-14.4.0 → cc_transcript-14.6.0}/rust/crates/core/src/literals/protocol.rs +0 -0
  120. {cc_transcript-14.4.0 → cc_transcript-14.6.0}/rust/crates/core/src/notifications.rs +0 -0
  121. {cc_transcript-14.4.0 → cc_transcript-14.6.0}/rust/crates/core/src/parse.rs +0 -0
  122. {cc_transcript-14.4.0 → cc_transcript-14.6.0}/rust/crates/core/src/protocol.rs +0 -0
  123. {cc_transcript-14.4.0 → cc_transcript-14.6.0}/rust/crates/core/src/pystr.rs +0 -0
  124. {cc_transcript-14.4.0 → cc_transcript-14.6.0}/rust/crates/core/src/query.rs +0 -0
  125. {cc_transcript-14.4.0 → cc_transcript-14.6.0}/rust/crates/core/src/render.rs +0 -0
  126. {cc_transcript-14.4.0 → cc_transcript-14.6.0}/rust/crates/core/src/sqlite.rs +0 -0
  127. {cc_transcript-14.4.0 → cc_transcript-14.6.0}/rust/crates/core/src/toolcall.rs +0 -0
  128. {cc_transcript-14.4.0 → cc_transcript-14.6.0}/rust/crates/core/src/types.rs +0 -0
  129. {cc_transcript-14.4.0 → cc_transcript-14.6.0}/rust/crates/core/src/value.rs +0 -0
  130. {cc_transcript-14.4.0 → cc_transcript-14.6.0}/rust/crates/core/src/watch.rs +0 -0
  131. {cc_transcript-14.4.0 → cc_transcript-14.6.0}/rust/crates/py/Cargo.toml +0 -0
  132. {cc_transcript-14.4.0 → cc_transcript-14.6.0}/rust/crates/py/build.rs +0 -0
  133. {cc_transcript-14.4.0 → cc_transcript-14.6.0}/rust/crates/py/data/command_prefix_pins.tsv +0 -0
  134. {cc_transcript-14.4.0 → cc_transcript-14.6.0}/rust/crates/py/src/bin/stub_gen.rs +0 -0
  135. {cc_transcript-14.4.0 → cc_transcript-14.6.0}/rust/crates/py/src/command.rs +0 -0
  136. {cc_transcript-14.4.0 → cc_transcript-14.6.0}/rust/crates/py/src/discovery.rs +0 -0
  137. {cc_transcript-14.4.0 → cc_transcript-14.6.0}/rust/crates/py/src/lexicon.rs +0 -0
  138. {cc_transcript-14.4.0 → cc_transcript-14.6.0}/rust/crates/py/src/mining.rs +0 -0
  139. {cc_transcript-14.4.0 → cc_transcript-14.6.0}/rust/crates/py/src/nlp.rs +0 -0
  140. {cc_transcript-14.4.0 → cc_transcript-14.6.0}/rust/crates/py/src/score.rs +0 -0
  141. {cc_transcript-14.4.0 → cc_transcript-14.6.0}/rust/crates/py/src/sqlite.rs +0 -0
  142. {cc_transcript-14.4.0 → cc_transcript-14.6.0}/rust/crates/py/src/toolcall.rs +0 -0
  143. {cc_transcript-14.4.0 → cc_transcript-14.6.0}/rust/crates/py/src/views/attachment.rs +0 -0
  144. {cc_transcript-14.4.0 → cc_transcript-14.6.0}/rust/crates/py/src/views/blocks.rs +0 -0
  145. {cc_transcript-14.4.0 → cc_transcript-14.6.0}/rust/crates/py/src/views/convert.rs +0 -0
  146. {cc_transcript-14.4.0 → cc_transcript-14.6.0}/rust/crates/py/src/views/dunder.rs +0 -0
  147. {cc_transcript-14.4.0 → cc_transcript-14.6.0}/rust/crates/py/src/views/events.rs +0 -0
  148. {cc_transcript-14.4.0 → cc_transcript-14.6.0}/rust/crates/py/src/views/meta.rs +0 -0
  149. {cc_transcript-14.4.0 → cc_transcript-14.6.0}/rust/crates/py/src/views/print.rs +0 -0
  150. {cc_transcript-14.4.0 → cc_transcript-14.6.0}/rust/crates/py/src/views/store.rs +0 -0
  151. {cc_transcript-14.4.0 → cc_transcript-14.6.0}/rust/crates/py/src/views/system.rs +0 -0
  152. {cc_transcript-14.4.0 → cc_transcript-14.6.0}/rust/crates/py/src/views/toolcall.rs +0 -0
  153. {cc_transcript-14.4.0 → cc_transcript-14.6.0}/rust/crates/py/src/views/toolresult.rs +0 -0
  154. {cc_transcript-14.4.0 → cc_transcript-14.6.0}/rust/crates/py/src/views/transcript.rs +0 -0
  155. {cc_transcript-14.4.0 → cc_transcript-14.6.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.4.0
3
+ Version: 14.6.0
4
4
  Classifier: Development Status :: 3 - Alpha
5
5
  Classifier: Environment :: Console
6
6
  Classifier: Intended Audience :: Developers
@@ -87,6 +87,7 @@ EXPORTS: dict[str, str] = {
87
87
  "CommandLineQuery",
88
88
  "Occurrence",
89
89
  "Redirect",
90
+ "Word",
90
91
  "command_prefixes",
91
92
  "parse_command_line",
92
93
  ),
@@ -33,6 +33,7 @@ from cc_transcript.command import (
33
33
  CommandLineQuery as CommandLineQuery,
34
34
  Occurrence as Occurrence,
35
35
  Redirect as Redirect,
36
+ Word as Word,
36
37
  command_prefixes as command_prefixes,
37
38
  parse_command_line as parse_command_line,
38
39
  )
@@ -421,6 +422,7 @@ __all__ = [
421
422
  "UserEvent",
422
423
  "WatchEvent",
423
424
  "Watcher",
425
+ "Word",
424
426
  "WorkflowCall",
425
427
  "WriteCall",
426
428
  "WriteResult",
@@ -1,6 +1,7 @@
1
1
  # This file is automatically generated by pyo3_stub_gen
2
2
  # ruff: noqa: E501, F401, F403, F405
3
3
 
4
+ import asyncio
4
5
  import builtins
5
6
  from cc_transcript import command
6
7
  from cc_transcript import ids
@@ -94,6 +95,7 @@ __all__ = [
94
95
  "Usage",
95
96
  "UserEvent",
96
97
  "WatchTailer",
98
+ "Word",
97
99
  "WorkflowCall",
98
100
  "WriteCall",
99
101
  "WriteResult",
@@ -410,6 +412,11 @@ class Command:
410
412
  @property
411
413
  def span(self) -> typing.Optional[tuple[builtins.int, builtins.int]]: ...
412
414
  @property
415
+ def words(self) -> tuple[Word, ...]:
416
+ r"""
417
+ The structural words: ``words[0]`` is the executable, ``words[1:]`` parallel ``args``.
418
+ """
419
+ @property
413
420
  def argv(self) -> tuple[str, ...]: ...
414
421
  @property
415
422
  def program(self) -> builtins.str: ...
@@ -469,6 +476,11 @@ class CommandLine:
469
476
  def parse(cls, raw: builtins.str) -> CommandLine: ...
470
477
  @staticmethod
471
478
  def dequote(text: builtins.str) -> builtins.str: ...
479
+ @staticmethod
480
+ def quote(text: builtins.str) -> builtins.str:
481
+ r"""
482
+ Quote ``text`` as one POSIX-sh word, with Python ``shlex.quote`` semantics.
483
+ """
472
484
  def splice(self, replacements: typing.Mapping[builtins.int, builtins.str]) -> builtins.str: ...
473
485
  def rewrite_occurrences(self, to: collections.abc.Callable[[Occurrence], str | None]) -> typing.Optional[builtins.str]: ...
474
486
  def __iter__(self) -> collections.abc.Iterator[Command]: ...
@@ -1086,7 +1098,30 @@ class Occurrence:
1086
1098
  def next_op(self) -> typing.Optional[builtins.str]: ...
1087
1099
  @property
1088
1100
  def piped(self) -> builtins.bool: ...
1101
+ @property
1102
+ def nesting(self) -> builtins.int:
1103
+ r"""
1104
+ Substitution/payload hops below top level; 0 for a top-level command.
1105
+ """
1106
+ @property
1107
+ def host(self) -> Occurrence | None:
1108
+ r"""
1109
+ The occurrence hosting this nested part, or None at top level.
1110
+ """
1111
+ @property
1112
+ def quote_contexts(self) -> tuple[str, ...]:
1113
+ r"""
1114
+ Enclosing payload quote layers outermost-first: ``"'"``, ``'"'``, or ``""`` (bare).
1115
+ """
1089
1116
  def __new__(cls, line: CommandLine, index: builtins.int) -> Occurrence: ...
1117
+ def embeddable(self, text: builtins.str) -> builtins.bool:
1118
+ r"""
1119
+ Whether splicing ``text`` at this occurrence survives every enclosing quote layer.
1120
+ """
1121
+ def quote_for(self, text: builtins.str) -> typing.Optional[builtins.str]:
1122
+ r"""
1123
+ One shell-word spelling of ``text`` that survives every enclosing layer, or None.
1124
+ """
1090
1125
 
1091
1126
  @typing.final
1092
1127
  class OtherAttachment:
@@ -1447,37 +1482,40 @@ class Redirect:
1447
1482
  @typing.final
1448
1483
  class RustCorrectionLog:
1449
1484
  def __new__(cls, path: builtins.str) -> RustCorrectionLog: ...
1450
- def append(self, ts_ms: builtins.int, session_id: builtins.str, source: builtins.str, anchor_uuid: builtins.str, incorrect_digest: typing.Optional[builtins.str], incorrect_file: builtins.str, incorrect_old: builtins.str, incorrect_new: builtins.str, correction_origin: typing.Optional[builtins.str], correction_file: typing.Optional[builtins.str], correction_old: typing.Optional[builtins.str], correction_new: typing.Optional[builtins.str], correction_commit: typing.Optional[builtins.str], correction_text: typing.Optional[builtins.str], overlap: builtins.float, detail: typing.Any) -> None: ...
1451
- def for_session(self, session_id: builtins.str) -> list[dict[str, typing.Any]]: ...
1452
- def for_repo(self, repo: builtins.str) -> list[dict[str, typing.Any]]: ...
1453
- def since(self, ts_ms: builtins.int, source: typing.Optional[builtins.str] = None) -> list[dict[str, typing.Any]]: ...
1454
- def for_anchor(self, session_id: builtins.str, anchor_uuid: builtins.str) -> list[dict[str, typing.Any]]: ...
1455
- def by_digest(self, session_id: builtins.str, incorrect_digest: builtins.str) -> list[dict[str, typing.Any]]: ...
1456
- def sql(self, statement: builtins.str) -> list[dict[str, typing.Any]]: ...
1485
+ def open(self) -> asyncio.Future[None]: ...
1486
+ def close(self) -> asyncio.Future[None]: ...
1487
+ def append(self, ts_ms: builtins.int, session_id: builtins.str, source: builtins.str, anchor_uuid: builtins.str, incorrect_digest: typing.Optional[builtins.str], incorrect_file: builtins.str, incorrect_old: builtins.str, incorrect_new: builtins.str, correction_origin: typing.Optional[builtins.str], correction_file: typing.Optional[builtins.str], correction_old: typing.Optional[builtins.str], correction_new: typing.Optional[builtins.str], correction_commit: typing.Optional[builtins.str], correction_text: typing.Optional[builtins.str], overlap: builtins.float, detail: typing.Any) -> asyncio.Future[None]: ...
1488
+ def for_session(self, session_id: builtins.str) -> asyncio.Future[list[dict[str, typing.Any]]]: ...
1489
+ def for_repo(self, repo: builtins.str) -> asyncio.Future[list[dict[str, typing.Any]]]: ...
1490
+ def since(self, ts_ms: builtins.int, source: typing.Optional[builtins.str] = None) -> asyncio.Future[list[dict[str, typing.Any]]]: ...
1491
+ def for_anchor(self, session_id: builtins.str, anchor_uuid: builtins.str) -> asyncio.Future[list[dict[str, typing.Any]]]: ...
1492
+ def by_digest(self, session_id: builtins.str, incorrect_digest: builtins.str) -> asyncio.Future[list[dict[str, typing.Any]]]: ...
1493
+ def sql(self, statement: builtins.str) -> asyncio.Future[list[dict[str, typing.Any]]]: ...
1457
1494
 
1458
1495
  @typing.final
1459
1496
  class RustFeedbackStore:
1460
1497
  def __new__(cls, path: builtins.str, extra_ddl: typing.Sequence[builtins.str], event_columns: typing.Sequence[builtins.str], migrations: typing.Sequence[tuple[builtins.str, builtins.str, builtins.str, typing.Optional[builtins.str]]], verdict_table: builtins.str, accepted_column: builtins.str, summary_column: builtins.str, event_filter: typing.Optional[builtins.str], readonly: builtins.bool = False, busy_timeout_ms: typing.Optional[builtins.int] = None) -> RustFeedbackStore: ...
1461
- def close(self) -> None: ...
1462
- def sql(self, statement: builtins.str, params: typing.Optional[typing.Sequence[typing.Any]] = None) -> list[dict[str, typing.Any]]: ...
1463
- def execute(self, statement: builtins.str, params: typing.Optional[typing.Sequence[typing.Any]] = None) -> builtins.int: ...
1464
- def executemany(self, statement: builtins.str, seq: typing.Sequence[typing.Sequence[typing.Any]]) -> builtins.int: ...
1465
- def executescript(self, script: builtins.str) -> None: ...
1466
- def last_insert_rowid(self) -> builtins.int: ...
1467
- def load_extension(self, path: builtins.str) -> None: ...
1468
- def begin_immediate(self) -> None: ...
1469
- def commit(self) -> None: ...
1470
- def rollback(self) -> None: ...
1471
- def insert_candidates(self, rows: typing.Sequence[typing.Sequence[typing.Any]], extras: typing.Optional[typing.Sequence[typing.Sequence[typing.Any]]] = None) -> builtins.list[builtins.str]: ...
1472
- def record_file(self, path: builtins.str, mtime: builtins.float) -> None: ...
1473
- def file_mtimes(self) -> list[dict[str, typing.Any]]: ...
1474
- def stats(self) -> tuple[int, int, list[dict[str, typing.Any]]]: ...
1475
- def recent(self, source_kind: typing.Optional[builtins.str] = None, limit: builtins.int = 20) -> list[dict[str, typing.Any]]: ...
1476
- def events(self, source_kind: typing.Optional[builtins.str] = None) -> list[dict[str, typing.Any]]: ...
1477
- def dedup_keys(self) -> builtins.list[builtins.str]: ...
1478
- def record_verdict(self, dedup_key: builtins.str, role: builtins.str, prompt_version: builtins.int, model: builtins.str, category: builtins.str, accepted: builtins.bool, summary: builtins.str, confidence: builtins.float, rationale: builtins.str, canonical_key: typing.Optional[builtins.str], fidelity: builtins.str, judged_at: builtins.str) -> builtins.bool: ...
1479
- def unjudged(self, role: builtins.str, prompt_version: builtins.int, refresh_summary: builtins.bool = False, limit: typing.Optional[builtins.int] = None, offset: typing.Optional[builtins.int] = None) -> list[dict[str, typing.Any]]: ...
1480
- def judged(self, role: builtins.str, prompt_version: builtins.int) -> list[dict[str, typing.Any]]: ...
1498
+ def open(self) -> asyncio.Future[None]: ...
1499
+ def close(self) -> asyncio.Future[None]: ...
1500
+ def sql(self, statement: builtins.str, params: typing.Optional[typing.Sequence[typing.Any]] = None) -> asyncio.Future[list[dict[str, typing.Any]]]: ...
1501
+ def execute(self, statement: builtins.str, params: typing.Optional[typing.Sequence[typing.Any]] = None) -> asyncio.Future[int]: ...
1502
+ def executemany(self, statement: builtins.str, seq: typing.Sequence[typing.Sequence[typing.Any]]) -> asyncio.Future[int]: ...
1503
+ def executescript(self, script: builtins.str) -> asyncio.Future[None]: ...
1504
+ def last_insert_rowid(self) -> asyncio.Future[int]: ...
1505
+ def load_extension(self, path: builtins.str) -> asyncio.Future[None]: ...
1506
+ def begin_immediate(self) -> asyncio.Future[None]: ...
1507
+ def commit(self) -> asyncio.Future[None]: ...
1508
+ def rollback(self) -> asyncio.Future[None]: ...
1509
+ def insert_candidates(self, rows: typing.Sequence[typing.Sequence[typing.Any]], extras: typing.Optional[typing.Sequence[typing.Sequence[typing.Any]]] = None) -> asyncio.Future[list[str]]: ...
1510
+ def record_file(self, path: builtins.str, mtime: builtins.float) -> asyncio.Future[None]: ...
1511
+ def file_mtimes(self) -> asyncio.Future[list[dict[str, typing.Any]]]: ...
1512
+ def stats(self) -> asyncio.Future[tuple[int, int, list[dict[str, typing.Any]]]]: ...
1513
+ def recent(self, source_kind: typing.Optional[builtins.str] = None, limit: builtins.int = 20) -> asyncio.Future[list[dict[str, typing.Any]]]: ...
1514
+ def events(self, source_kind: typing.Optional[builtins.str] = None) -> asyncio.Future[list[dict[str, typing.Any]]]: ...
1515
+ def dedup_keys(self) -> asyncio.Future[list[str]]: ...
1516
+ def record_verdict(self, dedup_key: builtins.str, role: builtins.str, prompt_version: builtins.int, model: builtins.str, category: builtins.str, accepted: builtins.bool, summary: builtins.str, confidence: builtins.float, rationale: builtins.str, canonical_key: typing.Optional[builtins.str], fidelity: builtins.str, judged_at: builtins.str) -> asyncio.Future[bool]: ...
1517
+ def unjudged(self, role: builtins.str, prompt_version: builtins.int, refresh_summary: builtins.bool = False, limit: typing.Optional[builtins.int] = None, offset: typing.Optional[builtins.int] = None) -> asyncio.Future[list[dict[str, typing.Any]]]: ...
1518
+ def judged(self, role: builtins.str, prompt_version: builtins.int) -> asyncio.Future[list[dict[str, typing.Any]]]: ...
1481
1519
 
1482
1520
  @typing.final
1483
1521
  class ServerToolUse:
@@ -2059,6 +2097,27 @@ class WatchTailer:
2059
2097
  size, mtime, session_id, seen}}}`, the Python `TailState` projection.
2060
2098
  """
2061
2099
 
2100
+ @typing.final
2101
+ class Word:
2102
+ r"""
2103
+ One structural word of a parsed command (``Command.words``).
2104
+ Attributes:
2105
+ raw: verbatim source text.
2106
+ value: literal with quotes/escapes removed; None when an expansion taints it.
2107
+ span: byte span in the owning ``CommandLine.raw``; None without verbatim source.
2108
+ expandable: bash would glob/brace/tilde-expand despite the literal value.
2109
+ """
2110
+ __match_args__: typing.ClassVar[tuple[str, ...]]
2111
+ @property
2112
+ def raw(self) -> builtins.str: ...
2113
+ @property
2114
+ def value(self) -> typing.Optional[builtins.str]: ...
2115
+ @property
2116
+ def span(self) -> typing.Optional[tuple[builtins.int, builtins.int]]: ...
2117
+ @property
2118
+ def expandable(self) -> builtins.bool: ...
2119
+ def __new__(cls, raw: builtins.str, value: typing.Optional[builtins.str] = None, span: typing.Optional[tuple[builtins.int, builtins.int]] = None, expandable: builtins.bool = False) -> Word: ...
2120
+
2062
2121
  @typing.final
2063
2122
  class WorkflowCall(ToolCallBase):
2064
2123
  r"""
@@ -2158,7 +2217,7 @@ def discovery_subagent_paths(path: builtins.str | os.PathLike | pathlib.Path) ->
2158
2217
 
2159
2218
  def discovery_subagent_transcripts(path: builtins.str | os.PathLike | pathlib.Path) -> dict[str, pathlib.Path]: ...
2160
2219
 
2161
- def embedded_literals() -> dict[str, str | float | list[str]]: ...
2220
+ def embedded_literals() -> dict[str, str | float | int | list[str]]: ...
2162
2221
 
2163
2222
  def expand_tool_names(spec: builtins.str) -> frozenset[str]:
2164
2223
  r"""
@@ -18,6 +18,10 @@ from cc_transcript._native import CommandLine as CommandLine
18
18
  from cc_transcript._native import CommandLineQuery as CommandLineQuery
19
19
  from cc_transcript._native import Occurrence as Occurrence
20
20
  from cc_transcript._native import Redirect as Redirect
21
+ from cc_transcript._native import Word as Word
22
+ from cc_transcript.literals import literal_int
23
+
24
+ PAYLOAD_DEPTH_LIMIT: int = literal_int("command.PAYLOAD_DEPTH_LIMIT")
21
25
 
22
26
  parse_command_line = lru_cache(maxsize=4096)(CommandLine.parse)
23
27
 
@@ -26,6 +26,7 @@ from cc_transcript.literals import literal_str
26
26
 
27
27
  if TYPE_CHECKING:
28
28
  from collections.abc import Mapping
29
+ from types import TracebackType
29
30
  from typing import Any
30
31
 
31
32
  from cc_transcript.ids import EventUuid, SessionId, ToolDigest
@@ -93,16 +94,16 @@ class CorrectionLog:
93
94
  the module docstring).
94
95
 
95
96
  Example:
96
- >>> log = CorrectionLog.open()
97
- >>> log.append(correction)
98
- >>> log.by_digest(session_id, incorrect_digest=digest)
97
+ >>> async with await CorrectionLog.open() as log:
98
+ ... await log.append(correction)
99
+ ... await log.by_digest(session_id, incorrect_digest=digest)
99
100
  """
100
101
 
101
102
  def __init__(self, engine: _native.RustCorrectionLog) -> None:
102
103
  self._engine = engine
103
104
 
104
105
  @classmethod
105
- def open(cls, path: Path | None = None) -> Self:
106
+ async def open(cls, path: Path | None = None) -> Self:
106
107
  """Opens (creating if needed) the ledger at ``path``.
107
108
 
108
109
  Args:
@@ -113,17 +114,37 @@ class CorrectionLog:
113
114
  The opened log.
114
115
  """
115
116
  path = path or Path.home() / ".cc-transcript" / "corrections.db"
116
- path.parent.mkdir(parents=True, exist_ok=True)
117
- return cls(_native.RustCorrectionLog(str(path)))
118
-
119
- def append(self, record: Correction) -> None:
117
+ engine = _native.RustCorrectionLog(str(path))
118
+ try:
119
+ await engine.open()
120
+ except BaseException:
121
+ await engine.close()
122
+ raise
123
+ return cls(engine)
124
+
125
+ async def close(self) -> None:
126
+ """Closes the underlying connection; a second close is a no-op."""
127
+ await self._engine.close()
128
+
129
+ async def __aenter__(self) -> Self:
130
+ return self
131
+
132
+ async def __aexit__(
133
+ self,
134
+ exc_type: type[BaseException] | None,
135
+ exc: BaseException | None,
136
+ tb: TracebackType | None,
137
+ ) -> None:
138
+ await self.close()
139
+
140
+ async def append(self, record: Correction) -> None:
120
141
  """Appends ``record`` as a single ``INSERT OR IGNORE``.
121
142
 
122
143
  Idempotent on the table's UNIQUE key, so re-running a writer writes one
123
144
  row; SQLite treats NULL key columns as distinct, so rows whose key
124
145
  carries a NULL rely on the writer not repeating the same values.
125
146
  """
126
- self._engine.append(
147
+ await self._engine.append(
127
148
  record.ts_ms,
128
149
  record.session_id,
129
150
  record.source,
@@ -142,39 +163,39 @@ class CorrectionLog:
142
163
  record.detail,
143
164
  )
144
165
 
145
- def for_session(self, session_id: SessionId) -> tuple[Correction, ...]:
166
+ async def for_session(self, session_id: SessionId) -> tuple[Correction, ...]:
146
167
  """All records for ``session_id``, ordered by timestamp."""
147
- return tuple(Correction(**row) for row in self._engine.for_session(session_id))
168
+ return tuple(Correction(**row) for row in await self._engine.for_session(session_id))
148
169
 
149
- def for_repo(self, repo: str) -> tuple[Correction, ...]:
170
+ async def for_repo(self, repo: str) -> tuple[Correction, ...]:
150
171
  """All corrections whose ``detail.repo`` is ``repo``, ordered by timestamp.
151
172
 
152
173
  The repo key producers stamp into ``detail`` so a per-repo consumer (the
153
174
  captain-hook reviewer) can pull every correction for its repo at once.
154
175
  """
155
- return tuple(Correction(**row) for row in self._engine.for_repo(repo))
176
+ return tuple(Correction(**row) for row in await self._engine.for_repo(repo))
156
177
 
157
- def since(self, ts_ms: int, *, source: str | None = None) -> tuple[Correction, ...]:
178
+ async def since(self, ts_ms: int, *, source: str | None = None) -> tuple[Correction, ...]:
158
179
  """Corrections with ``ts_ms`` strictly greater than ``ts_ms``, oldest first.
159
180
 
160
181
  A cursor read for incremental consumers; pass ``source`` to scope to one
161
182
  producer.
162
183
  """
163
- return tuple(Correction(**row) for row in self._engine.since(ts_ms, source))
184
+ return tuple(Correction(**row) for row in await self._engine.since(ts_ms, source))
164
185
 
165
- def for_anchor(self, session_id: SessionId, anchor_uuid: EventUuid) -> tuple[Correction, ...]:
186
+ async def for_anchor(self, session_id: SessionId, anchor_uuid: EventUuid) -> tuple[Correction, ...]:
166
187
  """The corrections harvested around one feedback ``anchor_uuid``."""
167
- return tuple(Correction(**row) for row in self._engine.for_anchor(session_id, anchor_uuid))
188
+ return tuple(Correction(**row) for row in await self._engine.for_anchor(session_id, anchor_uuid))
168
189
 
169
- def by_digest(self, session_id: SessionId, *, incorrect_digest: ToolDigest) -> tuple[Correction, ...]:
190
+ async def by_digest(self, session_id: SessionId, *, incorrect_digest: ToolDigest) -> tuple[Correction, ...]:
170
191
  """Corrections of the tool call with ``incorrect_digest`` in ``session_id``.
171
192
 
172
193
  The cross-consumer join: pass the ``tool_digest`` a hook recorded in
173
194
  the ``decisions`` ledger to learn whether that exact edit was later
174
195
  corrected.
175
196
  """
176
- return tuple(Correction(**row) for row in self._engine.by_digest(session_id, incorrect_digest))
197
+ return tuple(Correction(**row) for row in await self._engine.by_digest(session_id, incorrect_digest))
177
198
 
178
- def sql(self, statement: str) -> list[dict[str, Any]]:
199
+ async def sql(self, statement: str) -> list[dict[str, Any]]:
179
200
  """Runs one raw SQL ``statement`` — the escape hatch behind ``corrections sql``."""
180
- return self._engine.sql(statement)
201
+ return await self._engine.sql(statement)
@@ -14,7 +14,7 @@ import sqlite3
14
14
  from dataclasses import dataclass, field
15
15
  from typing import TYPE_CHECKING, Literal
16
16
 
17
- from cc_transcript.ledger import SyncLedger
17
+ from cc_transcript.ledger import AsyncLedger
18
18
 
19
19
  if TYPE_CHECKING:
20
20
  from collections.abc import Mapping
@@ -87,7 +87,7 @@ class Decision:
87
87
  detail: Mapping[str, Any] = field(default_factory=dict)
88
88
 
89
89
 
90
- class DecisionLog(SyncLedger[Decision]):
90
+ class DecisionLog(AsyncLedger[Decision]):
91
91
  """The ``decisions`` ledger at ``~/.cc-transcript/decisions.db``.
92
92
 
93
93
  Opened in WAL mode with a busy timeout because cc-review's Go daemon
@@ -95,9 +95,10 @@ class DecisionLog(SyncLedger[Decision]):
95
95
  auto-dropped. Requires a local disk — WAL does not work over NFS.
96
96
 
97
97
  Example:
98
- >>> log = DecisionLog.open()
99
- >>> log.append(decision)
100
- >>> log.attribute_tool(session_id, tool_digest=digest, near_ts_ms=ts)
98
+ >>> log = await DecisionLog.open()
99
+ >>> async with log:
100
+ ... await log.append(decision)
101
+ ... await log.attribute_tool(session_id, tool_digest=digest, near_ts_ms=ts)
101
102
  """
102
103
 
103
104
  DDL = DECISIONS_DDL
@@ -134,7 +135,7 @@ class DecisionLog(SyncLedger[Decision]):
134
135
  detail=json.loads(row["detail_json"]),
135
136
  )
136
137
 
137
- def attribute_tool(
138
+ async def attribute_tool(
138
139
  self, session_id: SessionId, *, tool_digest: ToolDigest, near_ts_ms: int, window_ms: int = 300_000
139
140
  ) -> Decision | None:
140
141
  """The nearest decision preceding ``near_ts_ms`` with this digest.
@@ -148,14 +149,17 @@ class DecisionLog(SyncLedger[Decision]):
148
149
  The matching decision, or None when no digest-equal row lies in
149
150
  ``[near_ts_ms - window_ms, near_ts_ms]``.
150
151
  """
151
- row = self.conn.execute(
152
- "SELECT * FROM decisions WHERE session_id = ? AND tool_digest = ? AND ts_ms BETWEEN ? AND ?"
153
- " ORDER BY ts_ms DESC, id DESC LIMIT 1",
154
- (session_id, tool_digest, near_ts_ms - window_ms, near_ts_ms),
155
- ).fetchone()
152
+ conn = self._actor.conn
153
+ row = await self._actor.run(
154
+ lambda: conn.execute(
155
+ "SELECT * FROM decisions WHERE session_id = ? AND tool_digest = ? AND ts_ms BETWEEN ? AND ?"
156
+ " ORDER BY ts_ms DESC, id DESC LIMIT 1",
157
+ (session_id, tool_digest, near_ts_ms - window_ms, near_ts_ms),
158
+ ).fetchone()
159
+ )
156
160
  return self.row_to_record(row) if row else None
157
161
 
158
- def attribute_nearest(
162
+ async def attribute_nearest(
159
163
  self,
160
164
  session_id: SessionId,
161
165
  *,
@@ -175,10 +179,13 @@ class DecisionLog(SyncLedger[Decision]):
175
179
  The nearest matching decision, or None when none lies in
176
180
  ``[near_ts_ms - window_ms, near_ts_ms + window_ms]``.
177
181
  """
182
+ conn = self._actor.conn
178
183
  kind_clause, kind_params = ("AND kind = ? ", (kind,)) if kind is not None else ("", ())
179
- row = self.conn.execute(
180
- f"SELECT * FROM decisions WHERE session_id = ? AND event = ? {kind_clause}AND ts_ms BETWEEN ? AND ?"
181
- " ORDER BY ABS(ts_ms - ?), id DESC LIMIT 1",
182
- (session_id, event, *kind_params, near_ts_ms - window_ms, near_ts_ms + window_ms, near_ts_ms),
183
- ).fetchone()
184
+ row = await self._actor.run(
185
+ lambda: conn.execute(
186
+ f"SELECT * FROM decisions WHERE session_id = ? AND event = ? {kind_clause}AND ts_ms BETWEEN ? AND ?"
187
+ " ORDER BY ABS(ts_ms - ?), id DESC LIMIT 1",
188
+ (session_id, event, *kind_params, near_ts_ms - window_ms, near_ts_ms + window_ms, near_ts_ms),
189
+ ).fetchone()
190
+ )
184
191
  return self.row_to_record(row) if row else None
@@ -199,7 +199,7 @@ def run_git(repo: Path, *args: str) -> str | None:
199
199
  return proc.stdout if proc.returncode == 0 else None
200
200
 
201
201
 
202
- def record_harvest(
202
+ async def record_harvest(
203
203
  log: CorrectionLog,
204
204
  activity: SessionActivity,
205
205
  anchor: EventRef,
@@ -219,7 +219,7 @@ def record_harvest(
219
219
  """
220
220
  rows = [correction for pair in pairs if (correction := lower_pair(activity, anchor, pair, source=source))]
221
221
  for row in rows:
222
- log.append(row)
222
+ await log.append(row)
223
223
  return len(rows)
224
224
 
225
225
 
@@ -163,7 +163,7 @@ async def extract_correction(
163
163
  already has a row, so re-runs and overlapping producers never duplicate.
164
164
  Returns the appended correction, or None when nothing is harvested or picked.
165
165
  """
166
- if log.for_anchor(anchor.session_id, anchor.event_uuid):
166
+ if await log.for_anchor(anchor.session_id, anchor.event_uuid):
167
167
  return None
168
168
  pairs = await asyncio.to_thread(harvest_pairs, activity, anchor, repo=repo)
169
169
  if not pairs or (turn := activity.turn_of(anchor)) is None:
@@ -173,5 +173,5 @@ async def extract_correction(
173
173
  return None
174
174
  if repo is not None:
175
175
  row = replace(row, detail={"repo": str(repo)})
176
- log.append(row)
176
+ await log.append(row)
177
177
  return row
@@ -12,15 +12,15 @@ most one row per ``(session, event)``, so the table stays small however hot the
12
12
 
13
13
  from __future__ import annotations
14
14
 
15
- import sqlite3
16
15
  from dataclasses import dataclass
17
- from pathlib import Path
18
16
  from typing import TYPE_CHECKING, Self
19
17
 
20
18
  from cc_transcript.decisions import DecisionLog
21
- from cc_transcript.ledger import open_sqlite
19
+ from cc_transcript.ledger import ConnectionActor, open_sqlite
22
20
 
23
21
  if TYPE_CHECKING:
22
+ from pathlib import Path
23
+
24
24
  from cc_transcript.ids import SessionId
25
25
 
26
26
  HEARTBEATS_DDL = """\
@@ -63,30 +63,42 @@ class HeartbeatLog:
63
63
  most one row per event. Requires a local disk — WAL does not work over NFS.
64
64
 
65
65
  Example:
66
- >>> log = HeartbeatLog.open()
67
- >>> log.beat(session_id, "PreToolUse", ts_ms)
68
- >>> log.for_session(session_id)
66
+ >>> log = await HeartbeatLog.open()
67
+ >>> async with log:
68
+ ... await log.beat(session_id, "PreToolUse", ts_ms)
69
+ ... await log.for_session(session_id)
69
70
  """
70
71
 
71
- def __init__(self, conn: sqlite3.Connection) -> None:
72
- self.conn = conn
72
+ def __init__(self, actor: ConnectionActor) -> None:
73
+ self._actor = actor
73
74
 
74
75
  @classmethod
75
- def open(cls, path: Path | None = None) -> Self:
76
+ async def open(cls, path: Path | None = None) -> Self:
76
77
  """Opens (creating if needed) the heartbeat table at ``path`` (defaults to ``decisions.db``)."""
77
- return cls(open_sqlite(path, filename=DecisionLog.FILENAME, ddl=HEARTBEATS_DDL))
78
+ actor = ConnectionActor()
79
+ await actor.start(lambda: open_sqlite(path, filename=DecisionLog.FILENAME, ddl=HEARTBEATS_DDL))
80
+ return cls(actor)
78
81
 
79
- def beat(self, session_id: SessionId, event: str, ts_ms: int) -> None:
82
+ async def beat(self, session_id: SessionId, event: str, ts_ms: int) -> None:
80
83
  """Records one dispatch of ``event`` in ``session_id`` at ``ts_ms`` (upsert; ``count += 1``)."""
81
- self.conn.execute(
82
- "INSERT INTO dispatch_heartbeats (session_id, event, first_ts_ms, last_ts_ms, count) "
83
- "VALUES (?, ?, ?, ?, 1) "
84
- "ON CONFLICT (session_id, event) DO UPDATE SET last_ts_ms = excluded.last_ts_ms, count = count + 1",
85
- (session_id, event, ts_ms, ts_ms),
84
+ conn = self._actor.conn
85
+ await self._actor.run(
86
+ lambda: conn.execute(
87
+ "INSERT INTO dispatch_heartbeats (session_id, event, first_ts_ms, last_ts_ms, count) "
88
+ "VALUES (?, ?, ?, ?, 1) "
89
+ "ON CONFLICT (session_id, event) DO UPDATE SET last_ts_ms = excluded.last_ts_ms, count = count + 1",
90
+ (session_id, event, ts_ms, ts_ms),
91
+ )
86
92
  )
87
93
 
88
- def for_session(self, session_id: SessionId) -> tuple[Heartbeat, ...]:
94
+ async def for_session(self, session_id: SessionId) -> tuple[Heartbeat, ...]:
89
95
  """Every ``(session, event)`` beat for ``session_id``, ordered by first dispatch."""
96
+ conn = self._actor.conn
97
+ rows = await self._actor.run(
98
+ lambda: conn.execute(
99
+ "SELECT * FROM dispatch_heartbeats WHERE session_id = ? ORDER BY first_ts_ms, event", (session_id,)
100
+ ).fetchall()
101
+ )
90
102
  return tuple(
91
103
  Heartbeat(
92
104
  session_id=row["session_id"],
@@ -95,7 +107,15 @@ class HeartbeatLog:
95
107
  last_ts_ms=row["last_ts_ms"],
96
108
  count=row["count"],
97
109
  )
98
- for row in self.conn.execute(
99
- "SELECT * FROM dispatch_heartbeats WHERE session_id = ? ORDER BY first_ts_ms, event", (session_id,)
100
- )
110
+ for row in rows
101
111
  )
112
+
113
+ async def close(self) -> None:
114
+ """Closes the underlying connection; a second call is a no-op."""
115
+ await self._actor.close()
116
+
117
+ async def __aenter__(self) -> Self:
118
+ return self
119
+
120
+ async def __aexit__(self, *exc: object) -> None:
121
+ await self.close()