0xC0000005 — memory the C side touched and shouldn’t have. Python can’t
catch it; the interpreter is already a corpse by the time anything is written to
disk. The culprit, buried in ComfyUI’s on-disk log, was an fp8 merge_loras path
inside ComfyUI-WanVideoWrapper: apply_lora at utils.py:338.
Here’s the part that matters. The Panel agent — the autonomous
assistant living in the ComfyUI sidebar — did not shrug and say “ComfyUI
restarted, want me to run it again?” It read the corpse, named the killer, and
fixed it. This post is how that loop works, end to end, because every piece of it
is real code you can read in the repo.
The blind spot: a native crash the agent never sees
When a normal node throws, the agent sees the error and can reason about it. A native fault is different. ComfyUI dies; the panel’s only signal is the socket dropping and, moments later, “reconnected.” From the agent’s point of view the render simply… didn’t finish. It has no idea why, so its instinct is the worst possible move: re-run the exact same graph and crash again. The faulthandler dump that explains everything is sitting right there inlogs/comfyui.log — the agent just isn’t looking at it. So we made it look.
Reading the corpse: crash-log parsing on resume
The panel auto-sends a fixed “resume” nudge after ComfyUI comes back (”✅ … restarted … continue where we left off”). The orchestrator keys off that nudge (isResumeNudge) to do one thing before
handing the turn to the agent: read the tail of ComfyUI’s log and look for a crash
signature.
That’s src/services/crash-log.ts. It’s
deliberately boring and pure (no I/O in the parser, fully unit-tested):
- It scans only the last 256 KiB of the log, and only treats native
signatures as fatal —
Windows fatal exception,access violation,Segmentation fault,Fatal Python error. A bare Python traceback is an ordinary handled error, not a process crash, so it’s ignored. (Otherwise a routine node error sitting in the log would get mis-reported as a “crash” on some later restart.) - It anchors on the most recent signature, so an old crash earlier in the tail can’t shadow a clean recent run.
- It extracts the culprit custom node and file:line by walking the traceback
for the deepest
custom_nodes/<NodeDir>/<file>.py:<line>frame. Trace order matters: faulthandler prints “most recent call first,” so it takes the top frame; a standard Python traceback prints “most recent call last,” so it takes the bottom one. For our crash that resolves to exactlyComfyUI-WanVideoWrapper→apply_loraatutils.py:338— the frame that actually faulted, not itsloadmodelcaller. - It fingerprints the crash (signature head + culprit) so a given fault is injected into the agent once, not re-surfaced on every later resume while it slowly scrolls out of the log tail.
apply_lora access-violated at
utils.py:338 — fix it before retrying.”
The escalation ladder
The system prompt the panel agent runs under (PANEL_SYSTEM_APPEND) has a whole
CRASH RECOVERY clause that turns that note into a procedure. When a turn opens
with a crash note (or a run dies with a node-level error pinned to one pack), the
agent does not re-run the same graph. It escalates, narrating each step in the
sidebar:
- Update the node.
panel_update_nodeon the culprit’s id — the built-in Manager pulls the latest code. Crucially, it tries versionnightlyto grab a just-landed upstream fix. Pollpanel_node_queue_status, thenpanel_restart_comfyui, and on resume retry the exact action to see if the crash is gone. - Go to the source. If updating didn’t help, the agent reaches into
COMFYUI_PATH/custom_nodes/<NodeDir>with its shell. If it’s a git repo it runsgit fetch && git pull(or checks out the nightly branch), reinstalls requirements if needed, restarts, and retries. - Patch the file:line. If there’s no git or it’s still broken, the agent
attempts a targeted source patch of the crashing frame — for our case, the
fp8 merge path in
utils.py— then verifies by restarting and re-running the same action to confirm it no longer faults. - Offer it upstream. Once a patch is verified, the agent offers to send the fix to the repo owner — an issue or PR describing the crash and the patch. It describes it and asks first; it never auto-files anything on your behalf.
The same loop powers install → restart → continue
Crash recovery isn’t a special case; it’s the same autonomy the agent uses to install nodes in the first place. When a workflow needs a custom node you don’t have, the agent doesn’t silently skip it. Using the built-in Manager tools:panel_search_nodesto find the pack,panel_install_nodeto install it,panel_node_queue_statusto confirm it finished,panel_restart_comfyuito load it — after which the panel auto-reconnects and the agent resumes automatically, carrying on with what it was building.
The busy guard: it won’t reboot mid-render
There’s one hard rule wired intopanel_restart_comfyui: a restart aborts any
in-progress or queued generation. So the tool has a busy guard — if ComfyUI is
generating, it refuses and tells the agent, rather than killing the render. The
agent is steered to tell you a render is running and wait for the queue to drain;
the only way past the guard is force:true, and only if you explicitly agree to
kill the running job. Self-healing, but never at the cost of the work you’ve got
in flight.
When the render doesn’t crash — it just wedges
A native crash is loud: the process dies and leaves a corpse to read. The quieter failure is a render that wedges — a high-res sampler step that stops advancing but never dies. ComfyUI only checks its interrupt flag between nodes, so a stuck multi-minute step ignores a polite cancel, and the agent, blind to it, does the worst thing again: it stacks more jobs behind the zombie. The same self-healing instinct handles this, with three best-effort guards:- Backpressure.
panel_runnotices a render is already running and appends a QUEUE WARNING to its own result — so the agent stops piling on. - Stall detection. A passive WebSocket to ComfyUI watches the running prompt, node,
and progress. When a step re-emits the same progress past a threshold
(
COMFYUI_MCP_STALL_S, default 180s — high, because video steps are legitimately slow), a one-line STALL/BACKLOG note is prepended to the agent’s next turn. - Escalating cancel.
queue(action:“cancel”) doesn’t trust the interrupt. It interrupts, then verifies the job actually stopped (withinCOMFYUI_MCP_INTERRUPT_S, default 30s); if it didn’t, it escalates to/free, and if it still won’t die it reports the render WEDGED and points atrestart_comfyui.clear_pendingdrops the queued backlog in the same call.
Bonus: the agent can watch its own videos
One more piece that makes the loop honest. An agent that makes video has a problem: it can generate a clip but it can’t see whether the clip is any good. So when a run finishes on your canvas, the panel feeds the result back to the agent as a turn (injectEvent in
panel-agent.ts). For a still image, the output is attached inline as a real
image block — the agent literally sees the render, no fetch required. For a
video, the panel samples it into a storyboard contact sheet and attaches
that, with a note that tells the agent accurately what it’s looking at — a
grid of sampled frames from a video, not a single still. The agent replies with a
one-line read on the result and a sensible next step.
That closes the loop in the other direction: it can judge its own output, notice a
clip that came out wrong, and offer to iterate — the same way it noticed the crash
and offered to fix it.
Why this is the interesting part
Plenty of tools can install a ComfyUI node. The hard, rarely-solved problem is what happens when a node crashes the whole process with a fault Python can’t even catch — the moment most automation gives up and hands you a stack trace, if you’re lucky. The self-healing path here is small and legible:Run an autonomous agent that recovers from its own crashes: install comfyui-mcp and add the Panel. Star the repo or file an idea at artokun/comfyui-mcp.