cctally 1.71.0 → 1.73.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 +20 -0
- package/bin/_cctally_cache.py +501 -49
- package/bin/_cctally_core.py +20 -40
- package/bin/_cctally_dashboard_conversation.py +731 -94
- package/bin/_cctally_db.py +286 -1
- package/bin/_cctally_doctor.py +59 -0
- package/bin/_cctally_parser.py +17 -4
- package/bin/_cctally_record.py +162 -33
- package/bin/_cctally_refresh.py +58 -36
- package/bin/_cctally_statusline.py +811 -82
- package/bin/_cctally_transcript.py +223 -7
- package/bin/_lib_codex_conversation.py +509 -0
- package/bin/_lib_codex_conversation_export.py +124 -0
- package/bin/_lib_codex_conversation_query.py +1422 -0
- package/bin/_lib_codex_conversation_watch.py +323 -0
- package/bin/_lib_conversation_dispatch.py +809 -0
- package/bin/_lib_conversation_query.py +88 -0
- package/bin/_lib_conversation_retention.py +344 -0
- package/bin/_lib_doctor.py +62 -0
- package/bin/_lib_statusline_candidates.py +734 -0
- package/bin/cctally +31 -2
- package/package.json +8 -1
|
@@ -0,0 +1,323 @@
|
|
|
1
|
+
"""Pure directory-frontier kernel for Codex live-tail child discovery (spec §5.4).
|
|
2
|
+
|
|
3
|
+
Layout-agnostic, budget-bounded, incremental discovery of brand-new Codex
|
|
4
|
+
rollout files that no table yet knows — the second of the two live-tail child-
|
|
5
|
+
discovery layers (the first being the driver's DB re-resolve of the watched
|
|
6
|
+
conversation's file set). This module has NO I/O of its own: the filesystem
|
|
7
|
+
``scandir`` / directory-``mtime`` / file-``size`` reads and the clock are
|
|
8
|
+
injected as callables, so the whole cycle is unit-testable without a real tree,
|
|
9
|
+
real timing, or a DB. The thin SSE driver in
|
|
10
|
+
``bin/_cctally_dashboard_conversation.py`` owns the DB re-resolve, the targeted
|
|
11
|
+
ingest of the paths this kernel surfaces, and the child classification that
|
|
12
|
+
feeds ``reap``; this kernel owns only the directory frontier, the pending-
|
|
13
|
+
candidate set, and the shared per-cycle operation budget.
|
|
14
|
+
|
|
15
|
+
All six §5.4 bullets are contract:
|
|
16
|
+
|
|
17
|
+
1. **walk_root-only seed.** The frontier index starts as JUST the configured
|
|
18
|
+
``CodexProviderRoot.walk_root`` — construction walks nothing. Directories are
|
|
19
|
+
discovered incrementally as the frontier is enumerated.
|
|
20
|
+
2. **One shared per-cycle operation budget** covering directory ``stat``s,
|
|
21
|
+
directory enumerations, and pending-candidate ``stat``s, with round-robin
|
|
22
|
+
fairness across the three work classes and rotating continuation cursors, so
|
|
23
|
+
a large tree or a large candidate population can neither starve the frontier
|
|
24
|
+
nor blow the cycle's ceiling. Every known directory / candidate is
|
|
25
|
+
*eventually* visited — not all of them every cycle.
|
|
26
|
+
3. **Re-enumeration on own-mtime change** or new frontier membership — never on
|
|
27
|
+
an ancestor's mtime (POSIX/APFS bumps only the immediate parent's mtime on
|
|
28
|
+
entry creation, so ancestor-mtime pruning would be unsound).
|
|
29
|
+
4. **Symlink boundary.** Descendant directory/file entries are traversed with
|
|
30
|
+
``follow_symlinks=False`` semantics: a symlinked entry is NEVER traversed, so
|
|
31
|
+
every directory the frontier reaches is connected to ``walk_root`` by a chain
|
|
32
|
+
of physical (non-symlink) parent links and therefore stays physically inside
|
|
33
|
+
the canonical ``walk_root`` — the realpath-containment guarantee, achieved
|
|
34
|
+
structurally. The configured ``walk_root`` spelling itself is preserved (it is
|
|
35
|
+
the seed and is never realpath-rewritten), matching discovery's configured-
|
|
36
|
+
spelling-vs-physical distinction.
|
|
37
|
+
5. **Rotation watermark pinned to rotation-start.** A "rotation" is one full pass
|
|
38
|
+
of the stat cursor over the frontier. The completed-rotation watermark
|
|
39
|
+
advances only when a rotation completes and is pinned to that rotation's
|
|
40
|
+
START time, so an entry created mid-rotation (which changes its own
|
|
41
|
+
directory's mtime) is re-examined on the next rotation.
|
|
42
|
+
6. **Pending candidates.** Every newly discovered file joins ``pending`` BEFORE
|
|
43
|
+
its first ingest attempt (so a discovered file is never lost even if its
|
|
44
|
+
first ingest is dirty/lock-contended and leaves no cursor row). A path is
|
|
45
|
+
retried while unclassified: with no committed cursor it is surfaced every
|
|
46
|
+
time it is visited; with a committed cursor it is surfaced only when its size
|
|
47
|
+
exceeds that cursor (an incomplete-``session_meta`` tail completing without
|
|
48
|
+
any directory-mtime change). A path leaves the set when the driver ``reap``s
|
|
49
|
+
it (it gained a thread row — child or non-child), when it vanishes, or on a
|
|
50
|
+
bounded per-candidate expiry (handed to the next full sync).
|
|
51
|
+
"""
|
|
52
|
+
from __future__ import annotations
|
|
53
|
+
|
|
54
|
+
import os
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
# ── production-default injected callables ─────────────────────────────────────
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def default_scandir(dirpath):
|
|
61
|
+
"""Yield ``(name, path, is_dir, is_symlink)`` for the entries of ``dirpath``.
|
|
62
|
+
|
|
63
|
+
``is_dir`` uses ``follow_symlinks=False`` (a symlink-to-dir reads as NOT a
|
|
64
|
+
dir), and ``is_symlink`` flags any symlink so the frontier can refuse to
|
|
65
|
+
traverse it. Returns ``[]`` on any ``OSError`` (vanished / permission)."""
|
|
66
|
+
out = []
|
|
67
|
+
try:
|
|
68
|
+
with os.scandir(dirpath) as it:
|
|
69
|
+
for de in it:
|
|
70
|
+
try:
|
|
71
|
+
is_sym = de.is_symlink()
|
|
72
|
+
is_dir = de.is_dir(follow_symlinks=False)
|
|
73
|
+
except OSError:
|
|
74
|
+
continue
|
|
75
|
+
out.append((de.name, de.path, is_dir, is_sym))
|
|
76
|
+
except OSError:
|
|
77
|
+
return []
|
|
78
|
+
return out
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def default_dir_mtime(dirpath):
|
|
82
|
+
"""The directory's own mtime (``st_mtime_ns``), or ``None`` if it can't be
|
|
83
|
+
stat'd. Follows a symlinked ``walk_root`` (the seed is intentionally the
|
|
84
|
+
configured spelling)."""
|
|
85
|
+
try:
|
|
86
|
+
return os.stat(dirpath).st_mtime_ns
|
|
87
|
+
except OSError:
|
|
88
|
+
return None
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def default_file_size(filepath):
|
|
92
|
+
"""Regular-file size in bytes, or ``None`` if the path is gone or not a
|
|
93
|
+
regular file (size-only, matching sync_codex_cache's append-only delta)."""
|
|
94
|
+
try:
|
|
95
|
+
st = os.stat(filepath)
|
|
96
|
+
except OSError:
|
|
97
|
+
return None
|
|
98
|
+
import stat as _stat
|
|
99
|
+
if not _stat.S_ISREG(st.st_mode):
|
|
100
|
+
return None
|
|
101
|
+
return st.st_size
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
_SENTINEL = object()
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
class CodexChildFrontier:
|
|
108
|
+
"""The budgeted directory frontier + pending-candidate set for one watched
|
|
109
|
+
Codex conversation's root (spec §5.4). See the module docstring for the
|
|
110
|
+
contract. ``cycle`` is the only per-tick entry point; ``reap`` is called by
|
|
111
|
+
the driver after a targeted ingest classifies discovered candidates."""
|
|
112
|
+
|
|
113
|
+
def __init__(
|
|
114
|
+
self,
|
|
115
|
+
walk_root,
|
|
116
|
+
*,
|
|
117
|
+
op_budget: int = 64,
|
|
118
|
+
pending_expiry_cycles: int = 50,
|
|
119
|
+
scandir_fn=default_scandir,
|
|
120
|
+
dir_mtime_fn=default_dir_mtime,
|
|
121
|
+
file_size_fn=default_file_size,
|
|
122
|
+
clock_fn=None,
|
|
123
|
+
):
|
|
124
|
+
self._walk_root = str(walk_root)
|
|
125
|
+
self._op_budget = max(1, int(op_budget))
|
|
126
|
+
self._pending_expiry = max(1, int(pending_expiry_cycles))
|
|
127
|
+
self._scandir = scandir_fn
|
|
128
|
+
self._dir_mtime = dir_mtime_fn
|
|
129
|
+
self._file_size = file_size_fn
|
|
130
|
+
self._clock = clock_fn or (lambda: 0)
|
|
131
|
+
|
|
132
|
+
# Frontier index — seeded with JUST walk_root (no enumeration at start).
|
|
133
|
+
self._dirs: list[str] = [self._walk_root]
|
|
134
|
+
self._dir_set: set[str] = {self._walk_root}
|
|
135
|
+
# dir -> mtime observed when it was last enumerated (absent => never).
|
|
136
|
+
self._enum_mtime: dict[str, int] = {}
|
|
137
|
+
# dirs awaiting a scandir, each carried with the mtime that triggered it.
|
|
138
|
+
self._enum_queue: list[tuple[str, int]] = []
|
|
139
|
+
|
|
140
|
+
# Pending candidates: path -> age (cycles since registration).
|
|
141
|
+
self._pending: dict[str, int] = {}
|
|
142
|
+
|
|
143
|
+
# Rotating continuation cursors (persist across cycles).
|
|
144
|
+
self._stat_cursor = 0
|
|
145
|
+
self._pending_rr = 0 # where the pending sweep resumes
|
|
146
|
+
self._rotation_start = None
|
|
147
|
+
self._completed_rotation_watermark = None
|
|
148
|
+
self._rotation_count = 0
|
|
149
|
+
|
|
150
|
+
# ── introspection (tests / diagnostics) ──────────────────────────────────
|
|
151
|
+
|
|
152
|
+
def known_directories(self) -> set[str]:
|
|
153
|
+
return set(self._dir_set)
|
|
154
|
+
|
|
155
|
+
def pending_candidates(self) -> set[str]:
|
|
156
|
+
return set(self._pending)
|
|
157
|
+
|
|
158
|
+
@property
|
|
159
|
+
def rotation_watermark(self):
|
|
160
|
+
return self._completed_rotation_watermark
|
|
161
|
+
|
|
162
|
+
# ── driver feedback ───────────────────────────────────────────────────────
|
|
163
|
+
|
|
164
|
+
def reap(self, classified_paths) -> None:
|
|
165
|
+
"""Drop pending candidates the driver has classified (they gained a
|
|
166
|
+
thread row — child or non-child). A child is now in the watched file set
|
|
167
|
+
and tailed by the main loop; a non-child needs no further attention."""
|
|
168
|
+
for p in classified_paths:
|
|
169
|
+
self._pending.pop(p, None)
|
|
170
|
+
|
|
171
|
+
# ── one discovery cycle ───────────────────────────────────────────────────
|
|
172
|
+
|
|
173
|
+
def cycle(self, *, known_paths, committed_sizes) -> set[str]:
|
|
174
|
+
"""One budgeted discovery cycle (§5.4).
|
|
175
|
+
|
|
176
|
+
``known_paths`` — every path already tracked in ``codex_session_files``,
|
|
177
|
+
so a newly enumerated file already known to some table is not
|
|
178
|
+
re-registered. ``committed_sizes`` — ``{path: size_bytes}`` committed
|
|
179
|
+
cursors, for the pending-growth check.
|
|
180
|
+
|
|
181
|
+
Returns the set of candidate paths to targeted-ingest this cycle (new
|
|
182
|
+
discoveries whose first ingest hasn't landed + pending retries that grew
|
|
183
|
+
or remain unclassified). Bounded to ``op_budget`` filesystem operations,
|
|
184
|
+
round-robin fair across directory stats, enumerations, and pending
|
|
185
|
+
checks."""
|
|
186
|
+
self._known = set(known_paths)
|
|
187
|
+
self._committed = committed_sizes or {}
|
|
188
|
+
ingest: set[str] = set()
|
|
189
|
+
|
|
190
|
+
# Start (or continue) a rotation of the stat cursor over the frontier.
|
|
191
|
+
if self._rotation_start is None or self._stat_cursor >= len(self._dirs):
|
|
192
|
+
self._begin_rotation()
|
|
193
|
+
|
|
194
|
+
# Per-cycle pending sweep window: a stable snapshot of the pending keys
|
|
195
|
+
# plus a persistent rotating start offset, so a large candidate
|
|
196
|
+
# population is visited over successive cycles (never all every cycle).
|
|
197
|
+
self._pending_keys = list(self._pending.keys())
|
|
198
|
+
self._pending_start = (
|
|
199
|
+
self._pending_rr % len(self._pending_keys)
|
|
200
|
+
if self._pending_keys else 0)
|
|
201
|
+
self._pending_checked = 0
|
|
202
|
+
|
|
203
|
+
ops = 0
|
|
204
|
+
stalled = 0
|
|
205
|
+
lane_idx = 0
|
|
206
|
+
while ops < self._op_budget and stalled < 3:
|
|
207
|
+
lane = lane_idx % 3
|
|
208
|
+
if lane == 0:
|
|
209
|
+
did = self._op_stat()
|
|
210
|
+
elif lane == 1:
|
|
211
|
+
did = self._op_enum(ingest)
|
|
212
|
+
else:
|
|
213
|
+
did = self._op_pending(ingest)
|
|
214
|
+
if did:
|
|
215
|
+
ops += 1
|
|
216
|
+
stalled = 0
|
|
217
|
+
else:
|
|
218
|
+
stalled += 1
|
|
219
|
+
lane_idx += 1
|
|
220
|
+
|
|
221
|
+
# Persist the pending rotation offset so the next cycle resumes past the
|
|
222
|
+
# candidates already visited this cycle (round-robin continuation).
|
|
223
|
+
if self._pending_keys:
|
|
224
|
+
self._pending_rr = self._pending_start + self._pending_checked
|
|
225
|
+
|
|
226
|
+
self._age_pending()
|
|
227
|
+
return ingest
|
|
228
|
+
|
|
229
|
+
# ── work lanes (each does at most ONE filesystem op per call) ─────────────
|
|
230
|
+
|
|
231
|
+
def _op_stat(self) -> bool:
|
|
232
|
+
"""Stat one frontier directory (rotating cursor). A directory that is new
|
|
233
|
+
to the frontier or whose own mtime changed since its last enumeration is
|
|
234
|
+
queued for (re-)enumeration (§5.4 bullet 3). Returns True iff it consumed
|
|
235
|
+
a filesystem op."""
|
|
236
|
+
if self._stat_cursor >= len(self._dirs):
|
|
237
|
+
return False
|
|
238
|
+
d = self._dirs[self._stat_cursor]
|
|
239
|
+
self._stat_cursor += 1
|
|
240
|
+
mt = self._dir_mtime(d) # 1 fs op
|
|
241
|
+
if mt is not None:
|
|
242
|
+
prev = self._enum_mtime.get(d, _SENTINEL)
|
|
243
|
+
if (prev is _SENTINEL or prev != mt) and not self._is_queued(d):
|
|
244
|
+
self._enum_queue.append((d, mt))
|
|
245
|
+
if self._stat_cursor >= len(self._dirs):
|
|
246
|
+
self._complete_rotation()
|
|
247
|
+
return True
|
|
248
|
+
|
|
249
|
+
def _op_enum(self, ingest: set) -> bool:
|
|
250
|
+
"""Enumerate one queued directory (scandir). Records the pre-enumeration
|
|
251
|
+
mtime as the dir's baseline (so a create landing during the enumeration
|
|
252
|
+
is re-examined next rotation), skips symlinked descendants (the
|
|
253
|
+
``follow_symlinks=False`` boundary), adds real subdirectories to the
|
|
254
|
+
frontier, and registers brand-new ``*.jsonl`` files in ``pending`` BEFORE
|
|
255
|
+
their first ingest — surfacing each for that first ingest attempt in the
|
|
256
|
+
SAME cycle it is discovered (the pending sweep then owns the retries).
|
|
257
|
+
Returns True iff it consumed a filesystem op."""
|
|
258
|
+
if not self._enum_queue:
|
|
259
|
+
return False
|
|
260
|
+
d, mt = self._enum_queue.pop(0)
|
|
261
|
+
entries = self._scandir(d) # 1 fs op
|
|
262
|
+
self._enum_mtime[d] = mt
|
|
263
|
+
for (name, path, is_dir, is_sym) in entries:
|
|
264
|
+
if is_sym:
|
|
265
|
+
continue # never traverse a symlink
|
|
266
|
+
if is_dir:
|
|
267
|
+
if path not in self._dir_set:
|
|
268
|
+
self._dir_set.add(path)
|
|
269
|
+
self._dirs.append(path) # new subdir joins frontier
|
|
270
|
+
elif path.endswith(".jsonl"):
|
|
271
|
+
if path not in self._known and path not in self._pending:
|
|
272
|
+
self._pending[path] = 0 # register BEFORE first ingest
|
|
273
|
+
ingest.add(path) # first ingest attempt now
|
|
274
|
+
return True
|
|
275
|
+
|
|
276
|
+
def _op_pending(self, ingest: set) -> bool:
|
|
277
|
+
"""Check one pending candidate (rotating cursor). Vanished → dropped; no
|
|
278
|
+
committed cursor (brand-new / lock-contended first ingest) → surfaced for
|
|
279
|
+
(re-)ingest; grown beyond its committed cursor → surfaced. Returns True
|
|
280
|
+
iff it consumed a filesystem op."""
|
|
281
|
+
keys = self._pending_keys
|
|
282
|
+
n = len(keys)
|
|
283
|
+
while self._pending_checked < n:
|
|
284
|
+
idx = (self._pending_start + self._pending_checked) % n
|
|
285
|
+
self._pending_checked += 1
|
|
286
|
+
p = keys[idx]
|
|
287
|
+
if p not in self._pending: # reaped/dropped mid-cycle
|
|
288
|
+
continue
|
|
289
|
+
size = self._file_size(p) # 1 fs op
|
|
290
|
+
if size is None:
|
|
291
|
+
self._pending.pop(p, None) # vanished → drop
|
|
292
|
+
else:
|
|
293
|
+
committed = self._committed.get(p)
|
|
294
|
+
if committed is None or size > committed:
|
|
295
|
+
ingest.add(p)
|
|
296
|
+
return True
|
|
297
|
+
return False
|
|
298
|
+
|
|
299
|
+
# ── rotation + expiry bookkeeping (no filesystem ops) ─────────────────────
|
|
300
|
+
|
|
301
|
+
def _begin_rotation(self) -> None:
|
|
302
|
+
self._stat_cursor = 0
|
|
303
|
+
self._rotation_start = self._clock()
|
|
304
|
+
|
|
305
|
+
def _complete_rotation(self) -> None:
|
|
306
|
+
# Pinned to the rotation's START time (§5.4 bullet 5): an entry created
|
|
307
|
+
# mid-rotation bumps its own directory's mtime and is re-examined next
|
|
308
|
+
# rotation. The cursor stays at len(_dirs); the next cycle's
|
|
309
|
+
# _begin_rotation resets it.
|
|
310
|
+
self._completed_rotation_watermark = self._rotation_start
|
|
311
|
+
self._rotation_count += 1
|
|
312
|
+
|
|
313
|
+
def _is_queued(self, d: str) -> bool:
|
|
314
|
+
return any(qd == d for (qd, _mt) in self._enum_queue)
|
|
315
|
+
|
|
316
|
+
def _age_pending(self) -> None:
|
|
317
|
+
expired = []
|
|
318
|
+
for p in list(self._pending.keys()):
|
|
319
|
+
self._pending[p] += 1
|
|
320
|
+
if self._pending[p] > self._pending_expiry:
|
|
321
|
+
expired.append(p)
|
|
322
|
+
for p in expired:
|
|
323
|
+
self._pending.pop(p, None)
|