vibego 0.2.58__py3-none-any.whl → 1.0.10__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.
@@ -0,0 +1,265 @@
1
+ #!/usr/bin/env python3
2
+ """Capture the session JSONL path generated after a worker launch and lock it.
3
+
4
+ This helper watches the Codex/Claude session directory after the tmux worker
5
+ starts. The first rollout file that matches the configured working directory is
6
+ recorded into both the pointer file (used by the worker for streaming) and a
7
+ dedicated lock file so subsequent bindings never drift to another CLI session.
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ import argparse
13
+ import json
14
+ import logging
15
+ import sys
16
+ import time
17
+ from datetime import datetime, timezone
18
+ from fnmatch import fnmatch
19
+ from pathlib import Path
20
+ from threading import Event, Lock
21
+ from typing import Iterable, Optional
22
+
23
+ try:
24
+ from watchdog.events import FileSystemEvent, FileSystemEventHandler
25
+ from watchdog.observers import Observer
26
+ except Exception: # pragma: no cover - watchdog is an optional dependency fallback
27
+ Observer = None
28
+ FileSystemEventHandler = object # type: ignore[assignment]
29
+
30
+
31
+ log = logging.getLogger("session_pointer_watch")
32
+
33
+
34
+ def _resolve(path: str) -> Path:
35
+ return Path(path).expanduser().resolve()
36
+
37
+
38
+ def _read_session_cwd(path: Path) -> Optional[str]:
39
+ """Read first JSON line and return payload.cwd when available."""
40
+
41
+ try:
42
+ with path.open("r", encoding="utf-8", errors="ignore") as fh:
43
+ first_line = fh.readline()
44
+ except OSError:
45
+ return None
46
+ if not first_line:
47
+ return None
48
+ try:
49
+ data = json.loads(first_line)
50
+ except json.JSONDecodeError:
51
+ return None
52
+ payload = data.get("payload")
53
+ if isinstance(payload, dict):
54
+ raw = payload.get("cwd")
55
+ if isinstance(raw, str):
56
+ return raw
57
+ return None
58
+
59
+
60
+ def _iter_candidate_files(roots: Iterable[Path], glob: str) -> Iterable[Path]:
61
+ for root in roots:
62
+ if not root.exists():
63
+ continue
64
+ try:
65
+ real_root = root.resolve()
66
+ except OSError:
67
+ real_root = root
68
+ yield from real_root.glob(f"**/{glob}")
69
+
70
+
71
+ class _RolloutCapture(FileSystemEventHandler):
72
+ """Handle filesystem events and pick the first rollout that matches criteria."""
73
+
74
+ def __init__(
75
+ self,
76
+ *,
77
+ pattern: str,
78
+ baseline: set[str],
79
+ start_wall: float,
80
+ start_monotonic: float,
81
+ target_cwd: Optional[str],
82
+ timeout: float,
83
+ poll_interval: float,
84
+ ) -> None:
85
+ self._pattern = pattern
86
+ self._baseline = baseline
87
+ self._start_wall = start_wall
88
+ self._target_cwd = target_cwd
89
+ self._deadline = start_monotonic + timeout
90
+ self._poll_interval = poll_interval
91
+ self._chosen: Optional[Path] = None
92
+ self._event = Event()
93
+ self._lock = Lock()
94
+
95
+ def _consider(self, candidate: Path) -> None:
96
+ if candidate.is_dir():
97
+ return
98
+ name = candidate.name
99
+ if not fnmatch(name, self._pattern):
100
+ return
101
+ try:
102
+ real_path = candidate.resolve()
103
+ except OSError:
104
+ real_path = candidate
105
+ real_key = str(real_path)
106
+ if real_key in self._baseline:
107
+ return
108
+ try:
109
+ stat = real_path.stat()
110
+ except OSError:
111
+ return
112
+ if stat.st_mtime + 0.01 < self._start_wall:
113
+ # Ignore historical files.
114
+ return
115
+
116
+ if self._target_cwd:
117
+ # Wait until the JSON header is flushed and matches our CWD.
118
+ deadline = time.monotonic() + self._poll_interval * 10
119
+ while time.monotonic() < deadline:
120
+ cwd = _read_session_cwd(real_path)
121
+ if cwd is None:
122
+ time.sleep(self._poll_interval)
123
+ continue
124
+ if cwd == self._target_cwd:
125
+ break
126
+ log.debug("Skip rollout with mismatched cwd=%s", cwd)
127
+ return
128
+ with self._lock:
129
+ if self._chosen is None:
130
+ self._chosen = real_path
131
+ self._event.set()
132
+
133
+ # The following methods are only called when watchdog is available.
134
+ def on_created(self, event: FileSystemEvent) -> None: # type: ignore[override]
135
+ if getattr(event, "is_directory", False):
136
+ return
137
+ self._consider(Path(event.src_path))
138
+
139
+ def on_moved(self, event: FileSystemEvent) -> None: # type: ignore[override]
140
+ if getattr(event, "is_directory", False):
141
+ return
142
+ self._consider(Path(event.dest_path))
143
+
144
+ def poll_until_found(self, roots: Iterable[Path]) -> Optional[Path]:
145
+ while time.monotonic() < self._deadline:
146
+ remaining = self._deadline - time.monotonic()
147
+ if remaining <= 0:
148
+ break
149
+ wait_time = min(self._poll_interval, remaining)
150
+ if self._event.wait(timeout=wait_time):
151
+ break
152
+ for candidate in _iter_candidate_files(roots, self._pattern):
153
+ self._consider(candidate)
154
+ if self._event.is_set():
155
+ break
156
+ if self._event.is_set():
157
+ break
158
+ return self._chosen
159
+
160
+
161
+ def _write_pointer(pointer: Path, session_path: Path) -> None:
162
+ pointer.parent.mkdir(parents=True, exist_ok=True)
163
+ pointer.write_text(str(session_path), encoding="utf-8")
164
+
165
+
166
+ def _write_lock(lock_file: Path, *, session_path: Path, tmux_session: str, project: str, workdir: str, method: str) -> None:
167
+ payload = {
168
+ "session_path": str(session_path),
169
+ "captured_at": datetime.now(timezone.utc).isoformat(),
170
+ "tmux_session": tmux_session,
171
+ "project": project,
172
+ "workdir": workdir,
173
+ "method": method,
174
+ }
175
+ lock_file.parent.mkdir(parents=True, exist_ok=True)
176
+ lock_file.write_text(json.dumps(payload, indent=2), encoding="utf-8")
177
+
178
+
179
+ def main(argv: Optional[list[str]] = None) -> int:
180
+ parser = argparse.ArgumentParser(description="Capture newly created Codex/Claude rollout JSONL and write pointer+lock files.")
181
+ parser.add_argument("--pointer", required=True, help="Path to current_session.txt pointer file")
182
+ parser.add_argument("--lock", required=True, help="Path to persistent lock metadata JSON")
183
+ parser.add_argument("--session-root", required=False, help="Primary sessions root directory")
184
+ parser.add_argument("--additional-root", action="append", default=[], help="Extra directories to monitor for rollouts")
185
+ parser.add_argument("--glob", default="rollout-*.jsonl", help="Glob pattern for rollout files")
186
+ parser.add_argument("--workdir", default="", help="Model working directory, used to filter sessions")
187
+ parser.add_argument("--tmux-session", default="", help="tmux session name for diagnostics")
188
+ parser.add_argument("--project", default="", help="Project slug for diagnostics")
189
+ parser.add_argument("--timeout", type=float, default=180.0, help="Maximum seconds to wait for a new rollout")
190
+ parser.add_argument("--poll", type=float, default=0.5, help="Polling interval when watchdog is unavailable")
191
+ args = parser.parse_args(argv)
192
+
193
+ logging.basicConfig(level=logging.INFO, format="%(asctime)s [%(levelname)s] %(message)s")
194
+
195
+ pointer_path = _resolve(args.pointer)
196
+ lock_path = _resolve(args.lock)
197
+ roots: list[Path] = []
198
+ if args.session_root:
199
+ roots.append(_resolve(args.session_root))
200
+ pointer_parent = pointer_path.parent
201
+ if pointer_parent not in roots:
202
+ roots.append(pointer_parent)
203
+ for raw in args.additional_root:
204
+ resolved = _resolve(raw)
205
+ if resolved not in roots:
206
+ roots.append(resolved)
207
+
208
+ baseline = {str(path.resolve()) for path in _iter_candidate_files(roots, args.glob)}
209
+ start_wall = time.time()
210
+ start_monotonic = time.monotonic()
211
+
212
+ capture = _RolloutCapture(
213
+ pattern=args.glob,
214
+ baseline=baseline,
215
+ start_wall=start_wall,
216
+ start_monotonic=start_monotonic,
217
+ target_cwd=args.workdir or None,
218
+ timeout=args.timeout,
219
+ poll_interval=max(args.poll, 0.1),
220
+ )
221
+
222
+ observer: Optional[Observer] = None
223
+ method = "watchdog"
224
+ if Observer is not None:
225
+ observer = Observer()
226
+ for root in roots:
227
+ if root.exists():
228
+ observer.schedule(capture, str(root), recursive=True)
229
+ observer.start()
230
+ else: # pragma: no cover - watchdog is optional
231
+ log.warning("watchdog not available, falling back to polling")
232
+ method = "polling"
233
+
234
+ try:
235
+ session_path = capture.poll_until_found(roots)
236
+ finally:
237
+ if observer is not None:
238
+ observer.stop()
239
+ observer.join(timeout=5)
240
+
241
+ if session_path is None:
242
+ log.error(
243
+ "Failed to detect rollout file within timeout %.1fs (tmux session=%s, project=%s)",
244
+ args.timeout,
245
+ args.tmux_session or "-",
246
+ args.project or "-",
247
+ )
248
+ return 1
249
+
250
+ _write_pointer(pointer_path, session_path)
251
+ _write_lock(
252
+ lock_path,
253
+ session_path=session_path,
254
+ tmux_session=args.tmux_session,
255
+ project=args.project,
256
+ workdir=args.workdir,
257
+ method=method,
258
+ )
259
+
260
+ log.info("Recorded session pointer -> %s", session_path)
261
+ return 0
262
+
263
+
264
+ if __name__ == "__main__":
265
+ sys.exit(main())