fix: Thinking indicator respringt nach chat:final durch trailing events

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) <noreply@anthropic.com>
This commit is contained in:
duffyduck 2026-04-18 11:51:22 +02:00
parent 271fc4edf6
commit 8c1014d281
2 changed files with 29 additions and 4 deletions

View File

@ -554,6 +554,9 @@ class ARIABridge:
# Letzter gesendeter agent_activity-State (zum Entduplizieren) # Letzter gesendeter agent_activity-State (zum Entduplizieren)
self._last_activity_state: Optional[tuple] = None 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: def initialize(self) -> None:
"""Initialisiert alle Komponenten. """Initialisiert alle Komponenten.
@ -779,6 +782,7 @@ class ARIABridge:
if state == "final": if state == "final":
text = self._extract_chat_text(payload) text = self._extract_chat_text(payload)
self._last_chat_final_at = asyncio.get_event_loop().time()
await self._emit_activity("idle", "") await self._emit_activity("idle", "")
if not text: if not text:
logger.warning("[core] chat final ohne Text: %s", json.dumps(payload)[:200]) logger.warning("[core] chat final ohne Text: %s", json.dumps(payload)[:200])
@ -790,6 +794,7 @@ class ARIABridge:
if state == "error": if state == "error":
error = payload.get("error", "Unbekannt") error = payload.get("error", "Unbekannt")
logger.error("[core] Chat-Fehler: %s", error) logger.error("[core] Chat-Fehler: %s", error)
self._last_chat_final_at = asyncio.get_event_loop().time()
await self._emit_activity("idle", "") await self._emit_activity("idle", "")
await self._send_to_rvs({ await self._send_to_rvs({
"type": "chat", "type": "chat",
@ -1468,7 +1473,14 @@ class ARIABridge:
logger.info("[cancel] Diagnostic /api/cancel: %s", status) logger.info("[cancel] Diagnostic /api/cancel: %s", status)
async def _emit_activity(self, activity: str, tool: str = "") -> None: 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) state = (activity, tool)
if state == self._last_activity_state: if state == self._last_activity_state:
return return

View File

@ -82,6 +82,12 @@ const browserClients = new Set();
let pipelineActive = false; let pipelineActive = false;
let pipelineStartTime = 0; 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) { function plog(message, level) {
const elapsed = pipelineActive ? `+${Date.now() - pipelineStartTime}ms` : ""; const elapsed = pipelineActive ? `+${Date.now() - pipelineStartTime}ms` : "";
const entry = { ts: new Date().toISOString(), level: level || "info", source: "pipeline", message: `${elapsed ? `[${elapsed}] ` : ""}${message}` }; 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 }); 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 // Tool-Nutzung erkennen und broadcasten
if (stream === "tool_use" || data.type === "tool_use") { if (stream === "tool_use" || data.type === "tool_use") {
const toolName = data.name || data.tool || payload.tool || ""; const toolName = data.name || data.tool || payload.tool || "";
if (toolName) { if (toolName && !settled) {
broadcast({ type: "agent_activity", activity: "tool", tool: toolName, data }); broadcast({ type: "agent_activity", activity: "tool", tool: toolName, data });
log("info", "gateway", `Tool: ${toolName}`); log("info", "gateway", `Tool: ${toolName}`);
} }
} }
// Genereller Activity-Heartbeat (ARIA denkt) if (!settled) {
broadcast({ type: "agent_activity", activity: stream || "thinking" }); broadcast({ type: "agent_activity", activity: stream || "thinking" });
}
updateAgentActivity(); updateAgentActivity();
return; return;
} }
@ -381,6 +392,7 @@ function handleGatewayMessage(msg) {
if (runId && seenFinalRuns.has(runId)) return; // Duplikat if (runId && seenFinalRuns.has(runId)) return; // Duplikat
if (runId) { seenFinalRuns.add(runId); setTimeout(() => seenFinalRuns.delete(runId), 60000); } if (runId) { seenFinalRuns.add(runId); setTimeout(() => seenFinalRuns.delete(runId), 60000); }
log("info", "gateway", `ANTWORT: "${text.slice(0, 200)}"`); log("info", "gateway", `ANTWORT: "${text.slice(0, 200)}"`);
lastChatFinalAt = Date.now();
if (pipelineActive) pipelineEnd(true, `"${text.slice(0, 120)}"`); if (pipelineActive) pipelineEnd(true, `"${text.slice(0, 120)}"`);
broadcast({ type: "chat_final", text, payload }); broadcast({ type: "chat_final", text, payload });
broadcast({ type: "agent_activity", activity: "idle" }); broadcast({ type: "agent_activity", activity: "idle" });
@ -424,6 +436,7 @@ function handleGatewayMessage(msg) {
if (runId) { seenFinalRuns.add(runId); setTimeout(() => seenFinalRuns.delete(runId), 60000); } if (runId) { seenFinalRuns.add(runId); setTimeout(() => seenFinalRuns.delete(runId), 60000); }
const text = extractChatText(payload) || payload.text || ""; const text = extractChatText(payload) || payload.text || "";
log("info", "gateway", `ANTWORT: "${text.slice(0, 200)}"`); log("info", "gateway", `ANTWORT: "${text.slice(0, 200)}"`);
lastChatFinalAt = Date.now();
if (pipelineActive) pipelineEnd(true, `"${text.slice(0, 120)}"`); if (pipelineActive) pipelineEnd(true, `"${text.slice(0, 120)}"`);
else broadcast({ type: "agent_activity", activity: "idle" }); else broadcast({ type: "agent_activity", activity: "idle" });
broadcast({ type: "chat_final", text, payload }); broadcast({ type: "chat_final", text, payload });