Skip to content

Commit 96549f9

Browse files
bkudiessCopilot
andcommitted
Preserve confirmed reasoning across reloads
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
1 parent 83386c8 commit 96549f9

2 files changed

Lines changed: 624 additions & 16 deletions

File tree

src/OpenClaw.Tray.WinUI/Chat/OpenClawChatDataProvider.cs

Lines changed: 119 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -103,6 +103,8 @@ public sealed class OpenClawChatDataProvider : IChatDataProvider
103103
private readonly TimeSpan _lastChatStateSaveDelay;
104104
private readonly Func<TimeSpan, CancellationToken, Func<Task>, Task> _scheduleHistoryRetry;
105105
private readonly ThinkingLevelClearReconciler _thinkingLevelClearReconciler;
106+
private readonly Dictionary<string, long> _confirmedNullThinkingAuthorityTokens =
107+
new(StringComparer.Ordinal);
106108
private readonly Action? _historyFailureReservedForTesting;
107109
private System.Threading.Timer? _toolMetaSaveTimer; // debounce cache writes
108110
private long _toolMetaSaveVersion;
@@ -223,6 +225,7 @@ private enum AssistantQueueFrameDisposition
223225
// was already in flight at disconnect time is discarded on completion rather
224226
// than resurrecting a catalog for a stale connection.
225227
private int _commandsEpoch;
228+
private long _nextConfirmedNullThinkingAuthorityToken;
226229
private ConnectionStatus _status;
227230
private bool _disposed;
228231

@@ -335,26 +338,34 @@ public Task<ChatDataSnapshot> LoadAsync(CancellationToken cancellationToken = de
335338
ChatDataSnapshot snapshot;
336339
lock (_gate)
337340
{
341+
ReleaseMissingConfirmedNullThinkingAuthoritiesLocked(sessions);
338342
_sessions = sessions.Select(session =>
339343
{
340344
if (string.IsNullOrEmpty(session.Key))
341345
return session;
342346

347+
var projectedSession = ApplyConfirmedNullThinkingAuthorityLocked(
348+
session,
349+
releaseOnNull: true);
343350
var resolution = _thinkingLevelClearReconciler.ObserveSnapshot(
344-
session.Key,
345-
session.ThinkingLevel);
351+
projectedSession.Key,
352+
projectedSession.ThinkingLevel);
346353
if (resolution.RefreshRequest is { } refresh)
347354
(thinkingLevelsToRefresh ??= []).Add(refresh);
355+
var effectiveThinkingLevel =
356+
_confirmedNullThinkingAuthorityTokens.ContainsKey(projectedSession.Key)
357+
? null
358+
: resolution.EffectiveThinkingLevel;
348359
if (string.Equals(
349-
session.ThinkingLevel,
350-
resolution.EffectiveThinkingLevel,
360+
projectedSession.ThinkingLevel,
361+
effectiveThinkingLevel,
351362
StringComparison.Ordinal))
352363
{
353-
return session;
364+
return projectedSession;
354365
}
355366

356-
var reconciled = session.Clone();
357-
reconciled.ThinkingLevel = resolution.EffectiveThinkingLevel;
367+
var reconciled = projectedSession.Clone();
368+
reconciled.ThinkingLevel = effectiveThinkingLevel;
358369
return reconciled;
359370
}).ToArray();
360371
EnsureTimelinesForSessionsLocked();
@@ -1823,11 +1834,16 @@ public async Task SetThinkingLevelAsync(string threadId, string thinkingLevel, C
18231834
{
18241835
cancellationToken.ThrowIfCancellationRequested();
18251836
ThinkingLevelClearReconciler.ConcreteSelection selection;
1837+
long? confirmedNullAuthorityToken;
18261838
lock (_gate)
18271839
{
18281840
var canonicalThinkingLevel = _sessions
18291841
.FirstOrDefault(session => string.Equals(session.Key, threadId, StringComparison.Ordinal))
18301842
?.ThinkingLevel;
1843+
confirmedNullAuthorityToken =
1844+
_confirmedNullThinkingAuthorityTokens.TryGetValue(threadId, out var token)
1845+
? token
1846+
: null;
18311847
selection = _thinkingLevelClearReconciler.BeginConcreteSelection(
18321848
threadId,
18331849
thinkingLevel,
@@ -1837,6 +1853,20 @@ public async Task SetThinkingLevelAsync(string threadId, string thinkingLevel, C
18371853
try
18381854
{
18391855
await _bridge.PatchSessionThinkingLevelAsync(threadId, thinkingLevel);
1856+
if (confirmedNullAuthorityToken is { } expectedToken)
1857+
{
1858+
lock (_gate)
1859+
{
1860+
if (!_disposed &&
1861+
_confirmedNullThinkingAuthorityTokens.TryGetValue(
1862+
threadId,
1863+
out var currentToken) &&
1864+
currentToken == expectedToken)
1865+
{
1866+
_confirmedNullThinkingAuthorityTokens.Remove(threadId);
1867+
}
1868+
}
1869+
}
18401870
}
18411871
catch
18421872
{
@@ -1857,6 +1887,7 @@ public async Task ClearThinkingLevelAsync(string threadId, CancellationToken can
18571887
operation = _thinkingLevelClearReconciler.BeginClear(
18581888
threadId,
18591889
canonicalThinkingLevel);
1890+
ObserveBackgroundTask(operation.Confirmation);
18601891
}
18611892

18621893
try
@@ -2243,6 +2274,7 @@ public ValueTask DisposeAsync()
22432274
_localSentTexts.Clear();
22442275
_locallyInitiatedThreads.Clear();
22452276
_resetSubmittedLocalEchoTexts.Clear();
2277+
_confirmedNullThinkingAuthorityTokens.Clear();
22462278
}
22472279
_thinkingLevelClearReconciler.Dispose();
22482280
CancelAndDisposeHistoryGeneration(historyGenerationToCancel);
@@ -2290,6 +2322,9 @@ private void OnStatusChanged(object? sender, ConnectionStatus status)
22902322

22912323
justReconnected = status == ConnectionStatus.Connected
22922324
&& _status != ConnectionStatus.Connected;
2325+
var connectedAuthorityChanged =
2326+
(status == ConnectionStatus.Connected) !=
2327+
(_status == ConnectionStatus.Connected);
22932328
// MEDIUM 5: detect Connected → Disconnected/Error transitions so
22942329
// we can synthesise a turn-end + status entry on every thread that
22952330
// had an in-flight turn (otherwise the UI sits "thinking" forever).
@@ -2298,6 +2333,8 @@ private void OnStatusChanged(object? sender, ConnectionStatus status)
22982333
_status = status;
22992334
thinkingLevelsToRefresh = _thinkingLevelClearReconciler
23002335
.OnConnectionChanged(status == ConnectionStatus.Connected);
2336+
if (connectedAuthorityChanged)
2337+
_confirmedNullThinkingAuthorityTokens.Clear();
23012338

23022339
// Reset the sessions-list-received gate whenever we leave the
23032340
// Connected state. Any cached sessions belong to the previous
@@ -2453,10 +2490,25 @@ private bool ApplySessionsUpdated(
24532490
return false;
24542491

24552492
correlatedResolution = resolution;
2493+
if (resolution.State == ThinkingLevelClearReconciler.ReconciliationState.Confirmed)
2494+
{
2495+
if (resolution.EffectiveThinkingLevel is null)
2496+
{
2497+
_confirmedNullThinkingAuthorityTokens[correlatedRequest.ThreadId] =
2498+
++_nextConfirmedNullThinkingAuthorityToken;
2499+
}
2500+
else
2501+
{
2502+
_confirmedNullThinkingAuthorityTokens.Remove(correlatedRequest.ThreadId);
2503+
}
2504+
}
24562505
if (resolution.RefreshRequest is { } retry)
24572506
(thinkingLevelsToRefresh ??= []).Add(retry);
24582507
}
24592508

2509+
if (refreshRequest is null)
2510+
ReleaseMissingConfirmedNullThinkingAuthoritiesLocked(incomingSessions);
2511+
24602512
var previousThreads = refreshRequest is null
24612513
? null
24622514
: BuildSnapshotLocked().Threads;
@@ -2467,32 +2519,44 @@ private bool ApplySessionsUpdated(
24672519
for (var index = 0; index < incomingSessions.Length; index++)
24682520
{
24692521
var incoming = incomingSessions[index];
2470-
reconciledSessions[index] = incoming;
2522+
var isCorrelatedTarget =
2523+
refreshRequest is { } request &&
2524+
string.Equals(request.ThreadId, incoming.Key, StringComparison.Ordinal);
2525+
var projectedSession = isCorrelatedTarget
2526+
? incoming
2527+
: ApplyConfirmedNullThinkingAuthorityLocked(
2528+
incoming,
2529+
releaseOnNull: refreshRequest is null);
2530+
reconciledSessions[index] = projectedSession;
24712531
if (string.IsNullOrEmpty(incoming.Key))
24722532
continue;
24732533

24742534
ThinkingLevelClearReconciler.SnapshotResolution resolution;
2475-
if (refreshRequest is { } request &&
2476-
string.Equals(request.ThreadId, incoming.Key, StringComparison.Ordinal))
2535+
if (isCorrelatedTarget)
24772536
{
24782537
resolution = correlatedResolution!.Value;
24792538
}
24802539
else
24812540
{
24822541
resolution = _thinkingLevelClearReconciler.ObserveSnapshot(
2483-
incoming.Key,
2484-
incoming.ThinkingLevel);
2542+
projectedSession.Key,
2543+
projectedSession.ThinkingLevel);
24852544
if (resolution.RefreshRequest is { } refresh)
24862545
(thinkingLevelsToRefresh ??= []).Add(refresh);
24872546
}
24882547

2548+
var effectiveThinkingLevel =
2549+
!isCorrelatedTarget &&
2550+
_confirmedNullThinkingAuthorityTokens.ContainsKey(projectedSession.Key)
2551+
? null
2552+
: resolution.EffectiveThinkingLevel;
24892553
if (!string.Equals(
2490-
incoming.ThinkingLevel,
2491-
resolution.EffectiveThinkingLevel,
2554+
projectedSession.ThinkingLevel,
2555+
effectiveThinkingLevel,
24922556
StringComparison.Ordinal))
24932557
{
2494-
var reconciled = incoming.Clone();
2495-
reconciled.ThinkingLevel = resolution.EffectiveThinkingLevel;
2558+
var reconciled = projectedSession.Clone();
2559+
reconciled.ThinkingLevel = effectiveThinkingLevel;
24962560
reconciledSessions[index] = reconciled;
24972561
}
24982562
}
@@ -2540,6 +2604,45 @@ private bool ApplySessionsUpdated(
25402604
return true;
25412605
}
25422606

2607+
private SessionInfo ApplyConfirmedNullThinkingAuthorityLocked(
2608+
SessionInfo session,
2609+
bool releaseOnNull)
2610+
{
2611+
if (string.IsNullOrEmpty(session.Key) ||
2612+
!_confirmedNullThinkingAuthorityTokens.ContainsKey(session.Key))
2613+
{
2614+
return session;
2615+
}
2616+
2617+
if (session.ThinkingLevel is null)
2618+
{
2619+
if (releaseOnNull)
2620+
_confirmedNullThinkingAuthorityTokens.Remove(session.Key);
2621+
return session;
2622+
}
2623+
2624+
var projected = session.Clone();
2625+
projected.ThinkingLevel = null;
2626+
return projected;
2627+
}
2628+
2629+
private void ReleaseMissingConfirmedNullThinkingAuthoritiesLocked(
2630+
IReadOnlyCollection<SessionInfo> sessions)
2631+
{
2632+
if (_confirmedNullThinkingAuthorityTokens.Count == 0)
2633+
return;
2634+
2635+
var incomingKeys = sessions
2636+
.Where(session => !string.IsNullOrEmpty(session.Key))
2637+
.Select(session => session.Key)
2638+
.ToHashSet(StringComparer.Ordinal);
2639+
var missingThreads = _confirmedNullThinkingAuthorityTokens.Keys
2640+
.Where(threadId => !incomingKeys.Contains(threadId))
2641+
.ToArray();
2642+
foreach (var threadId in missingThreads)
2643+
_confirmedNullThinkingAuthorityTokens.Remove(threadId);
2644+
}
2645+
25432646
internal static bool ShouldPreserveLiveEntryDuringAuthoritativeReload(
25442647
ChatEntryMetadata? metadata,
25452648
int maxHistorySequence,

0 commit comments

Comments
 (0)