DEV Community

Orquesta𝄢
Orquesta𝄢

Posted on Originally published at getorquesta.com

How Batuta AI Uses ReAct to Debug Servers Over SSH

Originally published at getorquesta.com/blog/batuta-ai-react-debug-servers-ssh

Manual server debugging is a time sink. You SSH in, run a few commands, stare at logs, scratch your head, try a fix, and repeat. It's even worse when the issue spans multiple systems or requires historical context. We built Batuta AI to eliminate this grind. Batuta is an autonomous agent that connects to your servers over SSH, figures out what's wrong, and fixes it—all while streaming every thought and command back to you. The secret sauce is a ReAct loop: Think, Act, Observe, Repeat. Here's how it works in practice.

The ReAct Loop: Think, Act, Observe, Repeat

ReAct is a pattern where the model alternates between reasoning and acting. Batuta uses this loop to interact with a live server. Each iteration follows four steps:

  1. Think – The model analyzes the current state, decides what to investigate next, and formulates a hypothesis.
  2. Act – It executes a command via SSH (e.g., df -h, tail -f /var/log/syslog, systemctl status nginx).
  3. Observe – The output of that command is fed back into the model's context.
  4. Repeat – The model uses the new information to refine its understanding and decide the next action.

This loop continues until the model determines the issue is resolved or it needs human intervention. Unlike simple scripted automation, ReAct allows Batuta to adapt to unexpected findings—the core of real debugging.

How Batuta Connects to Your Servers

Batuta runs as part of the Orquesta agent on your local machine. It doesn't rely on a cloud sandbox; all reasoning happens on your infrastructure. To debug a remote VM, you provide SSH credentials (or use an existing SSH config). Batuta then opens an SSH session and begins the loop.

Security is critical. The agent never stores credentials beyond the session, and all communication is encrypted. You can also limit Batuta's access to specific hosts and even restrict commands it can run (e.g., read-only mode). In practice, we use read-only mode for initial diagnosis, then switch to write mode once we're confident in the fix.

Real Example: Debugging a Full Disk

Let's walk through a real incident I handled with Batuta. A staging server started throwing 500 errors. I typed a one-line prompt:

Batuta, the staging API server is returning 500s. SSH into staging-api and find out why.
Enter fullscreen mode Exit fullscreen mode

Batuta connected and began its ReAct loop. Here's a condensed trace of its reasoning and actions:

Think: The 500 error suggests an application-level issue. Check disk space first; full disks often cause write failures.
Act: df -h /
Observe: /dev/sda1 50G 49G 0% / – disk is 98% full.
Think: Disk is almost full. Find the largest directories.
Act: sudo du -sh /var/* | sort -rh | head -n 10
Observe: /var/log is 30G, /var/lib/docker is 15G.
Think: Logs are likely the culprit. Check for oversized log files.
Act: sudo find /var/log -type f -size +1G -exec ls -lh {} \;
Observe: /var/log/nginx/error.log is 28G.
Think: The nginx error log has grown out of control. It's probably filled with repeated errors. Check the most recent entries to see if there's an underlying issue.
Act: sudo tail -n 20 /var/log/nginx/error.log
Observe: The log shows thousands of lines like [crit] 1234#1234: *1234 connect() to unix:/var/run/php-fpm.sock failed (2: No such file or directory).
Think: PHP-FPM socket is missing. That's causing nginx to fail and log errors rapidly. Check if PHP-FPM is running.
Act: systemctl status php-fpm
Observe: php-fpm service is inactive (dead).
Think: PHP-FPM crashed or was never started. Restart it and verify.
Act: sudo systemctl start php-fpm && sudo systemctl status php-fpm
Observe: php-fpm is now active (running).
Think: PHP-FPM is back. The log will stop growing. But we still have a 28G log file. We should truncate it to free space. Also check if disk usage is now acceptable.
Act: sudo truncate -s 0 /var/log/nginx/error.log && df -h /
Observe: Disk usage dropped to 70%. The 500 errors should resolve.
Think: The root cause was PHP-FPM failure. The full disk was a consequence. Need to verify the application is responding correctly.
Act: curl -I http://localhost/health
Observe: HTTP/1.1 200 OK
Think: Issue resolved. Summarize findings and actions.

Batuta then produced a concise summary of what happened, the commands it ran, and the fix. In less than two minutes, the server was back to normal.

Multi-Step Debugging: A Crash Loop Mystery

Another case involved a Node.js service that kept crashing every few minutes. Batuta's loop looked like this:

Act: journalctl -u node-app --since "10 minutes ago" --no-pager | tail -n 50
Observe: The logs showed FATAL ERROR: Ineffective mark-compacts near heap limit Allocation failed - JavaScript heap out of memory.
Think: The service is running out of memory. Check the current memory limit and usage.
Act: ps aux | grep node-app and cat /etc/systemd/system/node-app.service
Observe: The process has --max-old-space-size=512. The systemd unit has no memory limit.
Think: The Node.js heap limit is too low for the workload. Increase --max-old-space-size to 2048 and restart. Also consider adding a systemd memory limit to prevent OOM kills.
Act: Edit the service file to set Environment=NODE_OPTIONS=--max-old-space-size=2048, then systemctl daemon-reload && systemctl restart node-app.
Observe: Service restarted successfully. Memory usage initially stable.
Think: Monitor for a few minutes to confirm no further crashes.
Act: After waiting, systemctl status node-app and journalctl -u node-app --since "5 minutes ago"
Observe: No crash, service running normally.

Batuta even suggested adding a health check to alert if the service restarts too frequently. That's the kind of proactive insight that makes autonomous debugging valuable.

Handling Non-Determinism and Safety

One challenge with autonomous agents is that they can make mistakes or take destructive actions. We built several safeguards into Batuta:

  • Read-only mode for initial diagnosis. Batuta can only run commands that don't modify state (e.g., ls, cat, grep).
  • Command allowlist/denylist per host. You can block dangerous commands like rm -rf or shutdown.
  • Human approval for critical actions: By default, Batuta asks for confirmation before running commands that change system state (like systemctl restart). You can configure it to auto-approve in trusted environments.
  • Audit trail: Every command, output, and reasoning step is logged. You can replay the entire session to see exactly what happened.

We also added cost controls. Long loops can burn tokens, so Batuta has a maximum iteration count and a token budget. If it exceeds these, it stops and summarizes what it knows, asking for guidance.

The Future of Autonomous Ops

Batuta isn't just a fancy script runner. The ReAct loop enables it to handle novel situations that scripted automation can't. It can correlate symptoms across different services, consult man pages, and even write small scripts on the fly to gather data. We've seen it debug issues ranging from misconfigured DNS to race conditions in application code.

For engineering teams, this changes the nature of on-call work. Instead of SSHing into boxes at 3 AM, you approve a prompt and go back to sleep. Batuta handles the grunt work, and you get a detailed report in the morning.

The key takeaway: autonomous server debugging is not about replacing engineers—it's about compressing the time from symptom to resolution. The ReAct loop gives Batuta the flexibility to be a true debugging partner, not just a tool. And that's a meaningful step toward self-healing infrastructure.

Top comments (0)