cctally 1.45.0 → 1.47.0
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.
- package/CHANGELOG.md +10 -0
- package/bin/_cctally_cache.py +94 -19
- package/bin/_cctally_config.py +68 -0
- package/bin/_cctally_dashboard.py +144 -16
- package/bin/_lib_conversation_query.py +48 -3
- package/bin/_lib_conversation_watch.py +67 -0
- package/dashboard/static/assets/index-BOR4FBvt.js +68 -0
- package/dashboard/static/assets/index-CuAtCrys.css +1 -0
- package/dashboard/static/dashboard.html +2 -2
- package/package.json +2 -1
- package/dashboard/static/assets/index-Ci-5UHk3.js +0 -68
- package/dashboard/static/assets/index-Dgq5Vvml.css +0 -1
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
"""Pure watch-step kernel for the conversation live-tail (spec §2.4).
|
|
2
|
+
|
|
3
|
+
No I/O of its own — the filesystem `stat` and the targeted ingest are injected
|
|
4
|
+
as callables so the cycle is unit-testable without real timing, threads, or a
|
|
5
|
+
DB. The thin SSE/sleep/keep-alive driver lives in bin/_cctally_dashboard.py.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def file_sig(path):
|
|
10
|
+
"""Size-only signature (st_size, an int) for a path, or None if it can't be
|
|
11
|
+
stat'd (deleted / rotated). Size-only by design: it must match sync_cache's
|
|
12
|
+
own size-only delta signal (Claude Code's JSONL sessions are strictly
|
|
13
|
+
append-only, so a size change is sufficient) — mtime jitter must NOT drive a
|
|
14
|
+
re-emit, since a size-unchanged ingest does not refresh session_files.mtime_ns
|
|
15
|
+
and a stale mtime would otherwise re-detect "changed" every cycle forever.
|
|
16
|
+
Pure-ish — the only I/O, isolated here so callers can inject a fake in
|
|
17
|
+
tests."""
|
|
18
|
+
import os
|
|
19
|
+
try:
|
|
20
|
+
st = os.stat(path)
|
|
21
|
+
except OSError:
|
|
22
|
+
return None
|
|
23
|
+
return st.st_size
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def changed_paths(files, seen, stat_fn=file_sig):
|
|
27
|
+
"""Paths whose current size-only signature differs from `seen` (matches
|
|
28
|
+
sync_cache's size-only delta; mtime jitter must not drive a re-emit). An
|
|
29
|
+
unstatable path (stat_fn → None) is skipped (dropped this cycle, re-resolved
|
|
30
|
+
later); a path absent from `seen` counts as changed (first observation /
|
|
31
|
+
pre-connect growth)."""
|
|
32
|
+
out = []
|
|
33
|
+
for p in files:
|
|
34
|
+
sig = stat_fn(p)
|
|
35
|
+
if sig is None:
|
|
36
|
+
continue
|
|
37
|
+
if seen.get(p) != sig:
|
|
38
|
+
out.append(p)
|
|
39
|
+
return out
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def watch_step(files, seen, *, stat_fn=file_sig, ingest_fn, committed_sig_fn=None):
|
|
43
|
+
"""One watch cycle. Returns (new_seen, emitted).
|
|
44
|
+
|
|
45
|
+
Detect changed files (by size-only signature — matches sync_cache's
|
|
46
|
+
size-only delta; mtime jitter must not drive a re-emit) → run
|
|
47
|
+
ingest_fn(changed) (targeted sync_cache). Emit + advance `seen` ONLY on a
|
|
48
|
+
clean ingest (stats.targeted_clean). `seen` is advanced to the COMMITTED
|
|
49
|
+
cache cursor (committed_sig_fn) — NOT a fresh filesystem re-stat — so a file
|
|
50
|
+
that grew during the ingest is still seen as changed next cycle (the cache
|
|
51
|
+
cursor, in session_files, lags the new disk size). committed_sig_fn defaults
|
|
52
|
+
to stat_fn for pure unit tests with no cache. A contended/declined/failed
|
|
53
|
+
ingest leaves `seen` untouched so the next cycle retries (the 5s backstop is
|
|
54
|
+
the floor)."""
|
|
55
|
+
committed_sig_fn = committed_sig_fn or stat_fn
|
|
56
|
+
changed = changed_paths(files, seen, stat_fn)
|
|
57
|
+
if not changed:
|
|
58
|
+
return seen, False
|
|
59
|
+
stats = ingest_fn(changed)
|
|
60
|
+
if not getattr(stats, "targeted_clean", False):
|
|
61
|
+
return seen, False
|
|
62
|
+
new_seen = dict(seen)
|
|
63
|
+
for p in changed:
|
|
64
|
+
sig = committed_sig_fn(p)
|
|
65
|
+
if sig is not None:
|
|
66
|
+
new_seen[p] = sig
|
|
67
|
+
return new_seen, True
|