3232
3333# ── Model-swap detection (F16) ───────────────────────────────
3434def _make_model_key (cfg : SleepConfig ) -> str :
35- """Stable string identifying the effective backend/model role(s)."""
36- backend = str (cfg .get ("backend" , "mock" ) or "mock" )
37- model = str (cfg .get ("model" , "" ) or "" )
38- split_keys = (
39- "optimizer_backend" ,
40- "optimizer_model" ,
41- "target_backend" ,
42- "target_model" ,
43- )
44- if not any (cfg .get (key , "" ) for key in split_keys ):
45- # Preserve the original state format for ordinary single-backend runs.
46- return f"{ backend } ::{ model } "
47-
48- optimizer_backend = str (cfg .get ("optimizer_backend" , "" ) or backend )
49- optimizer_model = str (cfg .get ("optimizer_model" , "" ) or model )
50- target_backend = str (cfg .get ("target_backend" , "" ) or backend )
51- target_model = str (cfg .get ("target_model" , "" ) or model )
52- return (
53- f"optimizer={ optimizer_backend } ::{ optimizer_model } ;"
54- f"target={ target_backend } ::{ target_model } "
55- )
35+ """Stable string identifying the backend object(s) actually used.
36+
37+ Model-change detection is advisory, so resolving its diagnostic key must
38+ never become an earlier failure point than construction of the real
39+ backend. Fall back to a credential-free description of the configured
40+ roles if a backend constructor cannot be used in this diagnostic path.
41+ """
42+ try :
43+ effective = build_backend (
44+ backend = cfg .get ("backend" , "mock" ),
45+ model = cfg .get ("model" , "" ),
46+ optimizer_backend = cfg .get ("optimizer_backend" , "" ),
47+ optimizer_model = cfg .get ("optimizer_model" , "" ),
48+ target_backend = cfg .get ("target_backend" , "" ),
49+ target_model = cfg .get ("target_model" , "" ),
50+ codex_path = cfg .get ("codex_path" , "" ),
51+ cursor_path = cfg .get ("cursor_path" , "" ),
52+ azure_endpoint = cfg .get ("azure_endpoint" , "" ),
53+ project_dir = cfg .get ("invoked_project" , "" ) or os .getcwd (),
54+ )
55+ except Exception :
56+ backend = str (cfg .get ("backend" , "mock" ) or "mock" )
57+ model = str (cfg .get ("model" , "" ) or "" )
58+ split_keys = (
59+ "optimizer_backend" ,
60+ "optimizer_model" ,
61+ "target_backend" ,
62+ "target_model" ,
63+ )
64+ if not any (cfg .get (key , "" ) for key in split_keys ):
65+ return f"configured:{ backend } ::{ model } "
66+ optimizer_backend = str (cfg .get ("optimizer_backend" , "" ) or backend )
67+ optimizer_model = str (cfg .get ("optimizer_model" , "" ) or model )
68+ target_backend = str (cfg .get ("target_backend" , "" ) or backend )
69+ target_model = str (cfg .get ("target_model" , "" ) or model )
70+ return (
71+ f"configured:optimizer={ optimizer_backend } ::{ optimizer_model } ;"
72+ f"target={ target_backend } ::{ target_model } "
73+ )
74+ return _make_backend_key (effective )
5675
5776
58- def _check_model_change (cfg : SleepConfig , state : SleepState ) -> None :
77+ def _make_backend_key (backend : Backend ) -> str :
78+ """Describe resolved aliases/defaults without exposing credentials."""
79+ target = getattr (backend , "target" , None )
80+ optimizer = getattr (backend , "optimizer" , None )
81+ if target is not None and optimizer is not None :
82+ return (
83+ f"optimizer={ _make_backend_key (optimizer )} ;"
84+ f"target={ _make_backend_key (target )} "
85+ )
86+ name = str (getattr (backend , "name" , backend .__class__ .__name__ ) or "" )
87+ model = str (getattr (backend , "model" , "" ) or "" )
88+ return f"{ name } ::{ model } "
89+
90+
91+ def _check_model_change (
92+ cfg : SleepConfig , state : SleepState , backend : Backend | None = None
93+ ) -> None :
5994 """Warn when the backend/model has changed since the last night.
6095
6196 Skill text is backend-specific; adopting edits from a different model's
6297 reflections into a new model's skill file can cause regressions.
6398 This is advisory only — the cycle continues either way.
6499 """
65- current_key = _make_model_key (cfg )
100+ current_key = (
101+ _make_backend_key (backend ) if backend is not None else _make_model_key (cfg )
102+ )
66103 prior_key = state .last_model_key
104+ if prior_key and state .last_model_key_format < 2 :
105+ # Version 1 stored raw configuration rather than the resolved backend
106+ # model. Defaults and aliases make that value impossible to compare
107+ # truthfully, so migrate silently on the next successful night.
108+ return
67109 if prior_key and prior_key != current_key :
68110 print (
69111 f"[sleep] WARNING: model changed since last night "
@@ -143,7 +185,8 @@ def _render_report_md(report: SleepReport, cfg: SleepConfig) -> str:
143185 if report .unmatched_edits :
144186 lines .append ("## Proposed but changed nothing (never reached the gate)" )
145187 lines .append (
146- "_Anchor not found, duplicate/empty add, or an unknown op. "
188+ "_Anchor not found, replacement already present, duplicate/empty "
189+ "add, or an unknown op. "
147190 "These were never scored — check the anchor text if a rule you expected is missing._" )
148191 for e in report .unmatched_edits :
149192 anchor = f" \n _anchor: `{ e .anchor } `_" if e .anchor else ""
@@ -180,10 +223,7 @@ def run_sleep_cycle(
180223 """
181224 cfg = cfg or load_config ()
182225 state = SleepState .load (cfg .state_path )
183- _check_model_change (cfg , state ) # F16: warn if model changed between nights
184- night = state .begin_night (clock )
185226 project = _project_paths (cfg )
186- started = _now_iso (clock )
187227
188228 backend = backend or build_backend (
189229 backend = cfg .get ("backend" , "mock" ),
@@ -198,6 +238,9 @@ def run_sleep_cycle(
198238 preferences = cfg .get ("preferences" , "" ),
199239 project_dir = project ,
200240 )
241+ _check_model_change (cfg , state , backend ) # F16: warn if model changed between nights
242+ night = state .begin_night (clock )
243+ started = _now_iso (clock )
201244 backend .preferences = cfg .get ("preferences" , "" )
202245 _progress (cfg , f"night { night } : project={ project } backend={ backend .name } " )
203246
@@ -466,7 +509,7 @@ def run_sleep_cycle(
466509 "baseline" : result .baseline_score , "candidate" : result .candidate_score ,
467510 "n_tasks" : len (tasks ), "staging" : staging_dir ,
468511 })
469- state .set_last_model_key (_make_model_key ( cfg )) # F16: track model for next night
512+ state .set_last_model_key (_make_backend_key ( backend )) # F16: track resolved model
470513 # ── 6. adopt (opt-in) ────────────────────────────────────────────
471514 if cfg .get ("auto_adopt" ) and result .accepted :
472515 adopted_paths = adopt_staging (staging_dir )
0 commit comments