Skip to content

Commit 8ca14a9

Browse files
mcintyre94claude
andcommitted
Skip polling loop in reconnectToServiceLogs when service already stopped
reconnectIfNeeded now captures the service status it fetches rather than discarding it. If the service is already stopped, we pass serviceAlreadyStopped to reconnectToServiceLogs so it exits after a single log fetch instead of making a redundant status API call and looping. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent cf805f7 commit 8ca14a9

File tree

2 files changed

+16
-14
lines changed

2 files changed

+16
-14
lines changed

Wisp/ViewModels/ChatViewModel.swift

Lines changed: 13 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -594,7 +594,7 @@ final class ChatViewModel {
594594
// Cancel the stale stream and reconnect via service logs
595595
streamTask?.cancel()
596596
streamTask = Task {
597-
await reconnectToServiceLogs(apiClient: apiClient, modelContext: modelContext, serviceIsRunning: true)
597+
await reconnectToServiceLogs(apiClient: apiClient, modelContext: modelContext, serviceAlreadyStopped: false)
598598
}
599599
}
600600

@@ -650,8 +650,8 @@ final class ChatViewModel {
650650
guard let serviceInfo = try? await apiClient.getServiceStatus(spriteName: spriteName, serviceName: serviceName)
651651
else { return }
652652

653-
let isRunning = serviceInfo.state.status == "running"
654-
await reconnectToServiceLogs(apiClient: apiClient, modelContext: modelContext, serviceIsRunning: isRunning)
653+
let alreadyStopped = serviceInfo.state.status != "running"
654+
await reconnectToServiceLogs(apiClient: apiClient, modelContext: modelContext, serviceAlreadyStopped: alreadyStopped)
655655
}
656656
}
657657

@@ -796,7 +796,7 @@ final class ChatViewModel {
796796
// Attempt reconnection on disconnect
797797
if case .disconnected = streamResult {
798798
logger.info("[Chat] Disconnected mid-stream, attempting reconnect via service logs")
799-
await reconnectToServiceLogs(apiClient: apiClient, modelContext: modelContext, serviceIsRunning: true)
799+
await reconnectToServiceLogs(apiClient: apiClient, modelContext: modelContext, serviceAlreadyStopped: false)
800800
return
801801
}
802802

@@ -1041,10 +1041,10 @@ final class ChatViewModel {
10411041
func runReconnectLoop(
10421042
apiClient: some ServiceLogsProvider,
10431043
modelContext: ModelContext,
1044-
serviceIsRunning: Bool = false
1044+
serviceAlreadyStopped: Bool = false
10451045
) async {
10461046
isReplaying = true
1047-
isReplayingLiveService = serviceIsRunning
1047+
isReplayingLiveService = !serviceAlreadyStopped
10481048
defer {
10491049
applyReplayBuffer()
10501050
isReplaying = false
@@ -1111,10 +1111,12 @@ final class ChatViewModel {
11111111
// If we got a result event, Claude is done
11121112
if receivedResultEvent { break }
11131113

1114-
// Check if service is still running
1115-
let isRunning = (try? await apiClient.getServiceStatus(spriteName: spriteName, serviceName: serviceName))?.state.status == "running"
1114+
// If we already knew the service was stopped before entering, no need to re-check
1115+
if serviceAlreadyStopped { break }
11161116

1117-
if isRunning {
1117+
// Check if service is still running before retrying
1118+
if let serviceInfo = try? await apiClient.getServiceStatus(spriteName: spriteName, serviceName: serviceName),
1119+
serviceInfo.state.status == "running" {
11181120
logger.info("[Chat] Service still running, will re-poll after delay")
11191121
try? await Task.sleep(for: .seconds(2))
11201122
continue
@@ -1150,9 +1152,9 @@ final class ChatViewModel {
11501152
private func reconnectToServiceLogs(
11511153
apiClient: SpritesAPIClient,
11521154
modelContext: ModelContext,
1153-
serviceIsRunning: Bool = false
1155+
serviceAlreadyStopped: Bool = false
11541156
) async {
1155-
await runReconnectLoop(apiClient: apiClient, modelContext: modelContext, serviceIsRunning: serviceIsRunning)
1157+
await runReconnectLoop(apiClient: apiClient, modelContext: modelContext, serviceAlreadyStopped: serviceAlreadyStopped)
11561158

11571159
if let queued = queuedPrompt, !Task.isCancelled {
11581160
let prompt = buildPrompt(text: queued, attachments: queuedAttachments)

WispTests/ChatViewModelTests.swift

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1046,7 +1046,7 @@ struct ChatViewModelTests {
10461046
// MARK: - Live service reconnect
10471047

10481048
@Test func runReconnectLoop_liveService_appliesContentCorrectly() async throws {
1049-
// When serviceIsRunning is true, isReplaying is cleared on the first new event
1049+
// When serviceAlreadyStopped is false (service is live), isReplaying is cleared on the first new event
10501050
// so events render incrementally rather than being batched. The final content
10511051
// should be identical to the batched path.
10521052
let ctx = try makeModelContext()
@@ -1068,7 +1068,7 @@ struct ChatViewModelTests {
10681068
}
10691069
let mock = MockServiceLogsProvider(streams: [stream], statuses: ["stopped"])
10701070

1071-
await vm.runReconnectLoop(apiClient: mock, modelContext: ctx, serviceIsRunning: true)
1071+
await vm.runReconnectLoop(apiClient: mock, modelContext: ctx, serviceAlreadyStopped: false)
10721072

10731073
#expect(assistantMsg.content.count == 1)
10741074
if case .text(let text) = assistantMsg.content.first {
@@ -1106,7 +1106,7 @@ struct ChatViewModelTests {
11061106
// The text and result events are new and should be processed via the live path.
11071107
vm.processedEventUUIDs = ["s1-system"]
11081108

1109-
await vm.runReconnectLoop(apiClient: mock, modelContext: ctx, serviceIsRunning: true)
1109+
await vm.runReconnectLoop(apiClient: mock, modelContext: ctx, serviceAlreadyStopped: false)
11101110

11111111
// New content lands even though a historical event was skipped first
11121112
#expect(assistantMsg.content.count == 1)

0 commit comments

Comments
 (0)