Fix Windows desktop session lease self-lock on model switch / 修复 Windows 桌面端切换模型会话租约自锁#6023
Merged
SivanCola merged 4 commits intoJul 5, 2026
Conversation
The lease registry keys sessions through agent's canonical form (clean, absolute, lowercased on Windows), while desktop sessionRuntimeKey kept the raw path case. On Windows every tab path contains uppercase segments (C:\Users\...), so the tab's own lease never matched its session key: ensureSessionLease re-acquired against the process's own registry entry, reclaim then hit the tab's live OS lock handle, and every model / effort / token-mode / provider-key rebuild failed with the "already open in another Reasonix window" error on a single window — including fresh installs. - Export agent.CanonicalSessionPath and layer it into sessionRuntimeKey so desktop identity checks agree with the lease registry byte-for-byte. - Normalize the canonicalTabSessionPath fallback (project-scope sessions) with clean+abs so shape variants cannot split one file into two keys. - Keep case-folded keys out of SessionPath: detach/attach paths now carry the display path separately from the runtime key. - Release only the build's own lease on startup-build failure, and skip claiming a lease the build merely reused (a concurrent switch owns it). - Supersede in-flight startup builds when a model/effort/token switch installs its controller, so a slow blank-session build can no longer overwrite the new runtime or strip its lease. - Regression tests: lease/runtime key agreement, idempotence, Windows case folding, project-scope fallback normalization, fast-path lease reuse. Fixes esengine#5999. Fixes esengine#6006. Fixes esengine#5989. Fixes esengine#5990. Refs esengine#5996.
rebuildSettingLocked installed its controller without bumping the tab's build generation, so a startup build still in flight would later pass its generation check, overwrite the just-installed controller, and leak it - leaving the session on a runtime built before the new credentials were saved. Provider-key saves hit this window most often: users typically save a key right while the initial startup build is running. Apply the same supersede-after-successful-swap the model/effort/token switches use, and add a regression test that models an in-flight build (live generation plus cancellable build context) and asserts the rebuild supersedes and cancels it.
added 2 commits
July 6, 2026 00:09
…nd session clear Two more sync-swap paths shared the race fixed for settings rebuilds: - removeBuiltInProviderAccessAndRetargetTabs and deleteProviderAndRetargetTabs cleared tab.Ctrl without bumping the build generation, so a startup build planned against the removed provider could finish later, pass its generation check, and reinstall a controller for it - racing the generation-0 retarget rebuild. - clearActiveSessionRuntime installed the fresh controller without invalidating an in-flight build, whose completion would overwrite the swap and resurrect a runtime bound to the just-destroyed session. Supersede the build inside the same critical sections that perform the swap, and extract a shared assertion helper for the regression tests covering all four protected paths.
… the swap clearActiveSessionRuntime's swap-time identity check silently dropped the freshly built controller when the tab had been closed or replaced while the clear ran off-lock: newCtrl was never closed and the session lease it had just bound stayed held by the abandoned tab struct. Mirror rebuildSettingLocked's failure branch: close the fresh controller, release the lease, still finish the destroyed runtime's teardown, and report the lost swap instead of returning success.
SivanCola
added a commit
that referenced
this pull request
Jul 5, 2026
…rs, retire lock sidecars Follow-up polish on the lease/CAS stack after the #6023/#6025 hotfixes, closing the remaining ways a healthy single-window session could still present as busy, leak holder details, or litter the session directory. - Reclaim now arbitrates on the OS lock alone: a missing or unreadable lease.json (user cleanup, AV quarantine, crash tear) with a free lock is a leftover, not a holder. Previously an orphaned in-process entry whose info file disappeared could never be reclaimed - the fallback acquire re-hit the orphaned entry forever and every rebuild stayed busy. The desktop gate mirrors this: nil lease info allows the reclaim attempt, foreign holders are still refused. - Raw lease errors (session path + host-pid-writer id) can no longer reach the UI: startup bind failures (StartupErr feeds the topbar banner and tab metas), ClearSession's bridge returns, and the recovery-lease callback (chat notices via snapshot errors) all render the user-facing busy message; raw details stay in slog. The busy message drops its dangling 'before changing this setting' clause when no setting applies. - SessionLease.Release retires the .lock/.lease.lock sidecars through the existing atomic take-then-remove helpers (non-blocking, no-op against any live holder or in-flight save), so ordinary use stops accumulating lock files that previously waited for the next boot reconcile (#6014). - Autosave failures warn once per burst and at most once per 5 minutes per tab: retries are slog-only, the user notice fires when the burst gives up. Explicit action saves keep immediate feedback. Every failure is still logged. - SaveRecoveryBranch tolerates event-index write failures like the other save paths: the recovery transcript and meta are already durable, so a failed listing accelerator no longer discards a successful recovery. - Lease test suite now feeds user-shape (mixed-case) paths to the APIs under test instead of pre-canonicalized ones - the pattern that let the Windows case-fold mismatch (#5999) escape the suite - plus regressions for orphan-without-info, corrupt-info, foreign-info-refused, sidecar retirement on release, StartupErr sanitization, and the autosave warning debounce. Refs #6014 #5999 #6027.
SivanCola
pushed a commit
to SivanCola/DeepSeek-Reasonix
that referenced
this pull request
Jul 6, 2026
Same-shape sweep after the initial guard landed: - Case-variant bypass (confirmed by repro): EvalSymlinks does not normalize case, and default macOS/Windows volumes are case-insensitive, so ~/.reasonix/SESSIONS/x.jsonl reached the real store while sliding past the deny check — the same failure shape as the Windows lease-key case split (esengine#6023). Deny-side checks (the guard and forbid_read's confineRead) now fold case on those platforms; allow-side confine keeps exact matching, since folding an allow rule on a case-sensitive filesystem would wave a genuinely different directory through. - State-root-direct runtime ledgers (desktop-tabs.json and friends, heartbeat-tasks.json, metrics/crash-pending.json) are rewritten wholesale by the app, so agent edits vanish the same way session edits do — the "index files" from the user report. They join the guard; config.toml/credentials/skills stay writable on request. - reasonix review built its subagent bash from the init template, skipping the guard; it now carries the same session-data warning.
SivanCola
pushed a commit
to SivanCola/DeepSeek-Reasonix
that referenced
this pull request
Jul 6, 2026
Same-shape sweep after the initial guard landed: - Case-variant bypass (confirmed by repro): EvalSymlinks does not normalize case, and default macOS/Windows volumes are case-insensitive, so ~/.reasonix/SESSIONS/x.jsonl reached the real store while sliding past the deny check — the same failure shape as the Windows lease-key case split (esengine#6023). Deny-side checks (the guard and forbid_read's confineRead) now fold case on those platforms; allow-side confine keeps exact matching, since folding an allow rule on a case-sensitive filesystem would wave a genuinely different directory through. - State-root-direct runtime ledgers (desktop-tabs.json and friends, heartbeat-tasks.json, metrics/crash-pending.json) are rewritten wholesale by the app, so agent edits vanish the same way session edits do — the "index files" from the user report. They join the guard; config.toml/credentials/skills stay writable on request. - reasonix review built its subagent bash from the init template, skipping the guard; it now carries the same session-data warning.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Windows desktop users could not switch model / effort / token mode / provider key even in a single window — every attempt failed with:
This reproduced on fresh installs with the lease info PID pointing at the running process itself (#5999's verification), across v1.16.0–v1.17.0.
Root cause
Two canonical forms for one session path:
canonicalSessionSavePath— clean + absolute + lowercased on Windows (internal/agent/save.go).SessionLease.Path()returns this folded form.sessionRuntimeKeykept the raw path case (desktop/tabs.go).On Windows every real session path contains uppercase segments (
C:\Users\...), so the tab's own lease never compared equal to its session key.ensureSessionLeaseskipped the reuse fast path, re-acquired against the process's own in-process registry entry (ErrSessionLeaseHeld, with our own PID in the lease info), and the reclaim fallback then hit the tab's own liveLockFileExhandle. Single-window self-lock, deterministic.Two adjacent defects shared the same mismatch or surface:
releaseSessionLeaseForKeynever matched on Windows, so an abandoned startup build leaked a held lease (session stays busy for every window until restart).Changes
agent.CanonicalSessionPathand layer it intosessionRuntimeKey, so desktop identity checks agree with the lease registry byte-for-byte; normalize thecanonicalTabSessionPathfallback (project-scope sessions) with clean+abs so shape variants cannot split one file into two keys.SessionPath: detach/attach paths (cloneDetachedRuntimeTab,applyRuntimeTab,detachSessionRuntime) now carry the display path separately from the runtime key; the detached sink rebind hashes the runtime key consistently.acquiredLeaseKeywhen the fast path merely reused an existing lease.SetModelForTab/SetEffortForTab/SetTokenModeForTabsupersede any in-flight startup build (supersedeTabBuildLocked: generation bump + build-context cancel) inside the same critical section that installs the replacement controller.Tests
go test ./internal/agent(full, incl. lease suite) — pass.cd desktop && go test .(full suite) — pass; lease/switch subset with-race— pass.GOOS=windows GOARCH=amd64 go build ./...in both modules — clean.Cache impact
Cache-impact: none. Desktop runtime ownership and lease bookkeeping only; no provider-visible prompts, tool schemas, request serialization, or compaction changes.
Fixes #5999. Fixes #6006. Fixes #5989. Fixes #5990. Refs #5996.