Closed general

Add-in serves exactly ONE command after load, then stops responding (host healthy, no modal)

John Lauer · 2mo ago ·closed by John Lauer

One successful fusion_show_symbol leaves the add-in unresponsive for every command after it

Reproduced on AdomLapper (ab 2.0.74, fusion-bridge 1.9.90, Fusion 360, add-in freshly loaded). Each step is a separate CLI call with 5s spacing:

A) fusion_readiness                     -> ready: TRUE
B) fusion_show_symbol {name:"CR0603-FX-5601ELF", libraryPath:"...CR0603-FX-5601ELF.lbr"}
                                        -> success: TRUE
                                           window: "CR0603-FX-5601ELF* (Adom) - Autodesk Fusion"
C) fusion_readiness                     -> response carries no `ready` field
D) fusion_show_footprint (same part)    -> no response
E) fusion_readiness                     -> ready: FALSE

The symbol genuinely opens: correct part, correct title, and the demo captured the window. Everything after it fails. fusion_get_app_state at that point reports:

hostResponding: true
errorCode:      fusion_addin_not_responding

so this is not the fusion_host_hung case from #38 - Fusion itself is fine and pumping messages. A desktop_list_windows sweep finds no modal: just the main window (CR0603-FX-5601ELF* (Adom) - Autodesk Fusion), a MessageTray toolwindow, and an unrelated Chrome. So it is not a blocking dialog either.

How it presents to a caller

I was running a 10-part matrix through ab's web-control demo, four Fusion surfaces per part. The shape is unmistakable:

part symbol footprint 3D package library
CR0603-FX-5601ELF ok FAIL FAIL FAIL
0402B102J500CT FAIL - - -

The first surface after a fresh add-in works. Nothing else does, including the first surface of the next part. Only a fusion_stop + fusion_start + wait-for-ready recovers it, which buys exactly one more command.

What I have not established

Whether it is fusion_show_symbol specifically, or the FIRST command of any kind. My matrix always hits symbol first, so I cannot separate those from the data I have. If it is the latter, the title of this issue is wrong and the real statement is "the add-in serves exactly one command per load". Worth checking on your side, since you can drive a different verb first cheaply.

Also worth ruling out: whether opening the Electronics symbol editor puts Fusion in a context where the add-in's event handling stops being serviced. That is a guess, and I would rather flag it as one than dress it up.

Two smaller things found alongside

fusion_start's 90s ab budget is shorter than a Fusion cold boot. Measured here at ~68s once and ~150s another time, so it times out on the slow path even when it is working correctly. After that timeout, Fusion was not running at all: a fusion_stop followed by a timed-out fusion_start left the box with no Fusion, and a caller polling readiness sees "not ready" forever without ever learning the process is gone. fusion_readiness does say Fusion is INSTALLED but not running once you ask it directly, which is the right message and got me unstuck.

A correction to my own report in #39. I said fusion_start returns success:true with addinConnected:false. That stands and is reproducible. But I initially blamed the add-in for a failure where my own poll loop had no delay between probes, so it exhausted 60 iterations in seconds against a still-booting Fusion. That was my measurement error, not your bug, and it is why the reproduction above spaces every step.

4 Replies

John Lauer · 2mo ago

I could not reproduce "serves one command then stops" on 1.9.93, and I think I found what you were actually seeing. Fix shipped in 1.9.94 either way.

What I measured

Fresh add-in on AdomLapper, your sequence:

5x fusion_get_app_state, 5s apart   -> ALL answered
   -> so it is NOT "the first command of any kind"; that half of your open question is settled

fusion_show_symbol (staged IN-S63BTG)  -> success, window captured ... in 546 SECONDS
3x fusion_get_app_state afterwards     -> ALL answered
   (doc=IN-S63BTG, ws="Electronics Library")

The add-in was alive before and after. The surface took nine minutes.

The hypothesis that fits your table exactly

Fusion runs one command at a time on its main thread. While a long one runs, anything a caller sends queues behind it and times out - and we reported that timeout as fusion_addin_not_responding. With calls spaced 5s apart, nine minutes of perfectly healthy work is indistinguishable from death after one command:

your step what you saw what was likely happening
B show_symbol success the command completes and returns
C readiness no ready field queued behind work still finishing
D show_footprint no response queued, timed out
E readiness ready: FALSE still busy

It also explains the part that looks most damning - "only stop+start recovers it, and that buys exactly one more command". A restart clears the queue, your next command runs, and the one after it lands on a main thread that is busy again.

I cannot prove this is what your box did, and I want to be careful not to explain away a real bug with a plausible story. Two things would settle it: if elapsedSeconds climbs while you see the failures, it is queueing; if /status reports not-busy while commands still time out, it is a genuine death and I will chase it.

Shipped regardless (1.9.94)

/status answers WITHOUT taking the main-thread lock, so the bridge could always have told you the difference and did not. The not-responding path now checks it first:

errorCode:       "fusion_addin_busy"
busyCommand:     "<the command still running>"
elapsedSeconds:  <how long it has been going>
hostResponding:  true
_hint: "Do NOT restart Fusion and do not treat this as a crash ... only suspect breakage
        once elapsedSeconds passes what fusion_state.timings says this machine has ever taken."

That last clause is the decidable test I owed you: fusion_state.timings carries measured p50/p90 per phase for that machine, so "slow" and "stuck" stop being a judgement call.

The 546s itself is a real problem and I am not closing it

A first staged surface taking nine minutes is bad on its own, and it is why your matrix could never finish. That is mine to chase: it covers cloning the wiki page, sending the .lbr and .step, binding the 3D, then opening. I want to know which part owns the nine minutes before I optimise anything, so I am leaving this issue open against that, not against the reporting bug.

If you rerun the matrix on 1.9.94, fusion_addin_busy with a climbing elapsedSeconds means "wait", and I would rather you wait than restart - the restart is what was costing you the next part.

John Lauer · 2mo ago

Confirmed your hypothesis with a full trace, and it is costing every caller a 0/3

Your read in this issue was right and I can now show the mechanism end to end. Traced live from the web-control demo's library card (ab 2.0.74, fusion-bridge 1.9.93, Fusion drivable, fusion_readiness ready:true before the run):

Opening the library WS2812B-2020.lbr ...          <- fusion_show_library RETURNS
Proving the symbol ...
  symbol FAILED: "Fusion 360 is running but AdomBridge add-in not responding."
Proving the footprint ...
  footprint FAILED: "Fusion 360 is running but AdomBridge add-in not responding."
Proving the 3D package ...
  (same)
-> 0/3 artifacts, on a Fusion that was healthy the whole time

The library open returns to the caller while Fusion's main thread is still working. The next call queues behind it, exceeds the budget, and the bridge reports that as add-in not responding. Nothing is broken; the caller is simply told the add-in is dead when it is busy.

That single message is what makes this expensive. "Not responding" means stop, something is wrong. "Busy" means wait, then continue. A caller that reads the first will restart Fusion, re-run, and get the same result forever - which is roughly what I did for an hour before tracing it.

The ask, narrower than the title of this issue

Distinguish BUSY from NOT RESPONDING. If the add-in's HTTP server answers but the main thread is executing a command, that is not an unresponsive add-in:

errorCode: "fusion_busy"
busyWith:  "show_library"        (if known)
_hint:     "Fusion runs one command at a time. Poll fusion_readiness until
            ready:true, then retry - do not restart."

fusion_readiness already knows the difference: its hint literally says "the add-in's HTTP server is up but its main thread did not execute a test command yet - not drivable this second." That distinction just does not reach the error a caller gets from a show verb.

Secondary: consider declaring a timeouts block in bridge.json for the slow verbs. ab resolves per-verb budgets generically from that (timeout_meta::bridge_timeout_secs), so a 546s surface can carry a budget that fits instead of relying on the 90s default. That is a bridge-side declaration, no ab change needed.

What I changed on my side

The demo's library card now polls fusion_readiness until ready:true between artifacts instead of firing into a busy main thread. That is the right client behaviour regardless, but it is worth noting a caller can only do this once it knows "not responding" sometimes means "busy" - which is not currently discoverable from the error.

I am running a 10-part matrix until every part proves symbol + footprint + 3D package with a screenshot each, and will report what the wait changes.

John Lauer · 2mo ago

Pointer, so these do not drift apart: the conclusion from this trace now sits in #42, alongside the same theme in two other places.

The pattern across all of them is one thing - the caller compensating for something only the bridge can see:

  • here (#40): Fusion busy, reported as "add-in not responding", so my client polls readiness between every call
  • #42 name resolution: my client reads and regexes your .lbr to learn it must ask for CL21B104KBCNFN
  • #42 3D: show_3d_package returns success over a grey Placeholder, so my client has to inspect pixels to catch it

#42 also has the direct comparison with the KiCad bridge, which solves the naming half by simply reporting what it installed (symbolNames, footprintName). That one line of response shape is the difference between an afternoon and an evening.

This issue is still worth its own fix (fusion_busy as a distinct errorCode), so I am leaving it open. Everything else has moved to #42.

John Lauer · 2mo ago

The distinct errorCode you are holding this open for shipped in 1.9.94, and your pattern-naming prompted one more fix in 1.9.102.

fusion_addin_busy exists

errorCode:       "fusion_addin_busy"
busyCommand:     "<the command still running>"
elapsedSeconds:  <how long it has been going>
hostResponding:  true
_hint: "Do NOT restart Fusion and do not treat this as a crash ... only suspect breakage
        once elapsedSeconds passes what fusion_state.timings says this machine has ever taken."

/status answers without taking the main-thread lock, so the bridge could always have told you the difference and simply did not. Since 1.9.98 every verb's run time is recorded to verb-times.jsonl and fusion_verb_times reports p50/p90 per verb, so "slow or stuck?" is a lookup rather than a judgement call - your matrix would have had a number to compare elapsedSeconds against.

And your #41 trace exposed the dangerous half of this, which I have also fixed (1.9.101): fusion_start's warm path treated "add-in not answering" as "add-in missing" and force-restarted a busy, healthy Fusion. I had added the busy check to the path that merely improved a message and omitted it from the path that destroyed a run. It checks busy first now.

Your framing is the useful output of this thread

the caller compensating for something only the bridge can see

That is the right test, and all three instances were mine:

you had to because we fixed
poll readiness between every call reported BUSY as not-responding 1.9.94 fusion_addin_busy
regex our .lbr for names refused without saying what existed 1.9.100 near-miss resolution + available + fusion_list_library
inspect pixels for a grey slab returned success over a Placeholder 1.9.100 no_3d_package_bound

The kicad comparison landed: 1.9.102 makes show_symbol / show_footprint return resolvedName, symbolNames, packageNames, devicesetNames, package3dNames and has3d. The verb that opens the thing now says what it opened and what else is in there, so there is no discovery step to write on your side. has3d:false in particular lets you bind before showing instead of finding a Placeholder in a screenshot.

Closing, since the errorCode is shipped and the rest has moved to #42. The 546s first-surface is still mine and still unexplained - I am tracking that separately with the per-verb timings, and I will come to you with numbers rather than a theory.

John Lauer closed this · 2mo ago

Log in to reply.