fix(ios): preserve repeated optimistic user messages on refresh
This commit is contained in:
parent
9ee9006af5
commit
09618f91df
@ -376,9 +376,16 @@ public final class OpenClawChatViewModel {
|
|||||||
guard !previous.isEmpty else { return incoming }
|
guard !previous.isEmpty else { return incoming }
|
||||||
guard !incoming.isEmpty else { return previous }
|
guard !incoming.isEmpty else { return previous }
|
||||||
|
|
||||||
|
func countKeys(_ keys: [String]) -> [String: Int] {
|
||||||
|
keys.reduce(into: [:]) { counts, key in
|
||||||
|
counts[key, default: 0] += 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
var reconciled = Self.reconcileMessageIDs(previous: previous, incoming: incoming)
|
var reconciled = Self.reconcileMessageIDs(previous: previous, incoming: incoming)
|
||||||
let incomingIdentityKeys = Set(reconciled.compactMap(Self.messageIdentityKey(for:)))
|
let incomingIdentityKeys = Set(reconciled.compactMap(Self.messageIdentityKey(for:)))
|
||||||
let incomingUserRefreshKeys = Set(reconciled.compactMap(Self.userRefreshIdentityKey(for:)))
|
var remainingIncomingUserRefreshCounts = countKeys(
|
||||||
|
reconciled.compactMap(Self.userRefreshIdentityKey(for:)))
|
||||||
|
|
||||||
var lastMatchedPreviousIndex: Int?
|
var lastMatchedPreviousIndex: Int?
|
||||||
for (index, message) in previous.enumerated() {
|
for (index, message) in previous.enumerated() {
|
||||||
@ -389,8 +396,10 @@ public final class OpenClawChatViewModel {
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
if let userKey = Self.userRefreshIdentityKey(for: message),
|
if let userKey = Self.userRefreshIdentityKey(for: message),
|
||||||
incomingUserRefreshKeys.contains(userKey)
|
let remaining = remainingIncomingUserRefreshCounts[userKey],
|
||||||
|
remaining > 0
|
||||||
{
|
{
|
||||||
|
remainingIncomingUserRefreshCounts[userKey] = remaining - 1
|
||||||
lastMatchedPreviousIndex = index
|
lastMatchedPreviousIndex = index
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -401,7 +410,12 @@ public final class OpenClawChatViewModel {
|
|||||||
.filter { message in
|
.filter { message in
|
||||||
guard message.role.lowercased() == "user" else { return false }
|
guard message.role.lowercased() == "user" else { return false }
|
||||||
guard let key = Self.userRefreshIdentityKey(for: message) else { return false }
|
guard let key = Self.userRefreshIdentityKey(for: message) else { return false }
|
||||||
return !incomingUserRefreshKeys.contains(key)
|
let remaining = remainingIncomingUserRefreshCounts[key] ?? 0
|
||||||
|
if remaining > 0 {
|
||||||
|
remainingIncomingUserRefreshCounts[key] = remaining - 1
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
guard !trailingUserMessages.isEmpty else {
|
guard !trailingUserMessages.isEmpty else {
|
||||||
|
|||||||
@ -553,6 +553,49 @@ extension TestChatTransportState {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test func preservesRepeatedOptimisticUserMessagesWithIdenticalContentDuringRefresh() async throws {
|
||||||
|
let sessionId = "sess-main"
|
||||||
|
let now = Date().timeIntervalSince1970 * 1000
|
||||||
|
let history1 = historyPayload(sessionId: sessionId)
|
||||||
|
let history2 = historyPayload(
|
||||||
|
sessionId: sessionId,
|
||||||
|
messages: [
|
||||||
|
chatTextMessage(
|
||||||
|
role: "user",
|
||||||
|
text: "retry",
|
||||||
|
timestamp: now + 5_000),
|
||||||
|
chatTextMessage(
|
||||||
|
role: "assistant",
|
||||||
|
text: "first answer",
|
||||||
|
timestamp: now + 6_000),
|
||||||
|
])
|
||||||
|
|
||||||
|
let (transport, vm) = await makeViewModel(historyResponses: [history1, history2, history2])
|
||||||
|
try await loadAndWaitBootstrap(vm: vm, sessionId: sessionId)
|
||||||
|
try await sendMessageAndEmitFinal(
|
||||||
|
transport: transport,
|
||||||
|
vm: vm,
|
||||||
|
text: "retry")
|
||||||
|
try await sendMessageAndEmitFinal(
|
||||||
|
transport: transport,
|
||||||
|
vm: vm,
|
||||||
|
text: "retry")
|
||||||
|
|
||||||
|
try await waitUntil("repeated optimistic user message is preserved") {
|
||||||
|
await MainActor.run {
|
||||||
|
let retryMessages = vm.messages.filter { message in
|
||||||
|
message.role == "user" &&
|
||||||
|
message.content.compactMap(\.text).joined(separator: "\n") == "retry"
|
||||||
|
}
|
||||||
|
let hasAssistant = vm.messages.contains { message in
|
||||||
|
message.role == "assistant" &&
|
||||||
|
message.content.compactMap(\.text).joined(separator: "\n") == "first answer"
|
||||||
|
}
|
||||||
|
return hasAssistant && retryMessages.count == 2
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Test func acceptsCanonicalSessionKeyEventsForOwnPendingRun() async throws {
|
@Test func acceptsCanonicalSessionKeyEventsForOwnPendingRun() async throws {
|
||||||
let history1 = historyPayload()
|
let history1 = historyPayload()
|
||||||
let history2 = historyPayload(
|
let history2 = historyPayload(
|
||||||
|
|||||||
@ -5,7 +5,7 @@ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
|||||||
import type {
|
import type {
|
||||||
OpenClawPluginCommandDefinition,
|
OpenClawPluginCommandDefinition,
|
||||||
PluginCommandContext,
|
PluginCommandContext,
|
||||||
} from "../../src/plugins/types.js";
|
} from "openclaw/plugin-sdk/core";
|
||||||
import { createTestPluginApi } from "../../test/helpers/extensions/plugin-api.js";
|
import { createTestPluginApi } from "../../test/helpers/extensions/plugin-api.js";
|
||||||
import type { OpenClawPluginApi } from "./api.js";
|
import type { OpenClawPluginApi } from "./api.js";
|
||||||
|
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user