cc-transcript 14.2.0__tar.gz → 14.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 (152) hide show
  1. {cc_transcript-14.2.0 → cc_transcript-14.5.0}/PKG-INFO +1 -1
  2. {cc_transcript-14.2.0 → cc_transcript-14.5.0}/cc_transcript/__init__.py +18 -0
  3. {cc_transcript-14.2.0 → cc_transcript-14.5.0}/cc_transcript/__init__.pyi +35 -0
  4. {cc_transcript-14.2.0 → cc_transcript-14.5.0}/cc_transcript/_native.pyi +83 -36
  5. {cc_transcript-14.2.0 → cc_transcript-14.5.0}/cc_transcript/corrections.py +42 -21
  6. {cc_transcript-14.2.0 → cc_transcript-14.5.0}/cc_transcript/decisions.py +24 -17
  7. {cc_transcript-14.2.0 → cc_transcript-14.5.0}/cc_transcript/evidence.py +2 -2
  8. {cc_transcript-14.2.0 → cc_transcript-14.5.0}/cc_transcript/extract/correct.py +2 -2
  9. {cc_transcript-14.2.0 → cc_transcript-14.5.0}/cc_transcript/heartbeats.py +40 -20
  10. {cc_transcript-14.2.0 → cc_transcript-14.5.0}/cc_transcript/judge/similar.py +27 -26
  11. cc_transcript-14.5.0/cc_transcript/ledger.py +163 -0
  12. {cc_transcript-14.2.0 → cc_transcript-14.5.0}/cc_transcript/mining/spec.py +23 -8
  13. {cc_transcript-14.2.0 → cc_transcript-14.5.0}/cc_transcript/mining/store.py +95 -74
  14. {cc_transcript-14.2.0 → cc_transcript-14.5.0}/cc_transcript/parser.py +30 -0
  15. cc_transcript-14.5.0/cc_transcript/synthetic.py +219 -0
  16. {cc_transcript-14.2.0 → cc_transcript-14.5.0}/cc_transcript/tools.py +27 -8
  17. {cc_transcript-14.2.0 → cc_transcript-14.5.0}/cc_transcript/watch.py +23 -10
  18. {cc_transcript-14.2.0 → cc_transcript-14.5.0}/pyproject.toml +1 -1
  19. cc_transcript-14.5.0/rust/crates/core/src/actor.rs +166 -0
  20. cc_transcript-14.5.0/rust/crates/core/src/codex/lower.rs +1342 -0
  21. cc_transcript-14.5.0/rust/crates/core/src/codex/mod.rs +12 -0
  22. cc_transcript-14.5.0/rust/crates/core/src/codex/parse.rs +1270 -0
  23. cc_transcript-14.5.0/rust/crates/core/src/codex/protocol.rs +129 -0
  24. cc_transcript-14.5.0/rust/crates/core/src/codex/types.rs +260 -0
  25. {cc_transcript-14.2.0 → cc_transcript-14.5.0}/rust/crates/core/src/lib.rs +3 -0
  26. {cc_transcript-14.2.0 → cc_transcript-14.5.0}/rust/crates/core/src/toolcall.rs +227 -18
  27. {cc_transcript-14.2.0 → cc_transcript-14.5.0}/rust/crates/core/src/types.rs +4 -4
  28. cc_transcript-14.5.0/rust/crates/py/src/actor_bridge.rs +135 -0
  29. cc_transcript-14.5.0/rust/crates/py/src/corrections.rs +260 -0
  30. cc_transcript-14.5.0/rust/crates/py/src/feedback.rs +405 -0
  31. {cc_transcript-14.2.0 → cc_transcript-14.5.0}/rust/crates/py/src/lib.rs +1 -0
  32. {cc_transcript-14.2.0 → cc_transcript-14.5.0}/rust/crates/py/src/mining.rs +3 -3
  33. {cc_transcript-14.2.0 → cc_transcript-14.5.0}/rust/crates/py/src/toolcall.rs +1 -8
  34. {cc_transcript-14.2.0 → cc_transcript-14.5.0}/rust/crates/py/src/views/toolcall.rs +110 -11
  35. cc_transcript-14.2.0/cc_transcript/ledger.py +0 -94
  36. cc_transcript-14.2.0/rust/crates/py/src/corrections.rs +0 -200
  37. cc_transcript-14.2.0/rust/crates/py/src/feedback.rs +0 -321
  38. {cc_transcript-14.2.0 → cc_transcript-14.5.0}/Cargo.lock +0 -0
  39. {cc_transcript-14.2.0 → cc_transcript-14.5.0}/Cargo.toml +0 -0
  40. {cc_transcript-14.2.0 → cc_transcript-14.5.0}/LICENSE +0 -0
  41. {cc_transcript-14.2.0 → cc_transcript-14.5.0}/README.md +0 -0
  42. {cc_transcript-14.2.0 → cc_transcript-14.5.0}/cc_transcript/activity.py +0 -0
  43. {cc_transcript-14.2.0 → cc_transcript-14.5.0}/cc_transcript/activity_probe.py +0 -0
  44. {cc_transcript-14.2.0 → cc_transcript-14.5.0}/cc_transcript/builders.py +0 -0
  45. {cc_transcript-14.2.0 → cc_transcript-14.5.0}/cc_transcript/command.py +0 -0
  46. {cc_transcript-14.2.0 → cc_transcript-14.5.0}/cc_transcript/context.py +0 -0
  47. {cc_transcript-14.2.0 → cc_transcript-14.5.0}/cc_transcript/cost.py +0 -0
  48. {cc_transcript-14.2.0 → cc_transcript-14.5.0}/cc_transcript/discovery.py +0 -0
  49. {cc_transcript-14.2.0 → cc_transcript-14.5.0}/cc_transcript/disktruth.py +0 -0
  50. {cc_transcript-14.2.0 → cc_transcript-14.5.0}/cc_transcript/extract/__init__.py +0 -0
  51. {cc_transcript-14.2.0 → cc_transcript-14.5.0}/cc_transcript/facts.py +0 -0
  52. {cc_transcript-14.2.0 → cc_transcript-14.5.0}/cc_transcript/filterspec.py +0 -0
  53. {cc_transcript-14.2.0 → cc_transcript-14.5.0}/cc_transcript/ids.py +0 -0
  54. {cc_transcript-14.2.0 → cc_transcript-14.5.0}/cc_transcript/judge/__init__.py +0 -0
  55. {cc_transcript-14.2.0 → cc_transcript-14.5.0}/cc_transcript/judge/llm.py +0 -0
  56. {cc_transcript-14.2.0 → cc_transcript-14.5.0}/cc_transcript/judge/verdicts.py +0 -0
  57. {cc_transcript-14.2.0 → cc_transcript-14.5.0}/cc_transcript/literals.py +0 -0
  58. {cc_transcript-14.2.0 → cc_transcript-14.5.0}/cc_transcript/mining/__init__.py +0 -0
  59. {cc_transcript-14.2.0 → cc_transcript-14.5.0}/cc_transcript/mining/candidates.py +0 -0
  60. {cc_transcript-14.2.0 → cc_transcript-14.5.0}/cc_transcript/mining/confidence.py +0 -0
  61. {cc_transcript-14.2.0 → cc_transcript-14.5.0}/cc_transcript/mining/engine.py +0 -0
  62. {cc_transcript-14.2.0 → cc_transcript-14.5.0}/cc_transcript/mining/formats.py +0 -0
  63. {cc_transcript-14.2.0 → cc_transcript-14.5.0}/cc_transcript/mining/sampling.py +0 -0
  64. {cc_transcript-14.2.0 → cc_transcript-14.5.0}/cc_transcript/mining/signals.py +0 -0
  65. {cc_transcript-14.2.0 → cc_transcript-14.5.0}/cc_transcript/mining/sourcekind.py +0 -0
  66. {cc_transcript-14.2.0 → cc_transcript-14.5.0}/cc_transcript/models.py +0 -0
  67. {cc_transcript-14.2.0 → cc_transcript-14.5.0}/cc_transcript/nlp.py +0 -0
  68. {cc_transcript-14.2.0 → cc_transcript-14.5.0}/cc_transcript/notifications.py +0 -0
  69. {cc_transcript-14.2.0 → cc_transcript-14.5.0}/cc_transcript/py.typed +0 -0
  70. {cc_transcript-14.2.0 → cc_transcript-14.5.0}/cc_transcript/query.py +0 -0
  71. {cc_transcript-14.2.0 → cc_transcript-14.5.0}/cc_transcript/render.py +0 -0
  72. {cc_transcript-14.2.0 → cc_transcript-14.5.0}/cc_transcript/sentiment/__init__.py +0 -0
  73. {cc_transcript-14.2.0 → cc_transcript-14.5.0}/cc_transcript/sentiment/buckets.py +0 -0
  74. {cc_transcript-14.2.0 → cc_transcript-14.5.0}/cc_transcript/sentiment/data/afinn-en-165.tsv +0 -0
  75. {cc_transcript-14.2.0 → cc_transcript-14.5.0}/cc_transcript/sentiment/data/domain_overrides.tsv +0 -0
  76. {cc_transcript-14.2.0 → cc_transcript-14.5.0}/cc_transcript/sentiment/data/en-ewt.udpipe +0 -0
  77. {cc_transcript-14.2.0 → cc_transcript-14.5.0}/cc_transcript/sentiment/engine.py +0 -0
  78. {cc_transcript-14.2.0 → cc_transcript-14.5.0}/cc_transcript/sentiment/lexicon.py +0 -0
  79. {cc_transcript-14.2.0 → cc_transcript-14.5.0}/cc_transcript/sentiment/scorespec.py +0 -0
  80. {cc_transcript-14.2.0 → cc_transcript-14.5.0}/rust/crates/cli/Cargo.toml +0 -0
  81. {cc_transcript-14.2.0 → cc_transcript-14.5.0}/rust/crates/cli/src/commands/corrections.rs +0 -0
  82. {cc_transcript-14.2.0 → cc_transcript-14.5.0}/rust/crates/cli/src/commands/digest.rs +0 -0
  83. {cc_transcript-14.2.0 → cc_transcript-14.5.0}/rust/crates/cli/src/commands/facts.rs +0 -0
  84. {cc_transcript-14.2.0 → cc_transcript-14.5.0}/rust/crates/cli/src/commands/grep.rs +0 -0
  85. {cc_transcript-14.2.0 → cc_transcript-14.5.0}/rust/crates/cli/src/commands/list.rs +0 -0
  86. {cc_transcript-14.2.0 → cc_transcript-14.5.0}/rust/crates/cli/src/commands/mod.rs +0 -0
  87. {cc_transcript-14.2.0 → cc_transcript-14.5.0}/rust/crates/cli/src/commands/scratchpad.rs +0 -0
  88. {cc_transcript-14.2.0 → cc_transcript-14.5.0}/rust/crates/cli/src/commands/show.rs +0 -0
  89. {cc_transcript-14.2.0 → cc_transcript-14.5.0}/rust/crates/cli/src/commands/slice.rs +0 -0
  90. {cc_transcript-14.2.0 → cc_transcript-14.5.0}/rust/crates/cli/src/commands/stats.rs +0 -0
  91. {cc_transcript-14.2.0 → cc_transcript-14.5.0}/rust/crates/cli/src/commands/watch.rs +0 -0
  92. {cc_transcript-14.2.0 → cc_transcript-14.5.0}/rust/crates/cli/src/lib.rs +0 -0
  93. {cc_transcript-14.2.0 → cc_transcript-14.5.0}/rust/crates/cli/src/main.rs +0 -0
  94. {cc_transcript-14.2.0 → cc_transcript-14.5.0}/rust/crates/cli/src/output.rs +0 -0
  95. {cc_transcript-14.2.0 → cc_transcript-14.5.0}/rust/crates/cli/src/target.rs +0 -0
  96. {cc_transcript-14.2.0 → cc_transcript-14.5.0}/rust/crates/core/Cargo.toml +0 -0
  97. {cc_transcript-14.2.0 → cc_transcript-14.5.0}/rust/crates/core/benches/parse.rs +0 -0
  98. {cc_transcript-14.2.0 → cc_transcript-14.5.0}/rust/crates/core/benches/render.rs +0 -0
  99. {cc_transcript-14.2.0 → cc_transcript-14.5.0}/rust/crates/core/benches/toolcall.rs +0 -0
  100. {cc_transcript-14.2.0 → cc_transcript-14.5.0}/rust/crates/core/src/activity.rs +0 -0
  101. {cc_transcript-14.2.0 → cc_transcript-14.5.0}/rust/crates/core/src/buckets.rs +0 -0
  102. {cc_transcript-14.2.0 → cc_transcript-14.5.0}/rust/crates/core/src/command.rs +0 -0
  103. {cc_transcript-14.2.0 → cc_transcript-14.5.0}/rust/crates/core/src/context.rs +0 -0
  104. {cc_transcript-14.2.0 → cc_transcript-14.5.0}/rust/crates/core/src/corrections.rs +0 -0
  105. {cc_transcript-14.2.0 → cc_transcript-14.5.0}/rust/crates/core/src/cost.rs +0 -0
  106. {cc_transcript-14.2.0 → cc_transcript-14.5.0}/rust/crates/core/src/discovery.rs +0 -0
  107. {cc_transcript-14.2.0 → cc_transcript-14.5.0}/rust/crates/core/src/facts.rs +0 -0
  108. {cc_transcript-14.2.0 → cc_transcript-14.5.0}/rust/crates/core/src/feedback.rs +0 -0
  109. {cc_transcript-14.2.0 → cc_transcript-14.5.0}/rust/crates/core/src/filter.rs +0 -0
  110. {cc_transcript-14.2.0 → cc_transcript-14.5.0}/rust/crates/core/src/ids.rs +0 -0
  111. {cc_transcript-14.2.0 → cc_transcript-14.5.0}/rust/crates/core/src/literals/command.rs +0 -0
  112. {cc_transcript-14.2.0 → cc_transcript-14.5.0}/rust/crates/core/src/literals/corrections.rs +0 -0
  113. {cc_transcript-14.2.0 → cc_transcript-14.5.0}/rust/crates/core/src/literals/feedback.rs +0 -0
  114. {cc_transcript-14.2.0 → cc_transcript-14.5.0}/rust/crates/core/src/literals/mining.rs +0 -0
  115. {cc_transcript-14.2.0 → cc_transcript-14.5.0}/rust/crates/core/src/literals/mod.rs +0 -0
  116. {cc_transcript-14.2.0 → cc_transcript-14.5.0}/rust/crates/core/src/literals/protocol.rs +0 -0
  117. {cc_transcript-14.2.0 → cc_transcript-14.5.0}/rust/crates/core/src/notifications.rs +0 -0
  118. {cc_transcript-14.2.0 → cc_transcript-14.5.0}/rust/crates/core/src/parse.rs +0 -0
  119. {cc_transcript-14.2.0 → cc_transcript-14.5.0}/rust/crates/core/src/protocol.rs +0 -0
  120. {cc_transcript-14.2.0 → cc_transcript-14.5.0}/rust/crates/core/src/pystr.rs +0 -0
  121. {cc_transcript-14.2.0 → cc_transcript-14.5.0}/rust/crates/core/src/query.rs +0 -0
  122. {cc_transcript-14.2.0 → cc_transcript-14.5.0}/rust/crates/core/src/render.rs +0 -0
  123. {cc_transcript-14.2.0 → cc_transcript-14.5.0}/rust/crates/core/src/sqlite.rs +0 -0
  124. {cc_transcript-14.2.0 → cc_transcript-14.5.0}/rust/crates/core/src/value.rs +0 -0
  125. {cc_transcript-14.2.0 → cc_transcript-14.5.0}/rust/crates/core/src/watch.rs +0 -0
  126. {cc_transcript-14.2.0 → cc_transcript-14.5.0}/rust/crates/py/Cargo.toml +0 -0
  127. {cc_transcript-14.2.0 → cc_transcript-14.5.0}/rust/crates/py/build.rs +0 -0
  128. {cc_transcript-14.2.0 → cc_transcript-14.5.0}/rust/crates/py/data/command_prefix_pins.tsv +0 -0
  129. {cc_transcript-14.2.0 → cc_transcript-14.5.0}/rust/crates/py/src/bin/stub_gen.rs +0 -0
  130. {cc_transcript-14.2.0 → cc_transcript-14.5.0}/rust/crates/py/src/command.rs +0 -0
  131. {cc_transcript-14.2.0 → cc_transcript-14.5.0}/rust/crates/py/src/context.rs +0 -0
  132. {cc_transcript-14.2.0 → cc_transcript-14.5.0}/rust/crates/py/src/discovery.rs +0 -0
  133. {cc_transcript-14.2.0 → cc_transcript-14.5.0}/rust/crates/py/src/lexicon.rs +0 -0
  134. {cc_transcript-14.2.0 → cc_transcript-14.5.0}/rust/crates/py/src/nlp.rs +0 -0
  135. {cc_transcript-14.2.0 → cc_transcript-14.5.0}/rust/crates/py/src/python.rs +0 -0
  136. {cc_transcript-14.2.0 → cc_transcript-14.5.0}/rust/crates/py/src/render.rs +0 -0
  137. {cc_transcript-14.2.0 → cc_transcript-14.5.0}/rust/crates/py/src/score.rs +0 -0
  138. {cc_transcript-14.2.0 → cc_transcript-14.5.0}/rust/crates/py/src/sqlite.rs +0 -0
  139. {cc_transcript-14.2.0 → cc_transcript-14.5.0}/rust/crates/py/src/views/attachment.rs +0 -0
  140. {cc_transcript-14.2.0 → cc_transcript-14.5.0}/rust/crates/py/src/views/blocks.rs +0 -0
  141. {cc_transcript-14.2.0 → cc_transcript-14.5.0}/rust/crates/py/src/views/command.rs +0 -0
  142. {cc_transcript-14.2.0 → cc_transcript-14.5.0}/rust/crates/py/src/views/convert.rs +0 -0
  143. {cc_transcript-14.2.0 → cc_transcript-14.5.0}/rust/crates/py/src/views/dunder.rs +0 -0
  144. {cc_transcript-14.2.0 → cc_transcript-14.5.0}/rust/crates/py/src/views/events.rs +0 -0
  145. {cc_transcript-14.2.0 → cc_transcript-14.5.0}/rust/crates/py/src/views/meta.rs +0 -0
  146. {cc_transcript-14.2.0 → cc_transcript-14.5.0}/rust/crates/py/src/views/mod.rs +0 -0
  147. {cc_transcript-14.2.0 → cc_transcript-14.5.0}/rust/crates/py/src/views/print.rs +0 -0
  148. {cc_transcript-14.2.0 → cc_transcript-14.5.0}/rust/crates/py/src/views/store.rs +0 -0
  149. {cc_transcript-14.2.0 → cc_transcript-14.5.0}/rust/crates/py/src/views/system.rs +0 -0
  150. {cc_transcript-14.2.0 → cc_transcript-14.5.0}/rust/crates/py/src/views/toolresult.rs +0 -0
  151. {cc_transcript-14.2.0 → cc_transcript-14.5.0}/rust/crates/py/src/views/transcript.rs +0 -0
  152. {cc_transcript-14.2.0 → cc_transcript-14.5.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.2.0
3
+ Version: 14.5.0
4
4
  Classifier: Development Status :: 3 - Alpha
5
5
  Classifier: Environment :: Console
6
6
  Classifier: Intended Audience :: Developers
@@ -53,6 +53,7 @@ EXPORTS: dict[str, str] = {
53
53
  "ReadResult",
54
54
  "SkillCall",
55
55
  "SkillResult",
56
+ "SpanEditCall",
56
57
  "TaskCall",
57
58
  "TaskCreateCall",
58
59
  "TaskLaunchResult",
@@ -76,7 +77,9 @@ EXPORTS: dict[str, str] = {
76
77
  "mcp_parts",
77
78
  "parse_tool_call",
78
79
  "parse_tool_result",
80
+ "register_mcp_tool",
79
81
  "tool_name_matches",
82
+ "unregister_mcp_tool",
80
83
  ),
81
84
  "cc_transcript.command": (
82
85
  "Command",
@@ -142,10 +145,25 @@ EXPORTS: dict[str, str] = {
142
145
  "cc_transcript.parser": (
143
146
  "parse",
144
147
  "parse_event",
148
+ "parse_events",
145
149
  "parse_events_from_bytes",
146
150
  "parse_print_result",
147
151
  "stream",
148
152
  ),
153
+ "cc_transcript.synthetic": (
154
+ "assistant_line",
155
+ "meta_fields",
156
+ "mode_line",
157
+ "other_line",
158
+ "synthetic_assistant_event",
159
+ "synthetic_user_event",
160
+ "system_line",
161
+ "text_block",
162
+ "thinking_block",
163
+ "tool_result",
164
+ "tool_use",
165
+ "user_line",
166
+ ),
149
167
  "cc_transcript.cost": (
150
168
  "PRICING",
151
169
  "CostBreakdown",
@@ -187,6 +187,7 @@ from cc_transcript.notifications import (
187
187
  from cc_transcript.parser import (
188
188
  parse as parse,
189
189
  parse_event as parse_event,
190
+ parse_events as parse_events,
190
191
  parse_events_from_bytes as parse_events_from_bytes,
191
192
  parse_print_result as parse_print_result,
192
193
  stream as stream,
@@ -207,6 +208,21 @@ from cc_transcript.render import (
207
208
  render_turn as render_turn,
208
209
  )
209
210
 
211
+ from cc_transcript.synthetic import (
212
+ assistant_line as assistant_line,
213
+ meta_fields as meta_fields,
214
+ mode_line as mode_line,
215
+ other_line as other_line,
216
+ synthetic_assistant_event as synthetic_assistant_event,
217
+ synthetic_user_event as synthetic_user_event,
218
+ system_line as system_line,
219
+ text_block as text_block,
220
+ thinking_block as thinking_block,
221
+ tool_result as tool_result,
222
+ tool_use as tool_use,
223
+ user_line as user_line,
224
+ )
225
+
210
226
  from cc_transcript.tools import (
211
227
  AskUserQuestionResult as AskUserQuestionResult,
212
228
  BashCall as BashCall,
@@ -227,6 +243,7 @@ from cc_transcript.tools import (
227
243
  ReadResult as ReadResult,
228
244
  SkillCall as SkillCall,
229
245
  SkillResult as SkillResult,
246
+ SpanEditCall as SpanEditCall,
230
247
  TOOL_ALIASES as TOOL_ALIASES,
231
248
  TaskCall as TaskCall,
232
249
  TaskCreateCall as TaskCreateCall,
@@ -251,7 +268,9 @@ from cc_transcript.tools import (
251
268
  mcp_parts as mcp_parts,
252
269
  parse_tool_call as parse_tool_call,
253
270
  parse_tool_result as parse_tool_result,
271
+ register_mcp_tool as register_mcp_tool,
254
272
  tool_name_matches as tool_name_matches,
273
+ unregister_mcp_tool as unregister_mcp_tool,
255
274
  )
256
275
 
257
276
  from cc_transcript.watch import (
@@ -360,6 +379,7 @@ __all__ = [
360
379
  "SessionId",
361
380
  "SkillCall",
362
381
  "SkillResult",
382
+ "SpanEditCall",
363
383
  "StopHookSummary",
364
384
  "SubagentIndex",
365
385
  "SubagentSession",
@@ -406,6 +426,7 @@ __all__ = [
406
426
  "WriteResult",
407
427
  "annotate_spec",
408
428
  "apply_spec",
429
+ "assistant_line",
409
430
  "build_spec",
410
431
  "canonical_json",
411
432
  "capture_window",
@@ -440,15 +461,20 @@ __all__ = [
440
461
  "mcp_access",
441
462
  "mcp_parts",
442
463
  "mcp_summary",
464
+ "meta_fields",
465
+ "mode_line",
443
466
  "native_user_classifier",
467
+ "other_line",
444
468
  "parse",
445
469
  "parse_command_line",
446
470
  "parse_event",
471
+ "parse_events",
447
472
  "parse_events_from_bytes",
448
473
  "parse_print_result",
449
474
  "parse_tool_call",
450
475
  "parse_tool_result",
451
476
  "record_harvest",
477
+ "register_mcp_tool",
452
478
  "render_session",
453
479
  "render_tool_call",
454
480
  "render_turn",
@@ -457,9 +483,18 @@ __all__ = [
457
483
  "result_index",
458
484
  "stream",
459
485
  "subagent_paths",
486
+ "synthetic_assistant_event",
487
+ "synthetic_user_event",
488
+ "system_line",
489
+ "text_block",
490
+ "thinking_block",
460
491
  "thinking_chars",
461
492
  "tool_digest",
462
493
  "tool_facts",
463
494
  "tool_name_matches",
495
+ "tool_result",
496
+ "tool_use",
464
497
  "tool_uses",
498
+ "unregister_mcp_tool",
499
+ "user_line",
465
500
  ]
@@ -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
@@ -72,6 +73,7 @@ __all__ = [
72
73
  "ServerToolUse",
73
74
  "SkillCall",
74
75
  "SkillResult",
76
+ "SpanEditCall",
75
77
  "SpecMatcher",
76
78
  "StopHookSummary",
77
79
  "SystemEvent",
@@ -133,6 +135,7 @@ __all__ = [
133
135
  "parse_bytes",
134
136
  "parse_print_result",
135
137
  "query_session",
138
+ "register_mcp_tool",
136
139
  "render_compact_lines",
137
140
  "render_haystacks",
138
141
  "render_stats",
@@ -147,6 +150,7 @@ __all__ = [
147
150
  "tool_name_matches",
148
151
  "toolcall_parse_view",
149
152
  "toolresult_parse_view",
153
+ "unregister_mcp_tool",
150
154
  ]
151
155
 
152
156
  @typing.final
@@ -1131,6 +1135,11 @@ class OtherCall(ToolCallBase):
1131
1135
  __match_args__: typing.ClassVar[tuple[str, ...]]
1132
1136
  @property
1133
1137
  def name(self) -> builtins.str: ...
1138
+ @property
1139
+ def error(self) -> typing.Optional[builtins.str]:
1140
+ r"""
1141
+ The strict-parse failure this call degraded from; None when the payload was well-formed but the tool has no typed model.
1142
+ """
1134
1143
 
1135
1144
  @typing.final
1136
1145
  class OtherEvent:
@@ -1439,37 +1448,40 @@ class Redirect:
1439
1448
  @typing.final
1440
1449
  class RustCorrectionLog:
1441
1450
  def __new__(cls, path: builtins.str) -> RustCorrectionLog: ...
1442
- 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: ...
1443
- def for_session(self, session_id: builtins.str) -> list[dict[str, typing.Any]]: ...
1444
- def for_repo(self, repo: builtins.str) -> list[dict[str, typing.Any]]: ...
1445
- def since(self, ts_ms: builtins.int, source: typing.Optional[builtins.str] = None) -> list[dict[str, typing.Any]]: ...
1446
- def for_anchor(self, session_id: builtins.str, anchor_uuid: builtins.str) -> list[dict[str, typing.Any]]: ...
1447
- def by_digest(self, session_id: builtins.str, incorrect_digest: builtins.str) -> list[dict[str, typing.Any]]: ...
1448
- def sql(self, statement: builtins.str) -> list[dict[str, typing.Any]]: ...
1451
+ def open(self) -> asyncio.Future[None]: ...
1452
+ def close(self) -> asyncio.Future[None]: ...
1453
+ 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]: ...
1454
+ def for_session(self, session_id: builtins.str) -> asyncio.Future[list[dict[str, typing.Any]]]: ...
1455
+ def for_repo(self, repo: builtins.str) -> asyncio.Future[list[dict[str, typing.Any]]]: ...
1456
+ def since(self, ts_ms: builtins.int, source: typing.Optional[builtins.str] = None) -> asyncio.Future[list[dict[str, typing.Any]]]: ...
1457
+ def for_anchor(self, session_id: builtins.str, anchor_uuid: builtins.str) -> asyncio.Future[list[dict[str, typing.Any]]]: ...
1458
+ def by_digest(self, session_id: builtins.str, incorrect_digest: builtins.str) -> asyncio.Future[list[dict[str, typing.Any]]]: ...
1459
+ def sql(self, statement: builtins.str) -> asyncio.Future[list[dict[str, typing.Any]]]: ...
1449
1460
 
1450
1461
  @typing.final
1451
1462
  class RustFeedbackStore:
1452
1463
  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: ...
1453
- def close(self) -> None: ...
1454
- def sql(self, statement: builtins.str, params: typing.Optional[typing.Sequence[typing.Any]] = None) -> list[dict[str, typing.Any]]: ...
1455
- def execute(self, statement: builtins.str, params: typing.Optional[typing.Sequence[typing.Any]] = None) -> builtins.int: ...
1456
- def executemany(self, statement: builtins.str, seq: typing.Sequence[typing.Sequence[typing.Any]]) -> builtins.int: ...
1457
- def executescript(self, script: builtins.str) -> None: ...
1458
- def last_insert_rowid(self) -> builtins.int: ...
1459
- def load_extension(self, path: builtins.str) -> None: ...
1460
- def begin_immediate(self) -> None: ...
1461
- def commit(self) -> None: ...
1462
- def rollback(self) -> None: ...
1463
- 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]: ...
1464
- def record_file(self, path: builtins.str, mtime: builtins.float) -> None: ...
1465
- def file_mtimes(self) -> list[dict[str, typing.Any]]: ...
1466
- def stats(self) -> tuple[int, int, list[dict[str, typing.Any]]]: ...
1467
- def recent(self, source_kind: typing.Optional[builtins.str] = None, limit: builtins.int = 20) -> list[dict[str, typing.Any]]: ...
1468
- def events(self, source_kind: typing.Optional[builtins.str] = None) -> list[dict[str, typing.Any]]: ...
1469
- def dedup_keys(self) -> builtins.list[builtins.str]: ...
1470
- 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: ...
1471
- 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]]: ...
1472
- def judged(self, role: builtins.str, prompt_version: builtins.int) -> list[dict[str, typing.Any]]: ...
1464
+ def open(self) -> asyncio.Future[None]: ...
1465
+ def close(self) -> asyncio.Future[None]: ...
1466
+ def sql(self, statement: builtins.str, params: typing.Optional[typing.Sequence[typing.Any]] = None) -> asyncio.Future[list[dict[str, typing.Any]]]: ...
1467
+ def execute(self, statement: builtins.str, params: typing.Optional[typing.Sequence[typing.Any]] = None) -> asyncio.Future[int]: ...
1468
+ def executemany(self, statement: builtins.str, seq: typing.Sequence[typing.Sequence[typing.Any]]) -> asyncio.Future[int]: ...
1469
+ def executescript(self, script: builtins.str) -> asyncio.Future[None]: ...
1470
+ def last_insert_rowid(self) -> asyncio.Future[int]: ...
1471
+ def load_extension(self, path: builtins.str) -> asyncio.Future[None]: ...
1472
+ def begin_immediate(self) -> asyncio.Future[None]: ...
1473
+ def commit(self) -> asyncio.Future[None]: ...
1474
+ def rollback(self) -> asyncio.Future[None]: ...
1475
+ 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]]: ...
1476
+ def record_file(self, path: builtins.str, mtime: builtins.float) -> asyncio.Future[None]: ...
1477
+ def file_mtimes(self) -> asyncio.Future[list[dict[str, typing.Any]]]: ...
1478
+ def stats(self) -> asyncio.Future[tuple[int, int, list[dict[str, typing.Any]]]]: ...
1479
+ def recent(self, source_kind: typing.Optional[builtins.str] = None, limit: builtins.int = 20) -> asyncio.Future[list[dict[str, typing.Any]]]: ...
1480
+ def events(self, source_kind: typing.Optional[builtins.str] = None) -> asyncio.Future[list[dict[str, typing.Any]]]: ...
1481
+ def dedup_keys(self) -> asyncio.Future[list[str]]: ...
1482
+ 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]: ...
1483
+ 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]]]: ...
1484
+ def judged(self, role: builtins.str, prompt_version: builtins.int) -> asyncio.Future[list[dict[str, typing.Any]]]: ...
1473
1485
 
1474
1486
  @typing.final
1475
1487
  class ServerToolUse:
@@ -1514,6 +1526,27 @@ class SkillResult(ToolResultBase):
1514
1526
  @property
1515
1527
  def allowed_tools(self) -> tuple[str, ...] | None: ...
1516
1528
 
1529
+ @typing.final
1530
+ class SpanEditCall(ToolCallBase):
1531
+ r"""
1532
+ An in-place file edit addressed by an opaque locator.
1533
+
1534
+ A registered MCP tool whose spec carries a span-edit lowering. The payload
1535
+ carries no pre-image, so :func:`hunks_of` yields no hunk; ``new`` is ``None``
1536
+ when the call deletes rather than writes. Registrations are process-local.
1537
+
1538
+ Attributes:
1539
+ file_path: The file the edit targets.
1540
+ new: The written content, or ``None`` when the call is a deletion.
1541
+ """
1542
+ __match_args__: typing.ClassVar[tuple[str, ...]]
1543
+ @property
1544
+ def name(self) -> builtins.str: ...
1545
+ @property
1546
+ def file_path(self) -> builtins.str: ...
1547
+ @property
1548
+ def new(self) -> typing.Optional[builtins.str]: ...
1549
+
1517
1550
  @typing.final
1518
1551
  class SpecMatcher:
1519
1552
  def __new__(cls, spec_json: builtins.str) -> SpecMatcher: ...
@@ -2147,6 +2180,8 @@ def hunks_of(call: tools.ToolCall | tools.FallbackCall) -> tuple[Hunk, ...]:
2147
2180
 
2148
2181
  MultiEdit yields one hunk per span in application order — never just the
2149
2182
  first. Write and NotebookEdit are pure additions with an empty old side.
2183
+ :class:`~SpanEditCall` yields none: its payload carries no
2184
+ pre-image, so no honest hunk can be derived.
2150
2185
  """
2151
2186
 
2152
2187
  def ids_canonical_json(value_json: builtins.str) -> builtins.str: ...
@@ -2166,19 +2201,16 @@ def matches_names(actual: builtins.str, names: collections.abc.Container[str]) -
2166
2201
  Whether ``actual`` is one of ``names``, exactly or as an MCP tool suffix.
2167
2202
 
2168
2203
  True when ``actual`` is in ``names``, or when it splits as
2169
- ``mcp__<server>__<tool>`` on the first two ``__`` and ``<tool>`` — or its
2170
- native built-in edit-gate equivalent — is in ``names``. That alias closes
2171
- the edit-gate bypass where cc-context's ``ccx_code_edit`` /
2172
- ``ccx_code_replace`` write through the MCP under names no ``Edit``/``Write``
2173
- gate would catch. Display-name aliases from
2174
- :data:`~tools.TOOL_ALIASES` are not closed over — ``names`` is
2175
- taken verbatim; pre-expand with :func:`expand_tool_names` for those.
2204
+ ``mcp__<server>__<tool>`` on the first two ``__`` and ``<tool>`` — or the
2205
+ built-in gate its registered spec ``behaves_like`` — is in ``names``. That
2206
+ closure lets a registered MCP tool (e.g. one that gates as ``Edit``) match an
2207
+ ``Edit`` gate even under a name no built-in gate would catch. Display-name
2208
+ aliases from :data:`~tools.TOOL_ALIASES` are not closed over —
2209
+ ``names`` is taken verbatim; pre-expand with :func:`expand_tool_names` for those.
2176
2210
 
2177
2211
  Example:
2178
2212
  >>> matches_names("mcp__github__Grep", {"Grep"})
2179
2213
  True
2180
- >>> matches_names("mcp__cc-context__ccx_code_edit", {"Edit"})
2181
- True
2182
2214
  >>> matches_names("Execute", {"Bash"})
2183
2215
  False
2184
2216
  """
@@ -2230,6 +2262,16 @@ def parse_print_result(raw: bytes) -> PrintResult: ...
2230
2262
 
2231
2263
  def query_session(path: builtins.str, max_events: builtins.int) -> dict[str, typing.Any]: ...
2232
2264
 
2265
+ def register_mcp_tool(tool: builtins.str, behaves_like: builtins.str, span_edit: collections.abc.Mapping[str, str] | None = None) -> None:
2266
+ r"""
2267
+ Register an MCP tool spec, keyed by its bare segment; last write wins.
2268
+
2269
+ ``behaves_like`` names the built-in gate the tool matches. ``span_edit``, when
2270
+ given, maps the payload key names — ``path``, ``content``, and optionally
2271
+ ``delete`` — the tool's :class:`~SpanEditCall` lowering
2272
+ reads. Registrations are process-local.
2273
+ """
2274
+
2233
2275
  def render_compact_lines(raw: bytes, width: builtins.int, thinking: builtins.bool, uuids: builtins.bool) -> builtins.list[builtins.str]: ...
2234
2276
 
2235
2277
  def render_haystacks(raw: bytes, wheres: typing.Sequence[builtins.str]) -> builtins.list[builtins.str]: ...
@@ -2275,3 +2317,8 @@ def toolresult_parse_view(name: builtins.str, payload_json: builtins.str) -> too
2275
2317
  the typed view hierarchy; the ``cc_transcript.tools`` facade owns the public
2276
2318
  signature.
2277
2319
  """
2320
+
2321
+ def unregister_mcp_tool(tool: builtins.str) -> builtins.bool:
2322
+ r"""
2323
+ Unregister an MCP tool by its bare segment; returns whether it was registered.
2324
+ """
@@ -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