fix(fs): Windows-safe atomic replace everywhere — model switch now sticks
Root cause of 'model+effort switch works on Mac, not on Windows': the switch APPLIES in-session (the dispatch/apply chain is platform-identical, verified adversarially) but its persistence never sticks on Windows. Every tmp+rename atomic write except auth/storage.rs committed with a bare fs::rename, and Windows MoveFileExW(REPLACE_EXISTING) fails with a sharing violation whenever AV/search-indexer/cloud-sync transiently holds the destination open. Consequences: [models].default never persisted (next launch = original model), the session summary's current model never persisted (resume = original model), and the models cache went silently stale (all its write errors were swallowed). - New kigi_shell_base::util::fs::replace_file — THE commit step for tmp+rename: plain rename on Unix; on Windows delete-first + two short backoffs (the pattern auth/storage.rs shipped first), tmp cleaned on failure, error always returned. Windows branch type-checked against x86_64-pc-windows-msvc. - Adopted at every replace site: config.toml (save_config / atomic_write_string / mcp saves), models cache (plus unique tmp suffixes and tracing::warn on failure — writes were fully silent), session storage (summary/current-model, jsonl, plan/signals/ announcement/goal/graph state), auth.json, active-sessions registry, prompt history, claude/kimi import, campaigns state, goal artifacts. Directory-move renames (worktree pool, corrupt-file backups) keep plain rename — their destinations don't pre-exist. Verified: kigi-shell + kigi-shell-base 5318 tests green, clippy clean, msvc-target check of the new cfg(windows) code clean.
This commit is contained in:
@@ -1637,16 +1637,31 @@ impl ModelsCacheManager {
|
||||
}
|
||||
}
|
||||
|
||||
/// Sync; see `load_fresh` note.
|
||||
/// Unique tmp suffix (PID + nanos) so concurrent writers never share an
|
||||
/// inode (mirrors `util::config::persist`).
|
||||
fn tmp_path(&self) -> std::path::PathBuf {
|
||||
let nanos = std::time::SystemTime::now()
|
||||
.duration_since(std::time::UNIX_EPOCH)
|
||||
.map(|d| d.as_nanos())
|
||||
.unwrap_or(0);
|
||||
self.path
|
||||
.with_extension(format!("json.tmp.{}.{}", std::process::id(), nanos))
|
||||
}
|
||||
|
||||
/// Sync; see `load_fresh` note. Best-effort, but NEVER silent: a failed
|
||||
/// cache write leaves a stale catalog on disk, which on Windows (sharing
|
||||
/// violations) previously diverged picker behavior with zero trace.
|
||||
fn atomic_write(&self, cache: &ModelsCache) {
|
||||
if let Some(parent) = self.path.parent() {
|
||||
let _ = std::fs::create_dir_all(parent);
|
||||
}
|
||||
let tmp = self.path.with_extension("json.tmp");
|
||||
if let Ok(json) = serde_json::to_vec_pretty(cache)
|
||||
&& std::fs::write(&tmp, &json).is_ok()
|
||||
{
|
||||
let _ = std::fs::rename(&tmp, &self.path);
|
||||
let tmp = self.tmp_path();
|
||||
let result = serde_json::to_vec_pretty(cache)
|
||||
.map_err(std::io::Error::other)
|
||||
.and_then(|json| std::fs::write(&tmp, &json))
|
||||
.and_then(|()| crate::util::fs::replace_file(&tmp, &self.path));
|
||||
if let Err(e) = result {
|
||||
tracing::warn!(error = %e, path = %self.path.display(), "models cache write failed");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1654,12 +1669,25 @@ impl ModelsCacheManager {
|
||||
if let Some(parent) = self.path.parent() {
|
||||
let _ = tokio::fs::create_dir_all(parent).await;
|
||||
}
|
||||
let tmp = self.path.with_extension("json.tmp");
|
||||
let Ok(json) = serde_json::to_vec_pretty(cache) else {
|
||||
return;
|
||||
let tmp = self.tmp_path();
|
||||
let json = match serde_json::to_vec_pretty(cache) {
|
||||
Ok(json) => json,
|
||||
Err(e) => {
|
||||
tracing::warn!(error = %e, "models cache serialize failed");
|
||||
return;
|
||||
}
|
||||
};
|
||||
if tokio::fs::write(&tmp, &json).await.is_ok() {
|
||||
let _ = tokio::fs::rename(&tmp, &self.path).await;
|
||||
let result = match tokio::fs::write(&tmp, &json).await {
|
||||
Ok(()) => {
|
||||
let dest = self.path.clone();
|
||||
tokio::task::spawn_blocking(move || crate::util::fs::replace_file(&tmp, &dest))
|
||||
.await
|
||||
.unwrap_or_else(|e| Err(std::io::Error::other(e)))
|
||||
}
|
||||
Err(e) => Err(e),
|
||||
};
|
||||
if let Err(e) = result {
|
||||
tracing::warn!(error = %e, path = %self.path.display(), "models cache write failed");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user