Compare commits

...

4 Commits

Author SHA1 Message Date
duffyduck cd72068e76 release: bump version to 0.2.0.0 2026-07-03 02:22:58 +02:00
duffyduck 8b567e15bf feat(projects): Migration — alt-getaggte Nachrichten nachtraeglich in Projekte einsortieren
Projekt-Nachrichten aus der Zeit vor dem chat_backup-project_id-Feld (getaggt in
conversation.jsonl seit fc0f91d, aber chat_backup fuehrte project_id erst ab
f51ad15) lagen in der UI im Hauptchat statt im Projekt. Die App/Diagnostic
zeigen aus chat_backup.jsonl — dort fehlte der Tag.

Neue Einmal-Migration (Brain-Lifespan) schreibt project_id aus conversation.jsonl
per (role, text)-Match reihenfolge-erhaltend nach chat_backup.jsonl zurueck:
- idempotent via Marker /shared/config/.chat_backup_projectid_backfill_v1
- nicht-destruktiv: legt .pre-backfill-v1.bak an, setzt nur LEERE project_ids,
  entfernt/aendert nie einen bestehenden Tag
- atomar (tmp + os.replace)
- Duplikate: Deque je (role, normtext) inkl. "" fuer Hauptthread → korrekte
  Zuordnung auch bei wiederholten Texten, kein faelschliches Taggen von
  Hauptchat-Interleaving

Mit Logik-Tests (Zuordnung, Duplikat-Reihenfolge, Idempotenz) verifiziert.
Nachrichten aus der Zeit bevor es Projekte gab bleiben untagged im Hauptchat.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-03 02:20:41 +02:00
duffyduck 64c06db308 fix(diagnostic): keine untagged ARIA-Bubbles/Backup-Writes mehr (leere Projekte)
Das Dashboard hatte dieselbe Ursache wie die App (untagged Nachrichten in
chat_backup) PLUS zwei eigene untagged-Pfade ueber den Gateway-Watch:

- Frontend: chat_final rendert keine Chat-Bubble mehr. ARIA-Antworten kommen
  ausschliesslich via rvs_chat (traegt projectId). chat_final stammt vom
  Gateway-Watch ohne projectId → erzeugte eine Duplikat-Bubble im Hauptchat.
- server.js: kein chat_backup-Write im Gateway-final-Handler mehr. Die Bridge
  (_process_core_response) ist massgeblicher Writer und schreibt MIT project_id;
  der Write hier erzeugte untagged Duplikate, die beim Reload im Hauptchat
  statt im Projekt landeten.

Der App-Focus-Fix (63dde65) sorgt dafuer dass neue Nachrichten ueberhaupt
getaggt in chat_backup landen — davon profitiert das Dashboard direkt.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-03 02:15:02 +02:00
duffyduck 63dde6506f fix(projects): Drawer resettet App-Focus nicht mehr (leere Projekte-Ursache) + Zurueck-Button
Kernbug: ProjectsBrowser.load() pushte bei JEDEM Drawer-Oeffnen status.active
in den App-Focus. Im Multi-Threading hat das Brain keinen globalen
active_project-State mehr → status.active ist null → Focus wurde auf Hauptchat
zurueckgesetzt. Folge: nach jedem Drawer-Oeffnen landeten alle Nachrichten im
Hauptchat, Projekte blieben leer.

- ProjectsBrowser: neues Prop currentFocusId (App-Focus = Source-of-Truth).
  load() uebernimmt nur noch die Projektliste, kein onActiveChanged(status.active)
  mehr. Highlight (✓ FOCUS) folgt currentFocusId.
- ChatScreen: currentFocusId={focusedProjectId} durchgereicht.
- ChatScreen: direkter „← Hauptchat"-Button im Focus-Header (nur im Projekt
  sichtbar) — ein Tap statt Drawer→Hauptchat.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-03 02:07:33 +02:00
8 changed files with 208 additions and 12 deletions
+2 -2
View File
@@ -79,8 +79,8 @@ android {
applicationId "com.ariacockpit"
minSdkVersion rootProject.ext.minSdkVersion
targetSdkVersion rootProject.ext.targetSdkVersion
versionCode 10909
versionName "0.1.9.9"
versionCode 20000
versionName "0.2.0.0"
// Fallback fuer Libraries mit Product Flavors
missingDimensionStrategy 'react-native-camera', 'general'
}
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "aria-cockpit",
"version": "0.1.9.9",
"version": "0.2.0.0",
"private": true,
"scripts": {
"android": "react-native run-android",
+15 -3
View File
@@ -36,6 +36,12 @@ interface Props {
/** Wird gerufen wenn Stefan ein anderes Projekt fokussiert (App-lokale
* UI-Entscheidung, wechselt den Chat-Focus). */
onActiveChanged?: (project: Project | null) => void;
/** Der aktuell in der App fokussierte Kontext (App-lokale Source-of-Truth).
* Leer = Hauptchat. Steuert das ✓-FOCUS-Highlight. WICHTIG: der Drawer darf
* den Focus NICHT aus dem Brain-Status ableiten — im Multi-Threading gibt es
* kein globales active_project mehr (status.active ist null), das wuerde den
* Focus bei jedem Drawer-Oeffnen auf Hauptchat zuruecksetzen. */
currentFocusId?: string;
/** Queue-Status pro Kontext (key "__main__" = Hauptchat, sonst project_id).
* Wenn geliefert: Status-Dot pro Zeile gerendert. */
queueStatus?: Record<string, { busy: boolean; queue_size: number }>;
@@ -51,7 +57,7 @@ function _fmtRel(unixSec: number): string {
return new Date(unixSec * 1000).toLocaleDateString('de-DE');
}
export const ProjectsBrowser: React.FC<Props> = ({ visible = true, onClose, onActiveChanged, queueStatus }) => {
export const ProjectsBrowser: React.FC<Props> = ({ visible = true, onClose, onActiveChanged, currentFocusId, queueStatus }) => {
const _statusDot = (pid: string) => {
const s = queueStatus?.[pid];
if (!s) return { color: '#555570', label: '' };
@@ -80,9 +86,11 @@ export const ProjectsBrowser: React.FC<Props> = ({ visible = true, onClose, onAc
setLoading(true); setErr(null);
brainApi.getProjectStatus()
.then(status => {
// NUR die Projektliste + Queue uebernehmen. NICHT status.active in den
// App-Focus pushen — im Multi-Threading ist das Brain-active_project
// bedeutungslos (null), das wuerde den Focus bei jedem Drawer-Oeffnen
// auf Hauptchat zuruecksetzen und alle Nachrichten dort landen lassen.
setProjects(status.projects || []);
setActiveId(status.active_id || '');
onActiveChangedRef.current?.(status.active);
})
.catch(e => setErr(String(e?.message || e)))
.finally(() => setLoading(false));
@@ -90,6 +98,10 @@ export const ProjectsBrowser: React.FC<Props> = ({ visible = true, onClose, onAc
useEffect(() => { if (visible) load(); }, [visible, load]);
// Highlight („✓ FOCUS") folgt dem App-Focus (Source-of-Truth), nicht dem
// Brain. switchTo setzt activeId zusaetzlich sofort fuer Instant-Feedback.
useEffect(() => { setActiveId(currentFocusId || ''); }, [currentFocusId]);
// Reload bei RVS-Reconnect — sonst zeigt die Liste den Fast-Fail ewig
useEffect(() => {
if (!visible) return;
+16
View File
@@ -2607,6 +2607,21 @@ const ChatScreen: React.FC = () => {
</Text>
<View style={{ width: 8, height: 8, borderRadius: 4, backgroundColor: dot.color }} />
<Text style={{ fontSize: 10, color: '#8888AA' }}>{dot.label}</Text>
{/* Direkter Zurueck-zum-Hauptchat-Button — nur wenn man in einem
Projekt ist. Ein Tap statt Drawer→Hauptchat. */}
{!isMain && (
<TouchableOpacity
onPress={() => setFocusedProjectId('')}
hitSlop={{top:8,bottom:8,left:8,right:8}}
style={{
marginLeft: 4, paddingHorizontal: 10, paddingVertical: 4,
borderRadius: 12, backgroundColor: 'rgba(255,255,255,0.12)',
flexDirection: 'row', alignItems: 'center', gap: 4,
}}
>
<Text style={{ color: '#E0E0F0', fontSize: 12, fontWeight: '700' }}> Hauptchat</Text>
</TouchableOpacity>
)}
</View>
</View>
);
@@ -2617,6 +2632,7 @@ const ChatScreen: React.FC = () => {
visible={projectsVisible}
onClose={() => setProjectsVisible(false)}
onActiveChanged={(p) => setFocusedProjectId(p?.id || '')}
currentFocusId={focusedProjectId}
queueStatus={queueStatus}
/>
+11
View File
@@ -112,6 +112,17 @@ async def lifespan(app: FastAPI):
except Exception as exc:
logger.warning("Lifespan: spotify fast_patterns Migration: %s", exc)
# Einmalige Migration: project_id aus conversation.jsonl nach chat_backup.jsonl
# zurueckschreiben, damit alt-getaggte Projekt-Nachrichten (getaggt bevor
# chat_backup project_id fuehrte) in der UI wieder im richtigen Projekt
# landen. Idempotent (Marker), nicht-destruktiv (.bak), atomar.
try:
import migrate_backfill_projectid
res = migrate_backfill_projectid.run()
logger.info("Lifespan: chat_backup project_id Backfill: %s", res)
except Exception as exc:
logger.warning("Lifespan: project_id Backfill Migration: %s", exc)
task = asyncio.create_task(background_mod.run_loop(agent))
logger.info("Lifespan: Trigger-Loop gestartet")
try:
+153
View File
@@ -0,0 +1,153 @@
"""Einmalige Migration: project_id aus conversation.jsonl nach chat_backup.jsonl
zurueckschreiben.
Hintergrund: Seit es Projekte gibt (fc0f91d) taggt das Brain jeden Turn in
conversation.jsonl mit project_id. chat_backup.jsonl (die Anzeige-Quelle fuer
App + Diagnostic) bekam project_id aber erst spaeter (f51ad15). Alle Projekt-
Nachrichten aus dem Zeitfenster dazwischen liegen daher in conversation.jsonl
korrekt getaggt, in chat_backup.jsonl aber untagged → die UI zeigt sie im
Hauptchat statt im Projekt.
Diese Migration matcht chat_backup-Eintraege gegen conversation-Turns ueber
(role, text) in Reihenfolge und traegt die fehlende project_id nach. Sie ist:
- idempotent (Marker-Datei, laeuft genau einmal),
- nicht-destruktiv (legt .bak an, aendert nur LEERE project_ids, entfernt nie
einen bestehenden Tag),
- atomar (tmp-Datei + os.replace).
Reihenfolge-erhaltend: pro (role, normalisiertem Text) wird eine Deque der
project_ids aus conversation.jsonl aufgebaut (inklusive "" fuer Hauptthread-
Turns), damit wiederholte identische Texte ihre jeweils richtige Zuordnung
bekommen und Hauptchat-Interleaving nicht faelschlich getaggt wird.
"""
from __future__ import annotations
import json
import logging
import os
from collections import defaultdict, deque
from pathlib import Path
logger = logging.getLogger("aria.migrate.backfill_projectid")
CONVERSATION_FILE = Path(os.environ.get("CONVERSATION_FILE", "/data/conversation.jsonl"))
CHAT_BACKUP_FILE = Path(os.environ.get("CHAT_BACKUP_FILE", "/shared/config/chat_backup.jsonl"))
MARKER_FILE = Path("/shared/config/.chat_backup_projectid_backfill_v1")
def _norm(text: str) -> str:
"""Match-Key: getrimmt + auf 500 Zeichen begrenzt. Reicht um Turns eindeutig
zu unterscheiden, ist aber tolerant gegen minimale Trailing-Unterschiede."""
return (text or "").strip()[:500]
def run() -> dict:
"""Fuehrt die Migration aus. Returns Status-Dict fuers Logging.
Laeuft nur einmal (Marker). Fehlt eine der Quelldateien: still ueberspringen."""
if MARKER_FILE.exists():
return {"skipped": "marker_exists"}
if not CHAT_BACKUP_FILE.exists():
return {"skipped": "no_chat_backup"}
if not CONVERSATION_FILE.exists():
# Ohne Brain-Historie gibt es nichts zu uebernehmen — Marker trotzdem
# setzen, damit wir nicht bei jedem Start neu pruefen.
_write_marker(0, 0)
return {"skipped": "no_conversation"}
# 1) conversation.jsonl → Deque der project_ids je (role, normtext), in Reihenfolge.
tag_queues: dict[tuple[str, str], deque[str]] = defaultdict(deque)
conv_turns = 0
for line in _iter_jsonl(CONVERSATION_FILE):
role = line.get("role")
if role not in ("user", "assistant"):
continue
content = line.get("content")
if not isinstance(content, str):
continue
conv_turns += 1
tag_queues[(role, _norm(content))].append((line.get("project_id") or "").strip())
# 2) chat_backup.jsonl durchgehen, leere project_ids nachtragen.
try:
backup_lines = CHAT_BACKUP_FILE.read_text(encoding="utf-8").splitlines()
except Exception as exc:
logger.warning("[backfill] chat_backup lesen fehlgeschlagen: %s", exc)
return {"error": f"read_backup: {exc}"}
out_lines: list[str] = []
patched = 0
matched = 0
for raw in backup_lines:
raw = raw.strip()
if not raw:
continue
try:
obj = json.loads(raw)
except Exception:
out_lines.append(raw) # unveraendert durchreichen
continue
role = obj.get("role")
text = obj.get("text")
# Nur echte Chat-Bubbles matchen (keine file_deleted-/type-Marker).
if role in ("user", "assistant") and isinstance(text, str):
q = tag_queues.get((role, _norm(text)))
if q:
pid = q.popleft() # verbraucht → Reihenfolge fuer Duplikate bleibt korrekt
matched += 1
existing = (obj.get("project_id") or "").strip()
# Nur setzen wenn Backup-Eintrag noch KEINEN Tag hat und der
# conversation-Turn einem Projekt gehoert. Bestehende Tags bleiben.
if not existing and pid:
obj["project_id"] = pid
patched += 1
out_lines.append(json.dumps(obj, ensure_ascii=False))
# 3) Nichts zu tun? Marker setzen und raus.
if patched == 0:
_write_marker(conv_turns, 0)
logger.info("[backfill] nichts nachzutragen (conv_turns=%s, matched=%s)",
conv_turns, matched)
return {"conv_turns": conv_turns, "matched": matched, "patched": 0}
# 4) Sicherung + atomarer Rewrite.
try:
bak = CHAT_BACKUP_FILE.with_suffix(".jsonl.pre-backfill-v1.bak")
if not bak.exists():
bak.write_bytes(CHAT_BACKUP_FILE.read_bytes())
tmp = CHAT_BACKUP_FILE.with_suffix(".jsonl.tmp")
tmp.write_text("\n".join(out_lines) + "\n", encoding="utf-8")
os.replace(tmp, CHAT_BACKUP_FILE)
except Exception as exc:
logger.warning("[backfill] Rewrite fehlgeschlagen: %s", exc)
return {"error": f"rewrite: {exc}"}
_write_marker(conv_turns, patched)
logger.info("[backfill] %s Bubbles nachtraeglich getaggt (conv_turns=%s, matched=%s). Backup: %s",
patched, conv_turns, matched, bak.name)
return {"conv_turns": conv_turns, "matched": matched, "patched": patched}
def _iter_jsonl(path: Path):
try:
for raw in path.read_text(encoding="utf-8").splitlines():
raw = raw.strip()
if not raw:
continue
try:
yield json.loads(raw)
except Exception:
continue
except Exception as exc:
logger.warning("[backfill] %s lesen fehlgeschlagen: %s", path, exc)
def _write_marker(conv_turns: int, patched: int) -> None:
try:
MARKER_FILE.parent.mkdir(parents=True, exist_ok=True)
MARKER_FILE.write_text(
json.dumps({"conv_turns": conv_turns, "patched": patched}, ensure_ascii=False),
encoding="utf-8",
)
except Exception as exc:
logger.warning("[backfill] Marker schreiben fehlgeschlagen: %s", exc)
+5 -1
View File
@@ -1751,7 +1751,11 @@
}
if (msg.type === 'chat_final') {
addChat('received', msg.text || '', 'chat:final');
// KEINE Bubble mehr rendern: ARIA-Antworten kommen ausschliesslich via
// rvs_chat (traegt projectId → landet im richtigen Kontext). chat_final
// stammt vom Gateway-Watch und hat KEINE projectId — wuerde also eine
// untagged Duplikat-Bubble im Hauptchat erzeugen. Nur noch als
// Aktivitaets-/Trace-Ende-Signal relevant (das macht der Server).
return;
}
if (msg.type === 'file_from_aria') {
+5 -5
View File
@@ -660,11 +660,11 @@ function handleGatewayMessage(msg) {
broadcast({ type: "agent_activity", activity: "idle" });
pendingMessageTime = 0; // Watchdog: Antwort erhalten
updateAgentActivity();
// Antwort in Backup-Log schreiben
try {
const entry = JSON.stringify({ ts: Date.now(), role: "assistant", text: text.slice(0, 2000), session: activeSessionKey }) + "\n";
fs.appendFileSync("/shared/config/chat_backup.jsonl", entry);
} catch {}
// KEIN chat_backup-Write mehr hier: die Bridge (_process_core_response)
// ist der massgebliche Writer und schreibt den Assistant-Eintrag MIT
// project_id. Dieser Gateway-Watch-Pfad kennt die project_id nicht —
// ein Write hier erzeugte ein untagged Duplikat, das beim Reload im
// Hauptchat auftaucht (statt im Projekt).
return;
}