From b0155e148a2a26f3f206394e4e280150edbc4231 Mon Sep 17 00:00:00 2001
From: "bakudies@microsoft.com" <bakudies@microsoft.com>
Date: Sun, 2 Aug 2026 14:02:33 -0700
Subject: [PATCH 1/4] Add Default reasoning reset to chat

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
---
 src/OpenClaw.Chat/ChatModels.cs               |   1 +
 .../ThinkingLevelClearReconciler.cs           |  19 +
 src/OpenClaw.Shared/IOperatorGatewayClient.cs |  17 +
 src/OpenClaw.Shared/Models.cs                 |   1 +
 .../OpenClawGatewayClient.Protocol.cs         |  90 +++
 src/OpenClaw.Shared/OpenClawGatewayClient.cs  |  57 +-
 .../Chat/IChatGatewayBridge.cs                |  25 +
 .../Chat/OpenClawChatDataProvider.cs          | 315 +++++++-
 .../Chat/OpenClawChatRoot.cs                  |   3 +-
 .../Chat/OpenClawComposer.cs                  |  37 +-
 .../Chat/OpenClawReactorChatRoot.cs           |  42 +-
 .../Pages/ChatPage.xaml.cs                    |   3 +
 .../Strings/en-us/Resources.resw              |  12 +
 .../Strings/fr-fr/Resources.resw              |  12 +
 .../Strings/nl-nl/Resources.resw              |  12 +
 .../Strings/zh-cn/Resources.resw              |  12 +
 .../Strings/zh-tw/Resources.resw              |  12 +
 .../SessionsPatchThinkingLevelE2ETests.cs     | 227 ++++++
 .../GatewayProtocolLiveRoundTripTests.cs      | 111 ++-
 .../GatewayProtocolModelsTests.cs             |  27 +
 .../OpenClawGatewayClientTests.cs             |  16 +
 .../OpenClawChatDataProviderTests.cs          | 696 +++++++++++++++++-
 .../ThinkingLevelDefaultContractTests.cs      |  82 +++
 23 files changed, 1784 insertions(+), 45 deletions(-)
 create mode 100644 tests/OpenClaw.E2ETests/Setup/SessionsPatchThinkingLevelE2ETests.cs
 create mode 100644 tests/OpenClaw.Tray.Tests/ThinkingLevelDefaultContractTests.cs

diff --git a/src/OpenClaw.Chat/ChatModels.cs b/src/OpenClaw.Chat/ChatModels.cs
index 855b74430..9125d84bc 100644
--- a/src/OpenClaw.Chat/ChatModels.cs
+++ b/src/OpenClaw.Chat/ChatModels.cs
@@ -293,6 +293,7 @@ Task SendMessageAsync(string threadId, string message, CancellationToken cancell
     /// </summary>
     Task ClearModelAsync(string threadId, CancellationToken cancellationToken = default) => Task.CompletedTask;
     Task SetThinkingLevelAsync(string threadId, string thinkingLevel, CancellationToken cancellationToken = default);
+    Task ClearThinkingLevelAsync(string threadId, CancellationToken cancellationToken = default) => Task.CompletedTask;
     Task SetPermissionModeAsync(string threadId, bool allowAll, CancellationToken cancellationToken = default);
     Task RespondToPermissionAsync(string threadId, string requestId, string action, CancellationToken cancellationToken = default);
     Task RespondToPermissionAsync(string threadId, string requestId, bool allow, CancellationToken cancellationToken = default) =>
diff --git a/src/OpenClaw.Chat/ThinkingLevelClearReconciler.cs b/src/OpenClaw.Chat/ThinkingLevelClearReconciler.cs
index 0a3486f9f..3fc1bae6c 100644
--- a/src/OpenClaw.Chat/ThinkingLevelClearReconciler.cs
+++ b/src/OpenClaw.Chat/ThinkingLevelClearReconciler.cs
@@ -410,6 +410,25 @@ public SnapshotResolution ApplyCorrelatedSnapshot(
         }
     }
 
+    public bool TryGetRefreshCancellationToken(
+        RefreshRequest request,
+        out CancellationToken cancellationToken)
+    {
+        lock (_gate)
+        {
+            if (_disposed ||
+                !_entries.TryGetValue(request.ThreadId, out var entry) ||
+                !IsCurrentRefresh(entry, request))
+            {
+                cancellationToken = new CancellationToken(canceled: true);
+                return false;
+            }
+
+            cancellationToken = entry.LifetimeCancellation.Token;
+            return true;
+        }
+    }
+
     /// <summary>
     /// Advances connection authority. A reconnect or client swap invalidates all
     /// earlier refresh requests and requests current-generation convergence for
diff --git a/src/OpenClaw.Shared/IOperatorGatewayClient.cs b/src/OpenClaw.Shared/IOperatorGatewayClient.cs
index 6c93e99c2..81411ed12 100644
--- a/src/OpenClaw.Shared/IOperatorGatewayClient.cs
+++ b/src/OpenClaw.Shared/IOperatorGatewayClient.cs
@@ -157,6 +157,23 @@ Task<CommandCatalog> ListCommandsAsync(CommandCatalogQuery? query = null, int ti
     /// <summary>Apply an extended <see cref="SessionPatch"/> (rich field set) to a session.</summary>
     Task<bool> PatchSessionAsync(string key, SessionPatch patch)
         => Task.FromResult(false);
+    /// <summary>
+    /// Response-aware variant of <see cref="PatchSessionAsync(string, SessionPatch)"/>.
+    /// Returns the gateway's terminal result instead of completing after the frame is sent.
+    /// </summary>
+    Task<SessionCommandResult> PatchSessionDetailedAsync(
+        string key,
+        SessionPatch patch,
+        int timeoutMs = 15000,
+        CancellationToken cancellationToken = default)
+        => Task.FromResult(new SessionCommandResult
+        {
+            Method = "sessions.patch",
+            Ok = false,
+            IsSupported = false,
+            Key = key,
+            Error = "Response-aware sessions.patch is not supported by this gateway client."
+        });
     /// <summary>List session files, optionally scoped to a sub-path/search (<c>sessions.files.list</c>).</summary>
     Task<SessionFileList> ListSessionFilesAsync(string key, string? path = null, string? search = null, int timeoutMs = 15000)
         => Task.FromResult(new SessionFileList { Key = key, IsSupported = false });
diff --git a/src/OpenClaw.Shared/Models.cs b/src/OpenClaw.Shared/Models.cs
index e295118fb..23c4f3828 100644
--- a/src/OpenClaw.Shared/Models.cs
+++ b/src/OpenClaw.Shared/Models.cs
@@ -520,6 +520,7 @@ public class SessionCommandResult
 {
     public string Method { get; set; } = "";
     public bool Ok { get; set; }
+    public bool IsSupported { get; set; } = true;
     public string? Key { get; set; }
     public bool? Deleted { get; set; }
     public bool? Compacted { get; set; }
diff --git a/src/OpenClaw.Shared/OpenClawGatewayClient.Protocol.cs b/src/OpenClaw.Shared/OpenClawGatewayClient.Protocol.cs
index 25962ec1c..cd179e076 100644
--- a/src/OpenClaw.Shared/OpenClawGatewayClient.Protocol.cs
+++ b/src/OpenClaw.Shared/OpenClawGatewayClient.Protocol.cs
@@ -2,6 +2,7 @@
 using System.Collections.Generic;
 using System.Linq;
 using System.Text.Json;
+using System.Threading;
 using System.Threading.Tasks;
 
 namespace OpenClaw.Shared;
@@ -473,6 +474,95 @@ public Task<bool> PatchSessionAsync(string key, SessionPatch patch)
         return TrySendTrackedRequestAsync("sessions.patch", patch.ToPayload(key));
     }
 
+    /// <summary>
+    /// Applies an extended session patch and waits for the gateway response.
+    /// Unsupported gateways and protocol failures return a truthful typed result;
+    /// caller cancellation remains cancellation.
+    /// </summary>
+    public async Task<SessionCommandResult> PatchSessionDetailedAsync(
+        string key,
+        SessionPatch patch,
+        int timeoutMs = 15000,
+        CancellationToken cancellationToken = default)
+    {
+        if (string.IsNullOrWhiteSpace(key))
+        {
+            return CreateSessionPatchFailure(key, "Session key is required");
+        }
+        if (patch is null || !patch.HasChanges)
+        {
+            return CreateSessionPatchFailure(key, "Session patch has no changes");
+        }
+        if (!IsConnected)
+        {
+            return CreateSessionPatchFailure(key, "Gateway connection is not open");
+        }
+
+        try
+        {
+            var payload = await SendWizardRequestAsync(
+                "sessions.patch",
+                patch.ToPayload(key),
+                timeoutMs,
+                cancellationToken).ConfigureAwait(false);
+            return ParseSessionPatchResult(payload, key);
+        }
+        catch (OperationCanceledException)
+        {
+            throw;
+        }
+        catch (TimeoutException ex)
+        {
+            _logger.Warn($"sessions.patch timed out: {ex.Message}");
+            return CreateSessionPatchFailure(
+                key,
+                "The gateway did not respond before the session change timed out.");
+        }
+        catch (InvalidOperationException ex) when (IsUnknownMethodError(ex.Message))
+        {
+            _logger.Warn("sessions.patch unsupported on gateway");
+            return new SessionCommandResult
+            {
+                Method = "sessions.patch",
+                Ok = false,
+                IsSupported = false,
+                Key = key,
+                Error = ex.Message
+            };
+        }
+        catch (InvalidOperationException ex)
+        {
+            _logger.Warn($"sessions.patch failed: {ex.Message}");
+            return CreateSessionPatchFailure(key, ex.Message);
+        }
+    }
+
+    internal static SessionCommandResult ParseSessionPatchResult(JsonElement payload, string key)
+    {
+        var ok = !payload.TryGetProperty("ok", out var okElement) ||
+                 okElement.ValueKind == JsonValueKind.True;
+        var reason = GetStringSafe(payload, "reason");
+        var error = GetStringSafe(payload, "error");
+        return new SessionCommandResult
+        {
+            Method = "sessions.patch",
+            Ok = ok,
+            IsSupported = true,
+            Key = FirstNonEmpty(GetStringSafe(payload, "key"), key),
+            Reason = reason,
+            Error = ok ? null : error ?? reason ?? "The gateway could not change the session."
+        };
+    }
+
+    private static SessionCommandResult CreateSessionPatchFailure(string? key, string error) => new()
+    {
+        Method = "sessions.patch",
+        Ok = false,
+        IsSupported = true,
+        Key = key,
+        Error = error
+    };
+
     // ── sessions.files.list / sessions.files.get ──
 
     /// <summary>
diff --git a/src/OpenClaw.Shared/OpenClawGatewayClient.cs b/src/OpenClaw.Shared/OpenClawGatewayClient.cs
index 34c792310..a69810ac9 100644
--- a/src/OpenClaw.Shared/OpenClawGatewayClient.cs
+++ b/src/OpenClaw.Shared/OpenClawGatewayClient.cs
@@ -676,8 +676,19 @@ private static string ExtractMessageText(JsonElement message)
     /// Sends a wizard RPC request and waits for the response payload.
     /// Used for wizard.start, wizard.next, wizard.cancel, wizard.status.
     /// </summary>
-    public async Task<JsonElement> SendWizardRequestAsync(string method, object? parameters = null, int timeoutMs = 30000)
+    public Task<JsonElement> SendWizardRequestAsync(
+        string method,
+        object? parameters = null,
+        int timeoutMs = 30000) =>
+        SendWizardRequestAsync(method, parameters, timeoutMs, CancellationToken.None);
+
+    private async Task<JsonElement> SendWizardRequestAsync(
+        string method,
+        object? parameters,
+        int timeoutMs,
+        CancellationToken cancellationToken)
     {
+        cancellationToken.ThrowIfCancellationRequested();
         if (!IsConnected)
             throw new InvalidOperationException("Gateway connection is not open");
 
@@ -690,7 +701,9 @@ public async Task<JsonElement> SendWizardRequestAsync(string method, object? par
         {
             if (registration.Accepted)
                 await SendRawAsync(SerializeRequest(requestId, method, parameters));
-            return await completion.Task.WaitAsync(TimeSpan.FromMilliseconds(timeoutMs), CancellationToken);
+            return await completion.Task.WaitAsync(
+                TimeSpan.FromMilliseconds(timeoutMs),
+                cancellationToken);
         }
         catch (TimeoutException ex)
         {
@@ -712,6 +725,46 @@ public async Task RequestSessionsAsync(string? agentId = null)
             await SendTrackedRequestAsync("sessions.list");
     }
 
+    /// <summary>
+    /// Requests and returns the session snapshot from the matching
+    /// <c>sessions.list</c> response.
+    /// </summary>
+    public async Task<SessionInfo[]> RequestSessionsSnapshotAsync(
+        CancellationToken cancellationToken = default)
+    {
+        cancellationToken.ThrowIfCancellationRequested();
+        if (_operatorReadScopeUnavailable)
+            throw new NotSupportedException("sessions.list is unavailable for this connection.");
+        if (!IsConnected)
+            throw new InvalidOperationException("Gateway connection is not open");
+
+        const string method = "sessions.list";
+        var requestId = Guid.NewGuid().ToString();
+        var completion = new TaskCompletionSource<SessionInfo[]>(
+            TaskCreationOptions.RunContinuationsAsynchronously);
+        var registration = _pendingRequests.RegisterSessionSnapshot(
+            requestId,
+            completion,
+            method);
+
+        try
+        {
+            if (registration.Accepted)
+                await SendRawAsync(SerializeRequest(requestId, method, parameters: null));
+            return await completion.Task.WaitAsync(
+                TimeSpan.FromSeconds(30),
+                cancellationToken).ConfigureAwait(false);
+        }
+        catch (TimeoutException ex)
+        {
+            throw new TimeoutException($"Timed out waiting for {method} response", ex);
+        }
+        finally
+        {
+            _pendingRequests.Remove(registration);
+        }
+    }
+
     /// <summary>Subscribe to session change events so the gateway pushes
     /// <c>sessions.changed</c> notifications when sessions are mutated.</summary>
     public async Task SubscribeSessionEventsAsync()
diff --git a/src/OpenClaw.Tray.WinUI/Chat/IChatGatewayBridge.cs b/src/OpenClaw.Tray.WinUI/Chat/IChatGatewayBridge.cs
index 5788ce32a..0fc63bc11 100644
--- a/src/OpenClaw.Tray.WinUI/Chat/IChatGatewayBridge.cs
+++ b/src/OpenClaw.Tray.WinUI/Chat/IChatGatewayBridge.cs
@@ -70,6 +70,8 @@ Task<SessionCompactResult> CompactSessionDetailedAsync(string sessionKey) =>
             Error = "Response-aware sessions.compact is not supported by this chat bridge."
         });
     Task RequestSessionsAsync() => Task.CompletedTask;
+    Task<SessionInfo[]?> RequestSessionsSnapshotAsync(CancellationToken cancellationToken = default) =>
+        Task.FromResult<SessionInfo[]?>(null);
     Task PatchSessionModelAsync(string sessionKey, string model);
     /// <summary>
     /// Clears the session's model override (tri-state <c>sessions.patch</c> with
@@ -79,6 +81,17 @@ Task<SessionCompactResult> CompactSessionDetailedAsync(string sessionKey) =>
     /// </summary>
     Task ClearSessionModelAsync(string sessionKey);
     Task PatchSessionThinkingLevelAsync(string sessionKey, string thinkingLevel);
+    Task<SessionCommandResult> ClearSessionThinkingLevelAsync(
+        string sessionKey,
+        CancellationToken cancellationToken = default) =>
+        Task.FromResult(new SessionCommandResult
+        {
+            Method = "sessions.patch",
+            Ok = false,
+            IsSupported = false,
+            Key = sessionKey,
+            Error = "Response-aware thinking-level clear is not supported by this chat bridge."
+        });
     Task<ChatHistoryInfo> RequestChatHistoryAsync(string? sessionKey);
     Task SendChatAbortAsync(string runId, string? sessionKey = null);
     Task ResolveExecApprovalAsync(string approvalId, string decision);
@@ -214,6 +227,14 @@ public Task ClearSessionModelAsync(string sessionKey) =>
     public Task PatchSessionThinkingLevelAsync(string sessionKey, string thinkingLevel) =>
         _client.PatchSessionAsync(sessionKey, new SessionPatch { ThinkingLevel = thinkingLevel });
 
+    public Task<SessionCommandResult> ClearSessionThinkingLevelAsync(
+        string sessionKey,
+        CancellationToken cancellationToken = default) =>
+        _client.PatchSessionDetailedAsync(
+            sessionKey,
+            new SessionPatch { ThinkingLevel = SessionPatch.Clear },
+            cancellationToken: cancellationToken);
+
     public Task<CommandCatalog> ListCommandsAsync(CommandCatalogQuery? query = null) =>
         _client.ListCommandsAsync(query);
 
@@ -235,6 +256,10 @@ public Task<SessionCompactResult> CompactSessionDetailedAsync(string sessionKey)
     public Task RequestSessionsAsync() =>
         _client.RequestSessionsAsync();
 
+    public async Task<SessionInfo[]?> RequestSessionsSnapshotAsync(
+        CancellationToken cancellationToken = default) =>
+        await _client.RequestSessionsSnapshotAsync(cancellationToken).ConfigureAwait(false);
+
     public Task<ChatHistoryInfo> RequestChatHistoryAsync(string? sessionKey) =>
         _client.RequestChatHistoryAsync(sessionKey);
 
diff --git a/src/OpenClaw.Tray.WinUI/Chat/OpenClawChatDataProvider.cs b/src/OpenClaw.Tray.WinUI/Chat/OpenClawChatDataProvider.cs
index eeb155011..78b0c6ce4 100644
--- a/src/OpenClaw.Tray.WinUI/Chat/OpenClawChatDataProvider.cs
+++ b/src/OpenClaw.Tray.WinUI/Chat/OpenClawChatDataProvider.cs
@@ -31,6 +31,10 @@ internal static class LocalizationHelper
         "Chat_Permission_ResultSubmittedFormat" => "Approval {0} submitted for {1}.",
         "Chat_Error_SendReturnedStatusFormat" => "Gateway returned send status '{0}'.",
         "Chat_Error_SendFailedFormat" => "Send failed: {0}",
+        "Chat_Notification_ClearThinkingFailed" => "Reasoning setting not changed",
+        "Chat_Error_ClearThinkingFailedFormat" => "Could not use default reasoning: {0}",
+        "Chat_Error_ClearThinkingCanceled" => "The change was canceled.",
+        "Chat_Error_ClearThinkingInterrupted" => "The gateway connection changed before the update was confirmed.",
         _ => resourceKey
     };
 }
@@ -98,6 +102,7 @@ public sealed class OpenClawChatDataProvider : IChatDataProvider
     private readonly string _lastChatStateFilePath;
     private readonly TimeSpan _lastChatStateSaveDelay;
     private readonly Func<TimeSpan, CancellationToken, Func<Task>, Task> _scheduleHistoryRetry;
+    private readonly ThinkingLevelClearReconciler _thinkingLevelClearReconciler;
     private readonly Action? _historyFailureReservedForTesting;
     private System.Threading.Timer? _toolMetaSaveTimer; // debounce cache writes
     private long _toolMetaSaveVersion;
@@ -251,7 +256,9 @@ internal OpenClawChatDataProvider(
         string? lastChatStateFilePath = null,
         TimeSpan? lastChatStateSaveDelay = null,
         Func<TimeSpan, CancellationToken, Func<Task>, Task>? historyRetryScheduler = null,
-        Action? historyFailureReservedForTesting = null)
+        Action? historyFailureReservedForTesting = null,
+        TimeSpan? thinkingLevelConfirmationTimeout = null,
+        Func<TimeSpan, Func<Task>, Task>? thinkingLevelRetryScheduler = null)
     {
         _bridge = bridge ?? throw new ArgumentNullException(nameof(bridge));
         _post = post;
@@ -272,6 +279,14 @@ internal OpenClawChatDataProvider(
         });
         _historyFailureReservedForTesting = historyFailureReservedForTesting;
         _status = bridge.CurrentStatus;
+        _thinkingLevelClearReconciler = new ThinkingLevelClearReconciler(
+            connected: _status == ConnectionStatus.Connected,
+            confirmationTimeout: thinkingLevelConfirmationTimeout,
+            delay: thinkingLevelRetryScheduler is null
+                ? null
+                : (delay, cancellationToken) =>
+                    thinkingLevelRetryScheduler(delay, static () => Task.CompletedTask)
+                        .WaitAsync(cancellationToken));
         _persistedAbortedIds = LoadAbortedIds();
         _toolMetaCache = LoadToolMetaCache(_toolMetaCacheFilePath);
         _attachmentMetaCache = LoadAttachmentMetaCache(_attachmentMetaCacheFilePath);
@@ -316,13 +331,44 @@ public Task<ChatDataSnapshot> LoadAsync(CancellationToken cancellationToken = de
         cancellationToken.ThrowIfCancellationRequested();
         // Seed from whatever the bridge already knows about.
         var sessions = _bridge.GetSessionList() ?? Array.Empty<SessionInfo>();
+        List<ThinkingLevelClearReconciler.RefreshRequest>? thinkingLevelsToRefresh = null;
+        ChatDataSnapshot snapshot;
         lock (_gate)
         {
-            _sessions = sessions;
+            _sessions = sessions.Select(session =>
+            {
+                if (string.IsNullOrEmpty(session.Key))
+                    return session;
+
+                var resolution = _thinkingLevelClearReconciler.ObserveSnapshot(
+                    session.Key,
+                    session.ThinkingLevel);
+                if (resolution.RefreshRequest is { } refresh)
+                    (thinkingLevelsToRefresh ??= []).Add(refresh);
+                if (string.Equals(
+                        session.ThinkingLevel,
+                        resolution.EffectiveThinkingLevel,
+                        StringComparison.Ordinal))
+                {
+                    return session;
+                }
+
+                var reconciled = session.Clone();
+                reconciled.ThinkingLevel = resolution.EffectiveThinkingLevel;
+                return reconciled;
+            }).ToArray();
             EnsureTimelinesForSessionsLocked();
             RememberLastSessionStateLocked();
-            return Task.FromResult(BuildSnapshotLocked());
+            snapshot = BuildSnapshotLocked();
         }
+
+        if (thinkingLevelsToRefresh is not null)
+        {
+            foreach (var refresh in thinkingLevelsToRefresh)
+                ObserveBackgroundTask(ExecuteThinkingLevelRefreshAsync(refresh));
+        }
+
+        return Task.FromResult(snapshot);
     }
 
     internal void RememberSelectedThread(string? threadId)
@@ -1776,7 +1822,176 @@ private async Task AwaitPendingModelPatchAsync(string threadId, CancellationToke
     public async Task SetThinkingLevelAsync(string threadId, string thinkingLevel, CancellationToken cancellationToken = default)
     {
         cancellationToken.ThrowIfCancellationRequested();
-        await _bridge.PatchSessionThinkingLevelAsync(threadId, thinkingLevel);
+        ThinkingLevelClearReconciler.ConcreteSelection selection;
+        lock (_gate)
+        {
+            var canonicalThinkingLevel = _sessions
+                .FirstOrDefault(session => string.Equals(session.Key, threadId, StringComparison.Ordinal))
+                ?.ThinkingLevel;
+            selection = _thinkingLevelClearReconciler.BeginConcreteSelection(
+                threadId,
+                thinkingLevel,
+                canonicalThinkingLevel);
+        }
+
+        try
+        {
+            await _bridge.PatchSessionThinkingLevelAsync(threadId, thinkingLevel);
+        }
+        catch
+        {
+            _thinkingLevelClearReconciler.RejectConcreteSelection(selection);
+            throw;
+        }
+    }
+
+    public async Task ClearThinkingLevelAsync(string threadId, CancellationToken cancellationToken = default)
+    {
+        ThinkingLevelClearReconciler.ClearOperation operation;
+        var failureSurfaced = false;
+        lock (_gate)
+        {
+            var canonicalThinkingLevel = _sessions
+                .FirstOrDefault(session => string.Equals(session.Key, threadId, StringComparison.Ordinal))
+                ?.ThinkingLevel;
+            operation = _thinkingLevelClearReconciler.BeginClear(
+                threadId,
+                canonicalThinkingLevel);
+        }
+
+        try
+        {
+            var result = await _bridge.ClearSessionThinkingLevelAsync(threadId, cancellationToken);
+            if (!result.Ok)
+            {
+                var failure = new InvalidOperationException(
+                    result.Error ??
+                    (result.IsSupported
+                        ? "The gateway rejected the reasoning change."
+                        : "The gateway does not support clearing the reasoning override."));
+                if (_thinkingLevelClearReconciler.RejectPatch(operation, failure))
+                {
+                    SurfaceThinkingLevelClearFailure(threadId, failure, cancellationToken);
+                    failureSurfaced = true;
+                    throw failure;
+                }
+
+                await _thinkingLevelClearReconciler
+                    .WaitForConfirmationAsync(operation, cancellationToken)
+                    .ConfigureAwait(false);
+                throw failure;
+            }
+
+            if (_thinkingLevelClearReconciler.TryAcknowledgePatch(operation, out var refresh))
+                ObserveBackgroundTask(ExecuteThinkingLevelRefreshAsync(refresh));
+
+            var outcome = await _thinkingLevelClearReconciler
+                .WaitForConfirmationAsync(operation, cancellationToken)
+                .ConfigureAwait(false);
+            if (outcome == ThinkingLevelClearReconciler.ClearOutcome.Confirmed)
+            {
+                // Correlated confirmation occurs while ApplySessionsUpdated holds this lock.
+                // Reacquiring it prevents the clear from returning before the snapshot is stored.
+                lock (_gate)
+                {
+                }
+            }
+        }
+        catch (Exception ex)
+        {
+            if (!failureSurfaced &&
+                _thinkingLevelClearReconciler.RejectPatch(operation, ex))
+            {
+                SurfaceThinkingLevelClearFailure(threadId, ex, cancellationToken);
+            }
+            else if (!failureSurfaced &&
+                     !operation.PatchAcknowledged &&
+                     operation.State == ThinkingLevelClearReconciler.ReconciliationState.Interrupted)
+            {
+                SurfaceThinkingLevelClearFailure(threadId, ex, cancellationToken);
+            }
+
+            throw;
+        }
+    }
+
+    private async Task ExecuteThinkingLevelRefreshAsync(
+        ThinkingLevelClearReconciler.RefreshRequest request)
+    {
+        if (!_thinkingLevelClearReconciler.TryGetRefreshCancellationToken(
+                request,
+                out var requestCancellation))
+            return;
+
+        try
+        {
+            var sessions = await _bridge
+                .RequestSessionsSnapshotAsync(requestCancellation)
+                .ConfigureAwait(false);
+            if (sessions is null)
+            {
+                throw new NotSupportedException(
+                    "Response-correlated sessions.list is not supported by this chat bridge.");
+            }
+            if (!sessions.Any(session => string.Equals(
+                    session.Key,
+                    request.ThreadId,
+                    StringComparison.Ordinal)))
+            {
+                throw new InvalidOperationException(
+                    $"sessions.list did not include the expected session '{request.ThreadId}'.");
+            }
+
+            ApplySessionsUpdated(sessions, request);
+        }
+        catch (OperationCanceledException) when (requestCancellation.IsCancellationRequested)
+        {
+        }
+        catch
+        {
+            var retry = await _thinkingLevelClearReconciler
+                .RetryAfterFailureAsync(request)
+                .ConfigureAwait(false);
+            if (retry is { } next)
+                ObserveBackgroundTask(ExecuteThinkingLevelRefreshAsync(next));
+        }
+    }
+
+    private static void ObserveBackgroundTask(Task task)
+    {
+        _ = task.ContinueWith(
+            static completed => _ = completed.Exception,
+            CancellationToken.None,
+            TaskContinuationOptions.OnlyOnFaulted | TaskContinuationOptions.ExecuteSynchronously,
+            TaskScheduler.Default);
+    }
+
+    private void SurfaceThinkingLevelClearFailure(
+        string threadId,
+        Exception exception,
+        CancellationToken cancellationToken)
+    {
+        var reason = exception switch
+        {
+            ThinkingLevelClearInterruptedException =>
+                LocalizationHelper.GetString("Chat_Error_ClearThinkingInterrupted"),
+            OperationCanceledException when !cancellationToken.IsCancellationRequested &&
+                                            _bridge.CurrentStatus != ConnectionStatus.Connected =>
+                LocalizationHelper.GetString("Chat_Error_ClearThinkingInterrupted"),
+            OperationCanceledException =>
+                LocalizationHelper.GetString("Chat_Error_ClearThinkingCanceled"),
+            _ => exception.Message
+        };
+        var message = string.Format(
+            CultureInfo.CurrentCulture,
+            LocalizationHelper.GetString("Chat_Error_ClearThinkingFailedFormat"),
+            reason);
+        ApplyEventAndPublish(threadId, new ChatErrorEvent(message));
+        RaiseNotification(new ChatProviderNotification(
+            ChatProviderNotificationKind.Error,
+            threadId,
+            LocalizationHelper.GetString("Chat_Notification_ClearThinkingFailed"),
+            message));
     }
 
     public async Task EnsureCommandCatalogAsync(CancellationToken cancellationToken = default)
@@ -2029,6 +2244,7 @@ public ValueTask DisposeAsync()
             _locallyInitiatedThreads.Clear();
             _resetSubmittedLocalEchoTexts.Clear();
         }
+        _thinkingLevelClearReconciler.Dispose();
         CancelAndDisposeHistoryGeneration(historyGenerationToCancel);
         timerToDispose?.Dispose();
         chatStateTimerToDispose?.Dispose();
@@ -2062,6 +2278,7 @@ public IReadOnlyDictionary<string, ChatEntryMetadata> GetEntryMetadata(string th
 
     private void OnStatusChanged(object? sender, ConnectionStatus status)
     {
+        IReadOnlyList<ThinkingLevelClearReconciler.RefreshRequest> thinkingLevelsToRefresh;
         ChatDataSnapshot snapshot;
         bool justReconnected;
         string[] threadsToInterrupt;
@@ -2079,6 +2296,8 @@ private void OnStatusChanged(object? sender, ConnectionStatus status)
             var justDisconnected = (status == ConnectionStatus.Disconnected || status == ConnectionStatus.Error)
                                    && _status == ConnectionStatus.Connected;
             _status = status;
+            thinkingLevelsToRefresh = _thinkingLevelClearReconciler
+                .OnConnectionChanged(status == ConnectionStatus.Connected);
 
             // Reset the sessions-list-received gate whenever we leave the
             // Connected state. Any cached sessions belong to the previous
@@ -2155,6 +2374,11 @@ private void OnStatusChanged(object? sender, ConnectionStatus status)
         CancelAndDisposeHistoryGeneration(historyGenerationToCancel);
         Publish(snapshot);
 
+        foreach (var refresh in thinkingLevelsToRefresh)
+        {
+            ObserveBackgroundTask(ExecuteThinkingLevelRefreshAsync(refresh));
+        }
+
         // MEDIUM 5: synthesize the turn-end + status note for any threads
         // that were mid-turn when the connection dropped.
         var interruptedMsg = LocalizationHelper.GetString("Chat_Notification_ConnectionInterrupted");
@@ -2196,15 +2420,84 @@ private static void CancelAndDisposeHistoryGeneration(CancellationTokenSource? c
     }
 
     private void OnSessionsUpdated(object? sender, SessionInfo[] sessions)
+    {
+        ApplySessionsUpdated(sessions, refreshRequest: null);
+    }
+
+    private bool ApplySessionsUpdated(
+        SessionInfo[]? sessions,
+        ThinkingLevelClearReconciler.RefreshRequest? refreshRequest)
     {
         ChatDataSnapshot snapshot;
         string[] queuedThreadsToDrain;
+        bool shouldPublish;
+        List<ThinkingLevelClearReconciler.RefreshRequest>? thinkingLevelsToRefresh = null;
         lock (_gate)
         {
+            if (_disposed)
+                return false;
+
+            var incomingSessions = sessions ?? Array.Empty<SessionInfo>();
+            ThinkingLevelClearReconciler.SnapshotResolution? correlatedResolution = null;
+            if (refreshRequest is { } correlatedRequest)
+            {
+                var correlatedSession = incomingSessions.FirstOrDefault(session =>
+                    string.Equals(
+                        session.Key,
+                        correlatedRequest.ThreadId,
+                        StringComparison.Ordinal));
+                var resolution = _thinkingLevelClearReconciler.ApplyCorrelatedSnapshot(
+                    correlatedRequest,
+                    correlatedSession?.ThinkingLevel);
+                if (!resolution.Accepted)
+                    return false;
+
+                correlatedResolution = resolution;
+                if (resolution.RefreshRequest is { } retry)
+                    (thinkingLevelsToRefresh ??= []).Add(retry);
+            }
+
+            var previousThreads = refreshRequest is null
+                ? null
+                : BuildSnapshotLocked().Threads;
             var previousUsage = _sessions
                 .Where(s => !string.IsNullOrEmpty(s.Key))
                 .ToDictionary(s => s.Key, s => (s.InputTokens, s.OutputTokens, s.TotalTokens, s.ContextTokens));
-            _sessions = sessions ?? Array.Empty<SessionInfo>();
+            var reconciledSessions = new SessionInfo[incomingSessions.Length];
+            for (var index = 0; index < incomingSessions.Length; index++)
+            {
+                var incoming = incomingSessions[index];
+                reconciledSessions[index] = incoming;
+                if (string.IsNullOrEmpty(incoming.Key))
+                    continue;
+
+                ThinkingLevelClearReconciler.SnapshotResolution resolution;
+                if (refreshRequest is { } request &&
+                    string.Equals(request.ThreadId, incoming.Key, StringComparison.Ordinal))
+                {
+                    resolution = correlatedResolution!.Value;
+                }
+                else
+                {
+                    resolution = _thinkingLevelClearReconciler.ObserveSnapshot(
+                        incoming.Key,
+                        incoming.ThinkingLevel);
+                    if (resolution.RefreshRequest is { } refresh)
+                        (thinkingLevelsToRefresh ??= []).Add(refresh);
+                }
+
+                if (!string.Equals(
+                        incoming.ThinkingLevel,
+                        resolution.EffectiveThinkingLevel,
+                        StringComparison.Ordinal))
+                {
+                    var reconciled = incoming.Clone();
+                    reconciled.ThinkingLevel = resolution.EffectiveThinkingLevel;
+                    reconciledSessions[index] = reconciled;
+                }
+            }
+
+            _sessions = reconciledSessions;
             SeedSessionIdsFromSessionsLocked(_sessions);
             _sessionsListReceived = true;
             EnsureTimelinesForSessionsLocked();
@@ -2219,6 +2512,8 @@ private void OnSessionsUpdated(object? sender, SessionInfo[] sessions)
                     SnapshotLatestAssistantUsageLocked(s, ResolveTimelineKeyForSessionLocked(s));
             }
             snapshot = BuildSnapshotLocked();
+            shouldPublish = previousThreads is null ||
+                            !previousThreads.SequenceEqual(snapshot.Threads);
 
             if (_status == ConnectionStatus.Connected)
             {
@@ -2229,12 +2524,20 @@ private void OnSessionsUpdated(object? sender, SessionInfo[] sessions)
                 queuedThreadsToDrain = Array.Empty<string>();
             }
         }
-        Publish(snapshot);
+        if (shouldPublish)
+            Publish(snapshot);
+
+        if (thinkingLevelsToRefresh is not null)
+        {
+            foreach (var refresh in thinkingLevelsToRefresh)
+                ObserveBackgroundTask(ExecuteThinkingLevelRefreshAsync(refresh));
+        }
 
         foreach (var threadId in queuedThreadsToDrain)
         {
             TryDispatchNextQueuedSend(threadId);
         }
+        return true;
     }
 
     internal static bool ShouldPreserveLiveEntryDuringAuthoritativeReload(
diff --git a/src/OpenClaw.Tray.WinUI/Chat/OpenClawChatRoot.cs b/src/OpenClaw.Tray.WinUI/Chat/OpenClawChatRoot.cs
index f438e9756..ccca66e68 100644
--- a/src/OpenClaw.Tray.WinUI/Chat/OpenClawChatRoot.cs
+++ b/src/OpenClaw.Tray.WinUI/Chat/OpenClawChatRoot.cs
@@ -609,7 +609,8 @@ selectedThread is not null &&
                 },
                 OnModelChanged: model => ObserveFireAndForget(_provider.SetModelAsync(composerThread.Id!, model)),
                 OnModelCleared: () => ObserveFireAndForget(_provider.ClearModelAsync(composerThread.Id!)),
-                OnThinkingLevelChanged: level => RunFireAndForget(ct => _provider.SetThinkingLevelAsync(composerThread.Id!, level, ct)),
+                OnThinkingLevelChanged: level => ObserveFireAndForget(_provider.SetThinkingLevelAsync(composerThread.Id!, level)),
+                OnThinkingLevelCleared: () => ObserveFireAndForget(_provider.ClearThinkingLevelAsync(composerThread.Id!)),
                 OnPermissionsChanged: allowAll => RunFireAndForget(ct => _provider.SetPermissionModeAsync(composerThread.Id!, allowAll, ct)),
                 OnVoiceRequest: _onVoiceRequest,
                 OnAttachClick: _onAttachClick,
diff --git a/src/OpenClaw.Tray.WinUI/Chat/OpenClawComposer.cs b/src/OpenClaw.Tray.WinUI/Chat/OpenClawComposer.cs
index bc3cd4ee9..d1d48b360 100644
--- a/src/OpenClaw.Tray.WinUI/Chat/OpenClawComposer.cs
+++ b/src/OpenClaw.Tray.WinUI/Chat/OpenClawComposer.cs
@@ -63,6 +63,7 @@ public record OpenClawComposerProps(
     Action<string> OnChannelChanged,
     Action<string> OnModelChanged,
     Action<string> OnThinkingLevelChanged,
+    Action OnThinkingLevelCleared,
     Action<bool> OnPermissionsChanged,
     Func<CancellationToken, Action?, Task<string?>>? OnVoiceRequest = null,
     Action? OnAttachClick = null,
@@ -99,10 +100,8 @@ public sealed class OpenClawComposer : Component<OpenClawComposerProps>
     // rather than OnModelChanged.
     private static readonly object ClearModelTag = new();
 
-    // Thinking levels matching the gateway's sessions.patch thinkingLevel values.
-    // "medium" is the default when the session has no explicit thinkingLevel set.
-    private static readonly string[] ThinkingLevelIds    = { "off", "minimal", "low", "medium", "high" };
-    private static readonly string[] ThinkingLevelLabels = { "off", "minimal", "low", "medium (default)", "high" };
+    // Concrete gateway thinkingLevel values. Default is a distinct clear action.
+    private static readonly string[] ThinkingLevelIds = { "off", "minimal", "low", "medium", "high" };
 
     public override Element Render()
     {
@@ -374,16 +373,21 @@ public override Element Render()
             ? (defaultChoice?.DisplayName ?? "Default")
             : (currentChoice?.DisplayName ?? Props.CurrentModel ?? "Model");
 
-        var thinkingLevel = Props.CurrentThinkingLevel ?? "medium";
-        var thinkingIndex = Array.IndexOf(ThinkingLevelIds, thinkingLevel);
-        if (thinkingIndex < 0) thinkingIndex = 3; // default to "medium (default)"
+        var defaultReasoningLabel = LocalizationHelper.GetString("Chat_Composer_Reasoning_Default");
+        var thinkingIndex = Props.CurrentThinkingLevel is null
+            ? -1
+            : Array.IndexOf(ThinkingLevelIds, Props.CurrentThinkingLevel);
 
-        var reasoningMenuItems = new CoreMenuFlyoutItemBase[ThinkingLevelLabels.Length];
-        for (int i = 0; i < ThinkingLevelLabels.Length; i++)
+        var reasoningMenuItems = new CoreMenuFlyoutItemBase[ThinkingLevelIds.Length + 1];
+        reasoningMenuItems[0] = ToggleMenuItem(
+            defaultReasoningLabel,
+            isChecked: Props.CurrentThinkingLevel is null,
+            onClick: Props.OnThinkingLevelCleared);
+        for (int i = 0; i < ThinkingLevelIds.Length; i++)
         {
             var levelIndex = i;
-            reasoningMenuItems[i] = ToggleMenuItem(
-                ThinkingLevelLabels[i],
+            reasoningMenuItems[i + 1] = ToggleMenuItem(
+                ThinkingLevelIds[i],
                 isChecked: i == thinkingIndex,
                 onClick: () =>
                 {
@@ -509,12 +513,11 @@ Element SessionRow((string Id, string Title, string? Model, string? ModelProvide
             FlyoutPlacementMode.Top);
 
 
-        // Title-case the level id for the picker button (menu rows show the
-        // fuller "medium (default)" style labels).
-        var reasoningId = ThinkingLevelIds[thinkingIndex];
-        var reasoningPickerLabel = reasoningId.Length == 0
-            ? reasoningId
-            : char.ToUpperInvariant(reasoningId[0]) + reasoningId.Substring(1);
+        var reasoningPickerLabel = Props.CurrentThinkingLevel is null
+            ? defaultReasoningLabel
+            : thinkingIndex >= 0
+                ? char.ToUpperInvariant(ThinkingLevelIds[thinkingIndex][0]) + ThinkingLevelIds[thinkingIndex].Substring(1)
+                : Props.CurrentThinkingLevel;
 
         // ── Row 2: multi-line composer textbox ─────────────────────────
         var recording = isRecording.Value;
diff --git a/src/OpenClaw.Tray.WinUI/Chat/OpenClawReactorChatRoot.cs b/src/OpenClaw.Tray.WinUI/Chat/OpenClawReactorChatRoot.cs
index 06fbb94a4..29be5b65b 100644
--- a/src/OpenClaw.Tray.WinUI/Chat/OpenClawReactorChatRoot.cs
+++ b/src/OpenClaw.Tray.WinUI/Chat/OpenClawReactorChatRoot.cs
@@ -349,7 +349,8 @@ void SelectThread(string threadId)
                 SelectThread,
                 model => ObserveFireAndForget(props.Provider.SetModelAsync(effectiveThread.Id, model)),
                 () => ObserveFireAndForget(props.Provider.ClearModelAsync(effectiveThread.Id)),
-                level => RunFireAndForget(ct => props.Provider.SetThinkingLevelAsync(effectiveThread.Id, level, ct)),
+                level => ObserveFireAndForget(props.Provider.SetThinkingLevelAsync(effectiveThread.Id, level)),
+                () => ObserveFireAndForget(props.Provider.ClearThinkingLevelAsync(effectiveThread.Id)),
                 allowAll => RunFireAndForget(ct => props.Provider.SetPermissionModeAsync(effectiveThread.Id, allowAll, ct)),
                 props.OnVoiceRequest,
                 props.OnAttachClick,
@@ -562,6 +563,7 @@ public sealed record ReactorChatComposerProps(
     Action<string> OnModelChanged,
     Action OnModelCleared,
     Action<string> OnThinkingLevelChanged,
+    Action OnThinkingLevelCleared,
     Action<bool> OnPermissionsChanged,
     Func<CancellationToken, Action?, Task<string?>>? OnVoiceRequest,
     Action? OnAttachClick,
@@ -895,9 +897,10 @@ void Send()
             : Math.Max(0, Array.FindIndex(
                 selectableModels,
                 model => model.MatchesModel(props.CurrentThread.Model, props.CurrentThread.ModelProvider)) + 1);
-        var thinkingIndex = Math.Max(0, Array.IndexOf(
-            ThinkingLevels,
-            props.CurrentThread.ThinkingLevel ?? "medium"));
+        var defaultReasoningLabel = Localized("Chat_Composer_Reasoning_Default", "Default");
+        var thinkingIndex = props.CurrentThread.ThinkingLevel is null
+            ? -1
+            : Array.IndexOf(ThinkingLevels, props.CurrentThread.ThinkingLevel);
         var actionLabel = props.TurnActive
             ? Localized("Chat_Composer_Tooltip_Stop", "Stop")
             : Localized("Chat_Composer_Tooltip_Send", "Send");
@@ -1364,19 +1367,32 @@ void CommitSlashText(string value, ReactorSlashMenuState nextState)
                     }))
                 .ToArray());
 
+        var reasoningPickerLabel = props.CurrentThread.ThinkingLevel is null
+            ? defaultReasoningLabel
+            : thinkingIndex >= 0
+                ? ThinkingLevels[thinkingIndex]
+                : props.CurrentThread.ThinkingLevel;
+        var reasoningMenuItems = new[]
+            {
+                RadioMenuItem(
+                    defaultReasoningLabel,
+                    "chat-thinking-level",
+                    props.CurrentThread.ThinkingLevel is null,
+                    props.OnThinkingLevelCleared)
+            }
+            .Concat(ThinkingLevels.Select((level, index) => RadioMenuItem(
+                level,
+                "chat-thinking-level",
+                index == thinkingIndex,
+                () => props.OnThinkingLevelChanged(level))))
+            .ToArray();
         var reasoningPicker = MenuFlyout(
             PickerButton(
-                ThinkingLevels[thinkingIndex],
-                $"{Localized("Chat_Composer_Accessibility_Reasoning", "Reasoning")}: {ThinkingLevels[thinkingIndex]}",
+                reasoningPickerLabel,
+                $"{Localized("Chat_Composer_Accessibility_Reasoning", "Reasoning")}: {reasoningPickerLabel}",
                 !props.MessageOptionsDisabled,
                 props.IsCompact ? 54 : 96),
-            ThinkingLevels
-                .Select((level, index) => RadioMenuItem(
-                    level,
-                    "chat-thinking-level",
-                    index == thinkingIndex,
-                    () => props.OnThinkingLevelChanged(level)))
-                .ToArray());
+            reasoningMenuItems);
 
         var attachButton = IconButton(
             "\uE723",
diff --git a/src/OpenClaw.Tray.WinUI/Pages/ChatPage.xaml.cs b/src/OpenClaw.Tray.WinUI/Pages/ChatPage.xaml.cs
index 8eb325181..c762e8f1e 100644
--- a/src/OpenClaw.Tray.WinUI/Pages/ChatPage.xaml.cs
+++ b/src/OpenClaw.Tray.WinUI/Pages/ChatPage.xaml.cs
@@ -379,6 +379,9 @@ public Task SetModelAsync(string threadId, string model, CancellationToken cance
         public Task SetThinkingLevelAsync(string threadId, string thinkingLevel, CancellationToken cancellationToken = default)
             => Task.CompletedTask;
 
+        public Task ClearThinkingLevelAsync(string threadId, CancellationToken cancellationToken = default)
+            => Task.CompletedTask;
+
         public Task SetPermissionModeAsync(string threadId, bool allowAll, CancellationToken cancellationToken = default)
             => Task.CompletedTask;
 
diff --git a/src/OpenClaw.Tray.WinUI/Strings/en-us/Resources.resw b/src/OpenClaw.Tray.WinUI/Strings/en-us/Resources.resw
index 529a9db94..3b3af2747 100644
--- a/src/OpenClaw.Tray.WinUI/Strings/en-us/Resources.resw
+++ b/src/OpenClaw.Tray.WinUI/Strings/en-us/Resources.resw
@@ -3288,6 +3288,18 @@ Commands are blocked while sandboxing is unavailable because strict fallback blo
   <data name="Chat_Error_SendFailedFormat" xml:space="preserve">
     <value>Send failed: {0}</value>
   </data>
+  <data name="Chat_Notification_ClearThinkingFailed" xml:space="preserve">
+    <value>Reasoning setting not changed</value>
+  </data>
+  <data name="Chat_Error_ClearThinkingFailedFormat" xml:space="preserve">
+    <value>Could not use default reasoning: {0}</value>
+  </data>
+  <data name="Chat_Error_ClearThinkingCanceled" xml:space="preserve">
+    <value>The change was canceled.</value>
+  </data>
+  <data name="Chat_Error_ClearThinkingInterrupted" xml:space="preserve">
+    <value>The gateway connection changed before the update was confirmed.</value>
+  </data>
   <data name="Chat_Notification_AbortFailed" xml:space="preserve">
     <value>Abort failed</value>
   </data>
diff --git a/src/OpenClaw.Tray.WinUI/Strings/fr-fr/Resources.resw b/src/OpenClaw.Tray.WinUI/Strings/fr-fr/Resources.resw
index ac6fd09aa..faf6fcbbb 100644
--- a/src/OpenClaw.Tray.WinUI/Strings/fr-fr/Resources.resw
+++ b/src/OpenClaw.Tray.WinUI/Strings/fr-fr/Resources.resw
@@ -3241,6 +3241,18 @@ Les commandes sont bloquées tant que le sandboxing est indisponible, car le blo
   <data name="Chat_Error_SendFailedFormat" xml:space="preserve">
     <value>Échec de l’envoi : {0}</value>
   </data>
+  <data name="Chat_Notification_ClearThinkingFailed" xml:space="preserve">
+    <value>Paramètre de raisonnement inchangé</value>
+  </data>
+  <data name="Chat_Error_ClearThinkingFailedFormat" xml:space="preserve">
+    <value>Impossible d’utiliser le raisonnement par défaut : {0}</value>
+  </data>
+  <data name="Chat_Error_ClearThinkingCanceled" xml:space="preserve">
+    <value>La modification a été annulée.</value>
+  </data>
+  <data name="Chat_Error_ClearThinkingInterrupted" xml:space="preserve">
+    <value>La connexion à la passerelle a changé avant la confirmation de la mise à jour.</value>
+  </data>
   <data name="Chat_Notification_AbortFailed" xml:space="preserve">
     <value>Échec de l'abandon</value>
   </data>
diff --git a/src/OpenClaw.Tray.WinUI/Strings/nl-nl/Resources.resw b/src/OpenClaw.Tray.WinUI/Strings/nl-nl/Resources.resw
index 3c5262e8b..eb8474af4 100644
--- a/src/OpenClaw.Tray.WinUI/Strings/nl-nl/Resources.resw
+++ b/src/OpenClaw.Tray.WinUI/Strings/nl-nl/Resources.resw
@@ -3242,6 +3242,18 @@ Opdrachten worden geblokkeerd zolang sandboxing niet beschikbaar is, omdat strik
   <data name="Chat_Error_SendFailedFormat" xml:space="preserve">
     <value>Verzenden mislukt: {0}</value>
   </data>
+  <data name="Chat_Notification_ClearThinkingFailed" xml:space="preserve">
+    <value>Redeneerinstelling niet gewijzigd</value>
+  </data>
+  <data name="Chat_Error_ClearThinkingFailedFormat" xml:space="preserve">
+    <value>Standaardredenering kan niet worden gebruikt: {0}</value>
+  </data>
+  <data name="Chat_Error_ClearThinkingCanceled" xml:space="preserve">
+    <value>De wijziging is geannuleerd.</value>
+  </data>
+  <data name="Chat_Error_ClearThinkingInterrupted" xml:space="preserve">
+    <value>De gatewayverbinding is gewijzigd voordat de update werd bevestigd.</value>
+  </data>
   <data name="Chat_Notification_AbortFailed" xml:space="preserve">
     <value>Afbreken mislukt</value>
   </data>
diff --git a/src/OpenClaw.Tray.WinUI/Strings/zh-cn/Resources.resw b/src/OpenClaw.Tray.WinUI/Strings/zh-cn/Resources.resw
index 73d972a2e..3518b891b 100644
--- a/src/OpenClaw.Tray.WinUI/Strings/zh-cn/Resources.resw
+++ b/src/OpenClaw.Tray.WinUI/Strings/zh-cn/Resources.resw
@@ -3241,6 +3241,18 @@
   <data name="Chat_Error_SendFailedFormat" xml:space="preserve">
     <value>发送失败：{0}</value>
   </data>
+  <data name="Chat_Notification_ClearThinkingFailed" xml:space="preserve">
+    <value>推理设置未更改</value>
+  </data>
+  <data name="Chat_Error_ClearThinkingFailedFormat" xml:space="preserve">
+    <value>无法使用默认推理：{0}</value>
+  </data>
+  <data name="Chat_Error_ClearThinkingCanceled" xml:space="preserve">
+    <value>更改已取消。</value>
+  </data>
+  <data name="Chat_Error_ClearThinkingInterrupted" xml:space="preserve">
+    <value>在确认更新前，网关连接已更改。</value>
+  </data>
   <data name="Chat_Notification_AbortFailed" xml:space="preserve">
     <value>中止失败</value>
   </data>
diff --git a/src/OpenClaw.Tray.WinUI/Strings/zh-tw/Resources.resw b/src/OpenClaw.Tray.WinUI/Strings/zh-tw/Resources.resw
index 62cceb804..bf69ada9f 100644
--- a/src/OpenClaw.Tray.WinUI/Strings/zh-tw/Resources.resw
+++ b/src/OpenClaw.Tray.WinUI/Strings/zh-tw/Resources.resw
@@ -3241,6 +3241,18 @@
   <data name="Chat_Error_SendFailedFormat" xml:space="preserve">
     <value>傳送失敗：{0}</value>
   </data>
+  <data name="Chat_Notification_ClearThinkingFailed" xml:space="preserve">
+    <value>推理設定未變更</value>
+  </data>
+  <data name="Chat_Error_ClearThinkingFailedFormat" xml:space="preserve">
+    <value>無法使用預設推理：{0}</value>
+  </data>
+  <data name="Chat_Error_ClearThinkingCanceled" xml:space="preserve">
+    <value>變更已取消。</value>
+  </data>
+  <data name="Chat_Error_ClearThinkingInterrupted" xml:space="preserve">
+    <value>在確認更新前，閘道連線已變更。</value>
+  </data>
   <data name="Chat_Notification_AbortFailed" xml:space="preserve">
     <value>中止失敗</value>
   </data>
diff --git a/tests/OpenClaw.E2ETests/Setup/SessionsPatchThinkingLevelE2ETests.cs b/tests/OpenClaw.E2ETests/Setup/SessionsPatchThinkingLevelE2ETests.cs
new file mode 100644
index 000000000..864c25e46
--- /dev/null
+++ b/tests/OpenClaw.E2ETests/Setup/SessionsPatchThinkingLevelE2ETests.cs
@@ -0,0 +1,227 @@
+using System.Text.Json;
+using OpenClaw.Shared;
+
+namespace OpenClaw.E2ETests.Setup;
+
+[CollectionDefinition("Reasoning Gateway E2E")]
+public sealed class ReasoningGatewayE2ECollection : ICollectionFixture<E2ESetupFixture> { }
+
+[Collection("Reasoning Gateway E2E")]
+public sealed class SessionsPatchThinkingLevelE2ETests
+{
+    private static readonly TimeSpan s_timeout = TimeSpan.FromSeconds(30);
+    private readonly E2ESetupFixture _fixture;
+
+    public SessionsPatchThinkingLevelE2ETests(E2ESetupFixture fixture)
+    {
+        _fixture = fixture;
+
+        if (_fixture.SetupError is not null)
+            throw new InvalidOperationException($"E2E setup failed: {_fixture.SetupError}");
+        if (_fixture.Client is null)
+            throw new InvalidOperationException("E2E fixture MCP client not initialized");
+    }
+
+    [E2EFact]
+    public async Task RealGateway_ThinkingLevelOffThenDefault_PersistsCanonicalNull()
+    {
+        var gateway = _fixture.ReadActiveGatewayRecord();
+        var credentials = _fixture.ReadActiveGatewayCredentialState();
+        Assert.False(string.IsNullOrWhiteSpace(gateway.SharedGatewayToken));
+
+        using var client = new OpenClawGatewayClient(
+            gateway.GatewayUrl,
+            gateway.SharedGatewayToken!,
+            identityPath: credentials.IdentityDir);
+
+        string? sessionKey = null;
+        SessionInfo? original = null;
+        var mutated = false;
+        Exception? testFailure = null;
+        var cleanupFailures = new List<Exception>();
+
+        try
+        {
+            await client.ConnectAsync();
+            await WaitForAsync(
+                () => client.HasHandshakeSnapshot && !string.IsNullOrWhiteSpace(client.MainSessionKey),
+                "typed Gateway client handshake");
+
+            sessionKey = client.MainSessionKey!;
+            original = (await client.RequestSessionsSnapshotAsync()).SingleOrDefault(
+                candidate => string.Equals(candidate.Key, sessionKey, StringComparison.Ordinal));
+
+            mutated = true;
+            var offResult = await client.PatchSessionDetailedAsync(
+                sessionKey,
+                new SessionPatch { ThinkingLevel = "off" });
+            Assert.True(offResult.Ok, offResult.Error);
+
+            var offSnapshot = await ReadSessionAsync(client, sessionKey);
+            Assert.Equal("off", offSnapshot.ThinkingLevel);
+            await WaitForTrayThinkingLevelAsync(sessionKey, "off");
+
+            var clearResult = await client.PatchSessionDetailedAsync(
+                sessionKey,
+                new SessionPatch { ThinkingLevel = SessionPatch.Clear });
+            Assert.True(clearResult.Ok, clearResult.Error);
+
+            var defaultSnapshot = await ReadSessionAsync(client, sessionKey);
+            Assert.Null(defaultSnapshot.ThinkingLevel);
+
+            using (var reconnect = await _fixture.Client!.CallToolExpectSuccessAsync(
+                       "app.connection.reconnect"))
+            {
+                Assert.True(reconnect.RootElement.GetProperty("reconnected").GetBoolean());
+            }
+            await _fixture.WaitForConnectionReady();
+            await WaitForTrayThinkingLevelAsync(sessionKey, expected: null);
+
+            Console.WriteLine(
+                "[E2E] Real WSL Gateway persisted off -> null (Default), and the isolated Tray " +
+                "confirmed canonical null after a current-generation reconnect snapshot.");
+        }
+        catch (Exception ex)
+        {
+            testFailure = ex;
+        }
+        finally
+        {
+            if (mutated && sessionKey is not null)
+            {
+                try
+                {
+                    var restorePatch = new SessionPatch
+                    {
+                        ThinkingLevel = original?.ThinkingLevel is null
+                            ? SessionPatch.Clear
+                            : original.ThinkingLevel
+                    };
+                    var restoreResult = await client.PatchSessionDetailedAsync(sessionKey, restorePatch);
+                    if (!restoreResult.Ok)
+                    {
+                        throw new InvalidOperationException(
+                            $"Failed to restore the original thinking-level state: {restoreResult.Error}");
+                    }
+                }
+                catch (Exception ex)
+                {
+                    cleanupFailures.Add(ex);
+                }
+            }
+
+            try
+            {
+                await client.DisconnectAsync();
+            }
+            catch (Exception ex)
+            {
+                cleanupFailures.Add(ex);
+            }
+        }
+
+        if (testFailure is not null)
+        {
+            if (cleanupFailures.Count > 0)
+            {
+                cleanupFailures.Insert(0, testFailure);
+                throw new AggregateException("The E2E proof and its cleanup both failed.", cleanupFailures);
+            }
+
+            System.Runtime.ExceptionServices.ExceptionDispatchInfo.Capture(testFailure).Throw();
+        }
+
+        if (cleanupFailures.Count > 0)
+            throw new AggregateException("The E2E proof cleanup failed.", cleanupFailures);
+    }
+
+    private static async Task<SessionInfo> ReadSessionAsync(
+        OpenClawGatewayClient client,
+        string sessionKey)
+    {
+        var sessions = await client.RequestSessionsSnapshotAsync();
+        var session = sessions.SingleOrDefault(
+            candidate => string.Equals(candidate.Key, sessionKey, StringComparison.Ordinal));
+        return Assert.IsType<SessionInfo>(session);
+    }
+
+    private async Task WaitForTrayThinkingLevelAsync(string sessionKey, string? expected)
+    {
+        string? lastObserved = "<session missing>";
+        var deadline = DateTime.UtcNow.Add(s_timeout);
+
+        while (DateTime.UtcNow < deadline)
+        {
+            using var snapshot = await _fixture.Client!.CallToolExpectSuccessAsync(
+                "app.chat.snapshot",
+                new { sessionKey });
+            var root = snapshot.RootElement;
+            if (!TryGetPropertyIgnoreCase(root, "connectionStatus", out var connectionStatus) ||
+                connectionStatus.GetString() is not ("Connected" or "Ready") ||
+                !TryGetPropertyIgnoreCase(root, "composeTarget", out var composeTarget) ||
+                !TryGetPropertyIgnoreCase(composeTarget, "isReady", out var isReady) ||
+                !isReady.GetBoolean() ||
+                !TryGetPropertyIgnoreCase(composeTarget, "sessionKey", out var composeSessionKey) ||
+                !string.Equals(composeSessionKey.GetString(), sessionKey, StringComparison.Ordinal) ||
+                !TryGetPropertyIgnoreCase(root, "threads", out var threads) ||
+                threads.ValueKind != JsonValueKind.Array)
+            {
+                lastObserved = "<snapshot not ready>";
+                await Task.Delay(250);
+                continue;
+            }
+
+            var thread = threads.EnumerateArray().FirstOrDefault(
+                candidate =>
+                    TryGetPropertyIgnoreCase(candidate, "id", out var id) &&
+                    string.Equals(id.GetString(), sessionKey, StringComparison.Ordinal));
+
+            if (thread.ValueKind != JsonValueKind.Undefined &&
+                TryGetPropertyIgnoreCase(thread, "thinkingLevel", out var thinkingLevel))
+            {
+                lastObserved = thinkingLevel.ValueKind == JsonValueKind.Null
+                    ? null
+                    : thinkingLevel.GetString();
+                if (string.Equals(lastObserved, expected, StringComparison.Ordinal))
+                    return;
+            }
+
+            await Task.Delay(250);
+        }
+
+        throw new TimeoutException(
+            $"Tray did not converge to thinkingLevel '{expected ?? "<null>"}'. " +
+            $"Last observed: '{lastObserved ?? "<null>"}'.");
+    }
+
+    private static async Task WaitForAsync(Func<bool> condition, string description)
+    {
+        var deadline = DateTime.UtcNow.Add(s_timeout);
+        while (DateTime.UtcNow < deadline)
+        {
+            if (condition())
+                return;
+            await Task.Delay(100);
+        }
+
+        throw new TimeoutException($"Timed out waiting for {description}.");
+    }
+
+    private static bool TryGetPropertyIgnoreCase(
+        JsonElement element,
+        string propertyName,
+        out JsonElement property)
+    {
+        foreach (var candidate in element.EnumerateObject())
+        {
+            if (string.Equals(candidate.Name, propertyName, StringComparison.OrdinalIgnoreCase))
+            {
+                property = candidate.Value;
+                return true;
+            }
+        }
+
+        property = default;
+        return false;
+    }
+}
diff --git a/tests/OpenClaw.Shared.Tests/GatewayProtocolLiveRoundTripTests.cs b/tests/OpenClaw.Shared.Tests/GatewayProtocolLiveRoundTripTests.cs
index b5e6469b7..0550035f5 100644
--- a/tests/OpenClaw.Shared.Tests/GatewayProtocolLiveRoundTripTests.cs
+++ b/tests/OpenClaw.Shared.Tests/GatewayProtocolLiveRoundTripTests.cs
@@ -117,6 +117,33 @@ public async Task NewProtocolMethods_RealWebSocketRoundTrip_SendCorrectWireFrame
             var clearFrame = await server.WaitFrameAsync("sessions.patch", occurrence: 1, timeoutMs: rpc);
             Assert.Contains("\"model\":null", clearFrame);
 
+            var detailedClear = await client.PatchSessionDetailedAsync(
+                key,
+                new SessionPatch { ThinkingLevel = SessionPatch.Clear },
+                timeoutMs: rpc);
+            Assert.True(detailedClear.Ok);
+            Assert.True(detailedClear.IsSupported);
+            Assert.Equal(key, detailedClear.Key);
+            var detailedClearFrame = await server.WaitFrameAsync("sessions.patch", occurrence: 2, timeoutMs: rpc);
+            Assert.Contains("\"method\":\"sessions.patch\"", detailedClearFrame);
+            Assert.Contains("\"key\":\"agent:main:main\"", detailedClearFrame);
+            Assert.Contains("\"thinkingLevel\":null", detailedClearFrame);
+            Assert.DoesNotContain("\"thinkingLevel\":\"default\"", detailedClearFrame);
+            Assert.DoesNotContain("\"thinkingLevel\":\"medium\"", detailedClearFrame);
+
+            // ── 6. sessions.list correlated snapshot ──
+            var sessionsUpdated = 0;
+            client.SessionsUpdated += (_, _) => sessionsUpdated++;
+            var sessions = await client.RequestSessionsSnapshotAsync();
+            var refreshedSession = Assert.Single(sessions);
+            Assert.Equal(key, refreshedSession.Key);
+            Assert.Null(refreshedSession.ThinkingLevel);
+            Assert.Equal("gpt-5", refreshedSession.Model);
+            Assert.Equal(0, sessionsUpdated);
+            Assert.Empty(client.GetSessionList());
+            var sessionsFrame = await server.WaitFrameAsync("sessions.list", occurrence: 0, timeoutMs: rpc);
+            Assert.Contains("\"method\":\"sessions.list\"", sessionsFrame);
+
             PrintProof(server);
         }
         finally
@@ -214,6 +241,62 @@ public async Task ChatSend_UsesCallerProvidedIdempotencyKeyOnWire()
         }
     }
 
+    [Fact]
+    public async Task SessionPatchDetailed_RepresentsDisconnectedUnknownTimeoutAndCancellation()
+    {
+        var disconnected = new OpenClawGatewayClient(
+            "ws://127.0.0.1:1/",
+            "test-token",
+            new TestLogger(),
+            identityPath: _identityDir);
+        var disconnectedResult = await disconnected.PatchSessionDetailedAsync(
+            "agent:main:main",
+            new SessionPatch { ThinkingLevel = SessionPatch.Clear });
+        Assert.False(disconnectedResult.Ok);
+        Assert.True(disconnectedResult.IsSupported);
+        Assert.Contains("not open", disconnectedResult.Error);
+
+        using var server = new LoopbackGatewayServer();
+        server.OnMethod("sessions.patch", _ => LoopbackResponse.Fail("unknown method: sessions.patch"));
+        var client = new OpenClawGatewayClient(
+            server.WebSocketUrl,
+            "test-token",
+            new TestLogger(),
+            identityPath: _identityDir);
+        try
+        {
+            await ConnectAndWaitAsync(client, server);
+            var unknown = await client.PatchSessionDetailedAsync(
+                "agent:main:main",
+                new SessionPatch { ThinkingLevel = SessionPatch.Clear },
+                timeoutMs: 20000);
+            Assert.False(unknown.Ok);
+            Assert.False(unknown.IsSupported);
+            Assert.Contains("unknown method", unknown.Error);
+
+            server.OnMethod("sessions.patch", _ => LoopbackResponse.NoResponse());
+            var timeout = await client.PatchSessionDetailedAsync(
+                "agent:main:main",
+                new SessionPatch { ThinkingLevel = SessionPatch.Clear },
+                timeoutMs: 50);
+            Assert.False(timeout.Ok);
+            Assert.True(timeout.IsSupported);
+            Assert.Contains("timed out", timeout.Error);
+
+            using var cancellation = new CancellationTokenSource(50);
+            await Assert.ThrowsAnyAsync<OperationCanceledException>(() =>
+                client.PatchSessionDetailedAsync(
+                    "agent:main:main",
+                    new SessionPatch { ThinkingLevel = SessionPatch.Clear },
+                    timeoutMs: 20000,
+                    cancellationToken: cancellation.Token));
+        }
+        finally
+        {
+            await client.DisconnectAsync();
+        }
+    }
+
     private static void ConfigureResponders(LoopbackGatewayServer server)
     {
         // NOTE: intentionally NO hello-ok responder. The new methods only require
@@ -248,6 +331,21 @@ private static void ConfigureResponders(LoopbackGatewayServer server)
             }
         });
 
+        server.OnMethod("sessions.list", _ => new
+        {
+            sessions = new object[]
+            {
+                new
+                {
+                    key = "agent:main:main",
+                    status = "active",
+                    model = "gpt-5",
+                    thinkingLevel = (string?)null,
+                    isMain = true
+                }
+            }
+        });
+
         server.OnMethod("sessions.files.get", _ => new
         {
             sessionKey = "agent:main:main",
@@ -513,10 +611,14 @@ private async Task HandleFrameAsync(WebSocket socket, string frame)
                 : new { };
 
             var response = payload is LoopbackResponse loopbackResponse
-                ? loopbackResponse.Ok
+                ? loopbackResponse.SuppressResponse
+                    ? null
+                    : loopbackResponse.Ok
                     ? JsonSerializer.Serialize(new { type = "res", id, ok = true, payload = loopbackResponse.Payload })
                     : JsonSerializer.Serialize(new { type = "res", id, ok = false, error = loopbackResponse.Error })
                 : JsonSerializer.Serialize(new { type = "res", id, ok = true, payload });
+            if (response is null)
+                return;
             var bytes = Encoding.UTF8.GetBytes(response);
             await socket.SendAsync(new ArraySegment<byte>(bytes), WebSocketMessageType.Text, endOfMessage: true, CancellationToken.None);
         }
@@ -540,8 +642,13 @@ public void Dispose()
         }
     }
 
-    private sealed record LoopbackResponse(bool Ok, object? Payload = null, string? Error = null)
+    private sealed record LoopbackResponse(
+        bool Ok,
+        object? Payload = null,
+        string? Error = null,
+        bool SuppressResponse = false)
     {
         public static LoopbackResponse Fail(string error) => new(false, Error: error);
+        public static LoopbackResponse NoResponse() => new(false, SuppressResponse: true);
     }
 }
diff --git a/tests/OpenClaw.Shared.Tests/GatewayProtocolModelsTests.cs b/tests/OpenClaw.Shared.Tests/GatewayProtocolModelsTests.cs
index 634a9c0e9..b65511e79 100644
--- a/tests/OpenClaw.Shared.Tests/GatewayProtocolModelsTests.cs
+++ b/tests/OpenClaw.Shared.Tests/GatewayProtocolModelsTests.cs
@@ -27,6 +27,7 @@ public void NewGatewayProtocolMembers_AreDefaultInterfaceMethods_SoTheyDoNotSour
         {
             ("ListCommandsAsync", new[] { typeof(CommandCatalogQuery), typeof(int) }),
             ("PatchSessionAsync", new[] { typeof(string), typeof(SessionPatch) }),
+            ("PatchSessionDetailedAsync", new[] { typeof(string), typeof(SessionPatch), typeof(int), typeof(CancellationToken) }),
             ("ListSessionFilesAsync", new[] { typeof(string), typeof(string), typeof(string), typeof(int) }),
             ("GetSessionFileAsync", new[] { typeof(string), typeof(string), typeof(int) }),
             ("ListCompactionCheckpointsAsync", new[] { typeof(string), typeof(int) }),
@@ -470,6 +471,32 @@ public void SessionPatch_ToPayload_ClearSerializesToJsonNull()
 
         var fastJson = JsonSerializer.Serialize(new SessionPatch { FastMode = SessionPatch.Clear }.ToPayload("k"));
         Assert.Contains("\"fastMode\":null", fastJson);
+
+        var thinkingJson = JsonSerializer.Serialize(
+            new SessionPatch { ThinkingLevel = SessionPatch.Clear }.ToPayload("k"));
+        Assert.Contains("\"thinkingLevel\":null", thinkingJson);
+        Assert.DoesNotContain("\"thinkingLevel\":\"default\"", thinkingJson);
+        Assert.DoesNotContain("\"thinkingLevel\":\"medium\"", thinkingJson);
+    }
+
+    [Fact]
+    public void ParseSessionPatchResult_PreservesGatewaySuccessAndPayloadFailure()
+    {
+        var succeeded = OpenClawGatewayClient.ParseSessionPatchResult(
+            Parse("""{"ok":true,"key":"agent:main:main"}"""),
+            "fallback");
+        Assert.True(succeeded.Ok);
+        Assert.True(succeeded.IsSupported);
+        Assert.Equal("agent:main:main", succeeded.Key);
+        Assert.Null(succeeded.Error);
+
+        var failed = OpenClawGatewayClient.ParseSessionPatchResult(
+            Parse("""{"ok":false,"reason":"policy rejected the change"}"""),
+            "agent:main:main");
+        Assert.False(failed.Ok);
+        Assert.True(failed.IsSupported);
+        Assert.Equal("agent:main:main", failed.Key);
+        Assert.Equal("policy rejected the change", failed.Error);
     }
 
     [Fact]
diff --git a/tests/OpenClaw.Shared.Tests/OpenClawGatewayClientTests.cs b/tests/OpenClaw.Shared.Tests/OpenClawGatewayClientTests.cs
index 5239a878f..ba334b2d5 100644
--- a/tests/OpenClaw.Shared.Tests/OpenClawGatewayClientTests.cs
+++ b/tests/OpenClaw.Shared.Tests/OpenClawGatewayClientTests.cs
@@ -101,6 +101,22 @@ public Task<JsonElement> RegisterPendingWizardResponse(string requestId)
             return completion.Task;
         }
 
+        public Task<SessionInfo[]> RegisterPendingSessionSnapshot(string requestId)
+        {
+            var completion = new TaskCompletionSource<SessionInfo[]>(
+                TaskCreationOptions.RunContinuationsAsynchronously);
+            Assert.True(
+                _client.PendingRequests.RegisterSessionSnapshot(
+                    requestId,
+                    completion).Accepted);
+            return completion.Task;
+        }
+
+        public void RemovePendingSessionSnapshot(string requestId)
+        {
+            Assert.True(_client.PendingRequests.Remove(requestId));
+        }
+
         public void ClearPendingRequests()
         {
             _client.PendingRequests.CloseForDisconnect();
diff --git a/tests/OpenClaw.Tray.Tests/OpenClawChatDataProviderTests.cs b/tests/OpenClaw.Tray.Tests/OpenClawChatDataProviderTests.cs
index e3d294594..e92674ebf 100644
--- a/tests/OpenClaw.Tray.Tests/OpenClawChatDataProviderTests.cs
+++ b/tests/OpenClaw.Tray.Tests/OpenClawChatDataProviderTests.cs
@@ -78,6 +78,10 @@ private sealed class FakeBridge : IChatGatewayBridge
         public Func<string, string?, string?, Task>? SendBehavior { get; set; }
         public Func<string, string, Task>? PatchSessionModelBehavior { get; set; }
         public Func<string, Task>? ClearSessionModelBehavior { get; set; }
+        public Func<string, string, Task>? PatchSessionThinkingLevelBehavior { get; set; }
+        public Func<string, CancellationToken, Task<SessionCommandResult>>? ClearSessionThinkingLevelBehavior { get; set; }
+        public Func<Task>? RequestSessionsBehavior { get; set; }
+        public Func<CancellationToken, Task<SessionInfo[]?>>? RequestSessionsSnapshotBehavior { get; set; }
         public Func<string?, Task<ChatHistoryInfo>>? HistoryBehavior { get; set; }
         public Func<string, Task>? AbortBehavior { get; set; }
         public SessionInfo[] Sessions { get; set; } = Array.Empty<SessionInfo>();
@@ -110,6 +114,7 @@ private sealed class FakeBridge : IChatGatewayBridge
         };
         public Func<string, Task<SessionCompactResult>>? CompactSessionBehavior { get; set; }
         public int RequestSessionsCallCount { get; private set; }
+        public int RequestSessionsSnapshotCallCount { get; private set; }
         public List<string?> RequestedHistoryKeys { get; } = new();
 
         public SessionInfo[] GetSessionList() => Sessions;
@@ -156,7 +161,15 @@ public Task<SessionCompactResult> CompactSessionDetailedAsync(string sessionKey)
         public Task RequestSessionsAsync()
         {
             RequestSessionsCallCount++;
-            return Task.CompletedTask;
+            return RequestSessionsBehavior?.Invoke() ?? Task.CompletedTask;
+        }
+
+        public Task<SessionInfo[]?> RequestSessionsSnapshotAsync(
+            CancellationToken cancellationToken = default)
+        {
+            RequestSessionsSnapshotCallCount++;
+            return RequestSessionsSnapshotBehavior?.Invoke(cancellationToken)
+                ?? Task.FromResult<SessionInfo[]?>(Sessions.Select(session => session.Clone()).ToArray());
         }
 
         public Task SendChatMessageAsync(string message, string? sessionKey, string? sessionId, IReadOnlyList<ChatAttachment>? attachments = null)
@@ -194,7 +207,29 @@ public Task ClearSessionModelAsync(string sessionKey)
             return ClearSessionModelBehavior?.Invoke(sessionKey) ?? Task.CompletedTask;
         }
         public List<string> ClearedModelKeys { get; } = new();
-        public Task PatchSessionThinkingLevelAsync(string sessionKey, string thinkingLevel) => Task.CompletedTask;
+        public Task PatchSessionThinkingLevelAsync(string sessionKey, string thinkingLevel)
+        {
+            PatchedThinkingLevelKeys.Add(sessionKey);
+            PatchedThinkingLevels.Add(thinkingLevel);
+            return PatchSessionThinkingLevelBehavior?.Invoke(sessionKey, thinkingLevel)
+                ?? Task.CompletedTask;
+        }
+        public List<string> PatchedThinkingLevelKeys { get; } = new();
+        public List<string> PatchedThinkingLevels { get; } = new();
+        public Task<SessionCommandResult> ClearSessionThinkingLevelAsync(
+            string sessionKey,
+            CancellationToken cancellationToken = default)
+        {
+            ClearedThinkingLevelKeys.Add(sessionKey);
+            return ClearSessionThinkingLevelBehavior?.Invoke(sessionKey, cancellationToken)
+                ?? Task.FromResult(new SessionCommandResult
+                {
+                    Method = "sessions.patch",
+                    Ok = true,
+                    Key = sessionKey
+                });
+        }
+        public List<string> ClearedThinkingLevelKeys { get; } = new();
 
         public Task<ChatHistoryInfo> RequestChatHistoryAsync(string? sessionKey)
         {
@@ -244,11 +279,14 @@ private static (FakeBridge bridge, OpenClawChatDataProvider provider, List<ChatD
             string? lastChatStatePath = null,
             TimeSpan? lastChatStateSaveDelay = null,
             Func<TimeSpan, CancellationToken, Func<Task>, Task>? historyRetryScheduler = null,
-            Action? historyFailureReservedForTesting = null)
+            Action? historyFailureReservedForTesting = null,
+            TimeSpan? thinkingLevelConfirmationTimeout = null,
+            Func<TimeSpan, Func<Task>, Task>? thinkingLevelRetryScheduler = null)
     {
         var bridge = new FakeBridge { Sessions = initial ?? Array.Empty<SessionInfo>() };
         var provider = toolMetaCachePath is null && attachmentMetaCachePath is null && lastChatStatePath is null &&
             lastChatStateSaveDelay is null && historyRetryScheduler is null && historyFailureReservedForTesting is null
+            && thinkingLevelConfirmationTimeout is null && thinkingLevelRetryScheduler is null
             ? new OpenClawChatDataProvider(bridge)
             : new OpenClawChatDataProvider(
                 bridge,
@@ -258,7 +296,9 @@ lastChatStateSaveDelay is null && historyRetryScheduler is null && historyFailur
                 lastChatStateFilePath: lastChatStatePath,
                 lastChatStateSaveDelay: lastChatStateSaveDelay,
                 historyRetryScheduler: historyRetryScheduler,
-                historyFailureReservedForTesting: historyFailureReservedForTesting);
+                historyFailureReservedForTesting: historyFailureReservedForTesting,
+                thinkingLevelConfirmationTimeout: thinkingLevelConfirmationTimeout,
+                thinkingLevelRetryScheduler: thinkingLevelRetryScheduler);
         var snapshots = new List<ChatDataSnapshot>();
         var notifications = new List<ChatProviderNotification>();
         provider.Changed += (_, e) => snapshots.Add(e.Snapshot);
@@ -1625,6 +1665,654 @@ public async Task LoadAsync_CarriesSessionModelProviderToThreads()
         Assert.Equal("openrouter", snapshot.Threads[0].ModelProvider);
     }
 
+    [Fact]
+    public async Task ClearThinkingLevelAsync_PreAckNullWaitsForPostAckCanonicalNull()
+    {
+        var session = MainSession();
+        session.ThinkingLevel = "off";
+        session.Model = "gpt-5.4";
+        session.Provider = "openrouter";
+        var (bridge, provider, snapshots, notifications) = CreateProvider([session]);
+        var clearResponse = new TaskCompletionSource<SessionCommandResult>(
+            TaskCreationOptions.RunContinuationsAsynchronously);
+        var snapshotRequested = new TaskCompletionSource(
+            TaskCreationOptions.RunContinuationsAsynchronously);
+        var snapshotResponse = new TaskCompletionSource<SessionInfo[]?>(
+            TaskCreationOptions.RunContinuationsAsynchronously);
+        bridge.ClearSessionThinkingLevelBehavior = (_, _) => clearResponse.Task;
+        bridge.RequestSessionsSnapshotBehavior = _ =>
+        {
+            snapshotRequested.TrySetResult();
+            return snapshotResponse.Task;
+        };
+        await provider.LoadAsync();
+        bridge.RaiseStatus(ConnectionStatus.Connected);
+        snapshots.Clear();
+
+        var clearTask = provider.ClearThinkingLevelAsync("main");
+
+        Assert.False(clearTask.IsCompleted);
+        Assert.Equal(["main"], bridge.ClearedThinkingLevelKeys);
+        Assert.Equal(0, bridge.RequestSessionsSnapshotCallCount);
+        Assert.Empty(notifications);
+        Assert.Empty(snapshots);
+
+        var preAck = session.Clone();
+        preAck.ThinkingLevel = null;
+        bridge.RaiseSessions([preAck]);
+        Assert.False(clearTask.IsCompleted);
+        Assert.Equal("off", Assert.Single(snapshots[^1].Threads).ThinkingLevel);
+
+        clearResponse.SetResult(new SessionCommandResult
+        {
+            Method = "sessions.patch",
+            Ok = true,
+            Key = "main"
+        });
+        await snapshotRequested.Task.WaitAsync(TimeSpan.FromSeconds(5));
+        Assert.False(clearTask.IsCompleted);
+        Assert.Equal(1, bridge.RequestSessionsSnapshotCallCount);
+
+        var postAck = session.Clone();
+        postAck.ThinkingLevel = null;
+        snapshotResponse.SetResult([postAck]);
+        await clearTask;
+
+        var thread = Assert.Single(snapshots[^1].Threads);
+        Assert.Null(thread.ThinkingLevel);
+        Assert.Equal("gpt-5.4", thread.Model);
+        Assert.Equal("openrouter", thread.ModelProvider);
+        Assert.Empty(notifications);
+    }
+
+    [Fact]
+    public async Task SetThinkingLevelAsync_ConcreteValueUsesExistingPathWithoutOptimisticState()
+    {
+        var session = MainSession();
+        session.ThinkingLevel = "off";
+        session.Model = "gpt-5.4";
+        var (bridge, provider, snapshots, _) = CreateProvider([session]);
+        var before = await provider.LoadAsync();
+        snapshots.Clear();
+
+        await provider.SetThinkingLevelAsync("main", "high");
+
+        Assert.Equal(["main"], bridge.PatchedThinkingLevelKeys);
+        Assert.Equal(["high"], bridge.PatchedThinkingLevels);
+        Assert.Empty(bridge.ClearedThinkingLevelKeys);
+        Assert.Empty(snapshots);
+        Assert.Equal("off", Assert.Single(before.Threads).ThinkingLevel);
+        Assert.Equal("gpt-5.4", Assert.Single(before.Threads).Model);
+    }
+
+    [Fact]
+    public async Task SetThinkingLevelAsync_ConcreteValueSupersedesPendingClearSilently()
+    {
+        var session = MainSession();
+        session.ThinkingLevel = "high";
+        var (bridge, provider, snapshots, notifications) = CreateProvider([session]);
+        var clearResponse = new TaskCompletionSource<SessionCommandResult>(
+            TaskCreationOptions.RunContinuationsAsynchronously);
+        var snapshotRequested = new TaskCompletionSource(
+            TaskCreationOptions.RunContinuationsAsynchronously);
+        bridge.ClearSessionThinkingLevelBehavior = (_, _) => clearResponse.Task;
+        bridge.RequestSessionsSnapshotBehavior = _ =>
+        {
+            snapshotRequested.TrySetResult();
+            var current = session.Clone();
+            current.ThinkingLevel = "low";
+            return Task.FromResult<SessionInfo[]?>([current]);
+        };
+        await provider.LoadAsync();
+        bridge.RaiseStatus(ConnectionStatus.Connected);
+        snapshots.Clear();
+
+        var clearTask = provider.ClearThinkingLevelAsync("main");
+        await provider.SetThinkingLevelAsync("main", "low");
+
+        clearResponse.SetResult(new SessionCommandResult
+        {
+            Method = "sessions.patch",
+            Ok = false,
+            Key = "main",
+            Error = "stale clear failure"
+        });
+        await Assert.ThrowsAnyAsync<OperationCanceledException>(() => clearTask);
+
+        Assert.Equal(["main"], bridge.PatchedThinkingLevelKeys);
+        Assert.Equal(["low"], bridge.PatchedThinkingLevels);
+        Assert.Equal(0, bridge.RequestSessionsCallCount);
+        Assert.Empty(notifications);
+
+        var delayedClear = session.Clone();
+        delayedClear.ThinkingLevel = null;
+        bridge.RaiseSessions([delayedClear]);
+        await snapshotRequested.Task.WaitAsync(TimeSpan.FromSeconds(5));
+
+        Assert.Equal("low", Assert.Single(snapshots[^1].Threads).ThinkingLevel);
+    }
+
+    [Fact]
+    public async Task ClearThinkingLevelAsync_FailedResponseRetainsSelectionAndSurfacesOriginalThreadError()
+    {
+        var session = MainSession();
+        session.ThinkingLevel = "high";
+        session.Model = "gpt-5.4";
+        var (bridge, provider, snapshots, notifications) = CreateProvider([session]);
+        bridge.ClearSessionThinkingLevelBehavior = (key, _) =>
+            Task.FromResult(new SessionCommandResult
+            {
+                Method = "sessions.patch",
+                Ok = false,
+                IsSupported = false,
+                Key = key,
+                Error = "unknown method: sessions.patch"
+            });
+        await provider.LoadAsync();
+        snapshots.Clear();
+
+        var exception = await Assert.ThrowsAsync<InvalidOperationException>(
+            () => provider.ClearThinkingLevelAsync("main"));
+
+        Assert.Contains("unknown method", exception.Message);
+        var notification = Assert.Single(notifications);
+        Assert.Equal("main", notification.ThreadId);
+        Assert.Equal(ChatProviderNotificationKind.Error, notification.Kind);
+        Assert.Contains("Could not use default reasoning", notification.Message);
+        var latest = Assert.Single(snapshots);
+        var thread = Assert.Single(latest.Threads);
+        Assert.Equal("high", thread.ThinkingLevel);
+        Assert.Equal("gpt-5.4", thread.Model);
+        Assert.Contains(
+            latest.Timelines["main"].Entries,
+            entry => entry.Text.Contains("Could not use default reasoning", StringComparison.Ordinal));
+
+        var delayedNull = session.Clone();
+        delayedNull.ThinkingLevel = null;
+        delayedNull.Model = "gpt-5.5";
+        bridge.RaiseSessions([delayedNull]);
+
+        var afterDelayedNull = Assert.Single(snapshots[^1].Threads);
+        Assert.Equal("high", afterDelayedNull.ThinkingLevel);
+        Assert.Equal("gpt-5.5", afterDelayedNull.Model);
+    }
+
+    [Fact]
+    public async Task ClearThinkingLevelAsync_SessionSwitchReconnectAndStaleSnapshotStayOnOriginalThread()
+    {
+        var main = MainSession();
+        main.ThinkingLevel = "off";
+        main.Model = "gpt-5.4";
+        var other = new SessionInfo
+        {
+            Key = "other",
+            DisplayName = "Other",
+            Status = "active",
+            ThinkingLevel = "high",
+            Model = "claude-sonnet"
+        };
+        var (bridge, provider, snapshots, notifications) = CreateProvider([main, other]);
+        await provider.LoadAsync();
+        bridge.RaiseStatus(ConnectionStatus.Connected);
+        snapshots.Clear();
+
+        var clearTask = provider.ClearThinkingLevelAsync("main");
+        provider.RememberSelectedThread("other");
+        var otherChanged = other.Clone();
+        otherChanged.ThinkingLevel = null;
+        bridge.RaiseSessions([main.Clone(), otherChanged]);
+
+        Assert.False(clearTask.IsCompleted);
+        bridge.RaiseStatus(ConnectionStatus.Disconnected);
+        await clearTask;
+
+        var beforeStale = snapshots[^1];
+        Assert.Equal("off", Assert.Single(beforeStale.Threads, thread => thread.Id == "main").ThinkingLevel);
+        Assert.Null(Assert.Single(beforeStale.Threads, thread => thread.Id == "other").ThinkingLevel);
+
+        var staleMain = main.Clone();
+        staleMain.ThinkingLevel = null;
+        staleMain.Model = "gpt-5.5";
+        bridge.RaiseSessions([staleMain, otherChanged]);
+
+        var afterStale = snapshots[^1];
+        Assert.Equal("off", Assert.Single(afterStale.Threads, thread => thread.Id == "main").ThinkingLevel);
+        Assert.Equal("gpt-5.5", Assert.Single(afterStale.Threads, thread => thread.Id == "main").Model);
+        Assert.Equal("claude-sonnet", Assert.Single(afterStale.Threads, thread => thread.Id == "other").Model);
+        Assert.Empty(notifications);
+
+        var refreshed = staleMain.Clone();
+        refreshed.ThinkingLevel = "off";
+        bridge.RequestSessionsSnapshotBehavior = _ =>
+            Task.FromResult<SessionInfo[]?>([refreshed, otherChanged]);
+        bridge.RaiseStatus(ConnectionStatus.Connected);
+
+        await WaitForConditionAsync(() => bridge.RequestSessionsSnapshotCallCount == 1);
+        Assert.Equal("off", Assert.Single(snapshots[^1].Threads, thread => thread.Id == "main").ThinkingLevel);
+    }
+
+    [Fact]
+    public async Task ClearThinkingLevelAsync_CancelBeforeAckRetainsSelectionAndSurfacesError()
+    {
+        var session = MainSession();
+        session.ThinkingLevel = "minimal";
+        var (bridge, provider, snapshots, notifications) = CreateProvider([session]);
+        bridge.ClearSessionThinkingLevelBehavior = async (_, cancellationToken) =>
+        {
+            await Task.Delay(Timeout.InfiniteTimeSpan, cancellationToken);
+            return new SessionCommandResult { Method = "sessions.patch", Ok = true };
+        };
+        await provider.LoadAsync();
+        snapshots.Clear();
+        using var cancellation = new CancellationTokenSource();
+
+        cancellation.Cancel();
+        var clearTask = provider.ClearThinkingLevelAsync("main", cancellation.Token);
+
+        await Assert.ThrowsAnyAsync<OperationCanceledException>(() => clearTask);
+        Assert.Equal("minimal", Assert.Single(snapshots[^1].Threads).ThinkingLevel);
+        Assert.Single(notifications);
+        Assert.Equal("main", notifications[0].ThreadId);
+        Assert.Contains("The change was canceled", notifications[0].Message);
+
+        var delayedNull = session.Clone();
+        delayedNull.ThinkingLevel = null;
+        bridge.RaiseSessions([delayedNull]);
+        Assert.Equal("minimal", Assert.Single(snapshots[^1].Threads).ThinkingLevel);
+    }
+
+    [Fact]
+    public async Task ClearThinkingLevelAsync_DisconnectBeforeAckRetainsSelectionAndRejectsLateAck()
+    {
+        var session = MainSession();
+        session.ThinkingLevel = "off";
+        var (bridge, provider, snapshots, notifications) = CreateProvider([session]);
+        var clearResponse = new TaskCompletionSource<SessionCommandResult>(
+            TaskCreationOptions.RunContinuationsAsynchronously);
+        bridge.ClearSessionThinkingLevelBehavior = (_, _) => clearResponse.Task;
+        await provider.LoadAsync();
+        bridge.RaiseStatus(ConnectionStatus.Connected);
+        snapshots.Clear();
+
+        var clearTask = provider.ClearThinkingLevelAsync("main");
+        bridge.RaiseStatus(ConnectionStatus.Disconnected);
+        clearResponse.SetResult(new SessionCommandResult
+        {
+            Method = "sessions.patch",
+            Ok = true,
+            Key = "main"
+        });
+
+        await Assert.ThrowsAnyAsync<InvalidOperationException>(() => clearTask);
+        Assert.Equal("off", Assert.Single(snapshots[^1].Threads).ThinkingLevel);
+        Assert.Single(notifications);
+        Assert.Equal(0, bridge.RequestSessionsSnapshotCallCount);
+
+        var lateNull = session.Clone();
+        lateNull.ThinkingLevel = null;
+        bridge.RaiseSessions([lateNull]);
+        Assert.Equal("off", Assert.Single(snapshots[^1].Threads).ThinkingLevel);
+    }
+
+    [Fact]
+    public async Task ClearThinkingLevelAsync_CancelAfterAckReturnsWithoutErrorAndLateSnapshotConfirms()
+    {
+        var session = MainSession();
+        session.ThinkingLevel = "off";
+        var (bridge, provider, snapshots, notifications) = CreateProvider([session]);
+        var snapshotRequested = new TaskCompletionSource(
+            TaskCreationOptions.RunContinuationsAsynchronously);
+        var snapshotResponse = new TaskCompletionSource<SessionInfo[]?>(
+            TaskCreationOptions.RunContinuationsAsynchronously);
+        bridge.RequestSessionsSnapshotBehavior = _ =>
+        {
+            snapshotRequested.TrySetResult();
+            return snapshotResponse.Task;
+        };
+        await provider.LoadAsync();
+        bridge.RaiseStatus(ConnectionStatus.Connected);
+        snapshots.Clear();
+        using var cancellation = new CancellationTokenSource();
+
+        var clearTask = provider.ClearThinkingLevelAsync("main", cancellation.Token);
+        await snapshotRequested.Task.WaitAsync(TimeSpan.FromSeconds(5));
+        cancellation.Cancel();
+        await clearTask;
+
+        Assert.Empty(notifications);
+        Assert.Equal("off", Assert.Single((await provider.LoadAsync()).Threads).ThinkingLevel);
+
+        var confirmed = session.Clone();
+        confirmed.ThinkingLevel = null;
+        snapshotResponse.SetResult([confirmed]);
+        await WaitForConditionAsync(
+            () => snapshots.Count > 0 && snapshots[^1].Threads.Single().ThinkingLevel is null);
+
+        Assert.Empty(notifications);
+        Assert.Null(Assert.Single(snapshots[^1].Threads).ThinkingLevel);
+    }
+
+    [Fact]
+    public async Task ClearThinkingLevelAsync_PostAckTimeoutReturnsWithoutErrorAndLateSnapshotConfirms()
+    {
+        var session = MainSession();
+        session.ThinkingLevel = "high";
+        var (bridge, provider, snapshots, notifications) = CreateProvider(
+            [session],
+            thinkingLevelConfirmationTimeout: TimeSpan.FromMilliseconds(20));
+        var snapshotResponse = new TaskCompletionSource<SessionInfo[]?>(
+            TaskCreationOptions.RunContinuationsAsynchronously);
+        bridge.RequestSessionsSnapshotBehavior = _ => snapshotResponse.Task;
+        await provider.LoadAsync();
+        bridge.RaiseStatus(ConnectionStatus.Connected);
+        snapshots.Clear();
+
+        await provider.ClearThinkingLevelAsync("main");
+
+        Assert.Empty(notifications);
+        Assert.Equal("high", Assert.Single((await provider.LoadAsync()).Threads).ThinkingLevel);
+
+        var confirmed = session.Clone();
+        confirmed.ThinkingLevel = null;
+        snapshotResponse.SetResult([confirmed]);
+        await WaitForConditionAsync(
+            () => snapshots.Count > 0 && snapshots[^1].Threads.Single().ThinkingLevel is null);
+
+        Assert.Null(Assert.Single(snapshots[^1].Threads).ThinkingLevel);
+        Assert.Empty(notifications);
+    }
+
+    [Fact]
+    public async Task ClearThinkingLevelAsync_PostAckTimeoutThenConcreteSelectionRejectsLateNull()
+    {
+        var session = MainSession();
+        session.ThinkingLevel = "off";
+        var (bridge, provider, snapshots, notifications) = CreateProvider(
+            [session],
+            thinkingLevelConfirmationTimeout: TimeSpan.FromMilliseconds(20));
+        var staleClearSnapshot = new TaskCompletionSource<SessionInfo[]?>(
+            TaskCreationOptions.RunContinuationsAsynchronously);
+        var currentConcreteSnapshot = session.Clone();
+        currentConcreteSnapshot.ThinkingLevel = "low";
+        var refreshCall = 0;
+        CancellationToken staleRequestCancellation = default;
+        bridge.RequestSessionsSnapshotBehavior = cancellationToken =>
+        {
+            if (Volatile.Read(ref refreshCall) == 0)
+                staleRequestCancellation = cancellationToken;
+            return
+            Interlocked.Increment(ref refreshCall) == 1
+                ? staleClearSnapshot.Task
+                : Task.FromResult<SessionInfo[]?>([currentConcreteSnapshot]);
+        };
+        await provider.LoadAsync();
+        bridge.RaiseStatus(ConnectionStatus.Connected);
+        snapshots.Clear();
+
+        await provider.ClearThinkingLevelAsync("main");
+        await provider.SetThinkingLevelAsync("main", "low");
+        Assert.True(staleRequestCancellation.IsCancellationRequested);
+
+        var staleNull = session.Clone();
+        staleNull.ThinkingLevel = null;
+        staleClearSnapshot.SetResult([staleNull]);
+        bridge.RaiseSessions([staleNull]);
+        await WaitForConditionAsync(() => bridge.RequestSessionsSnapshotCallCount >= 2);
+
+        Assert.Equal("low", Assert.Single(snapshots[^1].Threads).ThinkingLevel);
+        Assert.Empty(notifications);
+    }
+
+    [Fact]
+    public async Task ClearThinkingLevelAsync_UnchangedRetryDoesNotPublishDuplicateChanged()
+    {
+        var session = MainSession();
+        session.ThinkingLevel = "off";
+        var (bridge, provider, snapshots, notifications) = CreateProvider(
+            [session],
+            thinkingLevelRetryScheduler: (_, retry) => retry());
+        var first = session.Clone();
+        var confirmed = session.Clone();
+        confirmed.ThinkingLevel = null;
+        var refreshCall = 0;
+        bridge.RequestSessionsSnapshotBehavior = _ =>
+            Task.FromResult<SessionInfo[]?>(
+                Interlocked.Increment(ref refreshCall) == 1
+                    ? [first]
+                    : [confirmed]);
+        await provider.LoadAsync();
+        bridge.RaiseStatus(ConnectionStatus.Connected);
+        snapshots.Clear();
+
+        await provider.ClearThinkingLevelAsync("main");
+
+        Assert.Equal(2, bridge.RequestSessionsSnapshotCallCount);
+        var changed = Assert.Single(snapshots);
+        Assert.Null(Assert.Single(changed.Threads).ThinkingLevel);
+        Assert.Empty(notifications);
+    }
+
+    [Fact]
+    public async Task ClearThinkingLevelAsync_CommittedReconciliationRetriesBoundedlyAndCanRestart()
+    {
+        var session = MainSession();
+        session.ThinkingLevel = "minimal";
+        var (bridge, provider, snapshots, notifications) = CreateProvider(
+            [session],
+            thinkingLevelConfirmationTimeout: TimeSpan.FromMilliseconds(50),
+            thinkingLevelRetryScheduler: (_, retry) => retry());
+        var externalNull = session.Clone();
+        externalNull.ThinkingLevel = null;
+        var confirmationAvailable = false;
+        bridge.RequestSessionsSnapshotBehavior = _ => confirmationAvailable
+            ? Task.FromResult<SessionInfo[]?>([externalNull])
+            : Task.FromException<SessionInfo[]?>(new TimeoutException("confirmation unavailable"));
+        await provider.LoadAsync();
+        bridge.RaiseStatus(ConnectionStatus.Connected);
+        snapshots.Clear();
+
+        await provider.ClearThinkingLevelAsync("main");
+        await WaitForConditionAsync(
+            () => bridge.RequestSessionsSnapshotCallCount == 3);
+
+        Assert.Equal(3, bridge.RequestSessionsSnapshotCallCount);
+        Assert.Equal("minimal", Assert.Single((await provider.LoadAsync()).Threads).ThinkingLevel);
+        Assert.Empty(notifications);
+
+        confirmationAvailable = true;
+        bridge.RaiseSessions([externalNull]);
+        await WaitForConditionAsync(
+            () => bridge.RequestSessionsSnapshotCallCount == 4 &&
+                  snapshots.Count > 0 &&
+                  snapshots[^1].Threads.Single().ThinkingLevel is null);
+
+        Assert.Null(Assert.Single(snapshots[^1].Threads).ThinkingLevel);
+        Assert.Empty(notifications);
+    }
+
+    [Fact]
+    public async Task ClearThinkingLevelAsync_FailedClearAcceptsExternalNullOnlyAfterCorrelatedRefresh()
+    {
+        var session = MainSession();
+        session.ThinkingLevel = "high";
+        var (bridge, provider, snapshots, _) = CreateProvider([session]);
+        bridge.ClearSessionThinkingLevelBehavior = (key, _) =>
+            Task.FromResult(new SessionCommandResult
+            {
+                Method = "sessions.patch",
+                Ok = false,
+                Key = key,
+                Error = "rejected"
+            });
+        await provider.LoadAsync();
+        bridge.RaiseStatus(ConnectionStatus.Connected);
+        snapshots.Clear();
+
+        await Assert.ThrowsAsync<InvalidOperationException>(
+            () => provider.ClearThinkingLevelAsync("main"));
+
+        var externalNull = session.Clone();
+        externalNull.ThinkingLevel = null;
+        externalNull.Model = "gpt-5.5";
+        var correlatedSnapshot = new TaskCompletionSource<SessionInfo[]?>(
+            TaskCreationOptions.RunContinuationsAsynchronously);
+        bridge.RequestSessionsSnapshotBehavior = _ =>
+            correlatedSnapshot.Task;
+        bridge.RaiseSessions([externalNull]);
+
+        Assert.Equal("high", Assert.Single(snapshots[^1].Threads).ThinkingLevel);
+        Assert.Equal("gpt-5.5", Assert.Single(snapshots[^1].Threads).Model);
+        correlatedSnapshot.SetResult([externalNull]);
+        await WaitForConditionAsync(
+            () => snapshots.Count > 0 && snapshots[^1].Threads.Single().ThinkingLevel is null);
+
+        Assert.Equal(1, bridge.RequestSessionsSnapshotCallCount);
+        Assert.Null(Assert.Single(snapshots[^1].Threads).ThinkingLevel);
+    }
+
+    [Fact]
+    public async Task LoadAsync_DuringFailedClearReconciliationProtectsCachedNullUntilCorrelatedRefresh()
+    {
+        var session = MainSession();
+        session.ThinkingLevel = "off";
+        var (bridge, provider, _, _) = CreateProvider([session]);
+        bridge.ClearSessionThinkingLevelBehavior = (key, _) =>
+            Task.FromResult(new SessionCommandResult
+            {
+                Method = "sessions.patch",
+                Ok = false,
+                IsSupported = false,
+                Key = key,
+                Error = "unknown method: sessions.patch"
+            });
+        await provider.LoadAsync();
+        bridge.RaiseStatus(ConnectionStatus.Connected);
+        await Assert.ThrowsAsync<InvalidOperationException>(
+            () => provider.ClearThinkingLevelAsync("main"));
+
+        var correlatedResponse = new TaskCompletionSource<SessionInfo[]?>(
+            TaskCreationOptions.RunContinuationsAsynchronously);
+        bridge.RequestSessionsSnapshotBehavior = _ => correlatedResponse.Task;
+        var cachedNull = session.Clone();
+        cachedNull.ThinkingLevel = null;
+        bridge.Sessions = [cachedNull];
+
+        var snapshot = await provider.LoadAsync();
+
+        Assert.Equal("off", Assert.Single(snapshot.Threads).ThinkingLevel);
+        Assert.Equal(1, bridge.RequestSessionsSnapshotCallCount);
+    }
+
+    [Fact]
+    public async Task ClearThinkingLevelAsync_TwoRapidClearsOnlyNewestCorrelatedSnapshotWins()
+    {
+        var session = MainSession();
+        session.ThinkingLevel = "off";
+        var (bridge, provider, snapshots, notifications) = CreateProvider([session]);
+        var firstResponse = new TaskCompletionSource<SessionCommandResult>(
+            TaskCreationOptions.RunContinuationsAsynchronously);
+        var secondResponse = new TaskCompletionSource<SessionCommandResult>(
+            TaskCreationOptions.RunContinuationsAsynchronously);
+        var clearCall = 0;
+        bridge.ClearSessionThinkingLevelBehavior = (key, _) =>
+        {
+            var response = Interlocked.Increment(ref clearCall) == 1
+                ? firstResponse
+                : secondResponse;
+            return response.Task;
+        };
+        var canonicalNull = session.Clone();
+        canonicalNull.ThinkingLevel = null;
+        bridge.RequestSessionsSnapshotBehavior = _ =>
+            Task.FromResult<SessionInfo[]?>([canonicalNull]);
+        await provider.LoadAsync();
+        bridge.RaiseStatus(ConnectionStatus.Connected);
+        snapshots.Clear();
+
+        var firstClear = provider.ClearThinkingLevelAsync("main");
+        var secondClear = provider.ClearThinkingLevelAsync("main");
+
+        var staleNull = session.Clone();
+        staleNull.ThinkingLevel = null;
+        bridge.RaiseSessions([staleNull]);
+        Assert.Equal("off", Assert.Single(snapshots[^1].Threads).ThinkingLevel);
+
+        firstResponse.SetResult(new SessionCommandResult
+        {
+            Method = "sessions.patch",
+            Ok = true,
+            Key = "main"
+        });
+        await Assert.ThrowsAnyAsync<OperationCanceledException>(() => firstClear);
+        Assert.Equal(0, bridge.RequestSessionsSnapshotCallCount);
+
+        secondResponse.SetResult(new SessionCommandResult
+        {
+            Method = "sessions.patch",
+            Ok = true,
+            Key = "main"
+        });
+        await secondClear;
+
+        Assert.Equal(1, bridge.RequestSessionsSnapshotCallCount);
+        Assert.Null(Assert.Single(snapshots[^1].Threads).ThinkingLevel);
+        Assert.Empty(notifications);
+    }
+
+    [Fact]
+    public async Task ClearThinkingLevelAsync_OldConnectionRefreshCannotOverwriteReconnectSnapshot()
+    {
+        var session = MainSession();
+        session.ThinkingLevel = "off";
+        session.Model = "gpt-5.4";
+        var (bridge, provider, snapshots, _) = CreateProvider([session]);
+        var clearResponse = new TaskCompletionSource<SessionCommandResult>(
+            TaskCreationOptions.RunContinuationsAsynchronously);
+        var oldRefresh = new TaskCompletionSource<SessionInfo[]?>(
+            TaskCreationOptions.RunContinuationsAsynchronously);
+        var newRefresh = new TaskCompletionSource<SessionInfo[]?>(
+            TaskCreationOptions.RunContinuationsAsynchronously);
+        var refreshCall = 0;
+        bridge.ClearSessionThinkingLevelBehavior = (_, _) => clearResponse.Task;
+        bridge.RequestSessionsSnapshotBehavior = _ =>
+            Interlocked.Increment(ref refreshCall) == 1
+                ? oldRefresh.Task
+                : newRefresh.Task;
+        await provider.LoadAsync();
+        bridge.RaiseStatus(ConnectionStatus.Connected);
+        snapshots.Clear();
+
+        var clearTask = provider.ClearThinkingLevelAsync("main");
+        clearResponse.SetResult(new SessionCommandResult
+        {
+            Method = "sessions.patch",
+            Ok = true,
+            Key = "main"
+        });
+        await WaitForConditionAsync(() => bridge.RequestSessionsSnapshotCallCount == 1);
+
+        bridge.RaiseStatus(ConnectionStatus.Disconnected);
+        await clearTask;
+
+        bridge.RaiseStatus(ConnectionStatus.Connected);
+        await WaitForConditionAsync(() => bridge.RequestSessionsSnapshotCallCount == 2);
+        var current = session.Clone();
+        current.Model = "gpt-5.5";
+        newRefresh.SetResult([current]);
+        await WaitForConditionAsync(
+            () => snapshots.Count > 0 && snapshots[^1].Threads.Single().Model == "gpt-5.5");
+
+        var stale = session.Clone();
+        stale.ThinkingLevel = null;
+        stale.Model = "stale-model";
+        oldRefresh.SetResult([stale]);
+        await Task.Delay(25);
+
+        var thread = Assert.Single(snapshots[^1].Threads);
+        Assert.Equal("off", thread.ThinkingLevel);
+        Assert.Equal("gpt-5.5", thread.Model);
+    }
+
     [Fact]
     public async Task SendMessageAsync_WhenIdle_RendersTranscriptEntryBeforeAwaitingGateway()
     {
diff --git a/tests/OpenClaw.Tray.Tests/ThinkingLevelDefaultContractTests.cs b/tests/OpenClaw.Tray.Tests/ThinkingLevelDefaultContractTests.cs
new file mode 100644
index 000000000..7f9a60d70
--- /dev/null
+++ b/tests/OpenClaw.Tray.Tests/ThinkingLevelDefaultContractTests.cs
@@ -0,0 +1,82 @@
+using System.Text.RegularExpressions;
+
+namespace OpenClaw.Tray.Tests;
+
+public sealed class ThinkingLevelDefaultContractTests
+{
+    [Fact]
+    public void LegacyAndReactorComposers_SeparateDefaultFromConcreteThinkingLevels()
+    {
+        var legacy = Read("src", "OpenClaw.Tray.WinUI", "Chat", "OpenClawComposer.cs");
+        var reactor = Read("src", "OpenClaw.Tray.WinUI", "Chat", "OpenClawReactorChatRoot.cs");
+
+        Assert.Contains("Action OnThinkingLevelCleared", legacy);
+        Assert.Contains("Props.OnThinkingLevelCleared", legacy);
+        Assert.Contains("Chat_Composer_Reasoning_Default", legacy);
+        Assert.Contains("Props.CurrentThinkingLevel is null", legacy);
+        Assert.Contains("ToggleMenuItem(", legacy);
+        Assert.DoesNotContain("medium (default)", legacy, StringComparison.OrdinalIgnoreCase);
+        Assert.DoesNotContain("CurrentThinkingLevel ?? \"medium\"", legacy);
+
+        Assert.Contains("Action OnThinkingLevelCleared", reactor);
+        Assert.Contains("props.OnThinkingLevelCleared", reactor);
+        Assert.Contains("Chat_Composer_Reasoning_Default", reactor);
+        Assert.Contains("props.CurrentThread.ThinkingLevel is null", reactor);
+        Assert.Contains("RadioMenuItem(", reactor);
+        Assert.DoesNotContain("ThinkingLevel ?? \"medium\"", reactor);
+        Assert.Matches(
+            new Regex(@"Accessibility_Reasoning.*reasoningPickerLabel", RegexOptions.Singleline),
+            reactor);
+    }
+
+    [Fact]
+    public void ChatRoots_UseTypedClearCallbackWithoutStringSentinel()
+    {
+        var legacyRoot = Read("src", "OpenClaw.Tray.WinUI", "Chat", "OpenClawChatRoot.cs");
+        var reactorRoot = Read("src", "OpenClaw.Tray.WinUI", "Chat", "OpenClawReactorChatRoot.cs");
+        var provider = Read("src", "OpenClaw.Tray.WinUI", "Chat", "OpenClawChatDataProvider.cs");
+        var bridge = Read("src", "OpenClaw.Tray.WinUI", "Chat", "IChatGatewayBridge.cs");
+
+        Assert.Contains("OnThinkingLevelCleared:", legacyRoot);
+        Assert.Contains("ClearThinkingLevelAsync", legacyRoot);
+        Assert.Contains("ClearThinkingLevelAsync", reactorRoot);
+        Assert.Contains(
+            "ObserveFireAndForget(_provider.SetThinkingLevelAsync",
+            legacyRoot);
+        Assert.Contains(
+            "ObserveFireAndForget(_provider.ClearThinkingLevelAsync",
+            legacyRoot);
+        Assert.Contains(
+            "ObserveFireAndForget(props.Provider.SetThinkingLevelAsync",
+            reactorRoot);
+        Assert.Contains(
+            "ObserveFireAndForget(props.Provider.ClearThinkingLevelAsync",
+            reactorRoot);
+        Assert.Contains("ClearSessionThinkingLevelAsync", provider);
+        Assert.Contains("ThinkingLevel = SessionPatch.Clear", bridge);
+        Assert.DoesNotContain("ThinkingLevel = \"default\"", bridge);
+        Assert.DoesNotContain("ThinkingLevel = \"medium\"", bridge);
+    }
+
+    [Fact]
+    public void ThinkingClearErrors_AreLocalizedInEverySupportedLocale()
+    {
+        var root = TestRepositoryPaths.GetRepositoryRoot();
+        var stringsRoot = Path.Combine(root, "src", "OpenClaw.Tray.WinUI", "Strings");
+        var locales = Directory.GetDirectories(stringsRoot);
+        Assert.NotEmpty(locales);
+
+        foreach (var locale in locales)
+        {
+            var resources = File.ReadAllText(Path.Combine(locale, "Resources.resw"));
+            Assert.Contains("Chat_Composer_Reasoning_Default", resources);
+            Assert.Contains("Chat_Notification_ClearThinkingFailed", resources);
+            Assert.Contains("Chat_Error_ClearThinkingFailedFormat", resources);
+            Assert.Contains("Chat_Error_ClearThinkingCanceled", resources);
+            Assert.Contains("Chat_Error_ClearThinkingInterrupted", resources);
+        }
+    }
+
+    private static string Read(params string[] segments) =>
+        File.ReadAllText(Path.Combine([TestRepositoryPaths.GetRepositoryRoot(), .. segments]));
+}

From a413708df33be74cee2276ead13aff7cf996e9b4 Mon Sep 17 00:00:00 2001
From: "bakudies@microsoft.com" <bakudies@microsoft.com>
Date: Wed, 5 Aug 2026 00:40:06 -0700
Subject: [PATCH 2/4] Run reasoning Gateway proof in CI

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
---
 .github/workflows/ci.yml                                        | 2 +-
 .../Setup/SessionsPatchThinkingLevelE2ETests.cs                 | 2 +-
 2 files changed, 2 insertions(+), 2 deletions(-)

diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 7ed6f0c03..81b2004de 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -348,7 +348,7 @@ jobs:
         include:
           - name: setup-connect
             timeout_minutes: 45
-            filter: "FullyQualifiedName~OpenClaw.E2ETests.Setup.SetupAndConnectTests|FullyQualifiedName~OpenClaw.E2ETests.Setup.MxcSetupAndConnectTests|FullyQualifiedName~OpenClaw.E2ETests.Setup.RequestRoutingProofCollectorTests"
+            filter: "FullyQualifiedName~OpenClaw.E2ETests.Setup.SetupAndConnectTests|FullyQualifiedName~OpenClaw.E2ETests.Setup.MxcSetupAndConnectTests|FullyQualifiedName~OpenClaw.E2ETests.Setup.RequestRoutingProofCollectorTests|FullyQualifiedName~OpenClaw.E2ETests.Setup.SessionsPatchThinkingLevelE2ETests"
           - name: revocation-recovery
             timeout_minutes: 25
             filter: FullyQualifiedName~OpenClaw.E2ETests.Setup.RevocationAndRecoveryTests
diff --git a/tests/OpenClaw.E2ETests/Setup/SessionsPatchThinkingLevelE2ETests.cs b/tests/OpenClaw.E2ETests/Setup/SessionsPatchThinkingLevelE2ETests.cs
index 864c25e46..5d6f9133d 100644
--- a/tests/OpenClaw.E2ETests/Setup/SessionsPatchThinkingLevelE2ETests.cs
+++ b/tests/OpenClaw.E2ETests/Setup/SessionsPatchThinkingLevelE2ETests.cs
@@ -3,7 +3,7 @@
 
 namespace OpenClaw.E2ETests.Setup;
 
-[CollectionDefinition("Reasoning Gateway E2E")]
+[CollectionDefinition("Reasoning Gateway E2E", DisableParallelization = true)]
 public sealed class ReasoningGatewayE2ECollection : ICollectionFixture<E2ESetupFixture> { }
 
 [Collection("Reasoning Gateway E2E")]

From 83386c86ec15546a17aeb0d293bc4e23034c1b3d Mon Sep 17 00:00:00 2001
From: "bakudies@microsoft.com" <bakudies@microsoft.com>
Date: Wed, 5 Aug 2026 02:33:28 -0700
Subject: [PATCH 3/4] Close reasoning provider delegation

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
---
 docs/ARCHITECTURE.md                          |  6 ++--
 ...ngLevelClearReconcilerArchitectureTests.cs | 30 +++++++++++++++++++
 2 files changed, 33 insertions(+), 3 deletions(-)

diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md
index 302f74fab..a495540ba 100644
--- a/docs/ARCHITECTURE.md
+++ b/docs/ARCHITECTURE.md
@@ -73,7 +73,7 @@ These are the canonical homes. Do not reintroduce private copies elsewhere.
 | If you are editing… | Do not grow it. Extract toward… |
 | --- | --- |
 | `src/OpenClaw.Tray.WinUI/App.xaml.cs` | `IWindowManager`, `ITrayController`, `IActivationRouter`, `ISettingsChangeCoordinator`, `AppBootstrapper` |
-| `src/OpenClaw.Tray.WinUI/Chat/OpenClawChatDataProvider.cs` | `ChatSendQueue`, `ChatBridgeEventPump`, `ChatHistoryLoader`, `ChatSnapshotProjector`, `AttachmentMetadataStore`, `ThinkingLevelClearReconciler` (production delegation planned in Layer 3) |
+| `src/OpenClaw.Tray.WinUI/Chat/OpenClawChatDataProvider.cs` | `ChatSendQueue`, `ChatBridgeEventPump`, `ChatHistoryLoader`, `ChatSnapshotProjector`, `AttachmentMetadataStore`; thinking-level clear reconciliation is closed and stays in `ThinkingLevelClearReconciler` |
 | `src/OpenClaw.Tray.WinUI/Chat/OpenClawChatTimeline.cs` | `ReactorChatTimeline` (production `ItemsView` / `ItemContainer`), `ChatBubbleRenderer`, `ToolCallCardRenderer`, `PermissionRequestCard`, `AttachmentBubbleRenderer` |
 | `src/OpenClaw.Tray.WinUI/Chat/OpenClawComposer.cs` | `ComposerViewModel`, `SlashCommandPalette`, `AttachmentPreviewStrip`, `VoiceComposerController` |
 | `src/OpenClaw.Tray.WinUI/Pages/ConnectionPage.xaml.cs` | `ConnectionPagePlan` (pure), `ConnectionPageViewModel`, gateway row models |
@@ -129,8 +129,8 @@ leading and trailing pipe. Columns, in order:
 | chat-send-queue | planned | src/OpenClaw.Tray.WinUI/Chat/OpenClawChatDataProvider.cs | send queue/admission/abort state | ChatSendQueue | - | queued send/abort/generation semantics preserved | none | review-only | extracted in Phase 4 |
 | gateway-pending-requests | authoritative | src/OpenClaw.Shared/OpenClawGatewayClient.cs | request-id -> method/completion tracking | PendingRequestRegistry | response payload interpretation remains in OpenClawGatewayClient | one atomic owner per request id; tombstoned duplicate and late responses are suppressed; genuinely ownerless responses preserve generic compatibility routing; disconnect closes registration and cancels active owners | PendingRequestRegistryTests.ConcurrentRegisterAndTake_LeavesNoActiveRequestsAndBoundsTombstones | behavioral | - |
 | gateway-client-pending-requests-closed | closed | src/OpenClaw.Shared/OpenClawGatewayClient.cs | independent method, wizard, chat-send, approval, or session-snapshot pending stores and locks | PendingRequestRegistry | request creation, typed response interpretation, and legacy ownerless response routing only | OpenClawGatewayClient has one PendingRequestRegistry and no private pending-request dictionaries or locks | PendingRequestRegistryArchitectureTests.OpenClawGatewayClient_DelegatesPendingBookkeepingToRegistry | source-shape | when gateway response interpretation moves to GatewayMessageRouter |
-| chat-thinking-level-clear-reconciliation | authoritative | dependent Layer 3 design for OpenClawChatDataProvider | UI-free per-thread thinking-level clear state-machine seam for versions, connection generations, protected canonical intent, patch acknowledgement, correlated snapshot acceptance, bounded retry, supersession, interruption, and disposal | ThinkingLevelClearReconciler | no production provider delegation in Layer 2; callers remain responsible for gateway requests and canonical snapshot storage | stale and uncorrelated snapshots cannot clear protected intent; only current-generation correlated snapshots confirm or externally converge; retries remain bounded and can restart on later current-generation evidence | ThinkingLevelClearReconcilerTests.PreAckNull_IsProtectedUntilPostAckCorrelatedNull | behavioral | - |
-| chat-provider-thinking-level-clear-closure | planned | src/OpenClaw.Tray.WinUI/Chat/OpenClawChatDataProvider.cs | production delegation of thinking-level clear reconciliation and removal of provider-local state | ThinkingLevelClearReconciler | provider remains unchanged in Layer 2 | production provider delegates reconciliation state and transitions to ThinkingLevelClearReconciler | none | review-only | delivered in Layer 3 |
+| chat-thinking-level-clear-reconciliation | authoritative | src/OpenClaw.Tray.WinUI/Chat/OpenClawChatDataProvider.cs | per-thread thinking-level clear versions, connection generations, protected canonical intent, patch acknowledgement, correlated snapshot acceptance, bounded retry, supersession, interruption, and disposal state | ThinkingLevelClearReconciler | provider invokes patch and snapshot requests, stores canonical session snapshots, and owns errors, notifications, and thread UI state | stale and uncorrelated snapshots cannot clear protected intent; only current-generation correlated snapshots confirm or externally converge; retries remain bounded and can restart on later current-generation evidence | ThinkingLevelClearReconcilerTests.PreAckNull_IsProtectedUntilPostAckCorrelatedNull | behavioral | - |
+| chat-provider-thinking-level-clear-closure | closed | src/OpenClaw.Tray.WinUI/Chat/OpenClawChatDataProvider.cs | production delegation of thinking-level clear reconciliation and removal of provider-local state | ThinkingLevelClearReconciler | narrow calls that begin operations, apply effective thinking levels, execute emitted refresh requests, and surface errors and notifications | production provider delegates reconciliation state and transitions to ThinkingLevelClearReconciler without independent maps, lifecycle records, retry counters, timers, or connection-generation state | ThinkingLevelClearReconcilerArchitectureTests.Provider_DelegatesThinkingLevelClearReconciliationState | source-shape | when OpenClawChatDataProvider is replaced by narrower chat coordinators |
 | connect-envelope | planned | src/OpenClaw.Shared/OpenClawGatewayClient.cs + WindowsNodeClient.cs | connect message + auth precedence + signature version | ConnectEnvelopeBuilder | - | credential precedence never downgrades a device token; v3->v2 fallback preserved | none | review-only | extracted in Phase 4 |
 | ui-dispatcher | authoritative | src/OpenClaw.Tray.WinUI/App.xaml.cs | UI-thread marshaling abstraction for presentation code | IUiDispatcher | App and existing WinUI code may call DispatcherQueue directly until the view-model migration | presentation view models depend on IUiDispatcher not a concrete DispatcherQueue | UiDispatcherContractTests.PageViewModel_ReceivesRegisteredDispatcher | behavioral | - |
 | navigation-scope | authoritative | src/OpenClaw.Tray.WinUI/Windows/HubWindow.xaml.cs | page view-model activation/deactivation and disposal lifetime | NavigationScopeManager | HubWindow keeps frame navigation back-stack and rail selection | transient page view models are activated on navigation and deactivated then disposed on navigate-away | NavigationScopeManagerTests.NavigatingAway_DeactivatesAndDisposesPreviousViewModel | behavioral | - |
diff --git a/tests/OpenClaw.Tray.Tests/ThinkingLevelClearReconcilerArchitectureTests.cs b/tests/OpenClaw.Tray.Tests/ThinkingLevelClearReconcilerArchitectureTests.cs
index 0e2971fc3..c54680070 100644
--- a/tests/OpenClaw.Tray.Tests/ThinkingLevelClearReconcilerArchitectureTests.cs
+++ b/tests/OpenClaw.Tray.Tests/ThinkingLevelClearReconcilerArchitectureTests.cs
@@ -19,4 +19,34 @@ public void Reconciler_DefinesUiFreeProviderFacingSeam()
         Assert.DoesNotContain("Microsoft.UI.Xaml", reconciler);
         Assert.DoesNotContain("OpenClawGatewayClient", reconciler);
     }
+
+    [Fact]
+    public void Provider_DelegatesThinkingLevelClearReconciliationState()
+    {
+        var root = TestRepositoryPaths.GetRepositoryRoot();
+        var provider = File.ReadAllText(Path.Combine(
+            root,
+            "src",
+            "OpenClaw.Tray.WinUI",
+            "Chat",
+            "OpenClawChatDataProvider.cs"));
+
+        Assert.Contains(
+            "private readonly ThinkingLevelClearReconciler _thinkingLevelClearReconciler;",
+            provider);
+        Assert.Contains(
+            "_thinkingLevelClearReconciler = new ThinkingLevelClearReconciler(",
+            provider);
+        Assert.Contains("_thinkingLevelClearReconciler.BeginClear(", provider);
+        Assert.Contains("_thinkingLevelClearReconciler.BeginConcreteSelection(", provider);
+        Assert.Contains("_thinkingLevelClearReconciler.ApplyCorrelatedSnapshot(", provider);
+        Assert.Contains(".OnConnectionChanged(status == ConnectionStatus.Connected)", provider);
+        Assert.Contains("_thinkingLevelClearReconciler.Dispose();", provider);
+
+        Assert.DoesNotContain("PendingThinkingLevelClear", provider);
+        Assert.DoesNotContain("ThinkingLevelReconciliation", provider);
+        Assert.DoesNotContain("_thinkingLevelClearVersions", provider);
+        Assert.DoesNotContain("MaxThinkingLevelRefreshAttempts", provider);
+        Assert.DoesNotContain("ScheduleThinkingLevelRetryAsync", provider);
+    }
 }

From 96549f9766e5e30812482251de75d76d628ea23a Mon Sep 17 00:00:00 2001
From: "bakudies@microsoft.com" <bakudies@microsoft.com>
Date: Wed, 5 Aug 2026 13:08:46 -0700
Subject: [PATCH 4/4] Preserve confirmed reasoning across reloads

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
---
 .../Chat/OpenClawChatDataProvider.cs          | 135 ++++-
 .../OpenClawChatDataProviderTests.cs          | 505 ++++++++++++++++++
 2 files changed, 624 insertions(+), 16 deletions(-)

diff --git a/src/OpenClaw.Tray.WinUI/Chat/OpenClawChatDataProvider.cs b/src/OpenClaw.Tray.WinUI/Chat/OpenClawChatDataProvider.cs
index 78b0c6ce4..9786bec43 100644
--- a/src/OpenClaw.Tray.WinUI/Chat/OpenClawChatDataProvider.cs
+++ b/src/OpenClaw.Tray.WinUI/Chat/OpenClawChatDataProvider.cs
@@ -103,6 +103,8 @@ public sealed class OpenClawChatDataProvider : IChatDataProvider
     private readonly TimeSpan _lastChatStateSaveDelay;
     private readonly Func<TimeSpan, CancellationToken, Func<Task>, Task> _scheduleHistoryRetry;
     private readonly ThinkingLevelClearReconciler _thinkingLevelClearReconciler;
+    private readonly Dictionary<string, long> _confirmedNullThinkingAuthorityTokens =
+        new(StringComparer.Ordinal);
     private readonly Action? _historyFailureReservedForTesting;
     private System.Threading.Timer? _toolMetaSaveTimer; // debounce cache writes
     private long _toolMetaSaveVersion;
@@ -223,6 +225,7 @@ private enum AssistantQueueFrameDisposition
     // was already in flight at disconnect time is discarded on completion rather
     // than resurrecting a catalog for a stale connection.
     private int _commandsEpoch;
+    private long _nextConfirmedNullThinkingAuthorityToken;
     private ConnectionStatus _status;
     private bool _disposed;
 
@@ -335,26 +338,34 @@ public Task<ChatDataSnapshot> LoadAsync(CancellationToken cancellationToken = de
         ChatDataSnapshot snapshot;
         lock (_gate)
         {
+            ReleaseMissingConfirmedNullThinkingAuthoritiesLocked(sessions);
             _sessions = sessions.Select(session =>
             {
                 if (string.IsNullOrEmpty(session.Key))
                     return session;
 
+                var projectedSession = ApplyConfirmedNullThinkingAuthorityLocked(
+                    session,
+                    releaseOnNull: true);
                 var resolution = _thinkingLevelClearReconciler.ObserveSnapshot(
-                    session.Key,
-                    session.ThinkingLevel);
+                    projectedSession.Key,
+                    projectedSession.ThinkingLevel);
                 if (resolution.RefreshRequest is { } refresh)
                     (thinkingLevelsToRefresh ??= []).Add(refresh);
+                var effectiveThinkingLevel =
+                    _confirmedNullThinkingAuthorityTokens.ContainsKey(projectedSession.Key)
+                        ? null
+                        : resolution.EffectiveThinkingLevel;
                 if (string.Equals(
-                        session.ThinkingLevel,
-                        resolution.EffectiveThinkingLevel,
+                        projectedSession.ThinkingLevel,
+                        effectiveThinkingLevel,
                         StringComparison.Ordinal))
                 {
-                    return session;
+                    return projectedSession;
                 }
 
-                var reconciled = session.Clone();
-                reconciled.ThinkingLevel = resolution.EffectiveThinkingLevel;
+                var reconciled = projectedSession.Clone();
+                reconciled.ThinkingLevel = effectiveThinkingLevel;
                 return reconciled;
             }).ToArray();
             EnsureTimelinesForSessionsLocked();
@@ -1823,11 +1834,16 @@ public async Task SetThinkingLevelAsync(string threadId, string thinkingLevel, C
     {
         cancellationToken.ThrowIfCancellationRequested();
         ThinkingLevelClearReconciler.ConcreteSelection selection;
+        long? confirmedNullAuthorityToken;
         lock (_gate)
         {
             var canonicalThinkingLevel = _sessions
                 .FirstOrDefault(session => string.Equals(session.Key, threadId, StringComparison.Ordinal))
                 ?.ThinkingLevel;
+            confirmedNullAuthorityToken =
+                _confirmedNullThinkingAuthorityTokens.TryGetValue(threadId, out var token)
+                    ? token
+                    : null;
             selection = _thinkingLevelClearReconciler.BeginConcreteSelection(
                 threadId,
                 thinkingLevel,
@@ -1837,6 +1853,20 @@ public async Task SetThinkingLevelAsync(string threadId, string thinkingLevel, C
         try
         {
             await _bridge.PatchSessionThinkingLevelAsync(threadId, thinkingLevel);
+            if (confirmedNullAuthorityToken is { } expectedToken)
+            {
+                lock (_gate)
+                {
+                    if (!_disposed &&
+                        _confirmedNullThinkingAuthorityTokens.TryGetValue(
+                            threadId,
+                            out var currentToken) &&
+                        currentToken == expectedToken)
+                    {
+                        _confirmedNullThinkingAuthorityTokens.Remove(threadId);
+                    }
+                }
+            }
         }
         catch
         {
@@ -1857,6 +1887,7 @@ public async Task ClearThinkingLevelAsync(string threadId, CancellationToken can
             operation = _thinkingLevelClearReconciler.BeginClear(
                 threadId,
                 canonicalThinkingLevel);
+            ObserveBackgroundTask(operation.Confirmation);
         }
 
         try
@@ -2243,6 +2274,7 @@ public ValueTask DisposeAsync()
             _localSentTexts.Clear();
             _locallyInitiatedThreads.Clear();
             _resetSubmittedLocalEchoTexts.Clear();
+            _confirmedNullThinkingAuthorityTokens.Clear();
         }
         _thinkingLevelClearReconciler.Dispose();
         CancelAndDisposeHistoryGeneration(historyGenerationToCancel);
@@ -2290,6 +2322,9 @@ private void OnStatusChanged(object? sender, ConnectionStatus status)
 
             justReconnected = status == ConnectionStatus.Connected
                               && _status != ConnectionStatus.Connected;
+            var connectedAuthorityChanged =
+                (status == ConnectionStatus.Connected) !=
+                (_status == ConnectionStatus.Connected);
             // MEDIUM 5: detect Connected → Disconnected/Error transitions so
             // we can synthesise a turn-end + status entry on every thread that
             // had an in-flight turn (otherwise the UI sits "thinking" forever).
@@ -2298,6 +2333,8 @@ private void OnStatusChanged(object? sender, ConnectionStatus status)
             _status = status;
             thinkingLevelsToRefresh = _thinkingLevelClearReconciler
                 .OnConnectionChanged(status == ConnectionStatus.Connected);
+            if (connectedAuthorityChanged)
+                _confirmedNullThinkingAuthorityTokens.Clear();
 
             // Reset the sessions-list-received gate whenever we leave the
             // Connected state. Any cached sessions belong to the previous
@@ -2453,10 +2490,25 @@ private bool ApplySessionsUpdated(
                     return false;
 
                 correlatedResolution = resolution;
+                if (resolution.State == ThinkingLevelClearReconciler.ReconciliationState.Confirmed)
+                {
+                    if (resolution.EffectiveThinkingLevel is null)
+                    {
+                        _confirmedNullThinkingAuthorityTokens[correlatedRequest.ThreadId] =
+                            ++_nextConfirmedNullThinkingAuthorityToken;
+                    }
+                    else
+                    {
+                        _confirmedNullThinkingAuthorityTokens.Remove(correlatedRequest.ThreadId);
+                    }
+                }
                 if (resolution.RefreshRequest is { } retry)
                     (thinkingLevelsToRefresh ??= []).Add(retry);
             }
 
+            if (refreshRequest is null)
+                ReleaseMissingConfirmedNullThinkingAuthoritiesLocked(incomingSessions);
+
             var previousThreads = refreshRequest is null
                 ? null
                 : BuildSnapshotLocked().Threads;
@@ -2467,32 +2519,44 @@ private bool ApplySessionsUpdated(
             for (var index = 0; index < incomingSessions.Length; index++)
             {
                 var incoming = incomingSessions[index];
-                reconciledSessions[index] = incoming;
+                var isCorrelatedTarget =
+                    refreshRequest is { } request &&
+                    string.Equals(request.ThreadId, incoming.Key, StringComparison.Ordinal);
+                var projectedSession = isCorrelatedTarget
+                    ? incoming
+                    : ApplyConfirmedNullThinkingAuthorityLocked(
+                        incoming,
+                        releaseOnNull: refreshRequest is null);
+                reconciledSessions[index] = projectedSession;
                 if (string.IsNullOrEmpty(incoming.Key))
                     continue;
 
                 ThinkingLevelClearReconciler.SnapshotResolution resolution;
-                if (refreshRequest is { } request &&
-                    string.Equals(request.ThreadId, incoming.Key, StringComparison.Ordinal))
+                if (isCorrelatedTarget)
                 {
                     resolution = correlatedResolution!.Value;
                 }
                 else
                 {
                     resolution = _thinkingLevelClearReconciler.ObserveSnapshot(
-                        incoming.Key,
-                        incoming.ThinkingLevel);
+                        projectedSession.Key,
+                        projectedSession.ThinkingLevel);
                     if (resolution.RefreshRequest is { } refresh)
                         (thinkingLevelsToRefresh ??= []).Add(refresh);
                 }
 
+                var effectiveThinkingLevel =
+                    !isCorrelatedTarget &&
+                    _confirmedNullThinkingAuthorityTokens.ContainsKey(projectedSession.Key)
+                        ? null
+                        : resolution.EffectiveThinkingLevel;
                 if (!string.Equals(
-                        incoming.ThinkingLevel,
-                        resolution.EffectiveThinkingLevel,
+                        projectedSession.ThinkingLevel,
+                        effectiveThinkingLevel,
                         StringComparison.Ordinal))
                 {
-                    var reconciled = incoming.Clone();
-                    reconciled.ThinkingLevel = resolution.EffectiveThinkingLevel;
+                    var reconciled = projectedSession.Clone();
+                    reconciled.ThinkingLevel = effectiveThinkingLevel;
                     reconciledSessions[index] = reconciled;
                 }
             }
@@ -2540,6 +2604,45 @@ private bool ApplySessionsUpdated(
         return true;
     }
 
+    private SessionInfo ApplyConfirmedNullThinkingAuthorityLocked(
+        SessionInfo session,
+        bool releaseOnNull)
+    {
+        if (string.IsNullOrEmpty(session.Key) ||
+            !_confirmedNullThinkingAuthorityTokens.ContainsKey(session.Key))
+        {
+            return session;
+        }
+
+        if (session.ThinkingLevel is null)
+        {
+            if (releaseOnNull)
+                _confirmedNullThinkingAuthorityTokens.Remove(session.Key);
+            return session;
+        }
+
+        var projected = session.Clone();
+        projected.ThinkingLevel = null;
+        return projected;
+    }
+
+    private void ReleaseMissingConfirmedNullThinkingAuthoritiesLocked(
+        IReadOnlyCollection<SessionInfo> sessions)
+    {
+        if (_confirmedNullThinkingAuthorityTokens.Count == 0)
+            return;
+
+        var incomingKeys = sessions
+            .Where(session => !string.IsNullOrEmpty(session.Key))
+            .Select(session => session.Key)
+            .ToHashSet(StringComparer.Ordinal);
+        var missingThreads = _confirmedNullThinkingAuthorityTokens.Keys
+            .Where(threadId => !incomingKeys.Contains(threadId))
+            .ToArray();
+        foreach (var threadId in missingThreads)
+            _confirmedNullThinkingAuthorityTokens.Remove(threadId);
+    }
+
     internal static bool ShouldPreserveLiveEntryDuringAuthoritativeReload(
         ChatEntryMetadata? metadata,
         int maxHistorySequence,
diff --git a/tests/OpenClaw.Tray.Tests/OpenClawChatDataProviderTests.cs b/tests/OpenClaw.Tray.Tests/OpenClawChatDataProviderTests.cs
index e92674ebf..0a29bb0cd 100644
--- a/tests/OpenClaw.Tray.Tests/OpenClawChatDataProviderTests.cs
+++ b/tests/OpenClaw.Tray.Tests/OpenClawChatDataProviderTests.cs
@@ -5,6 +5,7 @@
 using System.Collections.Concurrent;
 using System.Diagnostics;
 using System.Diagnostics.Metrics;
+using System.Runtime.CompilerServices;
 using System.Text.Json;
 
 namespace OpenClaw.Tray.Tests;
@@ -2313,6 +2314,426 @@ await WaitForConditionAsync(
         Assert.Equal("gpt-5.5", thread.Model);
     }
 
+    [Fact]
+    public async Task LoadAsync_FirstMountPreservesNonNullBridgeThinkingLevel()
+    {
+        var session = MainSession();
+        session.ThinkingLevel = "off";
+        session.Model = "gpt-5.4";
+        var (bridge, provider, _, _) = CreateProvider([session]);
+
+        var snapshot = await provider.LoadAsync();
+
+        var thread = Assert.Single(snapshot.Threads);
+        Assert.Equal("off", thread.ThinkingLevel);
+        Assert.Equal("gpt-5.4", thread.Model);
+        Assert.Same(session, Assert.Single(bridge.Sessions));
+    }
+
+    [Fact]
+    public async Task LoadAsync_AfterCorrelatedNullMasksRepeatedStaleBridgeReads()
+    {
+        var session = MainSession();
+        session.ThinkingLevel = "off";
+        session.Model = "gpt-5.4";
+        var (bridge, provider, snapshots, _) = CreateProvider([session]);
+        var confirmed = session.Clone();
+        confirmed.ThinkingLevel = null;
+        confirmed.Model = "gpt-5.5";
+        bridge.RequestSessionsSnapshotBehavior = _ =>
+            Task.FromResult<SessionInfo[]?>([confirmed]);
+        await provider.LoadAsync();
+        bridge.RaiseStatus(ConnectionStatus.Connected);
+        snapshots.Clear();
+
+        await provider.ClearThinkingLevelAsync("main");
+
+        Assert.Null(Assert.Single(snapshots[^1].Threads).ThinkingLevel);
+        Assert.Equal("off", Assert.Single(bridge.Sessions).ThinkingLevel);
+        var firstRemount = await provider.LoadAsync();
+        var secondRemount = await provider.LoadAsync();
+        Assert.Null(Assert.Single(firstRemount.Threads).ThinkingLevel);
+        Assert.Null(Assert.Single(secondRemount.Threads).ThinkingLevel);
+        Assert.Equal("gpt-5.4", Assert.Single(secondRemount.Threads).Model);
+        Assert.Equal("off", Assert.Single(bridge.Sessions).ThinkingLevel);
+    }
+
+    [Fact]
+    public async Task SessionsUpdated_ConfirmedNullMasksStaleOffUntilOrdinaryNullReleasesLatch()
+    {
+        var session = MainSession();
+        session.ThinkingLevel = "off";
+        session.Model = "gpt-5.4";
+        var (bridge, provider, snapshots, _) = CreateProvider([session]);
+        var confirmed = session.Clone();
+        confirmed.ThinkingLevel = null;
+        bridge.RequestSessionsSnapshotBehavior = _ =>
+            Task.FromResult<SessionInfo[]?>([confirmed]);
+        await provider.LoadAsync();
+        bridge.RaiseStatus(ConnectionStatus.Connected);
+        await provider.ClearThinkingLevelAsync("main");
+        snapshots.Clear();
+
+        var staleOff = session.Clone();
+        staleOff.Model = "gpt-5.5";
+        bridge.RaiseSessions([staleOff]);
+
+        var masked = Assert.Single(snapshots[^1].Threads);
+        Assert.Null(masked.ThinkingLevel);
+        Assert.Equal("gpt-5.5", masked.Model);
+        Assert.Equal("off", staleOff.ThinkingLevel);
+
+        var ordinaryNull = staleOff.Clone();
+        ordinaryNull.ThinkingLevel = null;
+        ordinaryNull.Model = "gpt-5.6";
+        bridge.RaiseSessions([ordinaryNull]);
+        Assert.Null(Assert.Single(snapshots[^1].Threads).ThinkingLevel);
+
+        var concrete = ordinaryNull.Clone();
+        concrete.ThinkingLevel = "high";
+        concrete.Model = "gpt-5.7";
+        bridge.RaiseSessions([concrete]);
+
+        var accepted = Assert.Single(snapshots[^1].Threads);
+        Assert.Equal("high", accepted.ThinkingLevel);
+        Assert.Equal("gpt-5.7", accepted.Model);
+    }
+
+    [Fact]
+    public async Task Status_AuthorityTransitionClearsConfirmedNullLatch()
+    {
+        var session = MainSession();
+        session.ThinkingLevel = "off";
+        var (bridge, provider, snapshots, _) = CreateProvider([session]);
+        var confirmed = session.Clone();
+        confirmed.ThinkingLevel = null;
+        bridge.RequestSessionsSnapshotBehavior = _ =>
+            Task.FromResult<SessionInfo[]?>([confirmed]);
+        await provider.LoadAsync();
+        bridge.RaiseStatus(ConnectionStatus.Connected);
+        await provider.ClearThinkingLevelAsync("main");
+        snapshots.Clear();
+
+        bridge.RaiseStatus(ConnectionStatus.Disconnected);
+        bridge.RaiseStatus(ConnectionStatus.Connected);
+        var nextGeneration = session.Clone();
+        nextGeneration.Model = "next-generation";
+        bridge.RaiseSessions([nextGeneration]);
+
+        var thread = Assert.Single(snapshots[^1].Threads);
+        Assert.Equal("off", thread.ThinkingLevel);
+        Assert.Equal("next-generation", thread.Model);
+    }
+
+    [Fact]
+    public async Task DisposeAsync_ConfirmedNullLatchDoesNotLeakToNewProvider()
+    {
+        var session = MainSession();
+        session.ThinkingLevel = "off";
+        var (bridgeA, providerA, snapshotsA, _) = CreateProvider([session]);
+        var confirmed = session.Clone();
+        confirmed.ThinkingLevel = null;
+        bridgeA.RequestSessionsSnapshotBehavior = _ =>
+            Task.FromResult<SessionInfo[]?>([confirmed]);
+        await providerA.LoadAsync();
+        bridgeA.RaiseStatus(ConnectionStatus.Connected);
+        await providerA.ClearThinkingLevelAsync("main");
+        Assert.Null(Assert.Single(snapshotsA[^1].Threads).ThinkingLevel);
+
+        await providerA.DisposeAsync();
+
+        var (_, providerB, _, _) = CreateProvider([session.Clone()]);
+        var freshSnapshot = await providerB.LoadAsync();
+        Assert.Equal("off", Assert.Single(freshSnapshot.Threads).ThinkingLevel);
+        await providerB.DisposeAsync();
+    }
+
+    [Fact]
+    public async Task CorrelatedSnapshot_IsAuthoritativeOnlyForTargetConfirmedNullLatch()
+    {
+        var sessionA = MainSession();
+        sessionA.Key = "a";
+        sessionA.ThinkingLevel = "off";
+        var sessionB = MainSession();
+        sessionB.Key = "b";
+        sessionB.IsMain = false;
+        sessionB.DisplayName = "B";
+        sessionB.ThinkingLevel = "off";
+        var (bridge, provider, snapshots, _) = CreateProvider([sessionA, sessionB]);
+        var delayedA = new TaskCompletionSource<SessionInfo[]?>(
+            TaskCreationOptions.RunContinuationsAsynchronously);
+        var requestCall = 0;
+        bridge.RequestSessionsSnapshotBehavior = _ =>
+        {
+            return Interlocked.Increment(ref requestCall) switch
+            {
+                1 => delayedA.Task,
+                2 => Task.FromResult<SessionInfo[]?>(
+                [
+                    sessionA.Clone(),
+                    new SessionInfo
+                    {
+                        Key = "b",
+                        DisplayName = "B",
+                        Status = "active",
+                        ThinkingLevel = null,
+                    },
+                ]),
+                _ => Task.FromResult<SessionInfo[]?>(
+                [
+                    new SessionInfo
+                    {
+                        Key = "a",
+                        IsMain = true,
+                        DisplayName = "A",
+                        Status = "active",
+                        ThinkingLevel = null,
+                    },
+                    new SessionInfo
+                    {
+                        Key = "b",
+                        DisplayName = "B",
+                        Status = "active",
+                        ThinkingLevel = null,
+                    },
+                ]),
+            };
+        };
+        await provider.LoadAsync();
+        bridge.RaiseStatus(ConnectionStatus.Connected);
+        snapshots.Clear();
+
+        var clearA = provider.ClearThinkingLevelAsync("a");
+        await WaitForConditionAsync(() => bridge.RequestSessionsSnapshotCallCount == 1);
+        await provider.ClearThinkingLevelAsync("b");
+        Assert.Null(Assert.Single(snapshots[^1].Threads, thread => thread.Id == "b").ThinkingLevel);
+
+        var nonTargetStaleB = sessionB.Clone();
+        nonTargetStaleB.Model = "fresh-b-model";
+        var confirmedA = sessionA.Clone();
+        confirmedA.ThinkingLevel = null;
+        delayedA.SetResult([confirmedA, nonTargetStaleB]);
+        await clearA;
+
+        var afterDelayedA = snapshots[^1];
+        Assert.Null(Assert.Single(afterDelayedA.Threads, thread => thread.Id == "a").ThinkingLevel);
+        var maskedB = Assert.Single(afterDelayedA.Threads, thread => thread.Id == "b");
+        Assert.Null(maskedB.ThinkingLevel);
+        Assert.Equal("fresh-b-model", maskedB.Model);
+        Assert.Equal("off", nonTargetStaleB.ThinkingLevel);
+
+        await provider.ClearThinkingLevelAsync("a");
+        var ordinaryStaleB = sessionB.Clone();
+        ordinaryStaleB.Model = "ordinary-b-model";
+        var ordinaryA = sessionA.Clone();
+        ordinaryA.ThinkingLevel = null;
+        bridge.RaiseSessions([ordinaryA, ordinaryStaleB]);
+
+        var afterNonTargetNull = snapshots[^1];
+        var stillMaskedB = Assert.Single(afterNonTargetNull.Threads, thread => thread.Id == "b");
+        Assert.Null(stillMaskedB.ThinkingLevel);
+        Assert.Equal("ordinary-b-model", stillMaskedB.Model);
+    }
+
+    [Fact]
+    public async Task SetThinkingLevelAsync_SuccessRetiresLatchButFailureRetainsNullAuthority()
+    {
+        var session = MainSession();
+        session.ThinkingLevel = "off";
+        var (bridge, provider, snapshots, _) = CreateProvider([session]);
+        var confirmed = session.Clone();
+        confirmed.ThinkingLevel = null;
+        bridge.RequestSessionsSnapshotBehavior = _ =>
+            Task.FromResult<SessionInfo[]?>([confirmed]);
+        await provider.LoadAsync();
+        bridge.RaiseStatus(ConnectionStatus.Connected);
+        await provider.ClearThinkingLevelAsync("main");
+        snapshots.Clear();
+
+        await provider.SetThinkingLevelAsync("main", "high");
+        var canonicalHigh = session.Clone();
+        canonicalHigh.ThinkingLevel = "high";
+        bridge.RaiseSessions([canonicalHigh]);
+        Assert.Equal("high", Assert.Single(snapshots[^1].Threads).ThinkingLevel);
+
+        await provider.ClearThinkingLevelAsync("main");
+        bridge.PatchSessionThinkingLevelBehavior = (_, _) =>
+            Task.FromException(new InvalidOperationException("concrete patch rejected"));
+        await Assert.ThrowsAsync<InvalidOperationException>(
+            () => provider.SetThinkingLevelAsync("main", "high"));
+
+        var staleAfterFailure = session.Clone();
+        staleAfterFailure.Model = "fresh-model";
+        bridge.RaiseSessions([staleAfterFailure]);
+        var protectedThread = Assert.Single(snapshots[^1].Threads);
+        Assert.Null(protectedThread.ThinkingLevel);
+        Assert.Equal("fresh-model", protectedThread.Model);
+        Assert.Equal("off", staleAfterFailure.ThinkingLevel);
+    }
+
+    [Fact]
+    public async Task SetThinkingLevelAsync_OlderConcreteCannotRetireNewerConfirmedNullAuthority()
+    {
+        var session = MainSession();
+        session.ThinkingLevel = "off";
+        var (bridge, provider, snapshots, _) = CreateProvider([session]);
+        var confirmed = session.Clone();
+        confirmed.ThinkingLevel = null;
+        bridge.RequestSessionsSnapshotBehavior = _ =>
+            Task.FromResult<SessionInfo[]?>([confirmed]);
+        await provider.LoadAsync();
+        bridge.RaiseStatus(ConnectionStatus.Connected);
+        await provider.ClearThinkingLevelAsync("main");
+        snapshots.Clear();
+
+        var concreteCompletion = new TaskCompletionSource(
+            TaskCreationOptions.RunContinuationsAsynchronously);
+        bridge.PatchSessionThinkingLevelBehavior = (_, _) => concreteCompletion.Task;
+        var olderConcrete = provider.SetThinkingLevelAsync("main", "high");
+        await WaitForConditionAsync(() => bridge.PatchedThinkingLevels.Count == 1);
+
+        await provider.ClearThinkingLevelAsync("main");
+        concreteCompletion.SetResult();
+        await olderConcrete;
+
+        var staleOff = session.Clone();
+        staleOff.Model = "newer-clear-model";
+        bridge.RaiseSessions([staleOff]);
+        var protectedThread = Assert.Single(snapshots[^1].Threads);
+        Assert.Null(protectedThread.ThinkingLevel);
+        Assert.Equal("newer-clear-model", protectedThread.Model);
+        Assert.Equal("off", staleOff.ThinkingLevel);
+    }
+
+    [Fact]
+    public async Task SetThinkingLevelAsync_FailedConcreteCannotProjectOverConfirmedNullAuthority()
+    {
+        var session = MainSession();
+        session.ThinkingLevel = "off";
+        var (bridge, provider, snapshots, _) = CreateProvider([session]);
+        var confirmed = session.Clone();
+        confirmed.ThinkingLevel = null;
+        bridge.RequestSessionsSnapshotBehavior = _ =>
+            Task.FromResult<SessionInfo[]?>([confirmed]);
+        await provider.LoadAsync();
+        bridge.RaiseStatus(ConnectionStatus.Connected);
+        await provider.ClearThinkingLevelAsync("main");
+        snapshots.Clear();
+
+        var clearCompletion = new TaskCompletionSource<SessionCommandResult>(
+            TaskCreationOptions.RunContinuationsAsynchronously);
+        var concreteCompletion = new TaskCompletionSource(
+            TaskCreationOptions.RunContinuationsAsynchronously);
+        var refreshCompletion = new TaskCompletionSource<SessionInfo[]?>(
+            TaskCreationOptions.RunContinuationsAsynchronously);
+        bridge.ClearSessionThinkingLevelBehavior = (_, _) => clearCompletion.Task;
+        bridge.PatchSessionThinkingLevelBehavior = (_, _) => concreteCompletion.Task;
+        bridge.RequestSessionsSnapshotBehavior = _ => refreshCompletion.Task;
+
+        var supersededClear = provider.ClearThinkingLevelAsync("main");
+        var failedConcrete = provider.SetThinkingLevelAsync("main", "high");
+        clearCompletion.SetResult(new SessionCommandResult
+        {
+            Method = "sessions.patch",
+            Ok = false,
+            Key = "main",
+            Error = "superseded clear",
+        });
+        await Assert.ThrowsAnyAsync<OperationCanceledException>(() => supersededClear);
+
+        var staleOff = session.Clone();
+        staleOff.Model = "fresh-while-concrete-pending";
+        bridge.RaiseSessions([staleOff]);
+        var whilePending = Assert.Single(snapshots[^1].Threads);
+        Assert.Null(whilePending.ThinkingLevel);
+        Assert.Equal("fresh-while-concrete-pending", whilePending.Model);
+
+        concreteCompletion.SetException(new InvalidOperationException("concrete rejected"));
+        await Assert.ThrowsAsync<InvalidOperationException>(() => failedConcrete);
+
+        var afterFailure = Assert.Single(snapshots[^1].Threads);
+        Assert.Null(afterFailure.ThinkingLevel);
+        Assert.Equal("fresh-while-concrete-pending", afterFailure.Model);
+        Assert.Equal("off", staleOff.ThinkingLevel);
+        refreshCompletion.SetResult([confirmed]);
+    }
+
+    [Fact]
+    public async Task SessionsUpdated_RowDisappearanceRemovesConfirmedNullLatch()
+    {
+        var session = MainSession();
+        session.ThinkingLevel = "off";
+        var (bridge, provider, snapshots, _) = CreateProvider([session]);
+        var confirmed = session.Clone();
+        confirmed.ThinkingLevel = null;
+        bridge.RequestSessionsSnapshotBehavior = _ =>
+            Task.FromResult<SessionInfo[]?>([confirmed]);
+        await provider.LoadAsync();
+        bridge.RaiseStatus(ConnectionStatus.Connected);
+        await provider.ClearThinkingLevelAsync("main");
+        snapshots.Clear();
+
+        bridge.RaiseSessions([]);
+        bridge.RaiseSessions([session.Clone()]);
+
+        Assert.Equal("off", Assert.Single(snapshots[^1].Threads).ThinkingLevel);
+    }
+
+    [Fact]
+    public async Task ClearThinkingLevelAsync_DirectRejectionConfirmationFaultIsObserved()
+    {
+        const string rejectionMarker = "direct rejection marker";
+        var matchingUnobserved = new ConcurrentQueue<Exception>();
+        EventHandler<UnobservedTaskExceptionEventArgs> handler = (_, args) =>
+        {
+            foreach (var exception in args.Exception.Flatten().InnerExceptions)
+            {
+                if (exception.Message.Contains(rejectionMarker, StringComparison.Ordinal))
+                    matchingUnobserved.Enqueue(exception);
+            }
+            args.SetObserved();
+        };
+        TaskScheduler.UnobservedTaskException += handler;
+
+        try
+        {
+            var providerReference = CreateAbandonedDirectRejectionProvider(rejectionMarker);
+            await AssertEventuallyCollectedAsync(providerReference);
+            Assert.Empty(matchingUnobserved);
+        }
+        finally
+        {
+            TaskScheduler.UnobservedTaskException -= handler;
+        }
+    }
+
+    [Fact]
+    public async Task ClearThinkingLevelAsync_PostAckDisconnectConfirmationFaultIsObserved()
+    {
+        var matchingUnobserved = new ConcurrentQueue<Exception>();
+        EventHandler<UnobservedTaskExceptionEventArgs> handler = (_, args) =>
+        {
+            foreach (var exception in args.Exception.Flatten().InnerExceptions)
+            {
+                if (exception is ThinkingLevelClearInterruptedException)
+                    matchingUnobserved.Enqueue(exception);
+            }
+            args.SetObserved();
+        };
+        TaskScheduler.UnobservedTaskException += handler;
+
+        try
+        {
+            var providerReference = CreateAbandonedPostAckDisconnectProvider();
+            await AssertEventuallyCollectedAsync(providerReference);
+            Assert.Empty(matchingUnobserved);
+        }
+        finally
+        {
+            TaskScheduler.UnobservedTaskException -= handler;
+        }
+    }
+
     [Fact]
     public async Task SendMessageAsync_WhenIdle_RendersTranscriptEntryBeforeAwaitingGateway()
     {
@@ -9698,6 +10119,90 @@ public async Task ApprovalRequested_DedupesSlugFirstUuidTwin_AndUuidOnlyResolved
         Assert.Null(snapshots[^1].Timelines["main"].PendingPermission);
     }
 
+    [MethodImpl(MethodImplOptions.NoInlining)]
+    private static WeakReference CreateAbandonedDirectRejectionProvider(string rejectionMarker)
+    {
+        var session = MainSession();
+        session.ThinkingLevel = "off";
+        var (bridge, provider, _, _) = CreateProvider([session]);
+        bridge.ClearSessionThinkingLevelBehavior = (key, _) =>
+            Task.FromResult(new SessionCommandResult
+            {
+                Method = "sessions.patch",
+                Ok = false,
+                Key = key,
+                Error = rejectionMarker,
+            });
+        provider.LoadAsync().GetAwaiter().GetResult();
+        bridge.RaiseStatus(ConnectionStatus.Connected);
+
+        var exception = Assert.Throws<InvalidOperationException>(
+            () => provider.ClearThinkingLevelAsync("main").GetAwaiter().GetResult());
+        Assert.Contains(rejectionMarker, exception.Message, StringComparison.Ordinal);
+        var providerReference = new WeakReference(provider);
+        provider.DisposeAsync().AsTask().GetAwaiter().GetResult();
+        return providerReference;
+    }
+
+    [MethodImpl(MethodImplOptions.NoInlining)]
+    private static WeakReference CreateAbandonedPostAckDisconnectProvider()
+    {
+        var session = MainSession();
+        session.ThinkingLevel = "off";
+        var (bridge, provider, _, _) = CreateProvider(
+            [session],
+            thinkingLevelConfirmationTimeout: TimeSpan.FromMilliseconds(20));
+        var requestStarted = new TaskCompletionSource(
+            TaskCreationOptions.RunContinuationsAsynchronously);
+        var requestCanceled = new TaskCompletionSource(
+            TaskCreationOptions.RunContinuationsAsynchronously);
+        bridge.RequestSessionsSnapshotBehavior = async cancellationToken =>
+        {
+            requestStarted.TrySetResult();
+            try
+            {
+                await Task.Delay(Timeout.InfiniteTimeSpan, cancellationToken);
+                return null;
+            }
+            finally
+            {
+                if (cancellationToken.IsCancellationRequested)
+                    requestCanceled.TrySetResult();
+            }
+        };
+        provider.LoadAsync().GetAwaiter().GetResult();
+        bridge.RaiseStatus(ConnectionStatus.Connected);
+
+        provider.ClearThinkingLevelAsync("main").GetAwaiter().GetResult();
+        requestStarted.Task.WaitAsync(TimeSpan.FromSeconds(5)).GetAwaiter().GetResult();
+        bridge.RaiseStatus(ConnectionStatus.Disconnected);
+        requestCanceled.Task.WaitAsync(TimeSpan.FromSeconds(5)).GetAwaiter().GetResult();
+        var providerReference = new WeakReference(provider);
+        provider.DisposeAsync().AsTask().GetAwaiter().GetResult();
+        return providerReference;
+    }
+
+    private static async Task AssertEventuallyCollectedAsync(WeakReference reference)
+    {
+        for (var attempt = 0; attempt < 20 && reference.IsAlive; attempt++)
+        {
+            GC.Collect(
+                GC.MaxGeneration,
+                GCCollectionMode.Forced,
+                blocking: true,
+                compacting: true);
+            GC.WaitForPendingFinalizers();
+            GC.Collect(
+                GC.MaxGeneration,
+                GCCollectionMode.Forced,
+                blocking: true,
+                compacting: true);
+            await Task.Delay(10);
+        }
+
+        Assert.False(reference.IsAlive, "Expected the abandoned provider to be collected.");
+    }
+
     private static async Task WaitForConditionAsync(Func<bool> condition, int attempts = 50)
     {
         for (var i = 0; i < attempts && !condition(); i++)
