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.
- {cc_transcript-14.4.0 → cc_transcript-14.6.0}/PKG-INFO +1 -1
- {cc_transcript-14.4.0 → cc_transcript-14.6.0}/cc_transcript/__init__.py +1 -0
- {cc_transcript-14.4.0 → cc_transcript-14.6.0}/cc_transcript/__init__.pyi +2 -0
- {cc_transcript-14.4.0 → cc_transcript-14.6.0}/cc_transcript/_native.pyi +87 -28
- {cc_transcript-14.4.0 → cc_transcript-14.6.0}/cc_transcript/command.py +4 -0
- {cc_transcript-14.4.0 → cc_transcript-14.6.0}/cc_transcript/corrections.py +42 -21
- {cc_transcript-14.4.0 → cc_transcript-14.6.0}/cc_transcript/decisions.py +24 -17
- {cc_transcript-14.4.0 → cc_transcript-14.6.0}/cc_transcript/evidence.py +2 -2
- {cc_transcript-14.4.0 → cc_transcript-14.6.0}/cc_transcript/extract/correct.py +2 -2
- {cc_transcript-14.4.0 → cc_transcript-14.6.0}/cc_transcript/heartbeats.py +40 -20
- {cc_transcript-14.4.0 → cc_transcript-14.6.0}/cc_transcript/judge/similar.py +27 -26
- cc_transcript-14.6.0/cc_transcript/ledger.py +163 -0
- {cc_transcript-14.4.0 → cc_transcript-14.6.0}/cc_transcript/literals.py +7 -1
- {cc_transcript-14.4.0 → cc_transcript-14.6.0}/cc_transcript/mining/store.py +95 -74
- {cc_transcript-14.4.0 → cc_transcript-14.6.0}/cc_transcript/watch.py +23 -10
- {cc_transcript-14.4.0 → cc_transcript-14.6.0}/pyproject.toml +1 -1
- {cc_transcript-14.4.0 → cc_transcript-14.6.0}/rust/crates/cli/src/target.rs +2 -2
- cc_transcript-14.6.0/rust/crates/core/src/actor.rs +166 -0
- cc_transcript-14.6.0/rust/crates/core/src/codex/discovery.rs +192 -0
- cc_transcript-14.6.0/rust/crates/core/src/codex/lower.rs +1342 -0
- {cc_transcript-14.4.0 → cc_transcript-14.6.0}/rust/crates/core/src/codex/mod.rs +6 -0
- cc_transcript-14.6.0/rust/crates/core/src/codex/probe.rs +546 -0
- {cc_transcript-14.4.0 → cc_transcript-14.6.0}/rust/crates/core/src/command.rs +772 -39
- cc_transcript-14.6.0/rust/crates/core/src/gateway.rs +207 -0
- {cc_transcript-14.4.0 → cc_transcript-14.6.0}/rust/crates/core/src/lib.rs +3 -0
- {cc_transcript-14.4.0 → cc_transcript-14.6.0}/rust/crates/core/src/literals/command.rs +5 -0
- cc_transcript-14.6.0/rust/crates/py/src/actor_bridge.rs +135 -0
- {cc_transcript-14.4.0 → cc_transcript-14.6.0}/rust/crates/py/src/context.rs +2 -2
- cc_transcript-14.6.0/rust/crates/py/src/corrections.rs +260 -0
- cc_transcript-14.6.0/rust/crates/py/src/feedback.rs +405 -0
- {cc_transcript-14.4.0 → cc_transcript-14.6.0}/rust/crates/py/src/lib.rs +1 -0
- {cc_transcript-14.4.0 → cc_transcript-14.6.0}/rust/crates/py/src/python.rs +26 -16
- {cc_transcript-14.4.0 → cc_transcript-14.6.0}/rust/crates/py/src/render.rs +4 -4
- {cc_transcript-14.4.0 → cc_transcript-14.6.0}/rust/crates/py/src/views/command.rs +126 -2
- {cc_transcript-14.4.0 → cc_transcript-14.6.0}/rust/crates/py/src/views/mod.rs +1 -0
- cc_transcript-14.4.0/cc_transcript/ledger.py +0 -94
- cc_transcript-14.4.0/rust/crates/py/src/corrections.rs +0 -200
- cc_transcript-14.4.0/rust/crates/py/src/feedback.rs +0 -321
- {cc_transcript-14.4.0 → cc_transcript-14.6.0}/Cargo.lock +0 -0
- {cc_transcript-14.4.0 → cc_transcript-14.6.0}/Cargo.toml +0 -0
- {cc_transcript-14.4.0 → cc_transcript-14.6.0}/LICENSE +0 -0
- {cc_transcript-14.4.0 → cc_transcript-14.6.0}/README.md +0 -0
- {cc_transcript-14.4.0 → cc_transcript-14.6.0}/cc_transcript/activity.py +0 -0
- {cc_transcript-14.4.0 → cc_transcript-14.6.0}/cc_transcript/activity_probe.py +0 -0
- {cc_transcript-14.4.0 → cc_transcript-14.6.0}/cc_transcript/builders.py +0 -0
- {cc_transcript-14.4.0 → cc_transcript-14.6.0}/cc_transcript/context.py +0 -0
- {cc_transcript-14.4.0 → cc_transcript-14.6.0}/cc_transcript/cost.py +0 -0
- {cc_transcript-14.4.0 → cc_transcript-14.6.0}/cc_transcript/discovery.py +0 -0
- {cc_transcript-14.4.0 → cc_transcript-14.6.0}/cc_transcript/disktruth.py +0 -0
- {cc_transcript-14.4.0 → cc_transcript-14.6.0}/cc_transcript/extract/__init__.py +0 -0
- {cc_transcript-14.4.0 → cc_transcript-14.6.0}/cc_transcript/facts.py +0 -0
- {cc_transcript-14.4.0 → cc_transcript-14.6.0}/cc_transcript/filterspec.py +0 -0
- {cc_transcript-14.4.0 → cc_transcript-14.6.0}/cc_transcript/ids.py +0 -0
- {cc_transcript-14.4.0 → cc_transcript-14.6.0}/cc_transcript/judge/__init__.py +0 -0
- {cc_transcript-14.4.0 → cc_transcript-14.6.0}/cc_transcript/judge/llm.py +0 -0
- {cc_transcript-14.4.0 → cc_transcript-14.6.0}/cc_transcript/judge/verdicts.py +0 -0
- {cc_transcript-14.4.0 → cc_transcript-14.6.0}/cc_transcript/mining/__init__.py +0 -0
- {cc_transcript-14.4.0 → cc_transcript-14.6.0}/cc_transcript/mining/candidates.py +0 -0
- {cc_transcript-14.4.0 → cc_transcript-14.6.0}/cc_transcript/mining/confidence.py +0 -0
- {cc_transcript-14.4.0 → cc_transcript-14.6.0}/cc_transcript/mining/engine.py +0 -0
- {cc_transcript-14.4.0 → cc_transcript-14.6.0}/cc_transcript/mining/formats.py +0 -0
- {cc_transcript-14.4.0 → cc_transcript-14.6.0}/cc_transcript/mining/sampling.py +0 -0
- {cc_transcript-14.4.0 → cc_transcript-14.6.0}/cc_transcript/mining/signals.py +0 -0
- {cc_transcript-14.4.0 → cc_transcript-14.6.0}/cc_transcript/mining/sourcekind.py +0 -0
- {cc_transcript-14.4.0 → cc_transcript-14.6.0}/cc_transcript/mining/spec.py +0 -0
- {cc_transcript-14.4.0 → cc_transcript-14.6.0}/cc_transcript/models.py +0 -0
- {cc_transcript-14.4.0 → cc_transcript-14.6.0}/cc_transcript/nlp.py +0 -0
- {cc_transcript-14.4.0 → cc_transcript-14.6.0}/cc_transcript/notifications.py +0 -0
- {cc_transcript-14.4.0 → cc_transcript-14.6.0}/cc_transcript/parser.py +0 -0
- {cc_transcript-14.4.0 → cc_transcript-14.6.0}/cc_transcript/py.typed +0 -0
- {cc_transcript-14.4.0 → cc_transcript-14.6.0}/cc_transcript/query.py +0 -0
- {cc_transcript-14.4.0 → cc_transcript-14.6.0}/cc_transcript/render.py +0 -0
- {cc_transcript-14.4.0 → cc_transcript-14.6.0}/cc_transcript/sentiment/__init__.py +0 -0
- {cc_transcript-14.4.0 → cc_transcript-14.6.0}/cc_transcript/sentiment/buckets.py +0 -0
- {cc_transcript-14.4.0 → cc_transcript-14.6.0}/cc_transcript/sentiment/data/afinn-en-165.tsv +0 -0
- {cc_transcript-14.4.0 → cc_transcript-14.6.0}/cc_transcript/sentiment/data/domain_overrides.tsv +0 -0
- {cc_transcript-14.4.0 → cc_transcript-14.6.0}/cc_transcript/sentiment/data/en-ewt.udpipe +0 -0
- {cc_transcript-14.4.0 → cc_transcript-14.6.0}/cc_transcript/sentiment/engine.py +0 -0
- {cc_transcript-14.4.0 → cc_transcript-14.6.0}/cc_transcript/sentiment/lexicon.py +0 -0
- {cc_transcript-14.4.0 → cc_transcript-14.6.0}/cc_transcript/sentiment/scorespec.py +0 -0
- {cc_transcript-14.4.0 → cc_transcript-14.6.0}/cc_transcript/synthetic.py +0 -0
- {cc_transcript-14.4.0 → cc_transcript-14.6.0}/cc_transcript/tools.py +0 -0
- {cc_transcript-14.4.0 → cc_transcript-14.6.0}/rust/crates/cli/Cargo.toml +0 -0
- {cc_transcript-14.4.0 → cc_transcript-14.6.0}/rust/crates/cli/src/commands/corrections.rs +0 -0
- {cc_transcript-14.4.0 → cc_transcript-14.6.0}/rust/crates/cli/src/commands/digest.rs +0 -0
- {cc_transcript-14.4.0 → cc_transcript-14.6.0}/rust/crates/cli/src/commands/facts.rs +0 -0
- {cc_transcript-14.4.0 → cc_transcript-14.6.0}/rust/crates/cli/src/commands/grep.rs +0 -0
- {cc_transcript-14.4.0 → cc_transcript-14.6.0}/rust/crates/cli/src/commands/list.rs +0 -0
- {cc_transcript-14.4.0 → cc_transcript-14.6.0}/rust/crates/cli/src/commands/mod.rs +0 -0
- {cc_transcript-14.4.0 → cc_transcript-14.6.0}/rust/crates/cli/src/commands/scratchpad.rs +0 -0
- {cc_transcript-14.4.0 → cc_transcript-14.6.0}/rust/crates/cli/src/commands/show.rs +0 -0
- {cc_transcript-14.4.0 → cc_transcript-14.6.0}/rust/crates/cli/src/commands/slice.rs +0 -0
- {cc_transcript-14.4.0 → cc_transcript-14.6.0}/rust/crates/cli/src/commands/stats.rs +0 -0
- {cc_transcript-14.4.0 → cc_transcript-14.6.0}/rust/crates/cli/src/commands/watch.rs +0 -0
- {cc_transcript-14.4.0 → cc_transcript-14.6.0}/rust/crates/cli/src/lib.rs +0 -0
- {cc_transcript-14.4.0 → cc_transcript-14.6.0}/rust/crates/cli/src/main.rs +0 -0
- {cc_transcript-14.4.0 → cc_transcript-14.6.0}/rust/crates/cli/src/output.rs +0 -0
- {cc_transcript-14.4.0 → cc_transcript-14.6.0}/rust/crates/core/Cargo.toml +0 -0
- {cc_transcript-14.4.0 → cc_transcript-14.6.0}/rust/crates/core/benches/parse.rs +0 -0
- {cc_transcript-14.4.0 → cc_transcript-14.6.0}/rust/crates/core/benches/render.rs +0 -0
- {cc_transcript-14.4.0 → cc_transcript-14.6.0}/rust/crates/core/benches/toolcall.rs +0 -0
- {cc_transcript-14.4.0 → cc_transcript-14.6.0}/rust/crates/core/src/activity.rs +0 -0
- {cc_transcript-14.4.0 → cc_transcript-14.6.0}/rust/crates/core/src/buckets.rs +0 -0
- {cc_transcript-14.4.0 → cc_transcript-14.6.0}/rust/crates/core/src/codex/parse.rs +0 -0
- {cc_transcript-14.4.0 → cc_transcript-14.6.0}/rust/crates/core/src/codex/protocol.rs +0 -0
- {cc_transcript-14.4.0 → cc_transcript-14.6.0}/rust/crates/core/src/codex/types.rs +0 -0
- {cc_transcript-14.4.0 → cc_transcript-14.6.0}/rust/crates/core/src/context.rs +0 -0
- {cc_transcript-14.4.0 → cc_transcript-14.6.0}/rust/crates/core/src/corrections.rs +0 -0
- {cc_transcript-14.4.0 → cc_transcript-14.6.0}/rust/crates/core/src/cost.rs +0 -0
- {cc_transcript-14.4.0 → cc_transcript-14.6.0}/rust/crates/core/src/discovery.rs +0 -0
- {cc_transcript-14.4.0 → cc_transcript-14.6.0}/rust/crates/core/src/facts.rs +0 -0
- {cc_transcript-14.4.0 → cc_transcript-14.6.0}/rust/crates/core/src/feedback.rs +0 -0
- {cc_transcript-14.4.0 → cc_transcript-14.6.0}/rust/crates/core/src/filter.rs +0 -0
- {cc_transcript-14.4.0 → cc_transcript-14.6.0}/rust/crates/core/src/ids.rs +0 -0
- {cc_transcript-14.4.0 → cc_transcript-14.6.0}/rust/crates/core/src/literals/corrections.rs +0 -0
- {cc_transcript-14.4.0 → cc_transcript-14.6.0}/rust/crates/core/src/literals/feedback.rs +0 -0
- {cc_transcript-14.4.0 → cc_transcript-14.6.0}/rust/crates/core/src/literals/mining.rs +0 -0
- {cc_transcript-14.4.0 → cc_transcript-14.6.0}/rust/crates/core/src/literals/mod.rs +0 -0
- {cc_transcript-14.4.0 → cc_transcript-14.6.0}/rust/crates/core/src/literals/protocol.rs +0 -0
- {cc_transcript-14.4.0 → cc_transcript-14.6.0}/rust/crates/core/src/notifications.rs +0 -0
- {cc_transcript-14.4.0 → cc_transcript-14.6.0}/rust/crates/core/src/parse.rs +0 -0
- {cc_transcript-14.4.0 → cc_transcript-14.6.0}/rust/crates/core/src/protocol.rs +0 -0
- {cc_transcript-14.4.0 → cc_transcript-14.6.0}/rust/crates/core/src/pystr.rs +0 -0
- {cc_transcript-14.4.0 → cc_transcript-14.6.0}/rust/crates/core/src/query.rs +0 -0
- {cc_transcript-14.4.0 → cc_transcript-14.6.0}/rust/crates/core/src/render.rs +0 -0
- {cc_transcript-14.4.0 → cc_transcript-14.6.0}/rust/crates/core/src/sqlite.rs +0 -0
- {cc_transcript-14.4.0 → cc_transcript-14.6.0}/rust/crates/core/src/toolcall.rs +0 -0
- {cc_transcript-14.4.0 → cc_transcript-14.6.0}/rust/crates/core/src/types.rs +0 -0
- {cc_transcript-14.4.0 → cc_transcript-14.6.0}/rust/crates/core/src/value.rs +0 -0
- {cc_transcript-14.4.0 → cc_transcript-14.6.0}/rust/crates/core/src/watch.rs +0 -0
- {cc_transcript-14.4.0 → cc_transcript-14.6.0}/rust/crates/py/Cargo.toml +0 -0
- {cc_transcript-14.4.0 → cc_transcript-14.6.0}/rust/crates/py/build.rs +0 -0
- {cc_transcript-14.4.0 → cc_transcript-14.6.0}/rust/crates/py/data/command_prefix_pins.tsv +0 -0
- {cc_transcript-14.4.0 → cc_transcript-14.6.0}/rust/crates/py/src/bin/stub_gen.rs +0 -0
- {cc_transcript-14.4.0 → cc_transcript-14.6.0}/rust/crates/py/src/command.rs +0 -0
- {cc_transcript-14.4.0 → cc_transcript-14.6.0}/rust/crates/py/src/discovery.rs +0 -0
- {cc_transcript-14.4.0 → cc_transcript-14.6.0}/rust/crates/py/src/lexicon.rs +0 -0
- {cc_transcript-14.4.0 → cc_transcript-14.6.0}/rust/crates/py/src/mining.rs +0 -0
- {cc_transcript-14.4.0 → cc_transcript-14.6.0}/rust/crates/py/src/nlp.rs +0 -0
- {cc_transcript-14.4.0 → cc_transcript-14.6.0}/rust/crates/py/src/score.rs +0 -0
- {cc_transcript-14.4.0 → cc_transcript-14.6.0}/rust/crates/py/src/sqlite.rs +0 -0
- {cc_transcript-14.4.0 → cc_transcript-14.6.0}/rust/crates/py/src/toolcall.rs +0 -0
- {cc_transcript-14.4.0 → cc_transcript-14.6.0}/rust/crates/py/src/views/attachment.rs +0 -0
- {cc_transcript-14.4.0 → cc_transcript-14.6.0}/rust/crates/py/src/views/blocks.rs +0 -0
- {cc_transcript-14.4.0 → cc_transcript-14.6.0}/rust/crates/py/src/views/convert.rs +0 -0
- {cc_transcript-14.4.0 → cc_transcript-14.6.0}/rust/crates/py/src/views/dunder.rs +0 -0
- {cc_transcript-14.4.0 → cc_transcript-14.6.0}/rust/crates/py/src/views/events.rs +0 -0
- {cc_transcript-14.4.0 → cc_transcript-14.6.0}/rust/crates/py/src/views/meta.rs +0 -0
- {cc_transcript-14.4.0 → cc_transcript-14.6.0}/rust/crates/py/src/views/print.rs +0 -0
- {cc_transcript-14.4.0 → cc_transcript-14.6.0}/rust/crates/py/src/views/store.rs +0 -0
- {cc_transcript-14.4.0 → cc_transcript-14.6.0}/rust/crates/py/src/views/system.rs +0 -0
- {cc_transcript-14.4.0 → cc_transcript-14.6.0}/rust/crates/py/src/views/toolcall.rs +0 -0
- {cc_transcript-14.4.0 → cc_transcript-14.6.0}/rust/crates/py/src/views/toolresult.rs +0 -0
- {cc_transcript-14.4.0 → cc_transcript-14.6.0}/rust/crates/py/src/views/transcript.rs +0 -0
- {cc_transcript-14.4.0 → cc_transcript-14.6.0}/rust/crates/py/src/watch.rs +0 -0
|
@@ -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
|
|
1451
|
-
def
|
|
1452
|
-
def
|
|
1453
|
-
def
|
|
1454
|
-
def
|
|
1455
|
-
def
|
|
1456
|
-
def
|
|
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
|
|
1462
|
-
def
|
|
1463
|
-
def
|
|
1464
|
-
def
|
|
1465
|
-
def
|
|
1466
|
-
def
|
|
1467
|
-
def
|
|
1468
|
-
def
|
|
1469
|
-
def
|
|
1470
|
-
def
|
|
1471
|
-
def
|
|
1472
|
-
def
|
|
1473
|
-
def
|
|
1474
|
-
def
|
|
1475
|
-
def
|
|
1476
|
-
def
|
|
1477
|
-
def
|
|
1478
|
-
def
|
|
1479
|
-
def
|
|
1480
|
-
def
|
|
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
|
-
>>>
|
|
97
|
-
|
|
98
|
-
|
|
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
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
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
|
|
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(
|
|
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
|
|
100
|
-
|
|
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
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
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.
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
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
|
-
>>>
|
|
68
|
-
|
|
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,
|
|
72
|
-
self.
|
|
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
|
-
|
|
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
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
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
|
|
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()
|