diff --git a/apps/shared/OpenClawKit/Sources/OpenClawChatUI/ChatViewModel.swift b/apps/shared/OpenClawKit/Sources/OpenClawChatUI/ChatViewModel.swift
index ca49f078d5..4dc8b9d8b1 100644
--- a/apps/shared/OpenClawKit/Sources/OpenClawChatUI/ChatViewModel.swift
+++ b/apps/shared/OpenClawKit/Sources/OpenClawChatUI/ChatViewModel.swift
@@ -190,7 +190,7 @@ public final class OpenClawChatViewModel {
let decoded = raw.compactMap { item in
(try? ChatPayloadDecoding.decode(item, as: OpenClawChatMessage.self))
}
- return Self.filterHeartbeatNoise(Self.dedupeMessages(decoded))
+ return Self.dedupeMessages(decoded)
}
private static func messageIdentityKey(for message: OpenClawChatMessage) -> String? {
@@ -275,56 +275,6 @@ public final class OpenClawChatViewModel {
return result
}
- private static func filterHeartbeatNoise(_ messages: [OpenClawChatMessage]) -> [OpenClawChatMessage] {
- messages.filter { !Self.isHeartbeatNoiseMessage($0) }
- }
-
- private static func isHeartbeatNoiseMessage(_ message: OpenClawChatMessage) -> Bool {
- let role = message.role.trimmingCharacters(in: .whitespacesAndNewlines).lowercased()
- let text = message.content.compactMap(\.text).joined(separator: "\n")
- .trimmingCharacters(in: .whitespacesAndNewlines)
- guard !text.isEmpty else { return false }
-
- if role == "assistant", Self.isHeartbeatAckText(text) {
- return true
- }
- if role == "user", Self.isHeartbeatPollText(text) {
- return true
- }
- // Some models occasionally echo the heartbeat prompt text as an assistant reply.
- if role == "assistant", Self.isHeartbeatPollText(text) {
- return true
- }
- return false
- }
-
- private static func isHeartbeatPollText(_ text: String) -> Bool {
- let lower = text.trimmingCharacters(in: .whitespacesAndNewlines).lowercased()
- // Match the default heartbeat prompt without requiring the entire multi-sentence string.
- return lower.hasPrefix("read heartbeat.md if it exists")
- }
-
- private static func isHeartbeatAckText(_ text: String) -> Bool {
- // Heartbeat acks are intended to be internal. Treat common markup wrappers as equivalent.
- var t = text.trimmingCharacters(in: .whitespacesAndNewlines)
- if t.isEmpty { return false }
-
- // Strip a few common wrappers (markdown/HTML) so **HEARTBEAT_OK** or HEARTBEAT_OK still matches.
- let wrappers = ["**", "__", "`", "", "", "", ""]
- for w in wrappers {
- t = t.replacingOccurrences(of: w, with: "")
- }
- t = t.trimmingCharacters(in: .whitespacesAndNewlines)
-
- // Allow a tiny amount of padding (some channels append a marker/emoji).
- if t == "HEARTBEAT_OK" { return true }
- if t.hasPrefix("HEARTBEAT_OK") {
- let rest = t.dropFirst("HEARTBEAT_OK".count)
- return rest.trimmingCharacters(in: .whitespacesAndNewlines).count <= 10
- }
- return false
- }
-
private static func dedupeKey(for message: OpenClawChatMessage) -> String? {
guard let timestamp = message.timestamp else { return nil }
let text = message.content.compactMap(\.text).joined(separator: "\n")
@@ -334,23 +284,15 @@ public final class OpenClawChatViewModel {
}
private func performSend() async {
- if self.isSending {
- chatUILogger.info("performSend ignored: already sending")
- return
- }
+ guard !self.isSending else { return }
let trimmed = self.input.trimmingCharacters(in: .whitespacesAndNewlines)
- if trimmed.isEmpty && self.attachments.isEmpty {
- chatUILogger.info("performSend ignored: empty input and no attachments")
+ guard !trimmed.isEmpty || !self.attachments.isEmpty else { return }
+
+ guard self.healthOK else {
+ self.errorText = "Gateway health not OK; cannot send"
return
}
- // Health checks are best-effort. If they fail (or the gateway doesn't implement them),
- // we still attempt to send and let the RPC result determine success.
- if !self.healthOK {
- self.errorText = "Gateway health unknown; attempting send anyway"
- }
- chatUILogger.info("performSend sending len=\(trimmed.count, privacy: .public) attachments=\(self.attachments.count, privacy: .public) sessionKey=\(self.sessionKey, privacy: .public)")
-
self.isSending = true
self.errorText = nil
let runId = UUID().uuidString
diff --git a/apps/shared/OpenClawKit/Sources/OpenClawKit/GatewayChannel.swift b/apps/shared/OpenClawKit/Sources/OpenClawKit/GatewayChannel.swift
index 37f3de3142..a255fc7a81 100644
--- a/apps/shared/OpenClawKit/Sources/OpenClawKit/GatewayChannel.swift
+++ b/apps/shared/OpenClawKit/Sources/OpenClawKit/GatewayChannel.swift
@@ -399,40 +399,8 @@ public actor GatewayChannelActor {
role: String
) async throws {
if res.ok == false {
- let code = res.error?["code"]?.value as? String
- let msg = res.error?["message"]?.value as? String
- let requestId: String? = {
- guard let detailsAny = res.error?["details"]?.value else { return nil }
- if let dict = detailsAny as? [String: ProtoAnyCodable],
- let id = dict["requestId"]?.value as? String
- {
- return id
- }
- if let dict = detailsAny as? [String: Any],
- let id = dict["requestId"] as? String
- {
- return id
- }
- if let dict = detailsAny as? [AnyHashable: Any],
- let id = dict["requestId"] as? String
- {
- return id
- }
- return nil
- }()
- let details: [String: AnyCodable] = (res.error ?? [:]).reduce(into: [:]) { acc, pair in
- acc[pair.key] = AnyCodable(pair.value.value)
- }
- let decoratedMessage: String? = {
- guard code == "NOT_PAIRED", let requestId, !requestId.isEmpty else { return msg }
- let base = (msg ?? "gateway connect failed")
- return "\(base) (requestId: \(requestId))"
- }()
- throw GatewayResponseError(
- method: "connect",
- code: code,
- message: decoratedMessage ?? msg,
- details: details)
+ let msg = (res.error?["message"]?.value as? String) ?? "gateway connect failed"
+ throw NSError(domain: "Gateway", code: 1008, userInfo: [NSLocalizedDescriptionKey: msg])
}
guard let payload = res.payload else {
throw NSError(