Skip to content

Commit 48d55f6

Browse files
bkudiessCopilot
andcommitted
Preserve session query scope state
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
1 parent 78f8f9f commit 48d55f6

3 files changed

Lines changed: 324 additions & 21 deletions

File tree

src/OpenClaw.Shared/OpenClawGatewayClient.Protocol.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -156,6 +156,7 @@ private void ResetSessionListCapability()
156156
{
157157
_sessionListConnectionGeneration++;
158158
_sessionListCapability = 0;
159+
_operatorReadScopeUnavailable = false;
159160
}
160161
}
161162

src/OpenClaw.Shared/OpenClawGatewayClient.cs

Lines changed: 109 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ public partial class OpenClawGatewayClient : WebSocketClientBase, IOperatorGatew
3838
private GatewayUsageStatusInfo? _usageStatus;
3939
private GatewayCostUsageInfo? _usageCost;
4040
private readonly Dictionary<string, string> _pendingRequestMethods = new();
41+
private readonly Dictionary<string, int> _pendingRequestConnectionGenerations = new();
4142
private readonly Dictionary<string, TaskCompletionSource<ChatSendResult>> _pendingChatSendRequests = new();
4243
private readonly object _pendingRequestLock = new();
4344
private readonly object _pendingChatSendLock = new();
@@ -143,7 +144,6 @@ private void ResetUnsupportedMethodFlags()
143144
_agentsListUnsupported = false;
144145
_agentFilesListUnsupported = false;
145146
_agentFileGetUnsupported = false;
146-
_operatorReadScopeUnavailable = false;
147147
}
148148

149149
/// <summary>
@@ -732,7 +732,8 @@ public async Task<JsonElement> SendWizardRequestAsync(
732732
/// <summary>Request session list from gateway.</summary>
733733
public async Task RequestSessionsAsync(string? agentId = null)
734734
{
735-
if (_operatorReadScopeUnavailable) return;
735+
var scopeState = CaptureOperatorReadScopeState();
736+
if (scopeState.Unavailable) return;
736737
try
737738
{
738739
var snapshot = await QuerySessionsAsync(new SessionQuery
@@ -754,23 +755,95 @@ public async Task RequestSessionsAsync(string? agentId = null)
754755
}
755756
catch (GatewayRequestException ex) when (IsMissingScopeError(ex.Message, "operator.read"))
756757
{
757-
_operatorReadScopeUnavailable = true;
758-
_logger.Warn("Gateway token lacks operator.read; disabling sessions polling");
758+
MarkOperatorReadScopeUnavailable(scopeState.Generation);
759759
}
760760
catch (Exception ex)
761761
{
762762
_logger.Warn($"sessions.list failed: {ex.Message}");
763763
}
764764
}
765765

766-
public Task<SessionQuerySnapshot> QuerySessionsAsync(
766+
public async Task<SessionQuerySnapshot> QuerySessionsAsync(
767767
SessionQuery query,
768768
CancellationToken cancellationToken = default)
769769
{
770770
ArgumentNullException.ThrowIfNull(query);
771-
return string.IsNullOrWhiteSpace(query.Search)
772-
? _sessionQueries.LoadRecentAsync(query, cancellationToken)
773-
: _sessionQueries.SearchAsync(query, cancellationToken);
771+
cancellationToken.ThrowIfCancellationRequested();
772+
var scopeState = CaptureOperatorReadScopeState();
773+
if (scopeState.Unavailable)
774+
{
775+
var queryGeneration = _sessionQueries.ConnectionGeneration;
776+
cancellationToken.ThrowIfCancellationRequested();
777+
if (!IsOperatorReadScopeUnavailable(scopeState.Generation))
778+
{
779+
throw new OperationCanceledException(
780+
"sessions.list query belongs to a stale connection generation.",
781+
cancellationToken);
782+
}
783+
784+
// The query was not executed, so report no paging/search mode while
785+
// retaining the requested search identity for upper consumers.
786+
return new SessionQuerySnapshot
787+
{
788+
Search = string.IsNullOrWhiteSpace(query.Search) ? null : query.Search.Trim(),
789+
ConnectionGeneration = queryGeneration,
790+
Sessions = Array.Empty<SessionInfo>(),
791+
MaterializedSessions = Array.Empty<SessionInfo>(),
792+
};
793+
}
794+
795+
EnsureCurrentSessionListGeneration(scopeState.Generation, cancellationToken);
796+
try
797+
{
798+
return await (string.IsNullOrWhiteSpace(query.Search)
799+
? _sessionQueries.LoadRecentAsync(query, cancellationToken)
800+
: _sessionQueries.SearchAsync(query, cancellationToken)).ConfigureAwait(false);
801+
}
802+
catch (GatewayRequestException ex) when (IsMissingScopeError(ex.Message, "operator.read"))
803+
{
804+
if (!MarkOperatorReadScopeUnavailable(scopeState.Generation))
805+
{
806+
throw new OperationCanceledException(
807+
"sessions.list response belongs to a stale connection generation.",
808+
ex,
809+
cancellationToken);
810+
}
811+
throw;
812+
}
813+
}
814+
815+
private (int Generation, bool Unavailable) CaptureOperatorReadScopeState()
816+
{
817+
lock (_sessionListCapabilityLock)
818+
return (_sessionListConnectionGeneration, _operatorReadScopeUnavailable);
819+
}
820+
821+
private bool IsOperatorReadScopeUnavailable(int connectionGeneration)
822+
{
823+
lock (_sessionListCapabilityLock)
824+
{
825+
return _sessionListConnectionGeneration == connectionGeneration &&
826+
_operatorReadScopeUnavailable;
827+
}
828+
}
829+
830+
private bool MarkOperatorReadScopeUnavailable(
831+
int connectionGeneration,
832+
string warning = "Gateway token lacks operator.read; disabling sessions polling")
833+
{
834+
var shouldLog = false;
835+
lock (_sessionListCapabilityLock)
836+
{
837+
if (_sessionListConnectionGeneration != connectionGeneration)
838+
return false;
839+
if (_operatorReadScopeUnavailable)
840+
return true;
841+
_operatorReadScopeUnavailable = true;
842+
shouldLog = true;
843+
}
844+
if (shouldLog)
845+
_logger.Warn(warning);
846+
return true;
774847
}
775848

776849
public SessionQuerySnapshot ClearSessionSearch(SessionQuery? query = null) =>
@@ -787,7 +860,7 @@ public async Task SubscribeSessionEventsAsync()
787860
/// <summary>Request usage/context info from gateway (may not be supported on all gateways).</summary>
788861
public async Task RequestUsageAsync()
789862
{
790-
if (_operatorReadScopeUnavailable) return;
863+
if (CaptureOperatorReadScopeState().Unavailable) return;
791864
if (!IsConnected) return;
792865
try
793866
{
@@ -812,7 +885,7 @@ public async Task RequestUsageAsync()
812885
/// <summary>Request connected node inventory from gateway.</summary>
813886
public async Task RequestNodesAsync()
814887
{
815-
if (_operatorReadScopeUnavailable) return;
888+
if (CaptureOperatorReadScopeState().Unavailable) return;
816889
if (_nodeListUnsupported) return;
817890
await SendTrackedRequestAsync("node.list");
818891
}
@@ -1724,26 +1797,36 @@ private static string SerializeRequest(string requestId, string method, object?
17241797

17251798
private void TrackPendingRequest(string requestId, string method)
17261799
{
1800+
var connectionGeneration = CaptureOperatorReadScopeState().Generation;
17271801
lock (_pendingRequestLock)
17281802
{
17291803
_pendingRequestMethods[requestId] = method;
1804+
_pendingRequestConnectionGenerations[requestId] = connectionGeneration;
17301805
}
17311806
}
17321807

17331808
private void RemovePendingRequest(string requestId)
17341809
{
17351810
lock (_pendingRequestLock)
1811+
{
17361812
_pendingRequestMethods.Remove(requestId);
1813+
_pendingRequestConnectionGenerations.Remove(requestId);
1814+
}
17371815
}
17381816

1739-
private string? TakePendingRequestMethod(string? requestId)
1817+
private (string? Method, int? ConnectionGeneration) TakePendingRequest(string? requestId)
17401818
{
1741-
if (string.IsNullOrWhiteSpace(requestId)) return null;
1819+
if (string.IsNullOrWhiteSpace(requestId)) return (null, null);
17421820
lock (_pendingRequestLock)
17431821
{
17441822
if (!_pendingRequestMethods.Remove(requestId, out var method))
1745-
return null;
1746-
return method;
1823+
return (null, null);
1824+
var generation = _pendingRequestConnectionGenerations.Remove(
1825+
requestId,
1826+
out var connectionGeneration)
1827+
? connectionGeneration
1828+
: (int?)null;
1829+
return (method, generation);
17471830
}
17481831
}
17491832

@@ -1752,6 +1835,7 @@ private void ClearPendingRequests()
17521835
lock (_pendingRequestLock)
17531836
{
17541837
_pendingRequestMethods.Clear();
1838+
_pendingRequestConnectionGenerations.Clear();
17551839
}
17561840

17571841
lock (_pendingChatSendLock)
@@ -1862,11 +1946,12 @@ private void ProcessMessage(string json)
18621946
private void HandleResponse(JsonElement root)
18631947
{
18641948
string? requestMethod = null;
1949+
int? requestConnectionGeneration = null;
18651950
string? requestId = null;
18661951
if (root.TryGetProperty("id", out var idProp))
18671952
{
18681953
requestId = idProp.GetString();
1869-
requestMethod = TakePendingRequestMethod(requestId);
1954+
(requestMethod, requestConnectionGeneration) = TakePendingRequest(requestId);
18701955
}
18711956

18721957
var pendingChatSend = TakePendingChatSend(requestId);
@@ -1940,7 +2025,7 @@ private void HandleResponse(JsonElement root)
19402025
if (root.TryGetProperty("ok", out var okProp) &&
19412026
okProp.ValueKind == JsonValueKind.False)
19422027
{
1943-
HandleRequestError(requestMethod, root);
2028+
HandleRequestError(requestMethod, requestConnectionGeneration, root);
19442029
return;
19452030
}
19462031

@@ -2231,7 +2316,10 @@ JsonValueKind.Object when value.TryGetProperty("message", out var message) => Tr
22312316
_ => null
22322317
};
22332318

2234-
private void HandleRequestError(string? method, JsonElement root)
2319+
private void HandleRequestError(
2320+
string? method,
2321+
int? requestConnectionGeneration,
2322+
JsonElement root)
22352323
{
22362324
var message = TryGetErrorMessage(root) ?? "request failed";
22372325
var detailCode = method == "connect" ? TryGetErrorDetailCode(root) : null;
@@ -2322,12 +2410,12 @@ private void HandleRequestError(string? method, JsonElement root)
23222410
if (IsMissingScopeError(message, "operator.read") &&
23232411
method is "sessions.list" or "usage.status" or "usage.cost" or "node.list")
23242412
{
2325-
if (!_operatorReadScopeUnavailable)
2413+
if (requestConnectionGeneration.HasValue)
23262414
{
2327-
_logger.Warn("Gateway token lacks operator.read; disabling sessions/usage/nodes polling");
2415+
MarkOperatorReadScopeUnavailable(
2416+
requestConnectionGeneration.Value,
2417+
"Gateway token lacks operator.read; disabling sessions/usage/nodes polling");
23282418
}
2329-
2330-
_operatorReadScopeUnavailable = true;
23312419
return;
23322420
}
23332421

0 commit comments

Comments
 (0)