mirror of
https://github.com/esengine/DeepSeek-Reasonix.git
synced 2026-11-10 17:11:30 -10:00
Root cause: canonical (v5) sessions had nowhere to keep their permission preset. The legacy BranchMeta sidecar is refused for them, and neither the Session Service projection nor workspace presentation carries a preset, so the value lived only on the tab. OpenSession applied the source surface's live preset to the target session (prepareCanonicalControllerRuntime read it from the outgoing controller). Session B inherited A's danger-full-access, and a surface built from New Chat forced workspace-write onto every session opened into it. The renderer had the same leak one layer up: its composer profile table is keyed by tab, and the push that follows a navigation sent the previous session's preset to the new one. Decision: keep the preset per canonical SessionID in a desktop-owned record (desktop-session-presets.json next to desktop-tabs.json), with no change to the session log schema. Opening a session, and restoring tabs at startup, use only the value recorded for that exact SessionID. A session with no record gets the new-session default. The value on the surface is never carried over. Recording rule: a record is written only by SetPermissionPresetForTab, the one preset setter fenced by session. The caller passes the SessionID of the permission snapshot it read; under the tab's turnStartMu, which also serializes navigation, the call is refused with reasonix_error:permission_session_changed (errPermissionSessionChanged) unless that id equals the live controller's session. The record is keyed by that same session and the file is written outside App.mu. The revision alone cannot fence this: it is per controller, so two freshly opened sessions share a number. SetToolApprovalModeForTab and SetModeForTab name no session and so never record; saving tabs, submissions, composer-profile sync and draft settings never record either. Fix: - sessionPresetStore exposes restore, record and forget. A failed write removes the file, or empties it in place when the directory refuses the removal, so a restart opens every session at the default; the full set is written again on the next record or forget. - The renderer passes the snapshot's sessionId to SetPermissionPresetForTab. On permission_session_changed it re-reads the tab and does not retry the choice on the session the tab now shows. - Whenever a tab's canonical SessionID changes (syncTabSessionIdentity: New Session, clear, legacy continue, rebuild), the tab and its controller take that session's own record or the new-session default. - New Session on a blank tab that is reused drops the blank session's record, resets it to the default, and advances the controller's permission revision (Controller.InvalidatePermissionSnapshots), so a snapshot read before the reuse cannot record a choice for the session handed out as new. - A fork tab starts at the new-session default rather than the source tab's preset. - The canonical open path passes the target session's restored preset to prepareCanonicalControllerRuntime, and startup restore does the same for tabs bound to a SessionID. Legacy tabs keep their existing sidecar path. A session that is permanently deleted drops its record. - docs/desktop-migration inventory regenerated for the new signature and line positions. - Composer profiles in the renderer record which session they were read for (owner). A profile, or a meta snapshot, whose owner belongs to a different session than the tab's current one is discarded, not reconciled. Tests: - TestCanonicalSessionPresetFollowsSessionAcrossNavigationAndRestart and TestRestartDoesNotTrustSurfacePresetForUnrecordedSession fail on origin/main-v2 (session-B / session-A tab preset = "danger-full-access", want "workspace-write"). - TestNewSessionInTabDoesNotInheritSourcePreset: A=full, New Session in the same tab -> the new session is the default, has no record, and stays the default after restart through desktop-tabs.json and after reopening; A is still full. - TestNewSessionOnBlankTabDropsItsEarlierChoice and TestForkStartsAtNewSessionDefault cover the reused blank and fork paths. These three fail without the recording rule above with tab preset = "danger-full-access", want "workspace-write". - TestUnreadablePresetRecordsRestoreDefault: a corrupt record file restores the default. - TestStalePresetChoiceDoesNotLandOnNavigatedSession: snapshot A, open B (same revision), then set full access with A's snapshot -> refused with errPermissionSessionChanged, B not recorded, B stays at the default. The same scenario through the unfenced signature fails with "stale choice for session-A accepted on session-B" and B recorded as danger-full-access. - TestPresetWriteFailureInReadOnlyDirFailsClosed: full access recorded, the config directory made read-only, read-only chosen -> restart does not restore full access. Without the in-place truncation it fails with narrowed to read-only, write failed, restart restored "danger-full-access". - TestSnapshotBeforeBlankSessionReuseIsRefused: snapshot the blank tab, New Session reuses it, set full access with the old snapshot -> refused and nothing recorded. Without the revision advance the set is accepted and recorded as danger-full-access. - The desktop preset tests pin a host that offers every preset (control.SetPresetSandboxForTest), so they assert the same thing on a runner without an OS sandbox, where read-only and workspace-write are not offered. Pinned to "no sandbox", three of them fail with permission preset "read-only" is unavailable: shell sandbox requested but unavailable on this host. - TestUnfencedModeSettersDoNotRecord: SetToolApprovalModeForTab and SetModeForTab leave no record. - frontend permission-preset-session-fence.test.tsx: a navigation between the snapshot and the set neither applies nor records the stale choice, the refusal is not retried, and the tab is re-read. - frontend composer-profile-session-navigation.test.tsx runs the real projection, lifecycle and controller-profile hooks through A -> B -> A.