cc-transcript 14.6.1__tar.gz → 14.7.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 (160) hide show
  1. {cc_transcript-14.6.1 → cc_transcript-14.7.0}/PKG-INFO +3 -2
  2. {cc_transcript-14.6.1 → cc_transcript-14.7.0}/README.md +2 -1
  3. {cc_transcript-14.6.1 → cc_transcript-14.7.0}/cc_transcript/__init__.py +7 -0
  4. {cc_transcript-14.6.1 → cc_transcript-14.7.0}/cc_transcript/__init__.pyi +14 -0
  5. {cc_transcript-14.6.1 → cc_transcript-14.7.0}/cc_transcript/_native.pyi +127 -2
  6. {cc_transcript-14.6.1 → cc_transcript-14.7.0}/cc_transcript/activity.py +10 -4
  7. {cc_transcript-14.6.1 → cc_transcript-14.7.0}/cc_transcript/codex.py +23 -0
  8. {cc_transcript-14.6.1 → cc_transcript-14.7.0}/cc_transcript/evidence.py +12 -15
  9. {cc_transcript-14.6.1 → cc_transcript-14.7.0}/cc_transcript/judge/similar.py +8 -35
  10. {cc_transcript-14.6.1 → cc_transcript-14.7.0}/cc_transcript/judge/verdicts.py +20 -56
  11. {cc_transcript-14.6.1 → cc_transcript-14.7.0}/cc_transcript/mining/__init__.py +1 -5
  12. cc_transcript-14.7.0/cc_transcript/mining/formats.py +56 -0
  13. {cc_transcript-14.6.1 → cc_transcript-14.7.0}/cc_transcript/mining/sampling.py +19 -31
  14. {cc_transcript-14.6.1 → cc_transcript-14.7.0}/cc_transcript/query.py +18 -12
  15. {cc_transcript-14.6.1 → cc_transcript-14.7.0}/cc_transcript/tools.py +26 -18
  16. {cc_transcript-14.6.1 → cc_transcript-14.7.0}/pyproject.toml +1 -1
  17. cc_transcript-14.7.0/rust/crates/cli/src/commands/list.rs +225 -0
  18. {cc_transcript-14.6.1 → cc_transcript-14.7.0}/rust/crates/cli/src/commands/slice.rs +63 -8
  19. {cc_transcript-14.6.1 → cc_transcript-14.7.0}/rust/crates/cli/src/lib.rs +19 -1
  20. {cc_transcript-14.6.1 → cc_transcript-14.7.0}/rust/crates/core/src/activity.rs +145 -25
  21. {cc_transcript-14.6.1 → cc_transcript-14.7.0}/rust/crates/core/src/codex/discovery.rs +126 -0
  22. {cc_transcript-14.6.1 → cc_transcript-14.7.0}/rust/crates/core/src/codex/mod.rs +1 -1
  23. {cc_transcript-14.6.1 → cc_transcript-14.7.0}/rust/crates/core/src/command.rs +232 -8
  24. {cc_transcript-14.6.1 → cc_transcript-14.7.0}/rust/crates/core/src/facts.rs +1 -1
  25. {cc_transcript-14.6.1 → cc_transcript-14.7.0}/rust/crates/core/src/feedback.rs +146 -1
  26. cc_transcript-14.7.0/rust/crates/core/src/judge.rs +296 -0
  27. {cc_transcript-14.6.1 → cc_transcript-14.7.0}/rust/crates/core/src/lib.rs +2 -0
  28. cc_transcript-14.7.0/rust/crates/core/src/literals/command.rs +97 -0
  29. {cc_transcript-14.6.1 → cc_transcript-14.7.0}/rust/crates/core/src/query.rs +54 -11
  30. cc_transcript-14.7.0/rust/crates/core/src/rng.rs +141 -0
  31. {cc_transcript-14.6.1 → cc_transcript-14.7.0}/rust/crates/core/src/toolcall.rs +709 -35
  32. {cc_transcript-14.6.1 → cc_transcript-14.7.0}/rust/crates/py/src/codex.rs +13 -2
  33. {cc_transcript-14.6.1 → cc_transcript-14.7.0}/rust/crates/py/src/feedback.rs +35 -0
  34. cc_transcript-14.7.0/rust/crates/py/src/judge.rs +48 -0
  35. {cc_transcript-14.6.1 → cc_transcript-14.7.0}/rust/crates/py/src/lib.rs +1 -0
  36. {cc_transcript-14.6.1 → cc_transcript-14.7.0}/rust/crates/py/src/mining.rs +37 -3
  37. {cc_transcript-14.6.1 → cc_transcript-14.7.0}/rust/crates/py/src/python.rs +34 -2
  38. {cc_transcript-14.6.1 → cc_transcript-14.7.0}/rust/crates/py/src/views/command.rs +15 -0
  39. {cc_transcript-14.6.1 → cc_transcript-14.7.0}/rust/crates/py/src/views/toolcall.rs +300 -3
  40. cc_transcript-14.6.1/cc_transcript/mining/formats.py +0 -142
  41. cc_transcript-14.6.1/rust/crates/cli/src/commands/list.rs +0 -75
  42. cc_transcript-14.6.1/rust/crates/core/src/literals/command.rs +0 -33
  43. {cc_transcript-14.6.1 → cc_transcript-14.7.0}/Cargo.lock +0 -0
  44. {cc_transcript-14.6.1 → cc_transcript-14.7.0}/Cargo.toml +0 -0
  45. {cc_transcript-14.6.1 → cc_transcript-14.7.0}/LICENSE +0 -0
  46. {cc_transcript-14.6.1 → cc_transcript-14.7.0}/cc_transcript/activity_probe.py +0 -0
  47. {cc_transcript-14.6.1 → cc_transcript-14.7.0}/cc_transcript/builders.py +0 -0
  48. {cc_transcript-14.6.1 → cc_transcript-14.7.0}/cc_transcript/command.py +0 -0
  49. {cc_transcript-14.6.1 → cc_transcript-14.7.0}/cc_transcript/context.py +0 -0
  50. {cc_transcript-14.6.1 → cc_transcript-14.7.0}/cc_transcript/corrections.py +0 -0
  51. {cc_transcript-14.6.1 → cc_transcript-14.7.0}/cc_transcript/cost.py +0 -0
  52. {cc_transcript-14.6.1 → cc_transcript-14.7.0}/cc_transcript/decisions.py +0 -0
  53. {cc_transcript-14.6.1 → cc_transcript-14.7.0}/cc_transcript/discovery.py +0 -0
  54. {cc_transcript-14.6.1 → cc_transcript-14.7.0}/cc_transcript/disktruth.py +0 -0
  55. {cc_transcript-14.6.1 → cc_transcript-14.7.0}/cc_transcript/extract/__init__.py +0 -0
  56. {cc_transcript-14.6.1 → cc_transcript-14.7.0}/cc_transcript/extract/correct.py +0 -0
  57. {cc_transcript-14.6.1 → cc_transcript-14.7.0}/cc_transcript/facts.py +0 -0
  58. {cc_transcript-14.6.1 → cc_transcript-14.7.0}/cc_transcript/filterspec.py +0 -0
  59. {cc_transcript-14.6.1 → cc_transcript-14.7.0}/cc_transcript/heartbeats.py +0 -0
  60. {cc_transcript-14.6.1 → cc_transcript-14.7.0}/cc_transcript/ids.py +0 -0
  61. {cc_transcript-14.6.1 → cc_transcript-14.7.0}/cc_transcript/judge/__init__.py +0 -0
  62. {cc_transcript-14.6.1 → cc_transcript-14.7.0}/cc_transcript/judge/llm.py +0 -0
  63. {cc_transcript-14.6.1 → cc_transcript-14.7.0}/cc_transcript/ledger.py +0 -0
  64. {cc_transcript-14.6.1 → cc_transcript-14.7.0}/cc_transcript/literals.py +0 -0
  65. {cc_transcript-14.6.1 → cc_transcript-14.7.0}/cc_transcript/mining/candidates.py +0 -0
  66. {cc_transcript-14.6.1 → cc_transcript-14.7.0}/cc_transcript/mining/confidence.py +0 -0
  67. {cc_transcript-14.6.1 → cc_transcript-14.7.0}/cc_transcript/mining/engine.py +0 -0
  68. {cc_transcript-14.6.1 → cc_transcript-14.7.0}/cc_transcript/mining/signals.py +0 -0
  69. {cc_transcript-14.6.1 → cc_transcript-14.7.0}/cc_transcript/mining/sourcekind.py +0 -0
  70. {cc_transcript-14.6.1 → cc_transcript-14.7.0}/cc_transcript/mining/spec.py +0 -0
  71. {cc_transcript-14.6.1 → cc_transcript-14.7.0}/cc_transcript/mining/store.py +0 -0
  72. {cc_transcript-14.6.1 → cc_transcript-14.7.0}/cc_transcript/models.py +0 -0
  73. {cc_transcript-14.6.1 → cc_transcript-14.7.0}/cc_transcript/nlp.py +0 -0
  74. {cc_transcript-14.6.1 → cc_transcript-14.7.0}/cc_transcript/notifications.py +0 -0
  75. {cc_transcript-14.6.1 → cc_transcript-14.7.0}/cc_transcript/parser.py +0 -0
  76. {cc_transcript-14.6.1 → cc_transcript-14.7.0}/cc_transcript/py.typed +0 -0
  77. {cc_transcript-14.6.1 → cc_transcript-14.7.0}/cc_transcript/render.py +0 -0
  78. {cc_transcript-14.6.1 → cc_transcript-14.7.0}/cc_transcript/sentiment/__init__.py +0 -0
  79. {cc_transcript-14.6.1 → cc_transcript-14.7.0}/cc_transcript/sentiment/buckets.py +0 -0
  80. {cc_transcript-14.6.1 → cc_transcript-14.7.0}/cc_transcript/sentiment/data/afinn-en-165.tsv +0 -0
  81. {cc_transcript-14.6.1 → cc_transcript-14.7.0}/cc_transcript/sentiment/data/domain_overrides.tsv +0 -0
  82. {cc_transcript-14.6.1 → cc_transcript-14.7.0}/cc_transcript/sentiment/data/en-ewt.udpipe +0 -0
  83. {cc_transcript-14.6.1 → cc_transcript-14.7.0}/cc_transcript/sentiment/engine.py +0 -0
  84. {cc_transcript-14.6.1 → cc_transcript-14.7.0}/cc_transcript/sentiment/lexicon.py +0 -0
  85. {cc_transcript-14.6.1 → cc_transcript-14.7.0}/cc_transcript/sentiment/scorespec.py +0 -0
  86. {cc_transcript-14.6.1 → cc_transcript-14.7.0}/cc_transcript/synthetic.py +0 -0
  87. {cc_transcript-14.6.1 → cc_transcript-14.7.0}/cc_transcript/watch.py +0 -0
  88. {cc_transcript-14.6.1 → cc_transcript-14.7.0}/rust/crates/cli/Cargo.toml +0 -0
  89. {cc_transcript-14.6.1 → cc_transcript-14.7.0}/rust/crates/cli/src/commands/corrections.rs +0 -0
  90. {cc_transcript-14.6.1 → cc_transcript-14.7.0}/rust/crates/cli/src/commands/digest.rs +0 -0
  91. {cc_transcript-14.6.1 → cc_transcript-14.7.0}/rust/crates/cli/src/commands/facts.rs +0 -0
  92. {cc_transcript-14.6.1 → cc_transcript-14.7.0}/rust/crates/cli/src/commands/grep.rs +0 -0
  93. {cc_transcript-14.6.1 → cc_transcript-14.7.0}/rust/crates/cli/src/commands/mod.rs +0 -0
  94. {cc_transcript-14.6.1 → cc_transcript-14.7.0}/rust/crates/cli/src/commands/scratchpad.rs +0 -0
  95. {cc_transcript-14.6.1 → cc_transcript-14.7.0}/rust/crates/cli/src/commands/show.rs +0 -0
  96. {cc_transcript-14.6.1 → cc_transcript-14.7.0}/rust/crates/cli/src/commands/stats.rs +0 -0
  97. {cc_transcript-14.6.1 → cc_transcript-14.7.0}/rust/crates/cli/src/commands/watch.rs +0 -0
  98. {cc_transcript-14.6.1 → cc_transcript-14.7.0}/rust/crates/cli/src/main.rs +0 -0
  99. {cc_transcript-14.6.1 → cc_transcript-14.7.0}/rust/crates/cli/src/output.rs +0 -0
  100. {cc_transcript-14.6.1 → cc_transcript-14.7.0}/rust/crates/cli/src/target.rs +0 -0
  101. {cc_transcript-14.6.1 → cc_transcript-14.7.0}/rust/crates/core/Cargo.toml +0 -0
  102. {cc_transcript-14.6.1 → cc_transcript-14.7.0}/rust/crates/core/benches/parse.rs +0 -0
  103. {cc_transcript-14.6.1 → cc_transcript-14.7.0}/rust/crates/core/benches/render.rs +0 -0
  104. {cc_transcript-14.6.1 → cc_transcript-14.7.0}/rust/crates/core/benches/toolcall.rs +0 -0
  105. {cc_transcript-14.6.1 → cc_transcript-14.7.0}/rust/crates/core/src/actor.rs +0 -0
  106. {cc_transcript-14.6.1 → cc_transcript-14.7.0}/rust/crates/core/src/buckets.rs +0 -0
  107. {cc_transcript-14.6.1 → cc_transcript-14.7.0}/rust/crates/core/src/codex/lower.rs +0 -0
  108. {cc_transcript-14.6.1 → cc_transcript-14.7.0}/rust/crates/core/src/codex/parse.rs +0 -0
  109. {cc_transcript-14.6.1 → cc_transcript-14.7.0}/rust/crates/core/src/codex/probe.rs +0 -0
  110. {cc_transcript-14.6.1 → cc_transcript-14.7.0}/rust/crates/core/src/codex/protocol.rs +0 -0
  111. {cc_transcript-14.6.1 → cc_transcript-14.7.0}/rust/crates/core/src/codex/types.rs +0 -0
  112. {cc_transcript-14.6.1 → cc_transcript-14.7.0}/rust/crates/core/src/context.rs +0 -0
  113. {cc_transcript-14.6.1 → cc_transcript-14.7.0}/rust/crates/core/src/corrections.rs +0 -0
  114. {cc_transcript-14.6.1 → cc_transcript-14.7.0}/rust/crates/core/src/cost.rs +0 -0
  115. {cc_transcript-14.6.1 → cc_transcript-14.7.0}/rust/crates/core/src/discovery.rs +0 -0
  116. {cc_transcript-14.6.1 → cc_transcript-14.7.0}/rust/crates/core/src/filter.rs +0 -0
  117. {cc_transcript-14.6.1 → cc_transcript-14.7.0}/rust/crates/core/src/gateway.rs +0 -0
  118. {cc_transcript-14.6.1 → cc_transcript-14.7.0}/rust/crates/core/src/ids.rs +0 -0
  119. {cc_transcript-14.6.1 → cc_transcript-14.7.0}/rust/crates/core/src/literals/corrections.rs +0 -0
  120. {cc_transcript-14.6.1 → cc_transcript-14.7.0}/rust/crates/core/src/literals/feedback.rs +0 -0
  121. {cc_transcript-14.6.1 → cc_transcript-14.7.0}/rust/crates/core/src/literals/mining.rs +0 -0
  122. {cc_transcript-14.6.1 → cc_transcript-14.7.0}/rust/crates/core/src/literals/mod.rs +0 -0
  123. {cc_transcript-14.6.1 → cc_transcript-14.7.0}/rust/crates/core/src/literals/protocol.rs +0 -0
  124. {cc_transcript-14.6.1 → cc_transcript-14.7.0}/rust/crates/core/src/notifications.rs +0 -0
  125. {cc_transcript-14.6.1 → cc_transcript-14.7.0}/rust/crates/core/src/parse.rs +0 -0
  126. {cc_transcript-14.6.1 → cc_transcript-14.7.0}/rust/crates/core/src/protocol.rs +0 -0
  127. {cc_transcript-14.6.1 → cc_transcript-14.7.0}/rust/crates/core/src/pystr.rs +0 -0
  128. {cc_transcript-14.6.1 → cc_transcript-14.7.0}/rust/crates/core/src/render.rs +0 -0
  129. {cc_transcript-14.6.1 → cc_transcript-14.7.0}/rust/crates/core/src/sqlite.rs +0 -0
  130. {cc_transcript-14.6.1 → cc_transcript-14.7.0}/rust/crates/core/src/types.rs +0 -0
  131. {cc_transcript-14.6.1 → cc_transcript-14.7.0}/rust/crates/core/src/value.rs +0 -0
  132. {cc_transcript-14.6.1 → cc_transcript-14.7.0}/rust/crates/core/src/watch.rs +0 -0
  133. {cc_transcript-14.6.1 → cc_transcript-14.7.0}/rust/crates/py/Cargo.toml +0 -0
  134. {cc_transcript-14.6.1 → cc_transcript-14.7.0}/rust/crates/py/build.rs +0 -0
  135. {cc_transcript-14.6.1 → cc_transcript-14.7.0}/rust/crates/py/data/command_prefix_pins.tsv +0 -0
  136. {cc_transcript-14.6.1 → cc_transcript-14.7.0}/rust/crates/py/src/actor_bridge.rs +0 -0
  137. {cc_transcript-14.6.1 → cc_transcript-14.7.0}/rust/crates/py/src/bin/stub_gen.rs +0 -0
  138. {cc_transcript-14.6.1 → cc_transcript-14.7.0}/rust/crates/py/src/command.rs +0 -0
  139. {cc_transcript-14.6.1 → cc_transcript-14.7.0}/rust/crates/py/src/context.rs +0 -0
  140. {cc_transcript-14.6.1 → cc_transcript-14.7.0}/rust/crates/py/src/corrections.rs +0 -0
  141. {cc_transcript-14.6.1 → cc_transcript-14.7.0}/rust/crates/py/src/discovery.rs +0 -0
  142. {cc_transcript-14.6.1 → cc_transcript-14.7.0}/rust/crates/py/src/lexicon.rs +0 -0
  143. {cc_transcript-14.6.1 → cc_transcript-14.7.0}/rust/crates/py/src/nlp.rs +0 -0
  144. {cc_transcript-14.6.1 → cc_transcript-14.7.0}/rust/crates/py/src/render.rs +0 -0
  145. {cc_transcript-14.6.1 → cc_transcript-14.7.0}/rust/crates/py/src/score.rs +0 -0
  146. {cc_transcript-14.6.1 → cc_transcript-14.7.0}/rust/crates/py/src/sqlite.rs +0 -0
  147. {cc_transcript-14.6.1 → cc_transcript-14.7.0}/rust/crates/py/src/toolcall.rs +0 -0
  148. {cc_transcript-14.6.1 → cc_transcript-14.7.0}/rust/crates/py/src/views/attachment.rs +0 -0
  149. {cc_transcript-14.6.1 → cc_transcript-14.7.0}/rust/crates/py/src/views/blocks.rs +0 -0
  150. {cc_transcript-14.6.1 → cc_transcript-14.7.0}/rust/crates/py/src/views/convert.rs +0 -0
  151. {cc_transcript-14.6.1 → cc_transcript-14.7.0}/rust/crates/py/src/views/dunder.rs +0 -0
  152. {cc_transcript-14.6.1 → cc_transcript-14.7.0}/rust/crates/py/src/views/events.rs +0 -0
  153. {cc_transcript-14.6.1 → cc_transcript-14.7.0}/rust/crates/py/src/views/meta.rs +0 -0
  154. {cc_transcript-14.6.1 → cc_transcript-14.7.0}/rust/crates/py/src/views/mod.rs +0 -0
  155. {cc_transcript-14.6.1 → cc_transcript-14.7.0}/rust/crates/py/src/views/print.rs +0 -0
  156. {cc_transcript-14.6.1 → cc_transcript-14.7.0}/rust/crates/py/src/views/store.rs +0 -0
  157. {cc_transcript-14.6.1 → cc_transcript-14.7.0}/rust/crates/py/src/views/system.rs +0 -0
  158. {cc_transcript-14.6.1 → cc_transcript-14.7.0}/rust/crates/py/src/views/toolresult.rs +0 -0
  159. {cc_transcript-14.6.1 → cc_transcript-14.7.0}/rust/crates/py/src/views/transcript.rs +0 -0
  160. {cc_transcript-14.6.1 → cc_transcript-14.7.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.6.1
3
+ Version: 14.7.0
4
4
  Classifier: Development Status :: 3 - Alpha
5
5
  Classifier: Environment :: Console
6
6
  Classifier: Intended Audience :: Developers
@@ -118,7 +118,7 @@ transcript = parse(discover()[0], drop=NOISE_SPEC)
118
118
  print(len(transcript.events), "events after the noise drop")
119
119
  ```
120
120
 
121
- `discover()` lists every transcript on disk newest first, `stream` fans a whole corpus across the parse pool, and `resolve` finds one session by UUID. The [getting-started guide](https://yasyf.github.io/cc-transcript/docs/getting-started/index.html) builds this out to a composed filter and a sentiment score.
121
+ `discover()` lists every transcript on disk newest first, `stream` fans a whole corpus across the parse pool, and `resolve` finds one session by UUID. `parse` also reads OpenAI Codex CLI rollouts — hand it a path from `~/.codex/sessions` and the same typed events come back. The [getting-started guide](https://yasyf.github.io/cc-transcript/docs/getting-started/index.html) builds this out to a composed filter and a sentiment score.
122
122
 
123
123
  ## More in the docs
124
124
 
@@ -127,6 +127,7 @@ print(len(transcript.events), "events after the noise drop")
127
127
  - [Sentiment scoring](https://yasyf.github.io/cc-transcript/docs/guide/scoring-sentiment.html) buckets conversations and scores them around any inference engine.
128
128
  - [Feedback mining](https://yasyf.github.io/cc-transcript/docs/guide/mining-feedback.html) runs detectors, confidence calibration, and LLM verdict passes over your corpus.
129
129
  - [The Rust engine](https://yasyf.github.io/cc-transcript/docs/guide/rust-engine.html) is the implementation — parsing, filtering, scoring, and mining — its correctness pinned by hand-owned literals and golden fixtures.
130
+ - [Codex rollouts](https://yasyf.github.io/cc-transcript/docs/guide/codex-rollouts.html) make the parser two-provider: OpenAI Codex CLI sessions lower into the same typed events, with rollout discovery, subagent joins, and per-session lifecycle and token totals. `list --provider codex` finds them from the shell.
130
131
  - [API reference](https://yasyf.github.io/cc-transcript/reference/index.html) documents the complete typed surface, from `TranscriptEvent` to `SessionActivity`.
131
132
 
132
133
  Read the [docs](https://yasyf.github.io/cc-transcript/) for the full guide. Licensed under [PolyForm Noncommercial 1.0.0](https://github.com/yasyf/cc-transcript/blob/main/LICENSE).
@@ -82,7 +82,7 @@ transcript = parse(discover()[0], drop=NOISE_SPEC)
82
82
  print(len(transcript.events), "events after the noise drop")
83
83
  ```
84
84
 
85
- `discover()` lists every transcript on disk newest first, `stream` fans a whole corpus across the parse pool, and `resolve` finds one session by UUID. The [getting-started guide](https://yasyf.github.io/cc-transcript/docs/getting-started/index.html) builds this out to a composed filter and a sentiment score.
85
+ `discover()` lists every transcript on disk newest first, `stream` fans a whole corpus across the parse pool, and `resolve` finds one session by UUID. `parse` also reads OpenAI Codex CLI rollouts — hand it a path from `~/.codex/sessions` and the same typed events come back. The [getting-started guide](https://yasyf.github.io/cc-transcript/docs/getting-started/index.html) builds this out to a composed filter and a sentiment score.
86
86
 
87
87
  ## More in the docs
88
88
 
@@ -91,6 +91,7 @@ print(len(transcript.events), "events after the noise drop")
91
91
  - [Sentiment scoring](https://yasyf.github.io/cc-transcript/docs/guide/scoring-sentiment.html) buckets conversations and scores them around any inference engine.
92
92
  - [Feedback mining](https://yasyf.github.io/cc-transcript/docs/guide/mining-feedback.html) runs detectors, confidence calibration, and LLM verdict passes over your corpus.
93
93
  - [The Rust engine](https://yasyf.github.io/cc-transcript/docs/guide/rust-engine.html) is the implementation — parsing, filtering, scoring, and mining — its correctness pinned by hand-owned literals and golden fixtures.
94
+ - [Codex rollouts](https://yasyf.github.io/cc-transcript/docs/guide/codex-rollouts.html) make the parser two-provider: OpenAI Codex CLI sessions lower into the same typed events, with rollout discovery, subagent joins, and per-session lifecycle and token totals. `list --provider codex` finds them from the shell.
94
95
  - [API reference](https://yasyf.github.io/cc-transcript/reference/index.html) documents the complete typed surface, from `TranscriptEvent` to `SessionActivity`.
95
96
 
96
97
  Read the [docs](https://yasyf.github.io/cc-transcript/) for the full guide. Licensed under [PolyForm Noncommercial 1.0.0](https://github.com/yasyf/cc-transcript/blob/main/LICENSE).
@@ -34,9 +34,11 @@ EXPORTS: dict[str, str] = {
34
34
  ),
35
35
  "cc_transcript.tools": (
36
36
  "TOOL_ALIASES",
37
+ "ApplyPatchCall",
37
38
  "AskUserQuestionResult",
38
39
  "BashCall",
39
40
  "BashResult",
41
+ "CodeModeCall",
40
42
  "EditCall",
41
43
  "EditResult",
42
44
  "EditSpan",
@@ -48,6 +50,7 @@ EXPORTS: dict[str, str] = {
48
50
  "NotebookEditCall",
49
51
  "OtherCall",
50
52
  "OtherResult",
53
+ "PatchEdit",
51
54
  "QuestionAnnotation",
52
55
  "ReadCall",
53
56
  "ReadResult",
@@ -66,11 +69,15 @@ EXPORTS: dict[str, str] = {
66
69
  "ToolResult",
67
70
  "ToolResultBase",
68
71
  "ToolResultError",
72
+ "UpdatePlanCall",
69
73
  "WorkflowCall",
70
74
  "WriteCall",
71
75
  "WriteResult",
76
+ "WriteStdinCall",
77
+ "edits_of",
72
78
  "expand_tool_names",
73
79
  "file_path_of",
80
+ "file_paths_of",
74
81
  "hunks_of",
75
82
  "matches_names",
76
83
  "mcp_access",
@@ -233,9 +233,11 @@ from cc_transcript.synthetic import (
233
233
  )
234
234
 
235
235
  from cc_transcript.tools import (
236
+ ApplyPatchCall as ApplyPatchCall,
236
237
  AskUserQuestionResult as AskUserQuestionResult,
237
238
  BashCall as BashCall,
238
239
  BashResult as BashResult,
240
+ CodeModeCall as CodeModeCall,
239
241
  EditCall as EditCall,
240
242
  EditResult as EditResult,
241
243
  EditSpan as EditSpan,
@@ -247,6 +249,7 @@ from cc_transcript.tools import (
247
249
  NotebookEditCall as NotebookEditCall,
248
250
  OtherCall as OtherCall,
249
251
  OtherResult as OtherResult,
252
+ PatchEdit as PatchEdit,
250
253
  QuestionAnnotation as QuestionAnnotation,
251
254
  ReadCall as ReadCall,
252
255
  ReadResult as ReadResult,
@@ -266,11 +269,15 @@ from cc_transcript.tools import (
266
269
  ToolResult as ToolResult,
267
270
  ToolResultBase as ToolResultBase,
268
271
  ToolResultError as ToolResultError,
272
+ UpdatePlanCall as UpdatePlanCall,
269
273
  WorkflowCall as WorkflowCall,
270
274
  WriteCall as WriteCall,
271
275
  WriteResult as WriteResult,
276
+ WriteStdinCall as WriteStdinCall,
277
+ edits_of as edits_of,
272
278
  expand_tool_names as expand_tool_names,
273
279
  file_path_of as file_path_of,
280
+ file_paths_of as file_paths_of,
274
281
  hunks_of as hunks_of,
275
282
  matches_names as matches_names,
276
283
  mcp_access as mcp_access,
@@ -295,6 +302,7 @@ __all__ = [
295
302
  "ASSISTANTS",
296
303
  "Action",
297
304
  "ApiError",
305
+ "ApplyPatchCall",
298
306
  "AskUserQuestionResult",
299
307
  "AssistantEvent",
300
308
  "AsyncHookResponse",
@@ -310,6 +318,7 @@ __all__ = [
310
318
  "CacheCreation",
311
319
  "CandidatePair",
312
320
  "CcVersion",
321
+ "CodeModeCall",
313
322
  "CodexPendingItem",
314
323
  "CodexRollout",
315
324
  "CodexSessionInfo",
@@ -373,6 +382,7 @@ __all__ = [
373
382
  "OtherResult",
374
383
  "OtherSystemDetail",
375
384
  "PRICING",
385
+ "PatchEdit",
376
386
  "Plugin",
377
387
  "PreservedMessages",
378
388
  "PreservedSegment",
@@ -430,6 +440,7 @@ __all__ = [
430
440
  "TurnDuration",
431
441
  "TurnRef",
432
442
  "USERS",
443
+ "UpdatePlanCall",
433
444
  "Usage",
434
445
  "UserClassifier",
435
446
  "UserEvent",
@@ -439,6 +450,7 @@ __all__ = [
439
450
  "WorkflowCall",
440
451
  "WriteCall",
441
452
  "WriteResult",
453
+ "WriteStdinCall",
442
454
  "annotate_spec",
443
455
  "apply_spec",
444
456
  "assistant_line",
@@ -459,9 +471,11 @@ __all__ = [
459
471
  "drop_short",
460
472
  "drop_sidechain",
461
473
  "drop_synthetic",
474
+ "edits_of",
462
475
  "expand_tool_names",
463
476
  "export_activity",
464
477
  "file_path_of",
478
+ "file_paths_of",
465
479
  "find_in",
466
480
  "git_corrections",
467
481
  "harvest_pairs",
@@ -14,6 +14,7 @@ import pathlib
14
14
  import typing
15
15
  __all__ = [
16
16
  "ApiError",
17
+ "ApplyPatchCall",
17
18
  "AskUserQuestionResult",
18
19
  "AssistantEvent",
19
20
  "AsyncHookResponse",
@@ -22,6 +23,7 @@ __all__ = [
22
23
  "BashCall",
23
24
  "BashResult",
24
25
  "CacheCreation",
26
+ "CodeModeCall",
25
27
  "Command",
26
28
  "CommandLine",
27
29
  "CommandLineQuery",
@@ -57,6 +59,7 @@ __all__ = [
57
59
  "OtherResult",
58
60
  "OtherSystemDetail",
59
61
  "ParseStream",
62
+ "PatchEdit",
60
63
  "Plugin",
61
64
  "PreservedMessages",
62
65
  "PreservedSegment",
@@ -92,6 +95,7 @@ __all__ = [
92
95
  "ToolUseBlock",
93
96
  "Transcript",
94
97
  "TurnDuration",
98
+ "UpdatePlanCall",
95
99
  "Usage",
96
100
  "UserEvent",
97
101
  "WatchTailer",
@@ -99,11 +103,15 @@ __all__ = [
99
103
  "WorkflowCall",
100
104
  "WriteCall",
101
105
  "WriteResult",
106
+ "WriteStdinCall",
102
107
  "activity_hunk_overlap",
103
108
  "activity_lift",
104
109
  "activity_lift_from_events",
110
+ "activity_overlap_between",
111
+ "activity_parse_show_hunks",
105
112
  "bucket_events_from_events",
106
113
  "cli_main",
114
+ "codex_children_of",
107
115
  "codex_discover",
108
116
  "codex_resolve",
109
117
  "codex_session_info",
@@ -119,12 +127,16 @@ __all__ = [
119
127
  "discovery_is_subagent_path",
120
128
  "discovery_subagent_paths",
121
129
  "discovery_subagent_transcripts",
130
+ "edits_of",
122
131
  "embedded_literals",
123
132
  "expand_tool_names",
124
133
  "file_path_of",
134
+ "file_paths_of",
125
135
  "hunks_of",
126
136
  "ids_canonical_json",
127
137
  "ids_tool_digest",
138
+ "judge_exact_upper_bound",
139
+ "judge_sample_audit",
128
140
  "lexicon_has_hit",
129
141
  "lexicon_overrides",
130
142
  "lexicon_polarity",
@@ -133,6 +145,7 @@ __all__ = [
133
145
  "mcp_access",
134
146
  "mcp_parts",
135
147
  "mine_events",
148
+ "mining_sample_refs",
136
149
  "nlp_analyze",
137
150
  "noise_spec_json",
138
151
  "notifications_from_events",
@@ -178,6 +191,17 @@ class ApiError:
178
191
  @property
179
192
  def details(self) -> typing.Optional[builtins.str]: ...
180
193
 
194
+ @typing.final
195
+ class ApplyPatchCall(ToolCallBase):
196
+ r"""
197
+ A codex apply_patch call: one :class:`PatchEdit` per file in the envelope.
198
+ """
199
+ __match_args__: typing.ClassVar[tuple[str, ...]]
200
+ @property
201
+ def name(self) -> builtins.str: ...
202
+ @property
203
+ def edits(self) -> tuple[PatchEdit, ...]: ...
204
+
181
205
  @typing.final
182
206
  class AskUserQuestionResult(ToolResultBase):
183
207
  r"""
@@ -385,6 +409,18 @@ class CacheCreation:
385
409
  @property
386
410
  def ephemeral_1h_input_tokens(self) -> builtins.int: ...
387
411
 
412
+ @typing.final
413
+ class CodeModeCall(ToolCallBase):
414
+ r"""
415
+ A codex code-mode ``exec`` call whose ``source`` is a free-form program, kept
416
+ verbatim — never JSON-decoded.
417
+ """
418
+ __match_args__: typing.ClassVar[tuple[str, ...]]
419
+ @property
420
+ def name(self) -> builtins.str: ...
421
+ @property
422
+ def source(self) -> builtins.str: ...
423
+
388
424
  @typing.final
389
425
  class Command:
390
426
  r"""
@@ -433,6 +469,7 @@ class Command:
433
469
  @classmethod
434
470
  def parse(cls, raw: builtins.str) -> typing.Optional[Command]: ...
435
471
  def runs(self, *argv: typing.Any) -> builtins.bool: ...
472
+ def split_options(self, value_flags: typing.Sequence[builtins.str]) -> tuple[tuple[Word, ...], tuple[Word, ...]]: ...
436
473
  def matches(self, pattern: builtins.str) -> builtins.bool: ...
437
474
  def has_arg(self, *patterns: typing.Any) -> builtins.bool: ...
438
475
  def __str__(self) -> builtins.str: ...
@@ -1231,6 +1268,23 @@ class ParseStream:
1231
1268
  def recv(self) -> Transcript | None: ...
1232
1269
  def recv_many(self, max: builtins.int) -> list[Transcript]: ...
1233
1270
 
1271
+ @typing.final
1272
+ class PatchEdit:
1273
+ r"""
1274
+ One file's edit within a codex apply_patch envelope: its ``file_path``, ``kind``
1275
+ (``"add"``/``"update"``/``"delete"``), rename ``move_path``, and before/after
1276
+ ``hunks`` (empty for a deletion, one addition hunk for an add).
1277
+ """
1278
+ __match_args__: typing.ClassVar[tuple[str, ...]]
1279
+ @property
1280
+ def file_path(self) -> builtins.str: ...
1281
+ @property
1282
+ def kind(self) -> typing.Literal["add", "update", "delete"]: ...
1283
+ @property
1284
+ def move_path(self) -> typing.Optional[builtins.str]: ...
1285
+ @property
1286
+ def hunks(self) -> tuple[Hunk, ...]: ...
1287
+
1234
1288
  @typing.final
1235
1289
  class Plugin:
1236
1290
  r"""
@@ -1519,6 +1573,8 @@ class RustFeedbackStore:
1519
1573
  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]: ...
1520
1574
  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]]]: ...
1521
1575
  def judged(self, role: builtins.str, prompt_version: builtins.int) -> asyncio.Future[list[dict[str, typing.Any]]]: ...
1576
+ def suggest_canonical_keys(self, query: typing.Sequence[builtins.int], prompt_version: builtins.int, k: builtins.int) -> asyncio.Future[list[tuple[str, float, list[str]]]]: ...
1577
+ def near_duplicate_keys(self, prompt_version: builtins.int, threshold: builtins.float) -> asyncio.Future[list[tuple[str, str, float]]]: ...
1522
1578
 
1523
1579
  @typing.final
1524
1580
  class ServerToolUse:
@@ -1813,13 +1869,14 @@ class ToolCallBase:
1813
1869
  Attributes:
1814
1870
  name: The tool name exactly as invoked (aliases are not normalized —
1815
1871
  the digest must match what the hook saw).
1816
- raw: The verbatim input mapping; the only digest substrate.
1872
+ raw: The verbatim input value — a mapping for structured calls, a plain
1873
+ string for Codex calls, or None when absent. The only digest substrate.
1817
1874
  """
1818
1875
  __match_args__: typing.ClassVar[tuple[str, ...]]
1819
1876
  @property
1820
1877
  def name(self) -> builtins.str: ...
1821
1878
  @property
1822
- def raw(self) -> collections.abc.Mapping[str, typing.Any]: ...
1879
+ def raw(self) -> collections.abc.Mapping[str, typing.Any] | str | None: ...
1823
1880
  @property
1824
1881
  def digest(self) -> ids.ToolDigest:
1825
1882
  r"""
@@ -1985,6 +2042,19 @@ class TurnDuration:
1985
2042
  @property
1986
2043
  def pending_background_agent_count(self) -> typing.Optional[builtins.int]: ...
1987
2044
 
2045
+ @typing.final
2046
+ class UpdatePlanCall(ToolCallBase):
2047
+ r"""
2048
+ A codex update_plan call: the plan-step array and its optional narration.
2049
+ """
2050
+ __match_args__: typing.ClassVar[tuple[str, ...]]
2051
+ @property
2052
+ def name(self) -> builtins.str: ...
2053
+ @property
2054
+ def plan(self) -> list[typing.Any] | None: ...
2055
+ @property
2056
+ def explanation(self) -> typing.Optional[builtins.str]: ...
2057
+
1988
2058
  @typing.final
1989
2059
  class Usage:
1990
2060
  r"""
@@ -2182,12 +2252,33 @@ class WriteResult(ToolResultBase):
2182
2252
  @property
2183
2253
  def user_modified(self) -> bool: ...
2184
2254
 
2255
+ @typing.final
2256
+ class WriteStdinCall(ToolCallBase):
2257
+ r"""
2258
+ A codex write_stdin call: the text written to a session's stdin and its target.
2259
+ """
2260
+ __match_args__: typing.ClassVar[tuple[str, ...]]
2261
+ @property
2262
+ def name(self) -> builtins.str: ...
2263
+ @property
2264
+ def chars(self) -> typing.Any | None: ...
2265
+ @property
2266
+ def session_id(self) -> builtins.int: ...
2267
+ @property
2268
+ def yield_time_ms(self) -> typing.Optional[builtins.int]: ...
2269
+ @property
2270
+ def max_output_tokens(self) -> typing.Optional[builtins.int]: ...
2271
+
2185
2272
  def activity_hunk_overlap(a_old: builtins.str, a_new: builtins.str, b_old: builtins.str, b_new: builtins.str) -> builtins.float: ...
2186
2273
 
2187
2274
  def activity_lift(path: builtins.str, max_events: builtins.int) -> dict[str, typing.Any]: ...
2188
2275
 
2189
2276
  def activity_lift_from_events(events: list[models.TranscriptEvent], opener_flags: typing.Optional[typing.Sequence[builtins.bool]] = None) -> list[dict[str, typing.Any]]: ...
2190
2277
 
2278
+ def activity_overlap_between(incorrect: typing.Sequence[tuple[builtins.str, builtins.str]], correction: typing.Sequence[tuple[builtins.str, builtins.str]]) -> builtins.float: ...
2279
+
2280
+ def activity_parse_show_hunks(diff: builtins.str) -> builtins.list[tuple[builtins.str, builtins.str]]: ...
2281
+
2191
2282
  def bucket_events_from_events(events: list[models.TranscriptEvent]) -> list[dict[str, typing.Any]]: ...
2192
2283
 
2193
2284
  def cli_main() -> builtins.int:
@@ -2197,6 +2288,8 @@ def cli_main() -> builtins.int:
2197
2288
  run, and the exit code returned verbatim for the wrapper's `sys.exit`.
2198
2289
  """
2199
2290
 
2291
+ def codex_children_of(session_id: builtins.str, root: typing.Optional[builtins.str] = None) -> builtins.list[tuple[pathlib.Path, builtins.str, builtins.bool]]: ...
2292
+
2200
2293
  def codex_discover(root: typing.Optional[builtins.str | os.PathLike | pathlib.Path] = None) -> builtins.list[tuple[pathlib.Path, builtins.str, builtins.bool]]: ...
2201
2294
 
2202
2295
  def codex_resolve(session_id: builtins.str, root: typing.Optional[builtins.str | os.PathLike | pathlib.Path] = None) -> typing.Optional[pathlib.Path]: ...
@@ -2227,6 +2320,12 @@ def discovery_subagent_paths(path: builtins.str | os.PathLike | pathlib.Path) ->
2227
2320
 
2228
2321
  def discovery_subagent_transcripts(path: builtins.str | os.PathLike | pathlib.Path) -> dict[str, pathlib.Path]: ...
2229
2322
 
2323
+ def edits_of(call: tools.ToolCall | tools.FallbackCall) -> tuple[tuple[str, tuple[Hunk, ...]], ...]:
2324
+ r"""
2325
+ Every ``(file_path, hunks)`` a call lowers to: one entry per patched file for
2326
+ apply_patch (empty hunks for a deletion), else the singular 0/1-element projection.
2327
+ """
2328
+
2230
2329
  def embedded_literals() -> dict[str, str | float | int | list[str]]: ...
2231
2330
 
2232
2331
  def expand_tool_names(spec: builtins.str) -> frozenset[str]:
@@ -2239,6 +2338,12 @@ def file_path_of(call: tools.ToolCall | tools.FallbackCall) -> typing.Optional[b
2239
2338
  The file a call targets, when it targets one.
2240
2339
  """
2241
2340
 
2341
+ def file_paths_of(call: tools.ToolCall | tools.FallbackCall) -> tuple[str, ...]:
2342
+ r"""
2343
+ Every file a call targets: one entry per patched file for apply_patch, else the
2344
+ singular projection as a 0/1-element tuple.
2345
+ """
2346
+
2242
2347
  def hunks_of(call: tools.ToolCall | tools.FallbackCall) -> tuple[Hunk, ...]:
2243
2348
  r"""
2244
2349
  Lower an edit-shaped call to before/after hunks; ``()`` for the rest.
@@ -2253,6 +2358,19 @@ def ids_canonical_json(value_json: builtins.str) -> builtins.str: ...
2253
2358
 
2254
2359
  def ids_tool_digest(name: builtins.str, input_json: builtins.str) -> builtins.str: ...
2255
2360
 
2361
+ def judge_exact_upper_bound(hits: builtins.int, n: builtins.int, alpha: builtins.float) -> builtins.float:
2362
+ r"""
2363
+ The exact (Clopper-Pearson) one-sided upper confidence bound
2364
+ (judge/verdicts.py exact_upper_bound).
2365
+ """
2366
+
2367
+ def judge_sample_audit(rows: typing.Sequence[tuple[builtins.str, builtins.str, builtins.float, builtins.bool]], accepts: builtins.int, rejects: builtins.int, seed: builtins.str, quotas: typing.Sequence[tuple[builtins.str, typing.Optional[builtins.int]]], remainder_kind: builtins.str, oversample_share: builtins.float) -> tuple[builtins.list[builtins.int], builtins.list[builtins.int]]:
2368
+ r"""
2369
+ The seeded stratified audit draw over pre-coerced judged rows
2370
+ (judge/verdicts.py sample_audit): returns `(core, oversample)` as row indexes
2371
+ into `rows`.
2372
+ """
2373
+
2256
2374
  def lexicon_has_hit(text: builtins.str, want_negative: builtins.bool) -> builtins.bool: ...
2257
2375
 
2258
2376
  def lexicon_overrides() -> builtins.list[tuple[builtins.str, builtins.int]]: ...
@@ -2310,6 +2428,13 @@ def mcp_parts(name: builtins.str) -> tuple[str, str] | None:
2310
2428
 
2311
2429
  def mine_events(events: list[models.TranscriptEvent], spec_json: builtins.str, callable_formats: typing.Sequence[tuple[builtins.str, typing.Any, typing.Any]]) -> list[dict[str, typing.Any]]: ...
2312
2430
 
2431
+ def mining_sample_refs(data: bytes, n: builtins.int, exclude: typing.Sequence[tuple[builtins.str, builtins.str]], exclusion_radius: builtins.int, seed: builtins.str) -> builtins.list[tuple[builtins.int, builtins.str, builtins.str]]:
2432
+ r"""
2433
+ The seeded negative-window draw over a transcript's completed turns
2434
+ (mining/sampling.py sample_windows candidate build + draw): returns each drawn
2435
+ anchor as `(turn_index, session_id, event_uuid)`, sorted by turn index.
2436
+ """
2437
+
2313
2438
  def nlp_analyze(text: builtins.str) -> builtins.list[tuple[builtins.str, builtins.str, builtins.str, builtins.str, builtins.int, builtins.int, builtins.int, builtins.bool]]:
2314
2439
  r"""
2315
2440
  Analyze `text` with the embedded UDPipe model. Returns, per token,
@@ -22,7 +22,7 @@ from cc_transcript.filterspec import event_meta
22
22
  from cc_transcript.ids import EventRef
23
23
  from cc_transcript.models import AssistantEvent, ToolResultBlock, ToolUseBlock, UserEvent
24
24
  from cc_transcript.parser import parse
25
- from cc_transcript.tools import file_path_of, hunks_of, parse_tool_call, parse_tool_result
25
+ from cc_transcript.tools import edits_of, parse_tool_call, parse_tool_result
26
26
 
27
27
  if TYPE_CHECKING:
28
28
  from collections.abc import Sequence
@@ -63,6 +63,8 @@ class ToolUse:
63
63
  result: The matching result block, or None when none ever arrived.
64
64
  result_ts: The timestamp of the user entry carrying the result, or
65
65
  None when no result ever arrived.
66
+ edits: The call's lowered ``(file_path, hunks)`` entries, one per
67
+ edited file.
66
68
  turn_index: The index of the turn the call fired in.
67
69
  ts: The timestamp of the assistant entry carrying the call.
68
70
  """
@@ -71,6 +73,7 @@ class ToolUse:
71
73
  call: ToolCall | FallbackCall
72
74
  result: ToolResultBlock | None
73
75
  result_ts: datetime | None
76
+ edits: tuple[tuple[str, tuple[Hunk, ...]], ...]
74
77
  turn_index: int
75
78
  ts: datetime
76
79
 
@@ -139,11 +142,12 @@ class Turn:
139
142
 
140
143
  @property
141
144
  def edits(self) -> tuple[Edit, ...]:
142
- """The turn's file modifications: tool uses with hunks and a file path."""
145
+ """The turn's file modifications: one entry per edited file (every file of an
146
+ apply_patch), in order."""
143
147
  return tuple(
144
148
  Edit(file_path=path, hunks=hunks, tool=use.call.name, ref=use.ref, turn_index=use.turn_index, ts=use.ts)
145
149
  for use in self.tool_uses
146
- if (hunks := hunks_of(use.call)) and (path := file_path_of(use.call)) is not None
150
+ for path, hunks in use.edits
147
151
  )
148
152
 
149
153
 
@@ -210,12 +214,14 @@ class SessionActivity:
210
214
  if isinstance(candidate, ToolResultBlock) and candidate.tool_use_id == use["tool_use_id"]
211
215
  )
212
216
  result_ts = result_event.meta.timestamp
217
+ call = parse_tool_call(block.name, block.input, on_error="other")
213
218
  tool_uses.append(
214
219
  ToolUse(
215
220
  ref=EventRef(session_id, event.meta.uuid, block.id),
216
- call=parse_tool_call(block.name, block.input, on_error="other"),
221
+ call=call,
217
222
  result=result,
218
223
  result_ts=result_ts,
224
+ edits=edits_of(call),
219
225
  turn_index=index,
220
226
  ts=event.meta.timestamp,
221
227
  )
@@ -158,6 +158,29 @@ def discover(root: Path | None = None) -> tuple[CodexRollout, ...]:
158
158
  return tuple(CodexRollout(*rollout) for rollout in _native.codex_discover(sessions_root(root)))
159
159
 
160
160
 
161
+ def children_of(
162
+ session_id: SessionId, *, root: Path | None = None
163
+ ) -> tuple[CodexRollout, ...]:
164
+ """Finds the direct child rollouts spawned by ``session_id``.
165
+
166
+ Compressed rollouts are excluded because their session metadata cannot yet be
167
+ inspected. Results are ordered newest first.
168
+
169
+ Args:
170
+ session_id: The parent rollout's thread id.
171
+ root: The sessions root; when None, :func:`sessions_root`.
172
+
173
+ Returns:
174
+ The direct child rollouts, newest first; ``()`` when there are none.
175
+ """
176
+ from cc_transcript import _native
177
+
178
+ return tuple(
179
+ CodexRollout(*rollout)
180
+ for rollout in _native.codex_children_of(session_id, str(sessions_root(root)))
181
+ )
182
+
183
+
161
184
  def find_transcript(session_id: SessionId, root: Path | None = None) -> Path | None:
162
185
  """Locates ``session_id``'s rollout under the codex sessions tree.
163
186
 
@@ -14,7 +14,7 @@ from dataclasses import dataclass
14
14
  from datetime import UTC, datetime
15
15
  from typing import TYPE_CHECKING
16
16
 
17
- from cc_transcript.activity import hunk_overlap
17
+ from cc_transcript import _native
18
18
  from cc_transcript.corrections import Correction
19
19
  from cc_transcript.ids import tool_digest
20
20
  from cc_transcript.models import AssistantEvent, ToolUseBlock
@@ -165,22 +165,29 @@ def harvest_pairs(
165
165
 
166
166
 
167
167
  def overlap_between(incorrect: tuple[Hunk, ...], correction: tuple[Hunk, ...]) -> float:
168
- return max((hunk_overlap(a, b) for a in incorrect for b in correction), default=0.0)
168
+ return _native.activity_overlap_between(
169
+ [(h.old, h.new) for h in incorrect], [(h.old, h.new) for h in correction]
170
+ )
169
171
 
170
172
 
171
173
  def harvest_one(activity: SessionActivity, edit: Edit, *, lookahead_turns: int, repo: Path | None) -> CandidatePair:
172
174
  if matches := match_corrections(activity, edit, lookahead_turns=lookahead_turns):
173
175
  return matches[0]
174
- if repo is not None and (fixes := git_corrections(repo, pickaxe_hunk(edit), path=edit.file_path, since=edit.ts)):
176
+ if (
177
+ repo is not None
178
+ and (hunk := pickaxe_hunk(edit)) is not None
179
+ and (fixes := git_corrections(repo, hunk, path=edit.file_path, since=edit.ts))
180
+ ):
175
181
  overlap, fix = max(((overlap_between(edit.hunks, fix.hunks), fix) for fix in fixes), key=lambda s: s[0])
176
182
  return CandidatePair(incorrect=edit, correction=fix, overlap=overlap)
177
183
  return CandidatePair(incorrect=edit, correction=None, overlap=0.0)
178
184
 
179
185
 
180
- def pickaxe_hunk(edit: Edit) -> Hunk:
186
+ def pickaxe_hunk(edit: Edit) -> Hunk | None:
181
187
  return max(
182
188
  edit.hunks,
183
189
  key=lambda hunk: max((len(line.strip()) for line in hunk.new.splitlines() if line.strip()), default=0),
190
+ default=None,
184
191
  )
185
192
 
186
193
 
@@ -280,14 +287,4 @@ def correction_columns(
280
287
 
281
288
 
282
289
  def parse_show_hunks(diff: str) -> tuple[Hunk, ...]:
283
- sections: list[tuple[list[str], list[str]]] = []
284
- for line in diff.splitlines():
285
- if line.startswith("@@"):
286
- sections.append(([], []))
287
- elif not sections or line.startswith(("---", "+++")):
288
- continue
289
- elif line.startswith("-"):
290
- sections[-1][0].append(line[1:])
291
- elif line.startswith("+"):
292
- sections[-1][1].append(line[1:])
293
- return tuple(Hunk("\n".join(old), "\n".join(new)) for old, new in sections)
290
+ return tuple(Hunk(old, new) for old, new in _native.activity_parse_show_hunks(diff))
@@ -288,19 +288,10 @@ async def suggest_canonical_keys(store: FeedbackStore, text: str, *, prompt_vers
288
288
  await prepare_connection(store)
289
289
  embedder = await asyncio.to_thread(default_embedder)
290
290
  query = serialize_vector(await asyncio.to_thread(embedder, text))
291
- ranked: dict[str, list[tuple[float, str]]] = {}
292
- for row in await store.sql(
293
- "SELECT e.canonical_key AS ck, e.evidence_text AS ev, vec_distance_cosine(v.embedding, ?) AS dist "
294
- "FROM verdict_vectors v JOIN verdict_evidence e ON e.vector_id = v.vector_id "
295
- "WHERE e.prompt_version = ? ORDER BY dist",
296
- [query, prompt_version],
297
- ):
298
- ranked.setdefault(str(row["ck"]), []).append((1.0 - float(row["dist"]), str(row["ev"])))
299
- return sorted(
300
- (Suggestion(ck, hits[0][0], tuple(ev for _, ev in hits[:3])) for ck, hits in ranked.items()),
301
- key=lambda suggestion: suggestion.score,
302
- reverse=True,
303
- )[:k]
291
+ return [
292
+ Suggestion(ck, score, tuple(sentences))
293
+ for ck, score, sentences in await store.engine.suggest_canonical_keys(query, prompt_version, k)
294
+ ]
304
295
 
305
296
 
306
297
  async def near_duplicate_keys(store: FeedbackStore, *, prompt_version: int, threshold: float) -> list[KeyOverlap]:
@@ -325,26 +316,8 @@ async def near_duplicate_keys(store: FeedbackStore, *, prompt_version: int, thre
325
316
  ImportError: When the ``cc-transcript[judge]`` extra is not installed.
326
317
  """
327
318
  require_judge_extra()
328
- import numpy as np
329
-
330
319
  await prepare_connection(store)
331
- groups: dict[str, list[np.ndarray]] = {}
332
- for row in await store.sql(
333
- "SELECT e.canonical_key AS ck, v.embedding AS emb "
334
- "FROM verdict_vectors v JOIN verdict_evidence e ON e.vector_id = v.vector_id "
335
- "WHERE e.prompt_version = ?",
336
- [prompt_version],
337
- ):
338
- groups.setdefault(str(row["ck"]), []).append(np.frombuffer(row["emb"], dtype=np.float32))
339
- centroids = {ck: (mean := np.mean(vectors, axis=0)) / np.linalg.norm(mean) for ck, vectors in groups.items()}
340
- keys = sorted(centroids)
341
- return sorted(
342
- (
343
- KeyOverlap(key_a, key_b, similarity)
344
- for i, key_a in enumerate(keys)
345
- for key_b in keys[i + 1 :]
346
- if (similarity := float(np.dot(centroids[key_a], centroids[key_b]))) > threshold
347
- ),
348
- key=lambda overlap: overlap.similarity,
349
- reverse=True,
350
- )
320
+ return [
321
+ KeyOverlap(key_a, key_b, similarity)
322
+ for key_a, key_b, similarity in await store.engine.near_duplicate_keys(prompt_version, threshold)
323
+ ]