Two stacked failures on the cad node, three days apart, surfacing as one outage. (1) https://cad.taild4189d.ts.net:8443/mcp returned HTTP 502 because the freecad-mcp proxy container had been Exited (128) for three days and its restart: unless-stopped policy never fired. (2) With the proxy back, execute_code still timed out — and this time the remedy in 2026-09-02-freecad-mcp-gui-dispatch-timeout-modal-dialog was not sufficient. All commands and outputs below were verified in-session on 2026-09-05.
The two traps in one line each
restart: unless-stoppeddoes NOT guarantee a container comes back — if the containerd shim dies before the task is created, there is no task to restart and the container stays dead indefinitely withRestartCount: 0. Killing the modal dialog does NOT always unwedge Qt — after ~3 days behind a modal, closing the window left the event loop just as dead, withgui_dispatchstill self-reportinghealthy.
Host: telep-mainframe, containers freecad-mcp (proxy) and freecad (GUI) — the cad node stack from 2026-08-26-freecad-cad-workstation. Third chapter after 2026-09-02-freecad-mcp-rpc-refused-gui-not-running and 2026-09-02-freecad-mcp-gui-dispatch-timeout-modal-dialog.
Symptom chain
- Claude Code reported the
freecadMCP server as failed at startup. https://cad.taild4189d.ts.net:8443/mcpreturned HTTP 502 onPOST /mcp.https://cad.taild4189d.ts.net:8443/returned HTTP 502 as well.
A 502 on both paths is the discriminator: it is not a routing/path problem inside the MCP app, it is the whole backend being absent. Note this is a different fingerprint from the two 2026-09-02 failures — those produced Errno 111 and RemoteDisconnected on :9875. A 502 on :8443 means the tailscale serve mount is up and its target is not.
Failure 1 — the proxy container had been dead for three days
Diagnosis — the serve mount was fine, the target was empty
tailscale --socket=/var/run/tailscale-cad/tailscaled.sock serve status:8443 -> http://127.0.0.2:9876
The mount was intact and correct. Nothing was listening on 9876. Serve was faithfully proxying to a hole, which is exactly what produces a 502.
Note the loopback:
127.0.0.2, not127.0.0.12026-08-26-freecad-cad-workstation documents this mount as
127.0.0.1:9876; the live config on 2026-09-05 ishttp://127.0.0.2:9876, i.e. it was migrated to the127.0.0.2convention from 2026-08-31-tailnet-plaintext-port-hardening. Same class of.1-vs-.2split that silently 502’d Homepage’s:8450mount (see 2026-09-02-lg-tv-network-control-presence). When a tailnet mount 502s on this host, readserve statusand check which loopback the backend actually binds — do not trust the note’s port map.
The next command found it:
docker ps -afreecad-mcp Exited (128) 3 days ago
docker ps alone would have hidden this — an exited container is invisible without -a. That one missing flag is the whole gap between “the proxy is broken” and “the proxy is not running.”
Root cause — the shim failed before the task existed
docker inspect freecad-mcp| Field | Value |
|---|---|
ExitCode | 128 |
Error | failed to create task for container: failed to create shim task: ttrpc: closed |
OOMKilled | false |
FinishedAt | 2026-09-02T03:27:08Z |
RestartCount | 0 |
Two things to read out of that table:
OOMKilled: falserules out the reflexive memory explanation. This was not a resource kill.RestartCount: 0is the proof that the restart policy never even tried. The compose service carriesrestart: unless-stopped, yet Docker made zero restart attempts across three days.
Why: the failure is failed to create task ... failed to create shim task: ttrpc: closed — the containerd shim died during task creation, so a running task was never established. Docker’s restart policy supervises a task that has started and then exited; with no task ever created there is nothing for the policy to act on. The container simply comes to rest in Exited (128) and stays there.
Note also the timestamp: FinishedAt is 2026-09-02T03:27:08Z — the same night as the FreeCAD crash behind 2026-09-02-freecad-mcp-gui-dispatch-timeout-modal-dialog. Both containers were disturbed in the same event.
restart: unless-stoppedis not a guarantee of livenessThis is the reusable lesson and it generalises well beyond FreeCAD. A container with a restart policy can stay dead indefinitely if the runtime failed below the task layer. Health-checking by “does it have a restart policy?” is not health-checking.
Whenever a tailnet-served service returns 502, run
docker ps -aearly and look forExitedcontainers. It costs one command and it is the single highest-yield probe for this failure class.
Fix
docker start freecad-mcpIt came up clean and 9876 began listening immediately. No rebuild, no recreate, no compose changes — the image and config were never the problem.
Failure 2 — the GUI thread was STILL wedged, and this is a CORRECTION
With the proxy restored, the MCP tools answered again — and landed straight into the second failure mode, the one 2026-09-02-freecad-mcp-gui-dispatch-timeout-modal-dialog already documents:
// mcp__freecad__get_rpc_status
{"rpc_server": "running", "gui_dispatch": {"state": "healthy", "task_id": 0}}…while every execute_code failed with GUI dispatch timed out after 90s.
Isolating it away from the MCP proxy
Because the proxy had just been the culprit, the first job was to prove the proxy was not still implicated. Direct XML-RPC to 127.0.0.1:9875, bypassing freecad-mcp entirely:
| Direct XML-RPC call | Result |
|---|---|
ping() | returned in 0.0 s |
list_documents() | returned in 0.0 s — value [] |
execute_code("print(42)") | 90 s timeout |
The RPC thread was completely healthy; only GUI-dispatched work was blocked. This cleanly exonerated the proxy and confirmed the fault was inside the FreeCAD process, exactly as the 2026-09-02 note predicted.
The dialog was there — but DISPLAY is :1, not :0
docker exec -u abc -e DISPLAY=:1 freecad xwininfo -root -tree0x400075 "Document Recovery"
The documented culprit, still standing — the same window, alive since the 2026-09-02 crash.
CORRECTION to 2026-09-02-freecad-mcp-gui-dispatch-timeout-modal-dialog — the display is
:1That note’s commands all use
DISPLAY=:0. The correct value isDISPLAY=:1. A:0socket does exist in the container, which is why the wrong value looks plausible, butxwininfocannot open it — you get a failure to connect and it reads as “no X server / no windows”, not as “wrong display number”. Use:1.
The correction that matters — closing the window did NOT fix it
The 2026-09-02 remedy is dismiss the dialog, and explicitly do not restart. That remedy was tried here and it was not sufficient.
docker exec -u abc -e DISPLAY=:1 freecad xdotool windowclose 4194421 # 4194421 == 0x400075The window was removed — verified gone from a re-run of xwininfo -root -tree. And then:
execute_codestill timed out at 90 s,- with no dialog present anywhere in the window tree,
- and
gui_dispatchstill self-reportinghealthy.
The Qt event loop stayed wedged. After roughly three days parked behind that modal, removing the window did not revive the loop. The dialog was the cause of the wedge, but by this point it was no longer the thing holding it — destroying it freed nothing.
Do not stop at "the dialog is gone"
xwininfoshowing a clean window tree is not proof the fix worked. The only proof isexecute_codereturning. If it still times out with no dialog on screen, the event loop is dead and no amount ofxdotoolwill bring it back — escalate to the stash-and-restart below.
What actually worked — stash the recovery snapshots, THEN restart
Inside the container as user abc:
# 1. Move (never delete) every recovery snapshot out of FreeCAD's sight
mkdir -p /config/.cache/FreeCAD/recovery-stash-2026-09-05
mv /config/.cache/FreeCAD/v1-1/Cache/FreeCAD_Doc_*/ \
/config/.cache/FreeCAD/recovery-stash-2026-09-05/# 2. Only then restart
docker restart freecadResult: RPC was back in ~10 s and execute_code returned in 0.0 s.
There were 10 FreeCAD_Doc_* directories, dated Aug 31 – Sep 2.
Why the stash is the load-bearing step
This resolves the apparent contradiction with the earlier note. The reason 2026-09-02-freecad-mcp-gui-dispatch-timeout-modal-dialog says “do NOT restart” is real and still correct as stated: a restart re-raises the Document Recovery dialog while recovery files are present, dropping you straight back into the wedge you were trying to escape. Restarting alone is a loop.
Moving the recovery files out first breaks the loop. With nothing in Cache/FreeCAD_Doc_*/, FreeCAD has no recovery to offer, comes up without the modal, and the fresh process gets a working event loop. The restart is not the fix — the stash is the fix, and the restart is how you collect it.
MOVE the recovery dirs — NEVER delete them
Those snapshots are the user’s only copy of any unsaved work.
mvthem into a dated stash directory; do notrm, and do not let a cleanup script near them. The stash is cheap; the data is not replaceable.
For Agents — when is the restart safe?
list_documents()returned[]— nothing was open in the GUI. That is what made the restart safe to run. Checklist_documents()before restarting: an empty list means no in-memory document state is at risk. A non-empty list means live documents whose unsaved state a restart would discard — stop and ask the human.
Open item — 10 stashed recovery dirs awaiting a decision
Needs the user's call
Path:
/config/.cache/FreeCAD/recovery-stash-2026-09-05/(inside thefreecadcontainer) Contents: 10FreeCAD_Doc_*directories, dated Aug 31 – Sep 2 Status: untouched, nothing deleted.They are either restored (move a dir back under
/config/.cache/FreeCAD/v1-1/Cache/and let FreeCAD offer recovery — expect the modal, so do it deliberately) or cleaned up. Until the user decides, leave the stash exactly where it is.
cad host access — confirmed, and the practical route
Confirmed:
cadis NOT Tailscale-SSH-enabledRe-verified 2026-09-05. Only telep-mainframe and telep-router carry Tailscale SSH.
cad(100.120.203.1) is online intailscale statusbut is not in the SSH-enabled machine list — as recorded in 2026-09-02-freecad-mcp-rpc-refused-gui-not-running.
This does not mean the box is unmanageable remotely. ssh 100.120.203.1 works — because cad is not a separate machine. It is a userspace tailscaled sidecar node running on telep-mainframe, and every per-service sidecar node (cad, knowledgebase, drive, chatcut, orcaslicer, bambuddy, …) lives on that same bare-metal host. SSHing to any of their tailnet IPs lands you on telep-mainframe, which is where the docker containers are anyway.
This is consistent with the userspace-networking raw forwarder documented in 2026-08-31-tailnet-plaintext-port-hardening — inbound TCP on a sidecar node’s tailnet IP is forwarded to 127.0.0.1 on the same port, so port 22 arrives at the mainframe’s own sshd.
ssh 100.120.203.1 # lands on telep-mainframe
ssh levander@100.115.209.87 # telep-mainframe directly — same destination
cd /home/levander/freecad # the compose stackBoth routes reach the same place. Use whichever is at hand; do not conclude from “cad has no Tailscale SSH” that this needs a person on site. Every command in this note was run remotely.
Small gotcha — command -v with multiple arguments
Chasing whether xdotool was even installed, this looked like a definitive “no”:
command -v xdotool xwininfo wmctrl # ← misleadingUnder the container’s dash, command -v only reports on the FIRST argument. The extra names are silently ignored, so a missing first binary makes every tool in the list look absent. xdotool had been installed all along.
for b in xdotool xwininfo wmctrl; do command -v "$b" || echo "MISSING: $b"; doneUse a loop, or one command -v per binary. This wasted a detour into “we need to install a window manager tool” on a container that already had one.
Revised triage flowchart
Supersedes the flowchart in 2026-09-02-freecad-mcp-gui-dispatch-timeout-modal-dialog — same tree, extended at both ends.
FreeCAD MCP unusable
│
├─ HTTP 502 on https://cad...:8443/ (both /mcp and /)
│ → the serve mount is up, the backend is not
│ 1. tailscale --socket=/var/run/tailscale-cad/tailscaled.sock serve status
│ (confirm target — it is 127.0.0.2:9876, NOT .1)
│ 2. docker ps -a ← the -a is essential
│ 3. Exited? docker inspect <c> → RestartCount:0 + "ttrpc: closed"
│ = the shim died pre-task, the restart policy never fired
│ 4. docker start freecad-mcp
│
├─ get_rpc_status fails (Errno 111 / connection refused)
│ → RPC/GUI is DOWN → docker restart freecad
│ [[2026-09-02-freecad-mcp-rpc-refused-gui-not-running]]
│
└─ get_rpc_status "healthy" but execute_code times out at 90s
→ GUI thread blocked. Isolate first: direct XML-RPC to 127.0.0.1:9875.
ping()/list_documents() fast + execute_code timing out
⇒ the fault is in FreeCAD, not the proxy.
1. docker exec -u abc -e DISPLAY=:1 freecad xwininfo -root -tree (:1 !!)
2. dialog present, wedged for MINUTES → xdotool windowclose <id>, retry
3. NO dialog, or dialog closed and execute_code STILL times out
(i.e. wedged for HOURS/DAYS) → the event loop is dead:
a. list_documents() must be [] ← safety check
b. mv /config/.cache/FreeCAD/v1-1/Cache/FreeCAD_Doc_*/ \
/config/.cache/FreeCAD/recovery-stash-<date>/ (MOVE, never rm)
c. docker restart freecad
Restarting WITHOUT (b) re-raises the dialog and re-wedges.
Related
- 2026-09-02-freecad-mcp-gui-dispatch-timeout-modal-dialog — the note this one corrects:
DISPLAYis:1not:0, and dismissing the dialog is not always sufficient - 2026-09-02-freecad-mcp-rpc-refused-gui-not-running — the first chapter: RPC refused entirely, the
ncfalse positive - 2026-08-26-freecad-cad-workstation — the FreeCAD + noVNC + MCP architecture, port map,
/exportspipeline - 2026-08-31-tailnet-plaintext-port-hardening — the
127.0.0.2convention and the userspace-networking raw forwarder - 2026-09-02-lg-tv-network-control-presence — the sibling
127.0.0.1-vs-127.0.0.2tailnet-mount 502 - 2026-08-26-cad-designer-agent — the on-prem agent that drives this MCP
- telep-mainframe — the host running every one of these containers and sidecar nodes
- service-unreachable · runbooks-index
- LOG · TOPICS