Compare commits

...

4 Commits

Author SHA1 Message Date
duffyduck 579a466402 release: bump version to 0.1.4.9 2026-05-15 11:45:53 +02:00
duffyduck 5133f0bc2d fix(chat): User-Bubble →failed bei langsamen ARIA-Antworten
Symptom: ARIA bearbeitet die Nachricht (im Gedanken-Stream sichtbar),
aber unter der User-Bubble bleibt die Sanduhr stehen und nach ~90 s
springt sie auf ⚠ failed. ARIA-Antwort kommt trotzdem irgendwann durch
— die Bubble war also nie weg, nur visuell schief.

Wurzel: chat_ack vom Bridge kam offenbar in manchen Faellen nicht
verlaesslich an. ACK-Timer (30 s × 3 Retries) lief durch → 'failed'.

Fix: agent_activity = thinking/tool/assistant ist impliziter Beweis,
dass das Brain die Nachricht bekommen und angefangen hat zu arbeiten.
Beim ersten non-idle Event:
- alle laufenden ACK-Timer cancelen
- alle 'sending'-User-Bubbles auf 'sent' (✓) setzen

ARIA-Reply markiert dann wie gehabt 'delivered' (✓✓). Damit kann keine
Bubble mehr auf failed gehen waehrend Brain noch laeuft.

Plus: ACK_TIMEOUT_MS 30 → 60 s als Backup-Reserve fuer den Fall dass
weder ACK noch agent_activity ankommt (sehr unwahrscheinlich, aber
billig).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-15 11:43:04 +02:00
duffyduck a476a4b734 release: bump version to 0.1.4.8 2026-05-15 11:28:06 +02:00
duffyduck 11b205ddaf fix(chat): chat_backup ts auf UNIX-ms umgestellt + Doppelpost-Schutz
Bug-1: _append_chat_backup nutzte asyncio.get_event_loop().time() —
das ist Container-Monotonic (bei Restart wieder 0), NICHT UNIX-Zeit.
Bridge schrieb so Eintraege mit ts wie 394M (=6.5 min Uptime), App-side
generiert User-Bubbles mit Date.now() = 1.778e12. Beim Sortieren in
der App: Server-Bubbles landeten alle als "uralt" (kleine ts) ueber den
lokalen Bubbles und teilweise unter dem 500er-Cap raus — Symptom:
"alles nach Hello Kitty fehlt in der App".

Fix: _append_chat_backup nutzt jetzt time.time() * 1000 (UNIX-ms).

Bug-2: doppelte User-Bubble nach App-Hintergrund/Restart mit Retry-Knopf.
Race-Fix von vorhin (text+timestamp-Heuristik, 5-Min-Fenster) griff
nicht weil bei kaputten Server-ts (394M) und lokalen UNIX-ms (1.778e12)
das Diff 1.7 Billionen ms war → Fenster nie zutreffend → lokale Bubble
blieb als Duplikat.

Fix: Text-Match alleine reicht — wenn der Server irgendwo eine
textgleiche User-Bubble hat, ist es dieselbe Nachricht. Greift jetzt
unabhaengig von ts-Konsistenz.

Plus: tools/migrate_chat_backup_ts.py — repariert vorhandene jsonl
(284 von 299 Eintraege auf der VM hatten Container-Uptime-ts). Datei-
Reihenfolge bleibt erhalten (war eh chronologisch), ts werden ab File-
Mtime rueckwaerts 60s-Schritten vergeben. Idempotent, .bak-Backup.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-15 11:26:39 +02:00
5 changed files with 141 additions and 19 deletions
+2 -2
View File
@@ -79,8 +79,8 @@ android {
applicationId "com.ariacockpit" applicationId "com.ariacockpit"
minSdkVersion rootProject.ext.minSdkVersion minSdkVersion rootProject.ext.minSdkVersion
targetSdkVersion rootProject.ext.targetSdkVersion targetSdkVersion rootProject.ext.targetSdkVersion
versionCode 10407 versionCode 10409
versionName "0.1.4.7" versionName "0.1.4.9"
// Fallback fuer Libraries mit Product Flavors // Fallback fuer Libraries mit Product Flavors
missingDimensionStrategy 'react-native-camera', 'general' missingDimensionStrategy 'react-native-camera', 'general'
} }
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "aria-cockpit", "name": "aria-cockpit",
"version": "0.1.4.7", "version": "0.1.4.9",
"private": true, "private": true,
"scripts": { "scripts": {
"android": "react-native run-android", "android": "react-native run-android",
+38 -14
View File
@@ -324,7 +324,12 @@ const ChatScreen: React.FC = () => {
// Wie lange wir auf das ACK warten bevor wir retryen. Bridge sollte // Wie lange wir auf das ACK warten bevor wir retryen. Bridge sollte
// unmittelbar zurueckmelden — 30s ist grosszuegig fuer schlechte Netze. // unmittelbar zurueckmelden — 30s ist grosszuegig fuer schlechte Netze.
const ACK_TIMEOUT_MS = 30_000; // 60s — grosszuegiger als 30s, weil langsame Brain-Calls (Multi-Tool) sonst
// 90s × 3 Retries lang die User-Bubble auf ⏳ stehen lassen wuerden. Der
// wichtige Pfad ist sowieso: agent_activity = thinking → markiert die
// Bubble sofort als 'sent' (siehe handler). Das hier ist Fallback wenn
// weder ACK noch agent_activity ankommt.
const ACK_TIMEOUT_MS = 60_000;
// Wie oft re-tryen wir bevor wir "failed" anzeigen. // Wie oft re-tryen wir bevor wir "failed" anzeigen.
const MAX_SEND_ATTEMPTS = 3; const MAX_SEND_ATTEMPTS = 3;
// Pending ACK-Timer pro clientMsgId — fuer cancel beim ACK. // Pending ACK-Timer pro clientMsgId — fuer cancel beim ACK.
@@ -686,23 +691,26 @@ const ChatScreen: React.FC = () => {
// gesetzt UND text leer/Placeholder) // gesetzt UND text leer/Placeholder)
// - User-Bubbles deren clientMsgId der Server noch nicht kennt: // - User-Bubbles deren clientMsgId der Server noch nicht kennt:
// z.B. waehrend Reconnect-Race oder solange flushQueuedMessages // z.B. waehrend Reconnect-Race oder solange flushQueuedMessages
// noch laeuft. ABER: wenn der Server eine textgleiche Bubble // noch laeuft. ABER: wenn der Server eine textgleiche User-
// im gleichen 5-Min-Fenster hat (Alter Backup-Eintrag ohne // Bubble hat (egal mit welcher cmid oder ohne — z.B. wenn die
// clientMsgId, vor dem Bridge-Patch geschrieben), werten wir // Bubble aus einer Bridge-Version vor dem clientMsgId-Patch
// das als Treffer und verwerfen die lokale Kopie — sonst // stammt oder wenn die ts kaputt sind), werten wir das als
// Doppelpost: einmal als Server-Bubble (delivered) und einmal // Treffer und verwerfen die lokale Kopie. Sonst Doppelpost:
// als lokale failed/queued mit Retry-Knopf. // einmal als Server-Bubble (delivered) und einmal als lokale
const FIVE_MIN = 5 * 60 * 1000; // failed/queued mit Retry-Knopf.
const serverUserTexts = new Set(
fromServer.filter(s => s.sender === 'user').map(s => s.text || '')
);
const localOnly = prev.filter(m => { const localOnly = prev.filter(m => {
if (m.skillCreated || m.triggerCreated || m.memorySaved) return true; if (m.skillCreated || m.triggerCreated || m.memorySaved) return true;
if (m.audioRequestId && (!m.text || m.text === '🎙 Aufnahme...' || m.text === 'Aufnahme...')) return true; if (m.audioRequestId && (!m.text || m.text === '🎙 Aufnahme...' || m.text === 'Aufnahme...')) return true;
if (m.sender === 'user' && m.clientMsgId && !serverCmids.has(m.clientMsgId)) { if (m.sender === 'user' && m.clientMsgId && !serverCmids.has(m.clientMsgId)) {
const serverHasIt = fromServer.some(s => // Text-Match-Fallback: wenn der Server irgendwo eine textgleiche
s.sender === 'user' && // User-Bubble hat, ist es dieselbe Nachricht (vor cmid-Aera, ts
s.text === m.text && // kaputt etc.) — wir verwerfen die lokale Kopie. Leerer Text
Math.abs((s.timestamp || 0) - (m.timestamp || 0)) < FIVE_MIN, // (z.B. nur Anhang) faellt nicht in den Vergleich.
); const text = m.text || '';
if (serverHasIt) return false; if (text && serverUserTexts.has(text)) return false;
return true; return true;
} }
return false; return false;
@@ -1023,6 +1031,22 @@ const ChatScreen: React.FC = () => {
const activity = (message.payload.activity as string) || 'idle'; const activity = (message.payload.activity as string) || 'idle';
const tool = (message.payload.tool as string) || ''; const tool = (message.payload.tool as string) || '';
setAgentActivity({ activity, tool }); setAgentActivity({ activity, tool });
// Implizite ACK-Bestaetigung: Brain hat angefangen zu arbeiten →
// unsere Nachricht ist offensichtlich angekommen, auch wenn das
// chat_ack aus irgendeinem Grund nicht durchkam. Alle laufenden
// ACK-Timer canceln + sending-Bubbles auf 'sent' setzen.
// Vermeidet das Symptom "Sanduhr bleibt + Timeout" bei langsamen
// Brain-Antworten (>90 s, also nach 3 ACK-Retries auf failed).
if (activity !== 'idle' && ackTimers.current.size > 0) {
for (const cmid of Array.from(ackTimers.current.keys())) {
clearAckTimer(cmid);
}
setMessages(prev => prev.map(m =>
m.sender === 'user' && m.deliveryStatus === 'sending'
? { ...m, deliveryStatus: 'sent' }
: m
));
}
// In den Gedanken-Stream einfuegen. Dedup gegen identische Folge- // In den Gedanken-Stream einfuegen. Dedup gegen identische Folge-
// Events (z.B. zwei mal 'thinking' direkt hintereinander). Tool- // Events (z.B. zwei mal 'thinking' direkt hintereinander). Tool-
// Events NIE dedupen — wenn ARIA dreimal Bash hintereinander ruft, // Events NIE dedupen — wenn ARIA dreimal Bash hintereinander ruft,
+7 -2
View File
@@ -997,8 +997,13 @@ class ARIABridge:
"""Schreibt eine Zeile in /shared/config/chat_backup.jsonl. """Schreibt eine Zeile in /shared/config/chat_backup.jsonl.
Wird von Diagnostic + App als History-Quelle gelesen. Wird von Diagnostic + App als History-Quelle gelesen.
entry braucht mindestens {role, text}; ts wird ergaenzt. entry braucht mindestens {role, text}; ts wird ergaenzt.
Returns den ts (auch fuer Bubble-Loeschen-Tracking).""" Returns den ts (auch fuer Bubble-Loeschen-Tracking).
ts = int(asyncio.get_event_loop().time() * 1000)
WICHTIG: ts ist UNIX-ms (time.time()*1000), NICHT loop-time.
Loop-time ist Container-monotonic — bei jedem Restart wieder 0.
Das brach die App-History-Sortierung weil App-side Date.now()
(echtes UNIX-ms) mit Bridge-Container-Uptime gemischt wurde."""
ts = int(time.time() * 1000)
try: try:
line = {"ts": ts} line = {"ts": ts}
line.update(entry) line.update(entry)
+93
View File
@@ -0,0 +1,93 @@
#!/usr/bin/env python3
"""
Migration: chat_backup.jsonl ts-Werte von Container-Uptime-ms auf UNIX-ms umstellen.
Hintergrund: vor dem Fix nutzte _append_chat_backup() `asyncio.get_event_loop().time()`,
was Container-Monotonic ist (bei Restart wieder 0). Mischte sich mit App-side
`Date.now()` (echtes UNIX-ms) → falsche Sortierung in der App-History.
Strategie: ts < 1e12 (keine UNIX-ms) werden umgeschrieben. Anker = file-mtime,
decay 60 Sekunden pro Eintrag rueckwaerts. Datei-Reihenfolge bleibt erhalten
(append-only war chronologisch korrekt, nur ts-Werte waren Unsinn).
Vorhandene UNIX-ms-Eintraege (file_deleted-Marker, neue Eintraege ab Bridge-Fix)
werden unveraendert gelassen.
Idempotent: zweimal laufen lassen ist sicher — beim zweiten Mal sind alle ts
schon UNIX-ms und werden nicht angefasst.
Backup: schreibt erst chat_backup.jsonl.bak, dann atomar replace.
"""
import json
import os
import shutil
import sys
import time
from pathlib import Path
UNIX_MS_THRESHOLD = 10 ** 12 # < 1e12 ms = vor 2001 = unrealistisch fuer UNIX
GAP_SECONDS = 60 # 1 Eintrag pro Minute rueckwaerts ab mtime
def migrate(path: Path) -> None:
if not path.exists():
print(f"Datei nicht da: {path}")
sys.exit(1)
raw = path.read_text(encoding="utf-8").splitlines()
entries = []
for raw_line in raw:
s = raw_line.strip()
if not s:
continue
try:
entries.append(json.loads(s))
except Exception as e:
print(f" ueberspringe kaputte Zeile: {e}")
continue
if not entries:
print("Datei leer")
return
file_mtime_ms = int(os.path.getmtime(path) * 1000)
n = len(entries)
fixed = 0
# Wir bauen einen Ersatz-ts (file_mtime - gap*minutes_back) nur fuer
# Eintraege deren ts < UNIX_MS_THRESHOLD. file_deleted etc. mit echtem
# UNIX-ms bleiben unangetastet.
for i, entry in enumerate(entries):
ts = entry.get("ts", 0)
if not isinstance(ts, (int, float)) or ts < UNIX_MS_THRESHOLD:
# Synth-ts vergeben: aelteste = mtime - n*gap, neueste = mtime
new_ts = file_mtime_ms - (n - 1 - i) * GAP_SECONDS * 1000
entry["ts"] = new_ts
fixed += 1
if fixed == 0:
print(f"Nichts zu migrieren ({n} Eintraege, alle ts schon UNIX-ms)")
return
# Backup
bak = path.with_suffix(path.suffix + ".bak")
shutil.copy2(path, bak)
print(f"Backup: {bak}")
# Atomic rewrite
tmp = path.with_suffix(path.suffix + ".tmp")
with open(tmp, "w", encoding="utf-8") as f:
for entry in entries:
f.write(json.dumps(entry, ensure_ascii=False) + "\n")
tmp.replace(path)
print(f"Migration fertig: {fixed}/{n} ts umgeschrieben")
print(f" aelteste neu : {time.strftime('%Y-%m-%d %H:%M:%S', time.localtime(entries[0]['ts'] / 1000))}")
print(f" neueste neu : {time.strftime('%Y-%m-%d %H:%M:%S', time.localtime(entries[-1]['ts'] / 1000))}")
if __name__ == "__main__":
default = Path("/var/lib/docker/volumes/aria-agent_aria-shared/_data/config/chat_backup.jsonl")
path = Path(sys.argv[1]) if len(sys.argv) > 1 else default
migrate(path)