diff --git a/PolyPilot.Tests/VolatileFieldGuardTests.cs b/PolyPilot.Tests/VolatileFieldGuardTests.cs
new file mode 100644
index 0000000000..9593de28db
--- /dev/null
+++ b/PolyPilot.Tests/VolatileFieldGuardTests.cs
@@ -0,0 +1,37 @@
+using System.Reflection;
+using System.Runtime.CompilerServices;
+using PolyPilot.Services;
+
+namespace PolyPilot.Tests;
+
+///
+/// Guards that fields accessed from multiple threads are declared volatile.
+/// Prevents regressions where someone removes the volatile modifier.
+///
+public class VolatileFieldGuardTests
+{
+ [Fact]
+ public void ActiveSessionName_IsDeclaredVolatile_OnCopilotService()
+ {
+ // _activeSessionName is read by WsBridge background threads (SyncRemoteSessions),
+ // restore background threads, and written by UI thread (SetActiveSession, CloseSession).
+ // Must be volatile for cross-thread visibility on ARM (iOS/Android).
+ var field = typeof(CopilotService)
+ .GetField("_activeSessionName", BindingFlags.NonPublic | BindingFlags.Instance)!;
+ Assert.NotNull(field);
+ Assert.True(
+ field.GetRequiredCustomModifiers().Any(m => m == typeof(IsVolatile)),
+ "_activeSessionName must be declared volatile for cross-thread visibility");
+ }
+
+ [Fact]
+ public void ActiveSessionName_IsDeclaredVolatile_OnDemoService()
+ {
+ var field = typeof(DemoService)
+ .GetField("_activeSessionName", BindingFlags.NonPublic | BindingFlags.Instance)!;
+ Assert.NotNull(field);
+ Assert.True(
+ field.GetRequiredCustomModifiers().Any(m => m == typeof(IsVolatile)),
+ "DemoService._activeSessionName must be declared volatile for consistency");
+ }
+}
diff --git a/PolyPilot/Services/CopilotService.cs b/PolyPilot/Services/CopilotService.cs
index 9bdb631d78..ffaf8fc8b2 100644
--- a/PolyPilot/Services/CopilotService.cs
+++ b/PolyPilot/Services/CopilotService.cs
@@ -51,7 +51,7 @@ public partial class CopilotService : IAsyncDisposable
// Cached dotfiles status — checked once when first SetupRequired state is encountered
private CodespaceService.DotfilesStatus? _dotfilesStatus;
private ConnectionSettings? _currentSettings;
- private string? _activeSessionName;
+ private volatile string? _activeSessionName;
private SynchronizationContext? _syncContext;
// Serializes the IsConnectionError reconnect path so concurrent workers
// don't destroy each other's freshly-created client (thundering herd fix).
diff --git a/PolyPilot/Services/DemoService.cs b/PolyPilot/Services/DemoService.cs
index 2e4fa81d33..7958f5b73e 100644
--- a/PolyPilot/Services/DemoService.cs
+++ b/PolyPilot/Services/DemoService.cs
@@ -10,7 +10,7 @@ namespace PolyPilot.Services;
public class DemoService : IDemoService
{
private readonly ConcurrentDictionary _sessions = new();
- private string? _activeSessionName;
+ private volatile string? _activeSessionName;
private int _sessionCounter;
public event Action? OnStateChanged;