fix(app): Barge-In kontext-scoped — Hauptchat-Frage killt/blockiert Projekt-Arbeit nicht mehr

Bug: waehrend ARIA in einem Projekt arbeitet, im Hauptchat eine Frage stellen →
sie wurde angezeigt aber nicht verarbeitet (erst wenn das Projekt fertig war).
Proxy kann nachweislich parallel (2 claude-Subprozesse) — der Flaschenhals war
die App: interruptAriaIfBusy wertete den GLOBALEN agentActivity aus und feuerte
bei jedem Senden-waehrend-irgendein-Kontext-arbeitet einen kontext-uebergreifenden
cancel_request.

Fix: Busy-Status kontextgenau aus queueStatus (/projects/queue-status) statt
global. Barge-In (Brain-Cancel) nur noch wenn GENAU der fokussierte Kontext
arbeitet; TTS wird weiterhin immer gestoppt wenn ARIA spricht. cancel_request
traegt jetzt die projectId (fuer spaeteres kontext-scoped Cancel in der Bridge).

Damit laufen Hauptchat und Projekt(e) in der App parallel — passend zum
Multi-Threading im Brain.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-07-10 20:58:01 +02:00
co-authored by Claude Opus 4.8
parent b9150f2b46
commit 885e825f8b
+21 -8
View File
@@ -295,6 +295,9 @@ const ChatScreen: React.FC = () => {
const [projectNameById, setProjectNameById] = useState<Record<string, string>>({}); const [projectNameById, setProjectNameById] = useState<Record<string, string>>({});
// Queue-Status pro Kontext — polled alle 2s, fuer Status-Dots im Drawer // Queue-Status pro Kontext — polled alle 2s, fuer Status-Dots im Drawer
const [queueStatus, setQueueStatus] = useState<Record<string, { busy: boolean; queue_size: number }>>({}); const [queueStatus, setQueueStatus] = useState<Record<string, { busy: boolean; queue_size: number }>>({});
// Ref-Spiegel fuer Callbacks (interruptAriaIfBusy liest den aktuellen
// Busy-Status des fokussierten Kontexts ohne stale Closure).
const queueStatusRef = useRef<Record<string, { busy: boolean; queue_size: number }>>({});
const [searchIndex, setSearchIndex] = useState(0); // welcher Treffer aktiv ist const [searchIndex, setSearchIndex] = useState(0); // welcher Treffer aktiv ist
const [pendingAttachments, setPendingAttachments] = useState<{file: any, isPhoto: boolean}[]>([]); const [pendingAttachments, setPendingAttachments] = useState<{file: any, isPhoto: boolean}[]>([]);
const [agentActivity, setAgentActivity] = useState<{activity: string, tool: string}>({activity: 'idle', tool: ''}); const [agentActivity, setAgentActivity] = useState<{activity: string, tool: string}>({activity: 'idle', tool: ''});
@@ -517,6 +520,7 @@ const ChatScreen: React.FC = () => {
const s = await brainApi.getProjectQueueStatus(); const s = await brainApi.getProjectQueueStatus();
if (cancelled) return; if (cancelled) return;
setQueueStatus(s.contexts || {}); setQueueStatus(s.contexts || {});
queueStatusRef.current = s.contexts || {};
} catch {} } catch {}
}; };
poll(); poll();
@@ -1939,18 +1943,27 @@ const ChatScreen: React.FC = () => {
// mach lieber X" sagen wie in einem echten Gespraech. // mach lieber X" sagen wie in einem echten Gespraech.
const interruptAriaIfBusy = useCallback(() => { const interruptAriaIfBusy = useCallback(() => {
const speaking = audioService.isPlayingAudio(); const speaking = audioService.isPlayingAudio();
const thinking = agentActivity.activity !== 'idle'; // Multi-Threading: NUR den fokussierten Kontext als "busy" werten — nicht
if (!speaking && !thinking) return false; // global. Sonst bricht eine Nachricht im Hauptchat die parallele Arbeit in
console.log('[Chat] Barge-In: speaking=%s thinking=%s — interrupting ARIA', // einem Projekt ab (bzw. wird faelschlich als Barge-In behandelt und die
speaking, thinking); // eigene Anfrage geht unter). Der Busy-Status kommt kontextgenau aus
// /projects/queue-status (queueStatusRef). agentActivity ist global und
// taugt dafuer nicht.
const pid = focusedProjectIdRef.current || '';
const focusKey = pid || '__main__';
const focusBusy = !!queueStatusRef.current?.[focusKey]?.busy;
if (!speaking && !focusBusy) return false;
console.log('[Chat] Barge-In: speaking=%s focusBusy=%s (ctx=%s) — interrupting',
speaking, focusBusy, focusKey);
// TTS immer stoppen wenn ARIA gerade spricht — egal welcher Kontext.
if (speaking) audioService.haltAllPlayback('user spricht (barge-in)'); if (speaking) audioService.haltAllPlayback('user spricht (barge-in)');
if (thinking) { // Brain-Arbeit nur abbrechen wenn GENAU dieser Kontext arbeitet.
setAgentActivity({ activity: 'idle', tool: '' }); if (focusBusy) {
clearStuckWatchdog(); clearStuckWatchdog();
rvs.send('cancel_request' as any, {}); rvs.send('cancel_request' as any, { projectId: pid });
} }
return true; return true;
}, [agentActivity]); }, []);
// Manueller Aufnahme-Knopf (VoiceButton) — Start. // Manueller Aufnahme-Knopf (VoiceButton) — Start.
// Streaming-Variante: PcmStreamRecorder + Whisper-ML-Endpointer ersetzen // Streaming-Variante: PcmStreamRecorder + Whisper-ML-Endpointer ersetzen