From 8c1014d281b6fed85cea2ae1432afebf876bd9dd Mon Sep 17 00:00:00 2001 From: duffyduck Date: Sat, 18 Apr 2026 11:51:22 +0200 Subject: [PATCH] fix: Thinking indicator respringt nach chat:final durch trailing events MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Nach chat:final kommen oft noch agent-Events rein (Core raeumt nach), die den Thinking-Indicator wieder anspringen liessen. - Diagnostic: 3s-Settled-Window nach chat:final, agent_activity-Broadcasts werden in dem Fenster unterdrueckt (idle kommt weiter durch). - Bridge: Gleiches Fenster in _emit_activity() — App bekommt keine trailing thinking/tool-Events mehr nach dem finalen Antwort. Co-Authored-By: Claude Opus 4.7 (1M context) --- bridge/aria_bridge.py | 14 +++++++++++++- diagnostic/server.js | 19 ++++++++++++++++--- 2 files changed, 29 insertions(+), 4 deletions(-) diff --git a/bridge/aria_bridge.py b/bridge/aria_bridge.py index 8100b8b..d7b3f3e 100644 --- a/bridge/aria_bridge.py +++ b/bridge/aria_bridge.py @@ -554,6 +554,9 @@ class ARIABridge: # Letzter gesendeter agent_activity-State (zum Entduplizieren) self._last_activity_state: Optional[tuple] = None + # Zeitstempel des letzten chat:final — waehrend 3s danach werden + # trailing Agent-Events unterdrueckt (Core raeumt manchmal nach). + self._last_chat_final_at: float = 0.0 def initialize(self) -> None: """Initialisiert alle Komponenten. @@ -779,6 +782,7 @@ class ARIABridge: if state == "final": text = self._extract_chat_text(payload) + self._last_chat_final_at = asyncio.get_event_loop().time() await self._emit_activity("idle", "") if not text: logger.warning("[core] chat final ohne Text: %s", json.dumps(payload)[:200]) @@ -790,6 +794,7 @@ class ARIABridge: if state == "error": error = payload.get("error", "Unbekannt") logger.error("[core] Chat-Fehler: %s", error) + self._last_chat_final_at = asyncio.get_event_loop().time() await self._emit_activity("idle", "") await self._send_to_rvs({ "type": "chat", @@ -1468,7 +1473,14 @@ class ARIABridge: logger.info("[cancel] Diagnostic /api/cancel: %s", status) async def _emit_activity(self, activity: str, tool: str = "") -> None: - """Sendet agent_activity an die App — nur wenn sich der State geaendert hat.""" + """Sendet agent_activity an die App — nur wenn sich der State geaendert hat. + + Trailing Agent-Events nach chat:final werden 3s lang unterdrueckt + (nur 'idle' kommt immer durch).""" + if activity != "idle" and self._last_chat_final_at > 0: + since_final = asyncio.get_event_loop().time() - self._last_chat_final_at + if since_final < 3.0: + return state = (activity, tool) if state == self._last_activity_state: return diff --git a/diagnostic/server.js b/diagnostic/server.js index b35628f..dc83d8b 100644 --- a/diagnostic/server.js +++ b/diagnostic/server.js @@ -82,6 +82,12 @@ const browserClients = new Set(); let pipelineActive = false; let pipelineStartTime = 0; +// Nach chat:final kommen oft noch Trailing Agent-Events. Waehrend dieses +// Fensters unterdruecken wir agent_activity-Broadcasts, damit der +// Thinking-Indicator nicht wieder anspringt. +let lastChatFinalAt = 0; +const SETTLED_WINDOW_MS = 3000; + function plog(message, level) { const elapsed = pipelineActive ? `+${Date.now() - pipelineStartTime}ms` : ""; const entry = { ts: new Date().toISOString(), level: level || "info", source: "pipeline", message: `${elapsed ? `[${elapsed}] ` : ""}${message}` }; @@ -356,17 +362,22 @@ function handleGatewayMessage(msg) { broadcast({ type: "chat_delta", delta, payload }); } + // Nach chat:final trickeln noch Aufraeum-Events rein — unterdruecken, + // damit der Thinking-Indicator nicht wieder anspringt. + const settled = lastChatFinalAt && (Date.now() - lastChatFinalAt) < SETTLED_WINDOW_MS; + // Tool-Nutzung erkennen und broadcasten if (stream === "tool_use" || data.type === "tool_use") { const toolName = data.name || data.tool || payload.tool || ""; - if (toolName) { + if (toolName && !settled) { broadcast({ type: "agent_activity", activity: "tool", tool: toolName, data }); log("info", "gateway", `Tool: ${toolName}`); } } - // Genereller Activity-Heartbeat (ARIA denkt) - broadcast({ type: "agent_activity", activity: stream || "thinking" }); + if (!settled) { + broadcast({ type: "agent_activity", activity: stream || "thinking" }); + } updateAgentActivity(); return; } @@ -381,6 +392,7 @@ function handleGatewayMessage(msg) { if (runId && seenFinalRuns.has(runId)) return; // Duplikat if (runId) { seenFinalRuns.add(runId); setTimeout(() => seenFinalRuns.delete(runId), 60000); } log("info", "gateway", `ANTWORT: "${text.slice(0, 200)}"`); + lastChatFinalAt = Date.now(); if (pipelineActive) pipelineEnd(true, `"${text.slice(0, 120)}"`); broadcast({ type: "chat_final", text, payload }); broadcast({ type: "agent_activity", activity: "idle" }); @@ -424,6 +436,7 @@ function handleGatewayMessage(msg) { if (runId) { seenFinalRuns.add(runId); setTimeout(() => seenFinalRuns.delete(runId), 60000); } const text = extractChatText(payload) || payload.text || ""; log("info", "gateway", `ANTWORT: "${text.slice(0, 200)}"`); + lastChatFinalAt = Date.now(); if (pipelineActive) pipelineEnd(true, `"${text.slice(0, 120)}"`); else broadcast({ type: "agent_activity", activity: "idle" }); broadcast({ type: "chat_final", text, payload });