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.
libc::EWOULDBLOCK is unix-only (kigi-shell links libc behind cfg(unix));
the v0.1.3 windows-msvc release build failed on it. fs2 exposes
lock_contended_error() precisely as the cross-platform classifier
(EWOULDBLOCK on unix, ERROR_LOCK_VIOLATION on Windows) — use it instead
of the raw errno. No behavior change on unix; graph_project lock tests
green.
The graph now follows the REPOSITORY, not the session. Every checkpoint
projects the orchestration to .kigi/graph.jsonl at the git root in a
beads-style, line-mergeable shape: line 1 is the header (orchestration
minus nodes — omitted entirely, and a header carrying inline nodes is
rejected on load rather than silently duplicating the per-line entries),
then one content-hash-id node per line. The session tracker stays the
single source of truth; projection failures warn loudly but never block
progress. kigi only writes the file — committing it stays a user decision.
Single-writer discipline via an fs2 flock on a sidecar .lock: the session
that creates or resumes a graph owns the projection; other instances get a
read-only /graph status view rendered from the file and an explicit
refusal on resume. Cross-session revive: /graph resume in a fresh session
loads the file UNDER the lock (locking after reading raced the owner's
final checkpoint and could resurrect a just-cleared graph), sanitizes it
with the from_snapshot demotions, and re-dispatches.
Holding the lock proves nobody writes NOW — not that the file's content is
yours. Every lock-then-mutate site therefore identity-checks the projected
graph_id: /graph <objective> refuses to overwrite a foreign non-Complete
projection (revive-or-clear guidance, mirroring the session-level guard);
session-restored graphs claim writership on resume (and best-effort at
spawn re-emit, so the shared file learns the demoted truth immediately)
but refuse when the projection belongs to a different graph; /graph clear
skips projection teardown entirely when no session graph exists, leaves
foreign projections in place, and warns instead of swallowing lock errors.
Resume arms validate their flags before taking the lock.
Tests: projection round-trip pinning the one-line-per-node shape,
inline-nodes rejection, malformed-content loud errors, exclusive-lock
semantics across handles, and an e2e driving create → checkpoint
projection → second-instance read-only refusal → owner death → fresh-
session revive (demoted node relaunches) → clear removing the projection.
kigi-shell 4941 lib tests green; workspace clippy clean.