When the agent cannot drive the machine: a symptom-first decision tree
You set up dvv, your agent is in the loop, and something refuses to do what you asked. Most refusals are not the tool failing. They are the tool protecting you from a click that would have landed in the wrong window, a key press that would have typed into a text field you did not see, or a hand off the wheel that you did not notice. Read this tree from the symptom your agent actually hit, not from the part of the protocol you suspect. The shape below is built to get a stuck loop moving again in under a minute, with the exact command for each branch.
If you want the same idea in a different shape, see the refusal index and the four-call loop.
The boxed pattern: read the screen again before you act
One
dvv_screenis what clears every "stale state" refusal. It is also the only call that reads pixels.dvv_statusreports numbers but does not look, so it never clears a fence. Make this the first thing you try, every time.
dvv_screen {"limbId": "<id>", "form": "full", "scale": 0.25}That call carries the new generation and refreshes the content fence, so the click, type, and key calls that follow are accepted again.
Symptom: "the screen looks stale"
The picture you read five seconds ago is the picture the server is still sending. The remote did update, but a network blip, a paused client on the other end, or a wake from sleep held the update for long enough that your generation is now behind.
The one most likely cause: your last dvv_screen is older than the last "something large repainted" event the server saw, and any typing or keys you try are being refused with SCREEN_CHANGED before they even reach the remote. This is the safety feature working: focus has moved, and the tool refuses to type into a window you have not seen.
The exact check: run dvv_wait with until: "screen-stable" and read the return value. If it reports stable but a fresh dvv_screen shows the same image, the connection itself is quiet, not stale.
The exact fix: read the screen again, then retry.
dvv_screen {"limbId": "<id>", "form": "full", "scale": 0.25}
dvv_wait {"limbId": "<id>", "until": "screen-stable"}
dvv_click {"limbId": "<id>", "x": 700, "y": 400, "generation": <from screen>}If the second dvv_screen still shows the old image, the connection is hung. Jump to "the connection keeps dropping" below.
Symptom: "my click was refused"
Your agent called dvv_click and the call came back with a refusal rather than a settlement.
The one most likely cause: the click was computed against a generation that is no longer current. The remote resized, the agent went through a full-screen video, or the screen changed between your dvv_screen and your dvv_click. The click was refused on purpose so it would not land in the wrong place after a resize, and that is the safety feature doing its job.
The exact check: read the geometry_generation field out of the dvv_screen result and out of the dvv_click call. If they differ, that is your refusal.
The exact fix: read the screen again, copy the new generation, and retry the click. Always convert from picture pixels to remote pixels with the current scale. With scale: 0.5, a point at (mx, my) on the picture is (mx2, my2) on the machine.
dvv_screen {"limbId": "<id>", "form": "full", "scale": 0.5}
dvv_click {"limbId": "<id>", "x": <mx*2>, "y": <my*2>, "generation": <g>}Symptom: "typing does nothing"
You sent dvv_type with a string and the remote end shows nothing, or the key sequence in dvv_key did not open the window you expected.
The one most likely cause: something window-sized has repainted since your last dvv_screen, and the fence refuses to let text or keys land in whatever window now has focus. This is the feature that stops an agent typing into a text editor that opened over the one it was looking at, with all of its contents selected. There is no override on this fence, by design.
The exact check: try dvv_screen first. If the call returns an image that is different from the one you read before the type or key call, you have your answer. If the image is identical, the issue is below in "the limb is gone" or "the lease was revoked".
The exact fix: read the screen, then retry. A single dvv_screen clears the content fence, and dvv_status does not.
dvv_screen {"limbId": "<id>", "form": "full", "scale": 0.25}
dvv_type {"limbId": "<id>", "text": "notepad", "wpm": 3000}
dvv_key {"limbId": "<id>", "keys": "Enter"}Symptom: "the lease was revoked"
Your agent has the wheel, then the next call comes back LEASE_REVOKED and nothing you send gets acted on.
The one most likely cause: a person at the remote end took control. Clicking into the pane is enough to take the wheel from an agent-driven session, and any held keys or buttons are released so a half-finished drag cannot strand the desktop. This is the safety feature that lets a human walk up to a misbehaving agent and take over in one click.
The exact check: run dvv_control with action: "yield_status". It returns who has the wheel and whether you can take it back.
The exact fix: if a person is in control, stop and tell the user. That is the only case in the loop where you stop. If the pane is now idle, ask for control again and read the screen, because everything you knew about focus and layout is from before the human moved it.
dvv_control {"limbId": "<id>", "action": "yield_status"}
dvv_control {"limbId": "<id>", "action": "acquire"}
dvv_screen {"limbId": "<id>", "form": "full", "scale": 0.25}Symptom: "the limb is gone"
Your agent has a limbId it was using, and the next call comes back LIMB_GONE. The machine is no longer attached to your peer.
The one most likely cause: the agent spawned a new process per call, and the new process does not see the limb the old process opened. A limb id belongs to the connection that opened it, and dvv mcp --stdio spawned per command attaches and detaches each time. The tool is doing exactly what it should: it will not let a fresh peer take over a session it did not start.
The exact check: run dvv_limbs to see what is open right now. If the machine is not listed, the limb was never reopened on this peer. If the machine is listed under "available", use that limbId directly.
The exact fix: list limbs, then open the machine on the same long-lived peer that will run the rest of the task.
dvv_limbs {}
dvv_open {"hostId": "<id>", "perceive": true}
dvv_wait {"limbId": "<id>", "until": "connected"}Symptom: "files are unavailable"
You called dvv_files to drag a file across, and the call reports that the remote side is not reachable, or the pane is greyed out.
The one most likely cause: the connection is not SSH, and dvv_files needs the SSH transport. VNC and RDP sessions transfer files through their own server-side mechanisms, which DeskVNCViewer surfaces, but the agent-driven dvv_files path is wired only on the SSH limb. That is the shape of the feature: a single tool for the transport that supports it.
The exact check: confirm the host is an SSH profile in the saved library. The same machine opened over RDP will not show the file pane to the agent.
The exact fix: open the same host over SSH, then transfer.
dvv_files {"limbId": "<id>", "action": "list", "remotePath": "/etc"}
dvv_files {"limbId": "<id>", "action": "put", "localPath": "./a.txt", "remotePath": "/tmp/a.txt"}Symptom: "the connection keeps dropping"
The screen goes black, comes back, goes black again, and dvv_screen keeps failing mid-call.
The one most likely cause: the remote woke from sleep, lost its network association, or the VNC server restarted. The client reconnects on its own, with a fast first retry and then backoff with jitter, so brief drops recover without you doing anything.
The exact check: run dvv_wait with until: "connected" and read the return value. It blocks until the limb is attached again or reports a hard failure.
The exact fix: wait for connected, then read the screen, because everything you knew about the layout is from before the drop. If waiting times out, force a reconnect and try once more.
dvv_wait {"limbId": "<id>", "until": "connected"}
dvv_screen {"limbId": "<id>", "form": "full", "scale": 0.25}
dvv_reconnect {"limbId": "<id>"}If dvv_reconnect itself fails three times in a row, the saved host is probably pointing at an address the network can no longer reach. Edit the profile, retest with dvv_open, and carry on.