Compare commits
4 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 579a466402 | |||
| 5133f0bc2d | |||
| a476a4b734 | |||
| 11b205ddaf |
@@ -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,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",
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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)
|
||||||
Reference in New Issue
Block a user