Aplikasi AI research workbench berbasis desktop yang mengintegrasikan Tauri dan Model Context Protocol (MCP) runner sering mengeksekusi komputasi saintifik lokal secara intensif. Masalah muncul ketika sub-agent MCP mengeksekusi script eksternal (seperti NumPy, PyTorch, atau binary kompilasi C++) yang mengalami deadlock, blocking call tanpa timeout, atau thread synchronization failure. Workbench membeku, IPC socket berhenti merespons, dan resource CPU/RAM tersandera oleh zombie process.

Artikel ini menyajikan panduan implementasi sistem pemantauan dan pemulihan otomatis untuk mendeteksi stalled agent, mengeksekusi terminasi bertahap, dan melakukan rollback workspace terisolasi tanpa merusak integritas file riset pengguna.

Arsitektur Observabilitas: IPC Socket Health Check & OTel

Runner MCP berjalan sebagai child process yang dikelola oleh backend Tauri (Rust). Untuk mendeteksi sub-agent yang tidak lagi memproses event queue, kita menggabungkan export tracing lokal via OpenTelemetry (OTel) dan low-overhead ping loop melalui IPC UNIX domain socket / named pipe.

1. Structured Heartbeat via IPC

Setiap child process MCP runner wajib mengekspos endpoint internal ping-pong. Rust backend mengirimkan query heartbeat secara periodik. Jika ACK tidak diterima dalam ambang batas tertentu, circuit breaker beralih status.

use std::time::Duration;
use tokio::net::UnixStream;
use tokio::io::{AsyncReadExt, AsyncWriteExt};

pub async fn check_subagent_liveness(socket_path: &str, timeout_dur: Duration) -> Result<bool, String> {
    tokio::select! {
        res = async {
            let mut stream = UnixStream::connect(socket_path).await.map_err(|e| e.to_string())?;
            stream.write_all(b"PING\n").await.map_err(|e| e.to_string())?;
            let mut buf = [0u8; 4];
            stream.read_exact(&mut buf).await.map_err(|e| e.to_string())?;
            Ok(&buf == b"PONG")
        } => res,
        _ = tokio::time::sleep(timeout_dur) => {
            Err("Heartbeat timeout: child process stalled in blocking call".into())
        }
    }
}

2. Local OpenTelemetry Collector

Tracing span lokal dikirimkan dari frontend (webview) dan backend Tauri ke OTel Collector lokal (sidecar binary atau direct in-process exporter) untuk memonitor tracing span tiap eksekusi tools saintifik. Jika span eksekusi tool tidak memicu status ended melebihi batas SLA tool (misalnya 120 detik), alert IPC internal dipicu untuk menghentikan workflow sebelum kehabisan memori OS.

Menangani Unhandled Rejection dan Child Process Lifecycle

Pada sub-agent berbasis Node.js atau Python runner, eksekusi kode saintifik pihak ketiga kerap menimbulkan native binding crash atau unhandled rejection yang membiarkan IPC pipe tergantung (hanging). Handle hook proses pada runner child:

// Inisialisasi fail-safe hook pada TypeScript MCP runner
process.on("unhandledRejection", (reason: unknown) => {
  process.stderr.write(`[CRITICAL] Unhandled Rejection: ${String(reason)}\n`);
  // Kirim sinyal failure terstruktur ke Tauri parent via stdout/named IPC sebelum exit
  process.exit(101);
});

Circuit Breaker dan Graceful Process Termination

Saat liveness check gagal, backend Tauri tidak boleh langsung melakukan SIGKILL. Langkah terminasi harus terstruktur untuk mencegah fragmentasi file atau file lock yang terkunci pada OS.

  1. State Tripped: Circuit breaker berpindah dari CLOSED ke OPEN. Eksekusi request MCP berikutnya ditolak seketika.
  2. Graceful SIGTERM: Kirim sinyal SIGTERM ke Process ID (PID) sub-agent runner. Berikan grace period terukur (misal: 3–5 detik) untuk melepaskan handle file dan buffer memori.
  3. Forced SIGKILL: Jika proses masih terdaftar di kernel process table setelah grace period habis, kirim SIGKILL (atau TerminateProcess di Windows) dan bersihkan zombie process handle.
use nix::sys::signal::{kill, Signal};
use nix::unistd::Pid;
use tokio::time::{sleep, Duration};

pub async fn terminate_stalled_process(pid: u32, grace_period: Duration) {
    let nix_pid = Pid::from_raw(pid as i32);
    
    // 1. Coba hentikan secara graceful
    let _ = kill(nix_pid, Signal::SIGTERM);
    
    let start = std::time::Instant::now();
    loop {
        if start.elapsed() >= grace_period {
            // 2. Force kill jika batas waktu terlewati
            let _ = kill(nix_pid, Signal::SIGKILL);
            break;
        }
        
        // Cek apakah proses masih eksis (signal 0)
        if kill(nix_pid, None).is_err() {
            // Proses telah berhenti
            return;
        }
        sleep(Duration::from_millis(200)).await;
    }
}

Strategi Rollback Workspace via Isolated Checkpoint

Ketika sub-agent mati di tengah penulisan file saintifik (misal modifikasi file CSV, HDF5, atau Jupyter Notebook), direktori kerja utama bisa korup. Hindari manipulasi langsung pada folder proyek utama.

Snapshotting Pola Copy-on-Write (CoW) / Staging Path

  • Staging Sandbox: Sebelum tool saintifik dieksekusi, sub-agent hanya diberi akses write ke temporary staging area: .workbench/checkpoints/{transaction_id}/.
  • Atomic Commit: Jika eksekusi berhasil dan validasi struktur lolos, file dipindahkan secara atomik (menggunakan rename filesystem system call) ke direktori proyek aktif.
  • Rollback Protocol: Jika circuit breaker mencatat kegagalan atau terjadi timeout/stalled process, buang direktori snapshot {transaction_id}. Workspace pengguna kembali utuh pada state stabil terakhir.

Rekomendasi Konfigurasi Timeout

Berikut batas konfigurasi timeout standar yang disarankan untuk workbench desktop:

  • IPC Liveness Heartbeat: 2000 ms interval, 5000 ms timeout threshold.
  • Scientific Tool Execution: Default 60000 ms (dapat dinaikkan per tool annotation via metadata MCP).
  • Graceful Shutdown Window: 3000 ms antara SIGTERM dan SIGKILL.
  • Circuit Breaker Cooldown: 15000 ms dalam status OPEN sebelum transisi ke HALF-OPEN untuk pengujian single ping.

Postmortem Singkat: Deadlock Matplotlib Backend pada Sub-Agent

Insiden: Workbench UI membeku saat menjalankan plotting dataset 5GB via sub-agent Python MCP.

Akar Masalah: Script sub-agent memicu Matplotlib dengan GUI backend default (TkAgg/Qt) alih-alih headless backend (Agg). Sub-agent menunggu loop GUI OS yang tidak pernah diinisialisasi oleh subprocess headless Tauri, menyebabkan blocking deadlock tak terbatas.

Pencegahan & Perbaikan:

  • Injeksi environment variable MPLBACKEND=Agg dan PYTHONUNBUFFERED=1 saat spawning child process dari Tauri backend.
  • Menambahkan batas waktu timeout hard cap 30 detik untuk semua tool bertipe I/O rendering.
  • Mengaktifkan IPC ping socket terpisah dari main thread subprocess runner.