Skip to content

Commit 887629c

Browse files
PureWeenCopilot
andcommitted
Fix force-sync not clearing stale IsProcessing when streaming guard is stuck
The streaming guard (_remoteStreamingSessions) blocks SyncRemoteSessions from updating IsProcessing. If TurnStart fires but TurnEnd is lost (connection drop), the guard stays active forever, causing permanent stale 'busy/sending' state that even the sync button can't fix. ForceRefreshRemoteAsync now: - Applies server's authoritative IsProcessing to ALL sessions (bypasses guards) - Clears stuck streaming guards for sessions the server reports as idle Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1 parent f9d0d96 commit 887629c

2 files changed

Lines changed: 49 additions & 0 deletions

File tree

PolyPilot.Tests/BridgeDisconnectTests.cs

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -348,4 +348,32 @@ public async Task SyncRemoteSessions_AllowsSessionsListToClearProcessing()
348348

349349
Assert.False(session.IsProcessing);
350350
}
351+
352+
[Fact]
353+
public async Task ForceSync_ClearsIsProcessing_EvenWithStreamingGuard()
354+
{
355+
// Scenario: Streaming guard is stuck (TurnStart received but TurnEnd lost).
356+
// SyncRemoteSessions skips the session. But ForceRefreshRemoteAsync should
357+
// always apply the server's authoritative IsProcessing state.
358+
var svc = CreateRemoteService();
359+
await AddRemoteSession(svc, "stuck-session");
360+
var session = svc.GetSession("stuck-session")!;
361+
362+
// Session appears processing with a stuck streaming guard
363+
session.IsProcessing = true;
364+
svc.SetRemoteStreamingGuardForTesting("stuck-session", true);
365+
366+
// SyncRemoteSessions should skip (streaming guard active)
367+
_bridgeClient.Sessions = new() { new SessionSummary { Name = "stuck-session", IsProcessing = false } };
368+
svc.SyncRemoteSessions();
369+
Assert.True(session.IsProcessing); // Guard blocks the update
370+
371+
// Force sync should override the streaming guard
372+
_bridgeClient.SessionHistories["stuck-session"] = new List<ChatMessage>();
373+
var result = await svc.ForceRefreshRemoteAsync("stuck-session");
374+
375+
Assert.True(result.Success);
376+
Assert.False(session.IsProcessing);
377+
Assert.False(svc.IsRemoteStreamingGuardActive("stuck-session"));
378+
}
351379
}

PolyPilot/Services/CopilotService.Bridge.cs

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -740,6 +740,27 @@ public async Task<SyncResult> ForceRefreshRemoteAsync(string? activeSessionName
740740
}
741741
}
742742

743+
// Force-sync processing state for ALL sessions from the server snapshot.
744+
// SyncRemoteSessions skips sessions in _remoteStreamingSessions, but a user-initiated
745+
// force sync should always apply the server's authoritative IsProcessing state.
746+
// Also clear stuck streaming guards — if the server says a session is idle,
747+
// any lingering guard from a dropped connection should be cleared.
748+
foreach (var rs in _bridgeClient.Sessions)
749+
{
750+
if (_sessions.TryGetValue(rs.Name, out var syncState))
751+
{
752+
if (syncState.Info.IsProcessing != rs.IsProcessing)
753+
Debug($"[SYNC] '{rs.Name}' IsProcessing {syncState.Info.IsProcessing} -> {rs.IsProcessing}");
754+
syncState.Info.IsProcessing = rs.IsProcessing;
755+
syncState.Info.ProcessingStartedAt = rs.ProcessingStartedAt;
756+
syncState.Info.ToolCallCount = rs.ToolCallCount;
757+
syncState.Info.ProcessingPhase = rs.ProcessingPhase;
758+
// Clear stuck streaming guard if server says session is idle
759+
if (!rs.IsProcessing)
760+
_remoteStreamingSessions.TryRemove(rs.Name, out _);
761+
}
762+
}
763+
743764
// Snapshot post-sync state
744765
var postSyncSessionCount = _sessions.Count;
745766
var postSyncMessageCount = 0;

0 commit comments

Comments
 (0)