fix: 3 Bugs — agent_activity haengt, Such-Scroll, STT-Bubble-Timing
Bug 1: "ARIA denkt..." in der App bleibt stehen
_process_core_response setzte am Ende kein idle — die alten Aufrufe waren
in der OpenClaw-WS-Loop, in der Brain-HTTP-Variante fehlten sie. Plus
send_to_core schickte agent_activity direkt via _send_to_rvs ohne den
_last_activity_state-Cache zu pflegen → _emit_activity("idle") wurde
spaeter dedupliziert.
Fix:
- _emit_activity statt direktem _send_to_rvs fuer thinking
- _emit_activity("idle") am Ende von _process_core_response
- _last_chat_final_at bewusst NICHT setzen — die 3s-Cooldown war fuer
trailing OpenClaw-Events, wuerde bei Voice die naechste thinking-Welle
unterdruecken
Bug 2: App Chat-Suche scrollt nicht zur Stelle
scrollToIndex wurde zu fruh aufgerufen (Layout noch nicht fertig) und
viewPosition: 0.4 in inverted-FlatList war ungenau.
Fix:
- requestAnimationFrame um den Scroll-Aufruf
- viewPosition: 0.5 (mittig)
- onScrollToIndexFailed: erst grob scrollen via averageItemLength,
dann nach 250ms praeziser nachfassen
Bug 3: Voice-Bubble bekommt STT-Text erst mit ARIA-Antwort
_process_app_audio rief erst send_to_core (blockt synchron auf Brain,
kann 300s dauern), DANN STT-Broadcast. App sah den eigenen Text erst
wenn ARIA fertig war.
Fix: Reihenfolge getauscht — STT-Broadcast zuerst, dann send_to_core.
Voice-Bubble bekommt jetzt den erkannten Text sofort.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -920,17 +920,25 @@ const ChatScreen: React.FC = () => {
|
|||||||
setSearchIndex(0);
|
setSearchIndex(0);
|
||||||
}, [searchQuery]);
|
}, [searchQuery]);
|
||||||
|
|
||||||
// Bei Index-Wechsel zu der entsprechenden Bubble scrollen
|
// Bei Index-Wechsel zu der entsprechenden Bubble scrollen.
|
||||||
|
// FlatList ist `inverted` → viewPosition 0.5 (mitte) ist beim inverted-Render
|
||||||
|
// tatsaechlich die Mitte des sichtbaren Bereichs. Wir verzoegern minimal
|
||||||
|
// damit Layout sicher fertig ist.
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!searchMatchIds.length) return;
|
if (!searchMatchIds.length) return;
|
||||||
const id = searchMatchIds[searchIndex];
|
const id = searchMatchIds[searchIndex];
|
||||||
if (!id) return;
|
if (!id) return;
|
||||||
// invertedMessages → index in der angezeigten Liste finden
|
|
||||||
const idx = invertedMessages.findIndex(m => m.id === id);
|
const idx = invertedMessages.findIndex(m => m.id === id);
|
||||||
if (idx < 0 || !flatListRef.current) return;
|
if (idx < 0 || !flatListRef.current) return;
|
||||||
|
const tryScroll = () => {
|
||||||
try {
|
try {
|
||||||
flatListRef.current.scrollToIndex({ index: idx, animated: true, viewPosition: 0.4 });
|
flatListRef.current?.scrollToIndex({ index: idx, animated: true, viewPosition: 0.5 });
|
||||||
} catch {}
|
} catch {
|
||||||
|
// wird von onScrollToIndexFailed nochmal versucht
|
||||||
|
}
|
||||||
|
};
|
||||||
|
// requestAnimationFrame statt setTimeout 0 — wartet auf naechsten Layout-Frame
|
||||||
|
requestAnimationFrame(tryScroll);
|
||||||
}, [searchIndex, searchMatchIds, invertedMessages]);
|
}, [searchIndex, searchMatchIds, invertedMessages]);
|
||||||
|
|
||||||
const activeSearchId = searchMatchIds[searchIndex] || '';
|
const activeSearchId = searchMatchIds[searchIndex] || '';
|
||||||
@@ -1440,10 +1448,14 @@ const ChatScreen: React.FC = () => {
|
|||||||
inverted
|
inverted
|
||||||
data={invertedMessages}
|
data={invertedMessages}
|
||||||
onScrollToIndexFailed={(info) => {
|
onScrollToIndexFailed={(info) => {
|
||||||
// Bei zu schnellem Aufruf vor Layout: einmal nachfassen
|
// FlatList kennt das Item-Layout noch nicht. Zuerst grob in die
|
||||||
|
// Naehe scrollen (Average-Item-Hoehe-Schaetzung), dann nach 250ms
|
||||||
|
// praezise nochmal versuchen.
|
||||||
|
const offset = info.averageItemLength * info.index;
|
||||||
|
try { flatListRef.current?.scrollToOffset({ offset, animated: false }); } catch {}
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
try { flatListRef.current?.scrollToIndex({ index: info.index, animated: true, viewPosition: 0.4 }); } catch {}
|
try { flatListRef.current?.scrollToIndex({ index: info.index, animated: true, viewPosition: 0.5 }); } catch {}
|
||||||
}, 200);
|
}, 250);
|
||||||
}}
|
}}
|
||||||
keyExtractor={item => item.id}
|
keyExtractor={item => item.id}
|
||||||
renderItem={renderMessage}
|
renderItem={renderMessage}
|
||||||
|
|||||||
+23
-28
@@ -1086,6 +1086,12 @@ class ARIABridge:
|
|||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error("[core] XTTS-Request fehlgeschlagen: %s — kein Audio", e)
|
logger.error("[core] XTTS-Request fehlgeschlagen: %s — kein Audio", e)
|
||||||
|
|
||||||
|
# ARIA ist fertig — App's "ARIA denkt..." Indicator zurueck auf idle.
|
||||||
|
# _last_chat_final_at bewusst NICHT setzen: die 3s-Cooldown war fuer
|
||||||
|
# trailing OpenClaw-Activity-Events; bei Voice-Chat wuerde sie die
|
||||||
|
# naechste thinking-Welle unterdruecken.
|
||||||
|
await self._emit_activity("idle", "")
|
||||||
|
|
||||||
# ── Mode Persistence (global, nicht pro Geraet) ──────
|
# ── Mode Persistence (global, nicht pro Geraet) ──────
|
||||||
_MODE_FILE = "/shared/config/mode.json"
|
_MODE_FILE = "/shared/config/mode.json"
|
||||||
|
|
||||||
@@ -1250,12 +1256,9 @@ class ARIABridge:
|
|||||||
# / Diagnostic-Reload als History-Quelle gelesen.
|
# / Diagnostic-Reload als History-Quelle gelesen.
|
||||||
self._append_chat_backup({"role": "user", "text": text, "source": source})
|
self._append_chat_backup({"role": "user", "text": text, "source": source})
|
||||||
|
|
||||||
# agent_activity broadcasten (App + Diagnostic "ARIA denkt..." Indicator)
|
# agent_activity → thinking. _emit_activity statt direktem _send_to_rvs
|
||||||
await self._send_to_rvs({
|
# damit der State-Cache fuer die spaetere idle-Dedup richtig steht.
|
||||||
"type": "agent_activity",
|
await self._emit_activity("thinking", "")
|
||||||
"payload": {"activity": "thinking"},
|
|
||||||
"timestamp": int(asyncio.get_event_loop().time() * 1000),
|
|
||||||
})
|
|
||||||
|
|
||||||
def _do_call():
|
def _do_call():
|
||||||
try:
|
try:
|
||||||
@@ -1272,11 +1275,7 @@ class ARIABridge:
|
|||||||
status, body = await asyncio.get_event_loop().run_in_executor(None, _do_call)
|
status, body = await asyncio.get_event_loop().run_in_executor(None, _do_call)
|
||||||
if status != 200:
|
if status != 200:
|
||||||
logger.error("[brain] /chat fehlgeschlagen: status=%s body=%s", status, body[:200])
|
logger.error("[brain] /chat fehlgeschlagen: status=%s body=%s", status, body[:200])
|
||||||
await self._send_to_rvs({
|
await self._emit_activity("idle", "")
|
||||||
"type": "agent_activity",
|
|
||||||
"payload": {"activity": "idle"},
|
|
||||||
"timestamp": int(asyncio.get_event_loop().time() * 1000),
|
|
||||||
})
|
|
||||||
await self._send_to_rvs({
|
await self._send_to_rvs({
|
||||||
"type": "chat",
|
"type": "chat",
|
||||||
"payload": {
|
"payload": {
|
||||||
@@ -1291,21 +1290,13 @@ class ARIABridge:
|
|||||||
data = json.loads(body)
|
data = json.loads(body)
|
||||||
except Exception:
|
except Exception:
|
||||||
logger.error("[brain] /chat lieferte ungueltiges JSON: %s", body[:200])
|
logger.error("[brain] /chat lieferte ungueltiges JSON: %s", body[:200])
|
||||||
await self._send_to_rvs({
|
await self._emit_activity("idle", "")
|
||||||
"type": "agent_activity",
|
|
||||||
"payload": {"activity": "idle"},
|
|
||||||
"timestamp": int(asyncio.get_event_loop().time() * 1000),
|
|
||||||
})
|
|
||||||
return
|
return
|
||||||
|
|
||||||
reply = (data.get("reply") or "").strip()
|
reply = (data.get("reply") or "").strip()
|
||||||
if not reply:
|
if not reply:
|
||||||
logger.warning("[brain] /chat: leerer Reply")
|
logger.warning("[brain] /chat: leerer Reply")
|
||||||
await self._send_to_rvs({
|
await self._emit_activity("idle", "")
|
||||||
"type": "agent_activity",
|
|
||||||
"payload": {"activity": "idle"},
|
|
||||||
"timestamp": int(asyncio.get_event_loop().time() * 1000),
|
|
||||||
})
|
|
||||||
return
|
return
|
||||||
|
|
||||||
# Side-Channel-Events VOR der Chat-Bubble broadcasten (z.B. skill_created)
|
# Side-Channel-Events VOR der Chat-Bubble broadcasten (z.B. skill_created)
|
||||||
@@ -1331,6 +1322,8 @@ class ARIABridge:
|
|||||||
await self._process_core_response(reply, {})
|
await self._process_core_response(reply, {})
|
||||||
except Exception:
|
except Exception:
|
||||||
logger.exception("[brain] _process_core_response Fehler")
|
logger.exception("[brain] _process_core_response Fehler")
|
||||||
|
await self._emit_activity("idle", "")
|
||||||
|
# Originaler Fallback-Send (toter Code, _emit_activity uebernimmt jetzt)
|
||||||
await self._send_to_rvs({
|
await self._send_to_rvs({
|
||||||
"type": "agent_activity",
|
"type": "agent_activity",
|
||||||
"payload": {"activity": "idle"},
|
"payload": {"activity": "idle"},
|
||||||
@@ -2050,13 +2043,11 @@ class ARIABridge:
|
|||||||
|
|
||||||
if text.strip():
|
if text.strip():
|
||||||
logger.info("[rvs] STT Ergebnis: '%s'", text[:80])
|
logger.info("[rvs] STT Ergebnis: '%s'", text[:80])
|
||||||
# Hints (Barge-In, GPS) als Praefix vorschalten — gemeinsamer Helper
|
|
||||||
# mit dem chat-Pfad damit das Verhalten konsistent ist.
|
# Reihenfolge wichtig: STT-Text ZUERST broadcasten damit die App
|
||||||
core_text = self._build_core_text(text, interrupted, location)
|
# die Voice-Bubble sofort mit dem erkannten Text aktualisieren
|
||||||
# ERST an aria-core senden (wichtigster Schritt)
|
# kann — send_to_core blockt danach synchron auf Brain (kann
|
||||||
await self.send_to_core(core_text, source="app-voice" + (" [barge-in]" if interrupted else ""))
|
# dauern), wuerde sonst die Anzeige verzoegern.
|
||||||
# STT-Text an RVS senden (fuer Anzeige in App + Diagnostic)
|
|
||||||
# sender="stt" damit Bridge es ignoriert (kein Loop)
|
|
||||||
try:
|
try:
|
||||||
stt_payload = {
|
stt_payload = {
|
||||||
"text": text,
|
"text": text,
|
||||||
@@ -2080,6 +2071,10 @@ class ARIABridge:
|
|||||||
logger.warning("[rvs] STT-Text NICHT broadcastet — _send_to_rvs lieferte False")
|
logger.warning("[rvs] STT-Text NICHT broadcastet — _send_to_rvs lieferte False")
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.warning("[rvs] STT-Text konnte nicht an RVS gesendet werden: %s", e)
|
logger.warning("[rvs] STT-Text konnte nicht an RVS gesendet werden: %s", e)
|
||||||
|
|
||||||
|
# Dann an Brain — der blockt synchron bis ARIA fertig ist.
|
||||||
|
core_text = self._build_core_text(text, interrupted, location)
|
||||||
|
await self.send_to_core(core_text, source="app-voice" + (" [barge-in]" if interrupted else ""))
|
||||||
else:
|
else:
|
||||||
logger.info("[rvs] Keine Sprache erkannt — ignoriert")
|
logger.info("[rvs] Keine Sprache erkannt — ignoriert")
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user