coding-agent-cli 0.1.0__py3-none-any.whl
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.
- coding_agent_cli/__init__.py +47 -0
- coding_agent_cli/_args.py +66 -0
- coding_agent_cli/_classify.py +170 -0
- coding_agent_cli/_drivers/__init__.py +1 -0
- coding_agent_cli/_drivers/_common.py +330 -0
- coding_agent_cli/_drivers/_usage.py +77 -0
- coding_agent_cli/_drivers/agy.py +340 -0
- coding_agent_cli/_drivers/claude.py +304 -0
- coding_agent_cli/_drivers/codex.py +333 -0
- coding_agent_cli/_drivers/codex_rollout.py +439 -0
- coding_agent_cli/_drivers/pi.py +333 -0
- coding_agent_cli/_platform.py +171 -0
- coding_agent_cli/_process.py +338 -0
- coding_agent_cli/api.py +190 -0
- coding_agent_cli/events.py +109 -0
- coding_agent_cli/py.typed +0 -0
- coding_agent_cli/registry.py +67 -0
- coding_agent_cli/types.py +90 -0
- coding_agent_cli-0.1.0.dist-info/METADATA +217 -0
- coding_agent_cli-0.1.0.dist-info/RECORD +22 -0
- coding_agent_cli-0.1.0.dist-info/WHEEL +4 -0
- coding_agent_cli-0.1.0.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,439 @@
|
|
|
1
|
+
"""Exact-session Codex rollout collector: bounded reads of one verified rollout file for telemetry.
|
|
2
|
+
|
|
3
|
+
The collector reads ``CODEX_HOME`` from the request's effective environment. It reports a usage change through
|
|
4
|
+
``on_change`` and never emits events itself.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import json
|
|
10
|
+
import logging
|
|
11
|
+
import os
|
|
12
|
+
import re
|
|
13
|
+
import stat as stat_module
|
|
14
|
+
from collections.abc import Callable, Mapping
|
|
15
|
+
from pathlib import Path
|
|
16
|
+
from typing import Any
|
|
17
|
+
|
|
18
|
+
from ._common import as_int
|
|
19
|
+
from ._usage import TokenCounters
|
|
20
|
+
|
|
21
|
+
log = logging.getLogger("coding_agent_cli.drivers.codex")
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class CodexRolloutCollector:
|
|
25
|
+
"""Bounded exact-session Codex rollout collector."""
|
|
26
|
+
|
|
27
|
+
MAX_DISCOVERY_ENTRIES = 50
|
|
28
|
+
MAX_DISCOVERY_RETRIES = 3
|
|
29
|
+
MAX_DISCOVERY_DEPTH = 3
|
|
30
|
+
|
|
31
|
+
def __init__(
|
|
32
|
+
self,
|
|
33
|
+
*,
|
|
34
|
+
cwd: Path,
|
|
35
|
+
env: Mapping[str, str],
|
|
36
|
+
session_id: str = "",
|
|
37
|
+
counters: TokenCounters,
|
|
38
|
+
on_change: Callable[[], None] | None = None,
|
|
39
|
+
) -> None:
|
|
40
|
+
self.cwd = cwd
|
|
41
|
+
self.env = env
|
|
42
|
+
self.session_id = session_id.strip()
|
|
43
|
+
self.counters = counters
|
|
44
|
+
self.on_change = on_change
|
|
45
|
+
self.session_file: Path | None = None
|
|
46
|
+
self.file_verified = False
|
|
47
|
+
self.file_dev: int | None = None
|
|
48
|
+
self.file_ino: int | None = None
|
|
49
|
+
self.header_verified = False
|
|
50
|
+
self.has_verified_context = False
|
|
51
|
+
self.pending_context: tuple[int, int | None] | None = None
|
|
52
|
+
self.budget_exhausted = False
|
|
53
|
+
self.offset = 0
|
|
54
|
+
self.partial_buffer = ""
|
|
55
|
+
self.model_name: str | None = None
|
|
56
|
+
self.is_compacted = False
|
|
57
|
+
self.max_bytes_per_poll = 65536
|
|
58
|
+
self.max_total_bytes = 1048576
|
|
59
|
+
self.total_bytes_read = 0
|
|
60
|
+
self.missing_discovery_retries = 0
|
|
61
|
+
|
|
62
|
+
def set_session_id(self, session_id: str) -> None:
|
|
63
|
+
sid = session_id.strip()
|
|
64
|
+
if sid and sid != self.session_id:
|
|
65
|
+
if self.session_id:
|
|
66
|
+
self._emit_invalidation()
|
|
67
|
+
self.session_id = sid
|
|
68
|
+
self._reset_file_state()
|
|
69
|
+
self.total_bytes_read = 0
|
|
70
|
+
self.missing_discovery_retries = 0
|
|
71
|
+
self.is_compacted = False
|
|
72
|
+
|
|
73
|
+
def _reset_file_state(self) -> None:
|
|
74
|
+
self.session_file = None
|
|
75
|
+
self.file_verified = False
|
|
76
|
+
self.file_dev = None
|
|
77
|
+
self.file_ino = None
|
|
78
|
+
self.header_verified = False
|
|
79
|
+
self.has_verified_context = False
|
|
80
|
+
self.pending_context = None
|
|
81
|
+
self.budget_exhausted = False
|
|
82
|
+
self.offset = 0
|
|
83
|
+
self.partial_buffer = ""
|
|
84
|
+
|
|
85
|
+
def _changed(self) -> None:
|
|
86
|
+
if self.on_change is not None:
|
|
87
|
+
self.on_change()
|
|
88
|
+
|
|
89
|
+
def _emit_invalidation(self, is_compaction: bool = False) -> None:
|
|
90
|
+
self.pending_context = None
|
|
91
|
+
self.has_verified_context = False
|
|
92
|
+
self.counters.clear_context()
|
|
93
|
+
if is_compaction:
|
|
94
|
+
self.is_compacted = True
|
|
95
|
+
self._changed()
|
|
96
|
+
|
|
97
|
+
def _find_exact_file(self) -> Path | None:
|
|
98
|
+
if not self.session_id or not _SESSION_ID_RE.fullmatch(self.session_id):
|
|
99
|
+
return None
|
|
100
|
+
if self.missing_discovery_retries >= self.MAX_DISCOVERY_RETRIES:
|
|
101
|
+
return None
|
|
102
|
+
|
|
103
|
+
sessions_dir = Path(self.env.get("CODEX_HOME") or Path.home() / ".codex") / "sessions"
|
|
104
|
+
if not sessions_dir.is_dir() or sessions_dir.is_symlink():
|
|
105
|
+
self.missing_discovery_retries += 1
|
|
106
|
+
return None
|
|
107
|
+
|
|
108
|
+
try:
|
|
109
|
+
resolved_sessions_dir = sessions_dir.resolve()
|
|
110
|
+
except OSError:
|
|
111
|
+
self.missing_discovery_retries += 1
|
|
112
|
+
return None
|
|
113
|
+
|
|
114
|
+
found_file = self._scan_sessions(sessions_dir, resolved_sessions_dir)
|
|
115
|
+
if found_file is None:
|
|
116
|
+
self.missing_discovery_retries += 1
|
|
117
|
+
return found_file
|
|
118
|
+
|
|
119
|
+
def _scan_sessions(self, sessions_dir: Path, resolved_sessions_dir: Path) -> Path | None:
|
|
120
|
+
entries_inspected = 0
|
|
121
|
+
dirs_to_visit = [(sessions_dir, 0)]
|
|
122
|
+
|
|
123
|
+
while dirs_to_visit and entries_inspected < self.MAX_DISCOVERY_ENTRIES:
|
|
124
|
+
current_dir, depth = dirs_to_visit.pop(0)
|
|
125
|
+
inspected, files, subdirs = self._read_dir(current_dir, self.MAX_DISCOVERY_ENTRIES - entries_inspected)
|
|
126
|
+
entries_inspected += inspected
|
|
127
|
+
for file_path in files:
|
|
128
|
+
if self._verify_rollout_file(file_path, resolved_sessions_dir):
|
|
129
|
+
return file_path
|
|
130
|
+
if depth < self.MAX_DISCOVERY_DEPTH:
|
|
131
|
+
dirs_to_visit.extend((subdir, depth + 1) for subdir in subdirs)
|
|
132
|
+
return None
|
|
133
|
+
|
|
134
|
+
def _read_dir(self, directory: Path, budget: int) -> tuple[int, list[Path], list[Path]]:
|
|
135
|
+
"""Inspect up to ``budget`` entries. Return the count and the candidate rollout files and subdirectories."""
|
|
136
|
+
inspected = 0
|
|
137
|
+
files: list[Path] = []
|
|
138
|
+
subdirs: list[Path] = []
|
|
139
|
+
try:
|
|
140
|
+
with os.scandir(directory) as it:
|
|
141
|
+
for entry in it:
|
|
142
|
+
inspected += 1
|
|
143
|
+
if inspected >= budget:
|
|
144
|
+
break
|
|
145
|
+
kind = _entry_kind(entry)
|
|
146
|
+
if kind == "dir":
|
|
147
|
+
subdirs.append(Path(entry.path))
|
|
148
|
+
elif kind == "file" and entry.name.endswith(".jsonl") and self.session_id in entry.name:
|
|
149
|
+
files.append(Path(entry.path))
|
|
150
|
+
except OSError:
|
|
151
|
+
pass
|
|
152
|
+
return inspected, files, subdirs
|
|
153
|
+
|
|
154
|
+
def _verify_rollout_file(self, file_path: Path, resolved_sessions_dir: Path) -> bool:
|
|
155
|
+
"""Accept ``file_path`` only when its header names this session and cwd. Record its identity."""
|
|
156
|
+
try:
|
|
157
|
+
if file_path.is_symlink() or not file_path.resolve().is_relative_to(resolved_sessions_dir):
|
|
158
|
+
return False
|
|
159
|
+
stat = file_path.stat()
|
|
160
|
+
if stat.st_size < 10:
|
|
161
|
+
return False
|
|
162
|
+
first = _read_first_line(file_path, stat)
|
|
163
|
+
if first is None or not first[1]:
|
|
164
|
+
return False
|
|
165
|
+
opened, first_line = first
|
|
166
|
+
if not self._header_matches(json.loads(first_line)):
|
|
167
|
+
return False
|
|
168
|
+
except (OSError, json.JSONDecodeError):
|
|
169
|
+
return False
|
|
170
|
+
self.file_dev = opened.st_dev
|
|
171
|
+
self.file_ino = opened.st_ino
|
|
172
|
+
return True
|
|
173
|
+
|
|
174
|
+
def _identity_matches(self, payload: dict[str, Any]) -> bool:
|
|
175
|
+
return str(payload.get("id") or "").strip() == self.session_id and _same_path(payload.get("cwd"), self.cwd)
|
|
176
|
+
|
|
177
|
+
def _header_matches(self, header: Any) -> bool:
|
|
178
|
+
if not isinstance(header, dict) or header.get("type") != "session_meta":
|
|
179
|
+
return False
|
|
180
|
+
payload = header.get("payload")
|
|
181
|
+
return isinstance(payload, dict) and self._identity_matches(payload)
|
|
182
|
+
|
|
183
|
+
def _invalidate_and_reset(self) -> None:
|
|
184
|
+
self._emit_invalidation()
|
|
185
|
+
self._reset_file_state()
|
|
186
|
+
|
|
187
|
+
def poll(self) -> None:
|
|
188
|
+
try:
|
|
189
|
+
self._poll_internal()
|
|
190
|
+
except Exception:
|
|
191
|
+
log.warning("codex: rollout poll error", exc_info=True)
|
|
192
|
+
|
|
193
|
+
def _poll_internal(self) -> None:
|
|
194
|
+
if not self.session_id:
|
|
195
|
+
return
|
|
196
|
+
if self.session_file is None and not self._discover_session_file():
|
|
197
|
+
return
|
|
198
|
+
if not self.file_verified:
|
|
199
|
+
return
|
|
200
|
+
|
|
201
|
+
stat = self._current_stat()
|
|
202
|
+
if stat is None:
|
|
203
|
+
return
|
|
204
|
+
self._apply_size_change(stat)
|
|
205
|
+
|
|
206
|
+
if stat.st_size == self.offset and not self.partial_buffer:
|
|
207
|
+
return
|
|
208
|
+
|
|
209
|
+
if self.total_bytes_read >= self.max_total_bytes:
|
|
210
|
+
self._exhaust_budget()
|
|
211
|
+
return
|
|
212
|
+
|
|
213
|
+
read_size = min(self.max_bytes_per_poll, self.max_total_bytes - self.total_bytes_read)
|
|
214
|
+
if read_size <= 0:
|
|
215
|
+
return
|
|
216
|
+
|
|
217
|
+
chunk = self._read_chunk(read_size)
|
|
218
|
+
if not chunk:
|
|
219
|
+
return
|
|
220
|
+
|
|
221
|
+
self.offset += len(chunk)
|
|
222
|
+
self.total_bytes_read += len(chunk)
|
|
223
|
+
self._process_chunk(chunk)
|
|
224
|
+
self._finish_read(stat)
|
|
225
|
+
|
|
226
|
+
def _discover_session_file(self) -> bool:
|
|
227
|
+
self.session_file = self._find_exact_file()
|
|
228
|
+
if self.session_file is None:
|
|
229
|
+
return False
|
|
230
|
+
try:
|
|
231
|
+
st = self.session_file.stat()
|
|
232
|
+
if self.file_dev is None or self.file_ino is None:
|
|
233
|
+
self.file_dev = st.st_dev
|
|
234
|
+
self.file_ino = st.st_ino
|
|
235
|
+
self.file_verified = True
|
|
236
|
+
except OSError:
|
|
237
|
+
self._reset_file_state()
|
|
238
|
+
return False
|
|
239
|
+
return True
|
|
240
|
+
|
|
241
|
+
def _current_stat(self) -> os.stat_result | None:
|
|
242
|
+
"""Stat the session file. A missing or replaced file invalidates context and resets state."""
|
|
243
|
+
if self.session_file is None:
|
|
244
|
+
return None
|
|
245
|
+
try:
|
|
246
|
+
stat = self.session_file.stat()
|
|
247
|
+
except OSError:
|
|
248
|
+
self._invalidate_and_reset()
|
|
249
|
+
return None
|
|
250
|
+
if (
|
|
251
|
+
self.file_dev is not None
|
|
252
|
+
and self.file_ino is not None
|
|
253
|
+
and (stat.st_dev, stat.st_ino) != (self.file_dev, self.file_ino)
|
|
254
|
+
):
|
|
255
|
+
self._invalidate_and_reset()
|
|
256
|
+
return None
|
|
257
|
+
return stat
|
|
258
|
+
|
|
259
|
+
def _apply_size_change(self, stat: os.stat_result) -> None:
|
|
260
|
+
if stat.st_size < self.offset:
|
|
261
|
+
self._emit_invalidation()
|
|
262
|
+
self.header_verified = False
|
|
263
|
+
self.offset = 0
|
|
264
|
+
self.partial_buffer = ""
|
|
265
|
+
elif stat.st_size > self.offset and self.has_verified_context:
|
|
266
|
+
self._emit_invalidation()
|
|
267
|
+
|
|
268
|
+
def _exhaust_budget(self) -> None:
|
|
269
|
+
if not self.budget_exhausted:
|
|
270
|
+
self._emit_invalidation()
|
|
271
|
+
self.budget_exhausted = True
|
|
272
|
+
|
|
273
|
+
def _read_chunk(self, read_size: int) -> bytes:
|
|
274
|
+
"""Read the next chunk. An empty result means nothing to process, with state reset on failure."""
|
|
275
|
+
if self.session_file is None:
|
|
276
|
+
return b""
|
|
277
|
+
try:
|
|
278
|
+
flags = os.O_RDONLY | getattr(os, "O_NOFOLLOW", 0) | getattr(os, "O_CLOEXEC", 0)
|
|
279
|
+
fd = os.open(self.session_file, flags)
|
|
280
|
+
try:
|
|
281
|
+
opened = os.fstat(fd)
|
|
282
|
+
if not stat_module.S_ISREG(opened.st_mode) or (opened.st_dev, opened.st_ino) != (
|
|
283
|
+
self.file_dev,
|
|
284
|
+
self.file_ino,
|
|
285
|
+
):
|
|
286
|
+
self._invalidate_and_reset()
|
|
287
|
+
return b""
|
|
288
|
+
with os.fdopen(fd, "rb") as f:
|
|
289
|
+
fd = -1
|
|
290
|
+
if not self.header_verified and not self._verify_header(f):
|
|
291
|
+
return b""
|
|
292
|
+
f.seek(self.offset)
|
|
293
|
+
return f.read(read_size)
|
|
294
|
+
finally:
|
|
295
|
+
if fd >= 0:
|
|
296
|
+
os.close(fd)
|
|
297
|
+
except OSError:
|
|
298
|
+
self._invalidate_and_reset()
|
|
299
|
+
return b""
|
|
300
|
+
|
|
301
|
+
def _verify_header(self, f: Any) -> bool:
|
|
302
|
+
try:
|
|
303
|
+
self.header_verified = self._header_matches(json.loads(f.readline(4096)))
|
|
304
|
+
except (json.JSONDecodeError, UnicodeDecodeError):
|
|
305
|
+
self.header_verified = False
|
|
306
|
+
if not self.header_verified:
|
|
307
|
+
self._invalidate_and_reset()
|
|
308
|
+
return self.header_verified
|
|
309
|
+
|
|
310
|
+
def _process_chunk(self, chunk: bytes) -> None:
|
|
311
|
+
text = self.partial_buffer + chunk.decode("utf-8", errors="replace")
|
|
312
|
+
lines = text.split("\n")
|
|
313
|
+
self.partial_buffer = lines.pop()
|
|
314
|
+
|
|
315
|
+
for line in lines:
|
|
316
|
+
line = line.strip()
|
|
317
|
+
if not line:
|
|
318
|
+
continue
|
|
319
|
+
try:
|
|
320
|
+
record = json.loads(line)
|
|
321
|
+
except json.JSONDecodeError:
|
|
322
|
+
continue
|
|
323
|
+
if isinstance(record, dict):
|
|
324
|
+
self._process_record(record)
|
|
325
|
+
|
|
326
|
+
def _finish_read(self, stat: os.stat_result) -> None:
|
|
327
|
+
if self.offset >= stat.st_size and not self.partial_buffer and self.pending_context is not None:
|
|
328
|
+
used, window = self.pending_context
|
|
329
|
+
self.counters.record_context(used=used, window=window)
|
|
330
|
+
self.has_verified_context = True
|
|
331
|
+
self._changed()
|
|
332
|
+
self.pending_context = None
|
|
333
|
+
|
|
334
|
+
if self.total_bytes_read >= self.max_total_bytes and stat.st_size > self.offset:
|
|
335
|
+
self._exhaust_budget()
|
|
336
|
+
|
|
337
|
+
def _process_record(self, record: dict[str, Any]) -> None:
|
|
338
|
+
rec_type = record.get("type", "")
|
|
339
|
+
payload = record.get("payload")
|
|
340
|
+
|
|
341
|
+
if rec_type == "session_meta":
|
|
342
|
+
if isinstance(payload, dict) and not self._identity_matches(payload):
|
|
343
|
+
self.file_verified = False
|
|
344
|
+
return
|
|
345
|
+
|
|
346
|
+
if not self.file_verified:
|
|
347
|
+
return
|
|
348
|
+
|
|
349
|
+
if rec_type == "event_msg" and isinstance(payload, dict):
|
|
350
|
+
self._process_event_msg(payload)
|
|
351
|
+
elif rec_type == "context_compacted":
|
|
352
|
+
self._emit_invalidation(is_compaction=True)
|
|
353
|
+
|
|
354
|
+
def _process_event_msg(self, payload: dict[str, Any]) -> None:
|
|
355
|
+
p_type = payload.get("type", "")
|
|
356
|
+
if p_type == "token_count":
|
|
357
|
+
self._record_token_count(payload)
|
|
358
|
+
elif p_type == "context_compacted":
|
|
359
|
+
self._emit_invalidation(is_compaction=True)
|
|
360
|
+
elif p_type in ("session_configured", "model_reroute"):
|
|
361
|
+
self._record_model(payload)
|
|
362
|
+
|
|
363
|
+
def _record_token_count(self, payload: dict[str, Any]) -> None:
|
|
364
|
+
info = payload.get("info")
|
|
365
|
+
if not isinstance(info, dict):
|
|
366
|
+
return
|
|
367
|
+
usage = info.get("last_token_usage")
|
|
368
|
+
window = as_int(info.get("model_context_window"))
|
|
369
|
+
if not isinstance(usage, dict):
|
|
370
|
+
return
|
|
371
|
+
input_tokens = as_int(usage.get("input_tokens"))
|
|
372
|
+
output_tokens = as_int(usage.get("output_tokens"))
|
|
373
|
+
reasoning_tokens = as_int(usage.get("reasoning_output_tokens"))
|
|
374
|
+
if input_tokens is None or output_tokens is None or reasoning_tokens is None:
|
|
375
|
+
return
|
|
376
|
+
used = input_tokens + output_tokens - reasoning_tokens
|
|
377
|
+
if used >= 0:
|
|
378
|
+
self.pending_context = (used, window)
|
|
379
|
+
|
|
380
|
+
def _record_model(self, payload: dict[str, Any]) -> None:
|
|
381
|
+
model = payload.get("model") or payload.get("to_model")
|
|
382
|
+
if not model:
|
|
383
|
+
return
|
|
384
|
+
model_str = str(model)
|
|
385
|
+
if self.model_name and self.model_name != model_str:
|
|
386
|
+
self._emit_invalidation(is_compaction=False)
|
|
387
|
+
self.model_name = model_str
|
|
388
|
+
|
|
389
|
+
|
|
390
|
+
def _entry_kind(entry: os.DirEntry[str]) -> str:
|
|
391
|
+
"""Return "dir" or "file" for a plain entry, "" for a symlink, other entry, or stat failure."""
|
|
392
|
+
try:
|
|
393
|
+
if entry.is_symlink():
|
|
394
|
+
return ""
|
|
395
|
+
if entry.is_dir(follow_symlinks=False):
|
|
396
|
+
return "dir"
|
|
397
|
+
return "file" if entry.is_file(follow_symlinks=False) else ""
|
|
398
|
+
except OSError:
|
|
399
|
+
return ""
|
|
400
|
+
|
|
401
|
+
|
|
402
|
+
def _read_first_line(path: Path, expected: os.stat_result) -> tuple[os.stat_result, str] | None:
|
|
403
|
+
"""Open ``path`` without following links and return its stat and first line, or None on an identity mismatch."""
|
|
404
|
+
flags = os.O_RDONLY | getattr(os, "O_NOFOLLOW", 0) | getattr(os, "O_CLOEXEC", 0)
|
|
405
|
+
fd = os.open(path, flags)
|
|
406
|
+
try:
|
|
407
|
+
opened = os.fstat(fd)
|
|
408
|
+
if not stat_module.S_ISREG(opened.st_mode) or (opened.st_dev, opened.st_ino) != (
|
|
409
|
+
expected.st_dev,
|
|
410
|
+
expected.st_ino,
|
|
411
|
+
):
|
|
412
|
+
return None
|
|
413
|
+
with os.fdopen(fd, "r", encoding="utf-8", errors="replace") as f:
|
|
414
|
+
fd = -1
|
|
415
|
+
return opened, f.readline(4096)
|
|
416
|
+
finally:
|
|
417
|
+
if fd >= 0:
|
|
418
|
+
os.close(fd)
|
|
419
|
+
|
|
420
|
+
|
|
421
|
+
_SESSION_ID_RE = re.compile(
|
|
422
|
+
r"^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$",
|
|
423
|
+
re.IGNORECASE,
|
|
424
|
+
)
|
|
425
|
+
|
|
426
|
+
|
|
427
|
+
def _same_path(left: Any, right: Path) -> bool:
|
|
428
|
+
if not isinstance(left, str) or not left.strip():
|
|
429
|
+
return False
|
|
430
|
+
try:
|
|
431
|
+
return Path(left).resolve() == right.resolve()
|
|
432
|
+
except (OSError, ValueError, TypeError):
|
|
433
|
+
try:
|
|
434
|
+
return os.path.normcase(os.path.abspath(left)) == os.path.normcase(os.path.abspath(os.fspath(right)))
|
|
435
|
+
except (OSError, ValueError, TypeError):
|
|
436
|
+
return False
|
|
437
|
+
|
|
438
|
+
|
|
439
|
+
__all__ = ["CodexRolloutCollector"]
|