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