# SuiteLink Runtime Reconnect Implementation Plan > **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task. **Goal:** Add a background receive loop with automatic reconnect and subscription replay so the client continues dispatching updates after transport/session failures. **Architecture:** The implementation extends `SuiteLinkClient` with a supervised runtime loop and reconnect flow while keeping durable subscription intent separate from ephemeral session mappings. Recovery rebuilds transport/session state, replays subscriptions, and resumes update dispatch without caller polling. **Tech Stack:** .NET 10, C#, xUnit, `SemaphoreSlim`, `CancellationTokenSource`, existing SuiteLink codec/session/transport layers --- ### Task 1: Add Durable Subscription Registry **Files:** - Create: `/Users/dohertj2/Desktop/suitelinkclient/src/SuiteLink.Client/Internal/SubscriptionRegistrationEntry.cs` - Modify: `/Users/dohertj2/Desktop/suitelinkclient/src/SuiteLink.Client/SuiteLinkClient.cs` - Test: `/Users/dohertj2/Desktop/suitelinkclient/tests/SuiteLink.Client.Tests/SuiteLinkClientSubscriptionRegistryTests.cs` **Step 1: Write the failing test** ```csharp [Fact] public async Task SubscribeAsync_StoresDurableSubscriptionIntent() { var client = TestClientFactory.CreateReadyClient(); await client.SubscribeAsync("Pump001.Run", _ => { }); Assert.True(client.HasSubscription("Pump001.Run")); } ``` **Step 2: Run test to verify it fails** Run: `dotnet test /Users/dohertj2/Desktop/suitelinkclient/SuiteLink.Client.slnx --filter SuiteLinkClientSubscriptionRegistryTests -v minimal` Expected: FAIL with missing durable registry behavior **Step 3: Write minimal implementation** Add a durable registry entry model storing: - `ItemName` - callback - requested tag id Store these entries in `SuiteLinkClient` separately from `SuiteLinkSession`. **Step 4: Run test to verify it passes** Run: `dotnet test /Users/dohertj2/Desktop/suitelinkclient/SuiteLink.Client.slnx --filter SuiteLinkClientSubscriptionRegistryTests -v minimal` Expected: PASS **Step 5: Commit** ```bash git add /Users/dohertj2/Desktop/suitelinkclient/src/SuiteLink.Client/Internal/SubscriptionRegistrationEntry.cs /Users/dohertj2/Desktop/suitelinkclient/src/SuiteLink.Client/SuiteLinkClient.cs /Users/dohertj2/Desktop/suitelinkclient/tests/SuiteLink.Client.Tests/SuiteLinkClientSubscriptionRegistryTests.cs git commit -m "feat: add durable subscription registry" ``` ### Task 2: Make Subscription Handles Remove Durable Intent **Files:** - Modify: `/Users/dohertj2/Desktop/suitelinkclient/src/SuiteLink.Client/SuiteLinkClient.cs` - Modify: `/Users/dohertj2/Desktop/suitelinkclient/src/SuiteLink.Client/SubscriptionHandle.cs` - Test: `/Users/dohertj2/Desktop/suitelinkclient/tests/SuiteLink.Client.Tests/SuiteLinkClientSubscriptionRegistryTests.cs` **Step 1: Write the failing test** ```csharp [Fact] public async Task DisposingSubscription_RemovesDurableSubscriptionIntent() { var client = TestClientFactory.CreateReadyClient(); var handle = await client.SubscribeAsync("Pump001.Run", _ => { }); await handle.DisposeAsync(); Assert.False(client.HasSubscription("Pump001.Run")); } ``` **Step 2: Run test to verify it fails** Run: `dotnet test /Users/dohertj2/Desktop/suitelinkclient/SuiteLink.Client.slnx --filter DisposingSubscription_RemovesDurableSubscriptionIntent -v minimal` Expected: FAIL **Step 3: Write minimal implementation** Ensure handle disposal removes durable registry entries even when wire unadvise cannot be sent. **Step 4: Run test to verify it passes** Run: `dotnet test /Users/dohertj2/Desktop/suitelinkclient/SuiteLink.Client.slnx --filter SuiteLinkClientSubscriptionRegistryTests -v minimal` Expected: PASS **Step 5: Commit** ```bash git add /Users/dohertj2/Desktop/suitelinkclient/src/SuiteLink.Client/SuiteLinkClient.cs /Users/dohertj2/Desktop/suitelinkclient/src/SuiteLink.Client/SubscriptionHandle.cs /Users/dohertj2/Desktop/suitelinkclient/tests/SuiteLink.Client.Tests/SuiteLinkClientSubscriptionRegistryTests.cs git commit -m "feat: persist subscription intent across reconnects" ``` ### Task 3: Add Runtime State For Background Loop **Files:** - Modify: `/Users/dohertj2/Desktop/suitelinkclient/src/SuiteLink.Client/Internal/SuiteLinkSessionState.cs` - Modify: `/Users/dohertj2/Desktop/suitelinkclient/src/SuiteLink.Client/SuiteLinkClient.cs` - Test: `/Users/dohertj2/Desktop/suitelinkclient/tests/SuiteLink.Client.Tests/SuiteLinkClientConnectionTests.cs` **Step 1: Write the failing test** ```csharp [Fact] public async Task ConnectAsync_TransitionsToReadyOnlyAfterRuntimeStarts() { var client = TestClientFactory.CreateReadyHandshakeClient(); await client.ConnectAsync(TestOptions.Create()); Assert.True(client.IsConnected); } ``` **Step 2: Run test to verify it fails** Run: `dotnet test /Users/dohertj2/Desktop/suitelinkclient/SuiteLink.Client.slnx --filter ConnectAsync_TransitionsToReadyOnlyAfterRuntimeStarts -v minimal` Expected: FAIL with missing ready/runtime state **Step 3: Write minimal implementation** Add: - `Ready` - `Reconnecting` and transition `ConnectAsync` into `Ready` when the runtime loop has been established. **Step 4: Run test to verify it passes** Run: `dotnet test /Users/dohertj2/Desktop/suitelinkclient/SuiteLink.Client.slnx --filter SuiteLinkClientConnectionTests -v minimal` Expected: PASS **Step 5: Commit** ```bash git add /Users/dohertj2/Desktop/suitelinkclient/src/SuiteLink.Client/Internal/SuiteLinkSessionState.cs /Users/dohertj2/Desktop/suitelinkclient/src/SuiteLink.Client/SuiteLinkClient.cs /Users/dohertj2/Desktop/suitelinkclient/tests/SuiteLink.Client.Tests/SuiteLinkClientConnectionTests.cs git commit -m "feat: add ready and reconnecting runtime states" ``` ### Task 4: Start Background Receive Loop **Files:** - Modify: `/Users/dohertj2/Desktop/suitelinkclient/src/SuiteLink.Client/SuiteLinkClient.cs` - Test: `/Users/dohertj2/Desktop/suitelinkclient/tests/SuiteLink.Client.Tests/SuiteLinkClientRuntimeLoopTests.cs` **Step 1: Write the failing test** ```csharp [Fact] public async Task ConnectAsync_StartsBackgroundLoop_AndDispatchesUpdateWithoutManualPolling() { var updateReceived = new TaskCompletionSource(); var client = TestClientFactory.CreateClientWithQueuedUpdate(updateReceived); await client.ConnectAsync(TestOptions.Create()); await client.SubscribeAsync("Pump001.Run", update => updateReceived.TrySetResult(update)); var update = await updateReceived.Task.WaitAsync(TimeSpan.FromSeconds(1)); Assert.True(update.Value.TryGetBoolean(out var value) && value); } ``` **Step 2: Run test to verify it fails** Run: `dotnet test /Users/dohertj2/Desktop/suitelinkclient/SuiteLink.Client.slnx --filter SuiteLinkClientRuntimeLoopTests -v minimal` Expected: FAIL because manual processing is still required **Step 3: Write minimal implementation** Start a long-lived receive loop task after initial connect, and dispatch updates through existing session logic. **Step 4: Run test to verify it passes** Run: `dotnet test /Users/dohertj2/Desktop/suitelinkclient/SuiteLink.Client.slnx --filter SuiteLinkClientRuntimeLoopTests -v minimal` Expected: PASS **Step 5: Commit** ```bash git add /Users/dohertj2/Desktop/suitelinkclient/src/SuiteLink.Client/SuiteLinkClient.cs /Users/dohertj2/Desktop/suitelinkclient/tests/SuiteLink.Client.Tests/SuiteLinkClientRuntimeLoopTests.cs git commit -m "feat: add suitelink background receive loop" ``` ### Task 5: Make ProcessIncomingAsync Internal Or Non-Primary **Files:** - Modify: `/Users/dohertj2/Desktop/suitelinkclient/src/SuiteLink.Client/SuiteLinkClient.cs` - Modify: `/Users/dohertj2/Desktop/suitelinkclient/README.md` - Test: `/Users/dohertj2/Desktop/suitelinkclient/tests/SuiteLink.Client.Tests/SuiteLinkClientRuntimeLoopTests.cs` **Step 1: Write the failing documentation/runtime check** Define the intended runtime contract: - normal operation uses background receive - manual polling is not required for normal subscriptions **Step 2: Run targeted tests** Run: `dotnet test /Users/dohertj2/Desktop/suitelinkclient/SuiteLink.Client.slnx --filter SuiteLinkClientRuntimeLoopTests -v minimal` Expected: PASS after Task 4 **Step 3: Write minimal implementation** Keep `ProcessIncomingAsync` only as an internal/test helper or document it as non-primary API. **Step 4: Run test and docs verification** Run: `rg -n "background receive|manual polling|ProcessIncomingAsync" /Users/dohertj2/Desktop/suitelinkclient/README.md` Expected: PASS with updated wording **Step 5: Commit** ```bash git add /Users/dohertj2/Desktop/suitelinkclient/src/SuiteLink.Client/SuiteLinkClient.cs /Users/dohertj2/Desktop/suitelinkclient/README.md git commit -m "docs: describe background runtime model" ``` ### Task 6: Detect EOF And Trigger Reconnect **Files:** - Modify: `/Users/dohertj2/Desktop/suitelinkclient/src/SuiteLink.Client/SuiteLinkClient.cs` - Test: `/Users/dohertj2/Desktop/suitelinkclient/tests/SuiteLink.Client.Tests/SuiteLinkClientReconnectTests.cs` **Step 1: Write the failing test** ```csharp [Fact] public async Task ReceiveLoop_Eof_TransitionsToReconnecting() { var client = TestClientFactory.CreateClientThatEofsAfterConnect(); await client.ConnectAsync(TestOptions.Create()); await Eventually.AssertAsync(() => Assert.Equal(SuiteLinkSessionState.Reconnecting, client.DebugState)); } ``` **Step 2: Run test to verify it fails** Run: `dotnet test /Users/dohertj2/Desktop/suitelinkclient/SuiteLink.Client.slnx --filter SuiteLinkClientReconnectTests -v minimal` Expected: FAIL **Step 3: Write minimal implementation** Treat `ReceiveAsync == 0` as a disconnect trigger and start recovery. **Step 4: Run test to verify it passes** Run: `dotnet test /Users/dohertj2/Desktop/suitelinkclient/SuiteLink.Client.slnx --filter SuiteLinkClientReconnectTests -v minimal` Expected: PASS **Step 5: Commit** ```bash git add /Users/dohertj2/Desktop/suitelinkclient/src/SuiteLink.Client/SuiteLinkClient.cs /Users/dohertj2/Desktop/suitelinkclient/tests/SuiteLink.Client.Tests/SuiteLinkClientReconnectTests.cs git commit -m "feat: detect disconnects and enter reconnect state" ``` ### Task 7: Add Bounded Reconnect Backoff **Files:** - Modify: `/Users/dohertj2/Desktop/suitelinkclient/src/SuiteLink.Client/SuiteLinkClient.cs` - Test: `/Users/dohertj2/Desktop/suitelinkclient/tests/SuiteLink.Client.Tests/SuiteLinkClientReconnectTests.cs` **Step 1: Write the failing test** ```csharp [Fact] public async Task Reconnect_UsesBoundedRetrySchedule() { var delays = new List(); var client = TestClientFactory.CreateReconnectTestClient(delays); await client.ConnectAsync(TestOptions.Create()); Assert.Contains(TimeSpan.FromSeconds(1), delays); } ``` **Step 2: Run test to verify it fails** Run: `dotnet test /Users/dohertj2/Desktop/suitelinkclient/SuiteLink.Client.slnx --filter Reconnect_UsesBoundedRetrySchedule -v minimal` Expected: FAIL **Step 3: Write minimal implementation** Add a small capped delay schedule: - 0s - 1s - 2s - 5s - 10s capped Inject delay behavior for tests if needed. **Step 4: Run test to verify it passes** Run: `dotnet test /Users/dohertj2/Desktop/suitelinkclient/SuiteLink.Client.slnx --filter SuiteLinkClientReconnectTests -v minimal` Expected: PASS **Step 5: Commit** ```bash git add /Users/dohertj2/Desktop/suitelinkclient/src/SuiteLink.Client/SuiteLinkClient.cs /Users/dohertj2/Desktop/suitelinkclient/tests/SuiteLink.Client.Tests/SuiteLinkClientReconnectTests.cs git commit -m "feat: add bounded reconnect backoff" ``` ### Task 8: Replay Subscriptions After Reconnect **Files:** - Modify: `/Users/dohertj2/Desktop/suitelinkclient/src/SuiteLink.Client/SuiteLinkClient.cs` - Modify: `/Users/dohertj2/Desktop/suitelinkclient/src/SuiteLink.Client/Internal/SuiteLinkSession.cs` - Test: `/Users/dohertj2/Desktop/suitelinkclient/tests/SuiteLink.Client.Tests/SuiteLinkClientReconnectTests.cs` **Step 1: Write the failing test** ```csharp [Fact] public async Task Reconnect_ReplaysSubscriptions_AndRestoresDispatch() { var callbackCount = 0; var client = TestClientFactory.CreateReconnectReplayClient(() => callbackCount++); await client.ConnectAsync(TestOptions.Create()); await client.SubscribeAsync("Pump001.Run", _ => callbackCount++); await client.WaitForReconnectReadyAsync(); Assert.True(callbackCount > 0); } ``` **Step 2: Run test to verify it fails** Run: `dotnet test /Users/dohertj2/Desktop/suitelinkclient/SuiteLink.Client.slnx --filter Reconnect_ReplaysSubscriptions_AndRestoresDispatch -v minimal` Expected: FAIL **Step 3: Write minimal implementation** On successful reconnect: - reset live session mappings - replay all durable subscriptions one-by-one - rebuild tag mappings from fresh ACKs - return to `Ready` **Step 4: Run test to verify it passes** Run: `dotnet test /Users/dohertj2/Desktop/suitelinkclient/SuiteLink.Client.slnx --filter SuiteLinkClientReconnectTests -v minimal` Expected: PASS **Step 5: Commit** ```bash git add /Users/dohertj2/Desktop/suitelinkclient/src/SuiteLink.Client/SuiteLinkClient.cs /Users/dohertj2/Desktop/suitelinkclient/src/SuiteLink.Client/Internal/SuiteLinkSession.cs /Users/dohertj2/Desktop/suitelinkclient/tests/SuiteLink.Client.Tests/SuiteLinkClientReconnectTests.cs git commit -m "feat: replay subscriptions after reconnect" ``` ### Task 9: Reject Writes During Reconnect **Files:** - Modify: `/Users/dohertj2/Desktop/suitelinkclient/src/SuiteLink.Client/SuiteLinkClient.cs` - Test: `/Users/dohertj2/Desktop/suitelinkclient/tests/SuiteLink.Client.Tests/SuiteLinkClientWriteTests.cs` **Step 1: Write the failing test** ```csharp [Fact] public async Task WriteAsync_DuringReconnect_ThrowsClearException() { var client = TestClientFactory.CreateReconnectingClient(); await Assert.ThrowsAsync(() => client.WriteAsync("Pump001.Run", SuiteLinkValue.FromBoolean(true))); } ``` **Step 2: Run test to verify it fails** Run: `dotnet test /Users/dohertj2/Desktop/suitelinkclient/SuiteLink.Client.slnx --filter WriteAsync_DuringReconnect_ThrowsClearException -v minimal` Expected: FAIL **Step 3: Write minimal implementation** Guard `WriteAsync` so it succeeds only in `Ready`. **Step 4: Run test to verify it passes** Run: `dotnet test /Users/dohertj2/Desktop/suitelinkclient/SuiteLink.Client.slnx --filter SuiteLinkClientWriteTests -v minimal` Expected: PASS **Step 5: Commit** ```bash git add /Users/dohertj2/Desktop/suitelinkclient/src/SuiteLink.Client/SuiteLinkClient.cs /Users/dohertj2/Desktop/suitelinkclient/tests/SuiteLink.Client.Tests/SuiteLinkClientWriteTests.cs git commit -m "feat: reject writes while reconnecting" ``` ### Task 10: Stop Runtime Cleanly On Disconnect **Files:** - Modify: `/Users/dohertj2/Desktop/suitelinkclient/src/SuiteLink.Client/SuiteLinkClient.cs` - Test: `/Users/dohertj2/Desktop/suitelinkclient/tests/SuiteLink.Client.Tests/SuiteLinkClientConnectionTests.cs` **Step 1: Write the failing test** ```csharp [Fact] public async Task DisconnectAsync_StopsReceiveAndReconnectLoops() { var client = TestClientFactory.CreateRunningClient(); await client.ConnectAsync(TestOptions.Create()); await client.DisconnectAsync(); Assert.False(client.IsConnected); } ``` **Step 2: Run test to verify it fails** Run: `dotnet test /Users/dohertj2/Desktop/suitelinkclient/SuiteLink.Client.slnx --filter DisconnectAsync_StopsReceiveAndReconnectLoops -v minimal` Expected: FAIL **Step 3: Write minimal implementation** Cancel runtime loop tokens and stop reconnect attempts on disconnect/dispose. **Step 4: Run test to verify it passes** Run: `dotnet test /Users/dohertj2/Desktop/suitelinkclient/SuiteLink.Client.slnx --filter SuiteLinkClientConnectionTests -v minimal` Expected: PASS **Step 5: Commit** ```bash git add /Users/dohertj2/Desktop/suitelinkclient/src/SuiteLink.Client/SuiteLinkClient.cs /Users/dohertj2/Desktop/suitelinkclient/tests/SuiteLink.Client.Tests/SuiteLinkClientConnectionTests.cs git commit -m "feat: stop runtime loops on disconnect" ``` ### Task 11: Update README And Integration Docs **Files:** - Modify: `/Users/dohertj2/Desktop/suitelinkclient/README.md` - Modify: `/Users/dohertj2/Desktop/suitelinkclient/tests/SuiteLink.Client.IntegrationTests/README.md` **Step 1: Write the failing documentation check** Define required README terms: - background receive loop - automatic reconnect - subscription replay - writes rejected during reconnect **Step 2: Run documentation review** Run: `rg -n "background receive|automatic reconnect|subscription replay|reconnecting" /Users/dohertj2/Desktop/suitelinkclient/README.md /Users/dohertj2/Desktop/suitelinkclient/tests/SuiteLink.Client.IntegrationTests/README.md` Expected: FAIL until docs are updated **Step 3: Write minimal implementation** Update docs to describe the runtime model and recovery behavior honestly. **Step 4: Run documentation review** Run: `rg -n "background receive|automatic reconnect|subscription replay|reconnecting" /Users/dohertj2/Desktop/suitelinkclient/README.md /Users/dohertj2/Desktop/suitelinkclient/tests/SuiteLink.Client.IntegrationTests/README.md` Expected: PASS **Step 5: Commit** ```bash git add /Users/dohertj2/Desktop/suitelinkclient/README.md /Users/dohertj2/Desktop/suitelinkclient/tests/SuiteLink.Client.IntegrationTests/README.md git commit -m "docs: describe runtime reconnect behavior" ``` ### Task 12: Full Verification Pass **Files:** - Modify: `/Users/dohertj2/Desktop/suitelinkclient/docs/plans/2026-03-17-runtime-reconnect-design.md` - Modify: `/Users/dohertj2/Desktop/suitelinkclient/docs/plans/2026-03-17-runtime-reconnect-implementation-plan.md` **Step 1: Run full test suite** Run: `dotnet test /Users/dohertj2/Desktop/suitelinkclient/SuiteLink.Client.slnx -v minimal` Expected: PASS with integration harness still conditional by default **Step 2: Run release build** Run: `dotnet build /Users/dohertj2/Desktop/suitelinkclient/SuiteLink.Client.slnx -c Release` Expected: PASS **Step 3: Run reconnect-focused tests** Run: `dotnet test /Users/dohertj2/Desktop/suitelinkclient/SuiteLink.Client.slnx --filter Reconnect -v minimal` Expected: PASS **Step 4: Update plan notes if implementation deviated** Add short notes to the design/plan docs if final runtime behavior differs from original assumptions. **Step 5: Commit** ```bash git add /Users/dohertj2/Desktop/suitelinkclient/docs/plans/2026-03-17-runtime-reconnect-design.md /Users/dohertj2/Desktop/suitelinkclient/docs/plans/2026-03-17-runtime-reconnect-implementation-plan.md git commit -m "docs: finalize reconnect implementation verification" ```