First, what a Fungi Computer actually is, because it’s easy to get wrong. A
Computer isn’t a machine. It’s a durable Workspace: one Durable Object (a small
program on Cloudflare with its own database) that holds the agent’s files. That
Workspace is the one true copy. Code runs against it through backends. Every
Computer has a just-bash shell in a Dynamic Worker, which sends each file
operation straight to the Workspace. No machine, nothing to keep awake. When an
agent needs real processes, like npm install or a dev server, the Computer
plugs in something heavier: a Cloudflare container, or a Firecracker microVM (a
tiny, real virtual machine) on our own box.
This note is about that VM, and what “asleep” means for it. Fifteen minutes after the agent’s last turn, the VM is supposed to stop. The files are fine, because they never lived only in the VM. The processes stop. The next turn starts a fresh VM against the same Workspace.
Sleep sounds like the boring part. It’s actually the bill. A VM that’s up is using memory on a real box, and we meter a Computer’s heavy runtime from start to stop, per backing, into the Team’s ledger. A VM that can’t fall asleep would charge someone for doing nothing.
So before any Team got near the VM backing, we ran it end to end on the box, over and over, to prove it slept and woke properly. At first it didn’t sleep, for three different reasons, one after another. Finding those is exactly what the test runs were for.
(The other half of this, who wakes an agent back up, is in the restart note.)
How the host decides a VM is idle
The service that runs the VMs is called vmd, and its rule is dumb on purpose.
Every VM has a network tap, basically a virtual cable between the VM and the
host. Linux counts the bytes on that cable, in
/sys/class/net/<tap>/statistics. Once a second, vmd reads rx_bytes and
tx_bytes. If the number moved, the VM is busy. If nothing moved for the whole
sleep window, vmd stops it.
I like that rule because the VM can’t talk its way out of it. vmd believes the wire, not the VM. Which is also why the next three problems were hard to see: something kept touching the wire, or something kept vmd from looking.
Problem one: a call that never hung up
In an early test run, a VM stayed up more than thirty minutes after its turn. The byte counters were flat. vmd’s log showed no attempt to sleep and no error.
The only thing still holding it was a request the VM had opened through the host and never closed. vmd counted an open request as “busy” for as long as it stayed open. So one idle keep-alive connection meant the VM could never sleep.
Now a request counts as activity when it starts and when it ends, and not in
between. Bytes that actually move still keep the VM awake, because they move the
counters. The cleanup is hooked up before the first await, so a request that
ends early can’t leave the count wrong.
Problem two: the question nobody answered
With that fixed, the VM did go to sleep, but about 33 minutes after boot, instead of 15 minutes after its last turn. Weird number. And it didn’t matter when the turn ended.
The health page showed the idle clock resetting at about 4, 8 and 15 minutes. No request was open. The VM’s network connections had been idle for over eight minutes. The long-lived stream between the Hub and the VM was quiet too; we traced it and saw nothing.
The byte counters, checked every second, gave it away. Each reset was exactly one packet, 70 bytes, from the VM to the host. No reply.
It was an IPv6 router solicitation: 14 bytes of Ethernet, 40 of IPv6 header, 16 of ICMPv6. When a Linux machine has IPv6 turned on, every so often it asks the network, “hey, is there a router here?” Each time nobody answers, it waits longer before asking again, until the gap is about fifteen minutes. Our VM network is IPv4 only, so nobody ever answered. The VM kept politely asking, each question put 70 bytes on the wire, and vmd reset the idle clock every time. The asking schedule ended up racing the fifteen-minute sleep window, so some VMs slept late and some never slept at all.
The fix is one boot setting: ipv6.disable_ipv6=1. I picked disable_ipv6 over
the harsher ipv6.disable on purpose. IPv6 sockets still exist, so a server in
the image that listens on [::] still starts up and still takes IPv4 traffic. A
test checks that the setting is in the boot line, and the boot line has a
comment saying why.
Problem three: waking up to a dead phone line
The next run finally slept after 900 seconds idle. Then the wake-up turn never showed up: no start request reached vmd within four minutes.
The Hub talks to the file and shell service inside the VM through one long-lived stream. vmd passed that stream along using a connection it wasn’t keeping track of. A stopped VM doesn’t say goodbye (no TCP FIN), so on the Hub’s side the stream just stayed open forever. The Computer kept talking down a line to a VM that wasn’t there anymore, instead of starting a new one.
Now that connection is one of the VM’s tracked sockets, so it gets torn down when the VM stops. A stream that breaks partway through is destroyed rather than quietly ended, so the caller sees a failure. And an upgraded connection closes both ends when either one closes.
What “asleep” has to mean
All three fixes are in our integration branch, with tests. Since then, a sleeping VM wakes for a new turn in about a second and a half, and an idle VM uses a few hundred megabytes of memory while it waits to fall asleep.
Here’s what I took from it. For something to really sleep:
- The host has to notice it’s idle from the outside. Trust a signal the VM can’t fake, like bytes on the wire, and give the operator a page that shows the clock.
- Nothing the VM does on its own can count as work. Background chatter, keep-alives, “is there a router?” all move bytes. Decide which of those are real work, and switch off the rest at the source.
- Everything talking to it has to notice when it’s gone. Something that disappears without a goodbye leaves open lines behind. Tie every connection to the life of the thing on the other end.
Our first build got the first one right. The test runs found the other two.
This is a narrower lesson than it looks, and that’s fine. Most of what an agent does in a Computer, reading and writing files or running a quick shell command, never touches a VM at all. The VM is for the heavy stuff. But when it is up, every minute it stays up by mistake is a minute someone would pay for. That’s why we chased a 70-byte question before anyone got billed for it.