fix(ios): harden chat stream acceptance and initial load

This commit is contained in:
Eulices Lopez 2026-03-18 09:08:33 -04:00
parent cb56c29fd3
commit 9c37ce5f9e
2 changed files with 50 additions and 3 deletions

View File

@ -42,6 +42,7 @@ public final class OpenClawChatViewModel {
@ObservationIgnored
private nonisolated(unsafe) var eventTask: Task<Void, Never>?
private var hasLoadedInitialState = false
private var pendingRuns = Set<String>() {
didSet { self.pendingRunCount = self.pendingRuns.count }
}
@ -103,6 +104,8 @@ public final class OpenClawChatViewModel {
}
public func load() {
guard !self.hasLoadedInitialState else { return }
self.hasLoadedInitialState = true
Task { await self.bootstrap() }
}
@ -905,12 +908,12 @@ public final class OpenClawChatViewModel {
}
private func shouldAcceptAgentEvent(_ evt: OpenClawAgentEventPayload) -> Bool {
if let sessionKey = evt.sessionKey {
return Self.matchesCurrentSessionKey(incoming: sessionKey, current: self.sessionKey)
}
if self.pendingRuns.contains(evt.runId) {
return true
}
if let sessionKey = evt.sessionKey {
return Self.matchesCurrentSessionKey(incoming: sessionKey, current: self.sessionKey)
}
if let sessionId {
return evt.runId == sessionId
}

View File

@ -338,6 +338,10 @@ private final class TestChatTransport: @unchecked Sendable, OpenClawChatTranspor
return ids.last
}
func historyRequestCount() async -> Int {
await self.state.historyCallCount
}
func abortedRunIds() async -> [String] {
await self.state.abortedRunIds
}
@ -514,6 +518,46 @@ extension TestChatTransportState {
#expect(await MainActor.run { vm.streamingAssistantText } == nil)
}
@Test func acceptsPendingRunAgentEventsEvenWhenSessionKeyMismatches() async throws {
let history = historyPayload(sessionId: "sess-main")
let (transport, vm) = await makeViewModel(historyResponses: [history])
try await loadAndWaitBootstrap(vm: vm, sessionId: "sess-main")
await sendUserMessage(vm)
try await waitUntil("pending run starts") { await transport.lastSentRunId() != nil }
let runId = try #require(await transport.lastSentRunId())
emitAssistantText(
transport: transport,
runId: runId,
text: "own run still streams",
sessionKey: "agent:main:other")
try await waitUntil("assistant stream visible for own run despite key mismatch") {
await MainActor.run { vm.streamingAssistantText == "own run still streams" }
}
}
@Test func loadIsIdempotentDuringActiveStream() async throws {
let history = historyPayload(messages: [chatTextMessage(role: "user", text: "hello", timestamp: 1)])
let (transport, vm) = await makeViewModel(historyResponses: [history])
try await loadAndWaitBootstrap(vm: vm)
await sendUserMessage(vm)
try await waitUntil("pending run starts") { await transport.lastSentRunId() != nil }
let runId = try #require(await transport.lastSentRunId())
emitAssistantText(transport: transport, runId: runId, text: "stream survives load")
try await waitUntil("assistant stream visible") {
await MainActor.run { vm.streamingAssistantText == "stream survives load" }
}
await MainActor.run { vm.load() }
try? await Task.sleep(nanoseconds: 150_000_000)
#expect(await transport.historyRequestCount() == 1)
#expect(await MainActor.run { vm.streamingAssistantText } == "stream survives load")
#expect(await MainActor.run { vm.pendingRunCount } == 1)
}
@Test func acceptsCanonicalSessionKeyEventsForExternalRuns() async throws {
let now = Date().timeIntervalSince1970 * 1000
let history1 = historyPayload(messages: [chatTextMessage(role: "user", text: "first", timestamp: now)])