11b205ddaf
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>
94 lines
3.1 KiB
Python
94 lines
3.1 KiB
Python
#!/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)
|