fix(dashboard): resolver bugs críticos de tiempo real en HITL — race conditions, In-Band Auth y multi-stream buffer

- AppShell: corregir condición de carrera REST/WS que perdía tokens de agent_stream_chunk
- init_state atómico + eliminación de doble fuente REST/WS para actualización en tiempo real
- conversation_ended e idempotencia de eventos en máquina de estados por conversación
- Seguridad: migrar JWT de query param a In-Band Auth (primer mensaje {action:auth}) con timeout 5s y cierre 1008
- Multi-stream buffer: reemplazar buffer plano por TTL LRU (200 entradas, 60s TTL) para evitar pisado de tokens entre agentes
- agent_stream_completed ya no borra buffer incondicionalmente — delega purge a la política LRU
- Timer: corregir display de 00:00 en estado PENDING con visualización inmediata + cleanup en stop()
- Tests: 8 tests multi-stream, tests In-Band Auth, tests idempotencia y máquina de estados, tests Timer
- Resultado: 86/86 tests pasan | TypeScript 0 errores
This commit is contained in:
2026-07-29 04:32:27 -05:00
parent 8f044567c0
commit 83e3ec2cff
37 changed files with 6979 additions and 1129 deletions
+18 -10
View File
@@ -104,16 +104,6 @@ const Timer = forwardRef<TimerHandle, TimerProps>(({ caseId }, ref) => {
}
}, [caseId]);
// ── Cleanup on unmount ────────────────────────────────────
useEffect(() => {
return () => {
if (intervalRef.current) {
clearInterval(intervalRef.current);
intervalRef.current = null;
}
};
}, []);
// ── Imperative API ────────────────────────────────────────
const start = useCallback(() => {
@@ -128,6 +118,9 @@ const Timer = forwardRef<TimerHandle, TimerProps>(({ caseId }, ref) => {
accumulated: accumulatedRef.current,
});
// Mostrar valor actual INMEDIATAMENTE (sin esperar 1s al primer tick)
setDisplaySeconds(accumulatedRef.current);
intervalRef.current = setInterval(() => {
if (startTimestampRef.current === null) return;
const elapsed = Math.floor(
@@ -182,6 +175,21 @@ const Timer = forwardRef<TimerHandle, TimerProps>(({ caseId }, ref) => {
getElapsed,
]);
// ── Cleanup on unmount ────────────────────────────────────
useEffect(() => {
return () => {
// 1. Primero: persistir estado vía stop() (ya es idempotente)
stop();
// 2. Segundo: anular el intervalo (por si stop no lo hizo)
if (intervalRef.current) {
clearInterval(intervalRef.current);
intervalRef.current = null;
}
// 3. Tercero: resetear bandera (para StrictMode)
isRunningRef.current = false;
};
}, [stop]);
// ── Render ────────────────────────────────────────────────
return (