18 KiB
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
[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
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
[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
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
[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:
ReadyReconnecting
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
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
[Fact]
public async Task ConnectAsync_StartsBackgroundLoop_AndDispatchesUpdateWithoutManualPolling()
{
var updateReceived = new TaskCompletionSource<SuiteLinkTagUpdate>();
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
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
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
[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
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
[Fact]
public async Task Reconnect_UsesBoundedRetrySchedule()
{
var delays = new List<TimeSpan>();
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
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
[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
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
[Fact]
public async Task WriteAsync_DuringReconnect_ThrowsClearException()
{
var client = TestClientFactory.CreateReconnectingClient();
await Assert.ThrowsAsync<InvalidOperationException>(() =>
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
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
[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
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
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
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"