Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion docs/sleep/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ normal agent requests.
One "night":

```
harvest Claude Code / Codex / Cursor transcripts → mine recurring tasks → replay in isolated model calls
harvest Claude Code / Codex / Cursor transcripts → mine recurring tasks → replay via the configured backend (isolation varies by backend; mock/handoff make no network calls)
→ consolidate (reflect → bounded edit → GATE on real held-out tasks)
→ stage proposal → (you) adopt
```
Expand Down
16 changes: 16 additions & 0 deletions scripts/train.py
Original file line number Diff line number Diff line change
Expand Up @@ -383,8 +383,24 @@ def parse_args() -> argparse.Namespace:

def load_config(args: argparse.Namespace) -> dict:
"""Load config with _base_ inheritance, then apply CLI overrides."""
import warnings
from skillopt.config import load_config as _load, flatten_config, is_structured

# F08: Warn when API keys are supplied on the CLI — prefer env vars or managed identity.
for _cli_key in (
"azure_api_key", "azure_openai_api_key",
"optimizer_azure_openai_api_key", "target_azure_openai_api_key",
"minimax_api_key",
):
if getattr(args, _cli_key, None):
warnings.warn(
f"--{_cli_key} is deprecated: provide credentials via the "
f"AZURE_OPENAI_API_KEY environment variable or set "
f"--azure_openai_auth_mode=managed_identity instead.",
DeprecationWarning,
stacklevel=2,
)
Comment on lines +389 to +402

cfg = _load(args.config, overrides=args.cfg_options)
structured = is_structured(cfg)

Expand Down
30 changes: 30 additions & 0 deletions skillopt_sleep/cycle.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,34 @@
from skillopt_sleep.types import SessionDigest, SleepReport, TaskRecord


# ── Model-swap detection (F16) ───────────────────────────────
def _make_model_key(cfg: SleepConfig) -> str:
"""Stable string identifying the backend+model combination for this cycle."""
return "{}::{}".format(
cfg.get("backend", "mock"),
cfg.get("model", ""),
)
Comment on lines +34 to +39


def _check_model_change(cfg: SleepConfig, state: SleepState) -> None:
"""Warn when the backend/model has changed since the last night.

Skill text is backend-specific; adopting edits from a different model's
reflections into a new model's skill file can cause regressions.
This is advisory only — the cycle continues either way.
"""
current_key = _make_model_key(cfg)
prior_key = state.last_model_key
if prior_key and prior_key != current_key:
print(
f"[sleep] WARNING: model changed since last night "
f"(was {prior_key!r}, now {current_key!r}). "
"Learned skill text may not transfer cleanly. "
"Consider starting from a fresh skill document.",
file=sys.stderr,
)


@dataclass
class CycleOutcome:
report: SleepReport
Expand Down Expand Up @@ -127,6 +155,7 @@ def run_sleep_cycle(
"""
cfg = cfg or load_config()
state = SleepState.load(cfg.state_path)
_check_model_change(cfg, state) # F16: warn if model changed between nights
night = state.begin_night(clock)
project = _project_paths(cfg)
started = _now_iso(clock)
Expand Down Expand Up @@ -409,6 +438,7 @@ def run_sleep_cycle(
"baseline": result.baseline_score, "candidate": result.candidate_score,
"n_tasks": len(tasks), "staging": staging_dir,
})
state.set_last_model_key(_make_model_key(cfg)) # F16: track model for next night
# ── 6. adopt (opt-in) ────────────────────────────────────────────
if cfg.get("auto_adopt") and result.accepted:
adopted_paths = adopt_staging(staging_dir)
Expand Down
9 changes: 9 additions & 0 deletions skillopt_sleep/state.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ def _now_iso(clock: Optional[float] = None) -> str:
"slow_memory": "", # cross-night consolidated lessons (meta-skill analogue)
"history": [], # list of per-night summaries
"task_archive": [], # capped list of past mined tasks (for associative recall)
"last_model_key": "", # "backend::model" string used in the last successful night (F16)
}


Expand Down Expand Up @@ -94,3 +95,11 @@ def add_to_archive(self, task_dicts: list, cap: int = 300) -> None:
arc.extend(task_dicts)
if len(arc) > cap:
self.data["task_archive"] = arc[-cap:]

# ── model-swap tracking (F16) ─────────────────────────────────────────
@property
def last_model_key(self) -> str:
return str(self.data.get("last_model_key", ""))

def set_last_model_key(self, key: str) -> None:
self.data["last_model_key"] = key
66 changes: 66 additions & 0 deletions tests/test_model_change_warning.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
"""Tests for the F16 model-change warning and F08 CLI credential warning."""
from __future__ import annotations

import argparse
import warnings

from skillopt_sleep.cycle import _check_model_change, _make_model_key
from skillopt_sleep.config import load_config
from skillopt_sleep.state import SleepState


def _cfg():
cfg = load_config()
return cfg


def test_last_model_key_roundtrips(tmp_path) -> None:
state = SleepState.load(str(tmp_path / "state.json"))
assert state.last_model_key == ""
state.set_last_model_key("anthropic::claude")
assert state.last_model_key == "anthropic::claude"
Comment on lines +17 to +21


def test_warns_when_model_changed(tmp_path, capsys) -> None:
cfg = _cfg()
state = SleepState.load(str(tmp_path / "state.json"))
state.set_last_model_key("some-other::model")
_check_model_change(cfg, state)
err = capsys.readouterr().err
assert "model changed since last night" in err


def test_no_warning_on_first_night(tmp_path, capsys) -> None:
cfg = _cfg()
state = SleepState.load(str(tmp_path / "state.json")) # last_model_key == ""
_check_model_change(cfg, state)
assert "model changed" not in capsys.readouterr().err


def test_no_warning_when_model_same(tmp_path, capsys) -> None:
cfg = _cfg()
state = SleepState.load(str(tmp_path / "state.json"))
state.set_last_model_key(_make_model_key(cfg))
_check_model_change(cfg, state)
assert "model changed" not in capsys.readouterr().err


def test_cli_api_key_emits_deprecation_warning() -> None:
from scripts.train import load_config as train_load_config

args = argparse.Namespace(
config="configs/_base_/default.yaml",
cfg_options=None,
azure_openai_api_key="sk-secret-value",
)
with warnings.catch_warnings(record=True) as caught:
warnings.simplefilter("always")
try:
train_load_config(args)
except Exception:
pass # config loading may fail; we only assert the warning fired
assert any(
issubclass(w.category, DeprecationWarning)
and "azure_openai_api_key" in str(w.message)
for w in caught
)