From 889238b2345f88e60ce15b25b426072ff1b62b5e Mon Sep 17 00:00:00 2001 From: carpedkm Date: Sat, 20 Jun 2026 14:07:50 +0000 Subject: [PATCH 1/4] fix: add SKILLOPT_SLEEP_PYTHON override + lookback_hours first-run fallback Two fixes from issue #57 feedback: 1. run-sleep.sh: support SKILLOPT_SLEEP_PYTHON env var to explicitly set the Python interpreter. Useful on macOS where system Python is 3.9 but a newer Python is available elsewhere (e.g. Codex Desktop's bundled Python 3.12). Applied to both the shared runner and the bundled Claude Code plugin copy. 2. cycle.py: on first run (no prior harvest recorded), apply the lookback_hours config (default 72h) as a time cutoff. Previously, first run scanned the entire transcript history, which could trigger massive LLM mining on users with months of session data. Co-Authored-By: Claude Fable 5 --- plugins/claude-code/scripts/run-sleep.sh | 17 +++++++++++------ plugins/run-sleep.sh | 17 +++++++++++------ skillopt_sleep/cycle.py | 8 ++++++++ 3 files changed, 30 insertions(+), 12 deletions(-) diff --git a/plugins/claude-code/scripts/run-sleep.sh b/plugins/claude-code/scripts/run-sleep.sh index e46e212..310d8de 100755 --- a/plugins/claude-code/scripts/run-sleep.sh +++ b/plugins/claude-code/scripts/run-sleep.sh @@ -30,12 +30,17 @@ if [ -z "${REPO_ROOT:-}" ]; then fi PY="" -for cand in python3.12 python3.11 python3.10 python3; do - if command -v "$cand" >/dev/null 2>&1; then - ver="$("$cand" -c 'import sys; print("%d%d" % sys.version_info[:2])' 2>/dev/null || echo 0)" - if [ "${ver:-0}" -ge 310 ]; then PY="$cand"; break; fi - fi -done +# Allow explicit Python override (useful on macOS with old system Python). +if [ -n "${SKILLOPT_SLEEP_PYTHON:-}" ]; then + PY="$SKILLOPT_SLEEP_PYTHON" +else + for cand in python3.12 python3.11 python3.10 python3; do + if command -v "$cand" >/dev/null 2>&1; then + ver="$("$cand" -c 'import sys; print("%d%d" % sys.version_info[:2])' 2>/dev/null || echo 0)" + if [ "${ver:-0}" -ge 310 ]; then PY="$cand"; break; fi + fi + done +fi if [ -z "$PY" ]; then echo "[sleep] ERROR: need Python >= 3.10 (found none)." >&2 exit 1 diff --git a/plugins/run-sleep.sh b/plugins/run-sleep.sh index e46e212..310d8de 100755 --- a/plugins/run-sleep.sh +++ b/plugins/run-sleep.sh @@ -30,12 +30,17 @@ if [ -z "${REPO_ROOT:-}" ]; then fi PY="" -for cand in python3.12 python3.11 python3.10 python3; do - if command -v "$cand" >/dev/null 2>&1; then - ver="$("$cand" -c 'import sys; print("%d%d" % sys.version_info[:2])' 2>/dev/null || echo 0)" - if [ "${ver:-0}" -ge 310 ]; then PY="$cand"; break; fi - fi -done +# Allow explicit Python override (useful on macOS with old system Python). +if [ -n "${SKILLOPT_SLEEP_PYTHON:-}" ]; then + PY="$SKILLOPT_SLEEP_PYTHON" +else + for cand in python3.12 python3.11 python3.10 python3; do + if command -v "$cand" >/dev/null 2>&1; then + ver="$("$cand" -c 'import sys; print("%d%d" % sys.version_info[:2])' 2>/dev/null || echo 0)" + if [ "${ver:-0}" -ge 310 ]; then PY="$cand"; break; fi + fi + done +fi if [ -z "$PY" ]; then echo "[sleep] ERROR: need Python >= 3.10 (found none)." >&2 exit 1 diff --git a/skillopt_sleep/cycle.py b/skillopt_sleep/cycle.py index 4678cff..e66f436 100644 --- a/skillopt_sleep/cycle.py +++ b/skillopt_sleep/cycle.py @@ -144,6 +144,14 @@ def run_sleep_cycle( _progress(cfg, f"using {len(tasks)} seeded tasks") else: since = state.last_harvest_for(project) + # On first run (no prior harvest), apply lookback_hours so we don't + # scan the entire transcript history and trigger massive LLM mining. + if since is None: + lookback_hours = cfg.get("lookback_hours", 72) + if lookback_hours and lookback_hours > 0: + import time + cutoff = time.time() - lookback_hours * 3600 + since = _now_iso(cutoff) max_tasks = cfg.get("max_tasks_per_night", 40) max_sessions = cfg.get("max_sessions_per_night", 0) or max_tasks * 3 candidate_limit = max_tasks From 6cc1cd2e95e0a3a6abb0f3572dfe7b919a40cb43 Mon Sep 17 00:00:00 2001 From: carpedkm Date: Sat, 20 Jun 2026 14:11:58 +0000 Subject: [PATCH 2/4] =?UTF-8?q?fix:=20address=20codex=20review=20=E2=80=94?= =?UTF-8?q?=20use=20clock=20for=20cutoff=20+=20early-exit=20harvest?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - cycle.py: use supplied `clock` parameter (not wall time) for the lookback cutoff, so deterministic tests/experiments get reproducible harvest windows - harvest.py: break (not continue) when a file is older than since_iso, since files are sorted newest-first by mtime — avoids scanning the entire transcript directory for quiet projects with large histories Co-Authored-By: Claude Fable 5 --- skillopt_sleep/cycle.py | 3 ++- skillopt_sleep/harvest.py | 4 +++- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/skillopt_sleep/cycle.py b/skillopt_sleep/cycle.py index e66f436..9375784 100644 --- a/skillopt_sleep/cycle.py +++ b/skillopt_sleep/cycle.py @@ -150,7 +150,8 @@ def run_sleep_cycle( lookback_hours = cfg.get("lookback_hours", 72) if lookback_hours and lookback_hours > 0: import time - cutoff = time.time() - lookback_hours * 3600 + ref_time = clock if clock is not None else time.time() + cutoff = ref_time - lookback_hours * 3600 since = _now_iso(cutoff) max_tasks = cfg.get("max_tasks_per_night", 40) max_sessions = cfg.get("max_sessions_per_night", 0) or max_tasks * 3 diff --git a/skillopt_sleep/harvest.py b/skillopt_sleep/harvest.py index 3645d3f..84446f8 100644 --- a/skillopt_sleep/harvest.py +++ b/skillopt_sleep/harvest.py @@ -294,7 +294,9 @@ def harvest( if not _project_matches(d.project or "", scope, invoked_project): continue if since_iso and d.ended_at and d.ended_at < since_iso: - continue + # Files are sorted newest-first by mtime; once we see one that + # is older than the cutoff, all remaining files are older too. + break digests.append(d) if limit and len(digests) >= limit: break From 01075c90d36fa8973e5342ce0165def490cf5753 Mon Sep 17 00:00:00 2001 From: carpedkm Date: Sat, 20 Jun 2026 14:21:18 +0000 Subject: [PATCH 3/4] =?UTF-8?q?fix:=20address=20codex=20round=202=20?= =?UTF-8?q?=E2=80=94=20revert=20harvest=20break=20+=20allow=20lookback=200?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - harvest.py: revert break to continue — mtime ordering can diverge from embedded ended_at timestamps (copy/touch), so we must check all files rather than early-exiting on the first old one - cycle.py: use `is not None and > 0` so lookback_hours=0 means "scan full history" (opt-out of the cutoff) - __main__.py: propagate --lookback-hours 0 to config as explicit 0 Co-Authored-By: Claude Fable 5 --- skillopt_sleep/__main__.py | 4 +++- skillopt_sleep/cycle.py | 2 +- skillopt_sleep/harvest.py | 7 ++++--- 3 files changed, 8 insertions(+), 5 deletions(-) diff --git a/skillopt_sleep/__main__.py b/skillopt_sleep/__main__.py index 78f22f3..3a3e047 100644 --- a/skillopt_sleep/__main__.py +++ b/skillopt_sleep/__main__.py @@ -111,8 +111,10 @@ def _cfg_from_args(args, task_meta: Dict[str, Any] | None = None) -> Any: overrides["codex_home"] = os.path.abspath(args.codex_home) if getattr(args, "source", ""): overrides["transcript_source"] = args.source - if getattr(args, "lookback_hours", 0): + if getattr(args, "lookback_hours", None) is not None and args.lookback_hours != 0: overrides["lookback_hours"] = args.lookback_hours + elif getattr(args, "lookback_hours", None) == 0: + overrides["lookback_hours"] = 0 # explicit opt-out: scan full history if getattr(args, "edit_budget", 0): overrides["edit_budget"] = args.edit_budget if getattr(args, "max_sessions", 0): diff --git a/skillopt_sleep/cycle.py b/skillopt_sleep/cycle.py index 9375784..57b06a9 100644 --- a/skillopt_sleep/cycle.py +++ b/skillopt_sleep/cycle.py @@ -148,7 +148,7 @@ def run_sleep_cycle( # scan the entire transcript history and trigger massive LLM mining. if since is None: lookback_hours = cfg.get("lookback_hours", 72) - if lookback_hours and lookback_hours > 0: + if lookback_hours is not None and lookback_hours > 0: import time ref_time = clock if clock is not None else time.time() cutoff = ref_time - lookback_hours * 3600 diff --git a/skillopt_sleep/harvest.py b/skillopt_sleep/harvest.py index 84446f8..851e5f1 100644 --- a/skillopt_sleep/harvest.py +++ b/skillopt_sleep/harvest.py @@ -294,9 +294,10 @@ def harvest( if not _project_matches(d.project or "", scope, invoked_project): continue if since_iso and d.ended_at and d.ended_at < since_iso: - # Files are sorted newest-first by mtime; once we see one that - # is older than the cutoff, all remaining files are older too. - break + # Note: files are sorted by mtime but we compare the embedded + # ended_at timestamp — mtime can diverge (copy/touch), so we + # cannot break here; we must continue to check all files. + continue digests.append(d) if limit and len(digests) >= limit: break From 01b3e01804385a38aba68ede7446786b729f6b28 Mon Sep 17 00:00:00 2001 From: carpedkm Date: Sat, 20 Jun 2026 14:23:17 +0000 Subject: [PATCH 4/4] fix: use None default for --lookback-hours to distinguish omitted vs 0 Codex round 3: argparse default=0 made every CLI invocation without --lookback-hours clobber the config's 72h default. Now default=None; only explicit --lookback-hours N (including 0) overrides config. Co-Authored-By: Claude Fable 5 --- skillopt_sleep/__main__.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/skillopt_sleep/__main__.py b/skillopt_sleep/__main__.py index 3a3e047..608487a 100644 --- a/skillopt_sleep/__main__.py +++ b/skillopt_sleep/__main__.py @@ -76,7 +76,8 @@ def _add_common(p: argparse.ArgumentParser) -> None: p.add_argument("--codex-home", default="", help="override ~/.codex for archived session harvest") p.add_argument("--source", default="", choices=["", "claude", "codex", "auto"], help="session transcript source") - p.add_argument("--lookback-hours", type=int, default=0) + p.add_argument("--lookback-hours", type=int, default=None, + help="harvest window in hours; 0 = scan full history") p.add_argument("--edit-budget", type=int, default=0) p.add_argument("--max-sessions", type=int, default=0, help="cap harvested sessions before mining; default derives from max tasks") @@ -111,10 +112,9 @@ def _cfg_from_args(args, task_meta: Dict[str, Any] | None = None) -> Any: overrides["codex_home"] = os.path.abspath(args.codex_home) if getattr(args, "source", ""): overrides["transcript_source"] = args.source - if getattr(args, "lookback_hours", None) is not None and args.lookback_hours != 0: - overrides["lookback_hours"] = args.lookback_hours - elif getattr(args, "lookback_hours", None) == 0: - overrides["lookback_hours"] = 0 # explicit opt-out: scan full history + lh = getattr(args, "lookback_hours", None) + if lh is not None: # --lookback-hours was explicitly passed (0 = full history) + overrides["lookback_hours"] = lh if getattr(args, "edit_budget", 0): overrides["edit_budget"] = args.edit_budget if getattr(args, "max_sessions", 0):