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.
@@ -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