The VM was free. The complexity was not.

300 seconds.

That is the polling interval on the new local launchd job that replaced my Oracle VM reader runner. The old setup polled at exactly the same cadence, from a remote machine at 129.213.20.247. The only thing that actually changed is where the job runs.

That is a problem I should have caught a year ago.

The x engine reader runner did two things: it fetched data from X on a loop, and it served that data over HTTP so the main engine could consume it. Splitting those two concerns across a network boundary felt principled at the time. Separate concerns, separate machines, clean interface. The VM was free on Oracle’s always free tier, so the overhead felt like a non issue.

It was not free. It was deferred cost.

Every time something broke, I was debugging across two environments. The systemd units on the remote machine needed separate monitoring. deploy.sh existed because “deploy” was a real operation, not a file copy. serve.py existed because HTTP was the only transport that crossed the machine boundary. None of this was complexity serving the product. It was all complexity serving the infrastructure choice.

The fix was straightforward once I admitted the architecture was wrong.

Delete deploy.sh, serve.py, and the systemd units entirely. Add a single launchd plist (com.leviathan.x runner fetch.plist) that fires every 300 seconds with X_READ_ENV=runner and writes output to a local file. The main engine reads that file via X_RUNNER_FILE. HTTP transport gone. The “network boundary” is now a file path.

The surface area shrank considerably. The reader runner went from “remote service with a deployment pipeline” to “local periodic job.” Debugging is now one environment, one log file, one place to look.

The honest tradeoff: a local job means the reader goes down when my machine goes down. The VM had better uptime than my laptop. But the reader fetching X data is not a latency sensitive operation. Missing a 300 second window because the lid is closed does not matter. The fetch catches up when the machine wakes. The VM’s uptime advantage was solving a problem that was not real in this context.

The other thing I cleaned up while touching all this code: docstrings and comments that still referenced the VM architecture. “Fetches data from the remote runner service” is a lie once the remote runner is gone. Dead documentation is worse than no documentation. It misleads the next reader (me, six months from now) into thinking there is a VM to care about. So I went through fetch.py, runner_file.py, syndication.py, base.py, router.py, and config.py and cut every mention of a remote setup that no longer exists.

While I was in there, I also caught that the root CLAUDE.md had stale x engine test instructions and that the LinkedIn test suite had been quietly missing two dev dependencies (responses and pypdf) since a uv workspace migration in May. Neither was urgent. Both were the kind of rot that accumulates when you ship fast and move on.

If I were starting this from scratch: never reach for a remote VM to host a periodic fetch job unless you genuinely need the separate uptime. The “free infrastructure” framing is seductive but every machine boundary is a complexity cost you pay every time you debug. Start local. Add infrastructure only when you have a specific reason the local version fails.

The Oracle VM still exists. I am not deleting it. But the x engine no longer needs it to run, and that is the right direction.

Write a comment