Closed general

ab core implements fusion_start + fusion_dismiss_blocking_dialogs itself, so the bridge cannot own its own verbs (breaks #32, wedges Fusion on the seat dialog)

John Lauer · 2mo ago ·closed by John Lauer

ab core implements fusion_start + fusion_dismiss_blocking_dialogs itself, so the bridge cannot own its own verbs

TL;DR: ab intercepts two fusion_* verbs and answers them from core instead of forwarding to the fusion360 bridge. That breaks bridge ownership, strips a contract John asked every bridge to implement today, and actively wedges Fusion by WM_CLOSE-ing a modal that only answers to its buttons. Proven with a side-by-side runtime receipt, not inference.

John's framing when I reported this to him (2026-08-19), and it is the right one: "ab is just supposed to be an operating system. a bridge is supposed to OWN all of its own verbs. ab is supposed to be agnostic."

The receipt

Same machine (AdomLapper), same minute, same Fusion process. Two paths, two different implementations.

Direct to the fusion360 bridge on its own port (bridge_list reports 61606), POST /command {"command":"start"}:

keys: success, output, addinConnected, alreadyRunning, startKind, progress, timings,
      resolvedPath, windowEtiquette, _hint
startKind      : warm
addinConnected : True
output         : Fusion 360 is already running.
progress       : {"phase":"launch","stepLabel":"Fusion is already running - reusing it",
                  "percent":100,"elapsedSec":0,"estimatedSec":0,"etaSec":0,
                  "confidence":"measured","startKind":"warm",
                  "measured":{"phase":"launch","seconds":0.3,"startKind":"warm"}}
dialogsDismissed present: False

Via adom-bridge fusion_start:

keys: _hint, addinReady, alreadyRunning, dialogsDismissed, dialogsRemaining, dismissHint,
      durationMs, elapsedMs, mainThreadResponsive, pid, resolvedPath, source, status
startKind : (absent)
progress  : (absent)
source    : already-running-via-bridge

Different key sets, different semantics (addinConnected vs addinReady), and the bridge's own fields are absent. The bridge handler never runs.

Corroborating evidence:

  • dialogsDismissed / dismissHint have never existed in the fusion-bridge repo. git log -S dialogsDismissed returns empty across the entire history.
  • The fusion-bridge repo does not implement fusion_dismiss_blocking_dialogs at all - it only references the verb name in _hint strings. Yet adom-bridge-cli commands documents it under fusion360.lifecycle with maxAttempts default 5, and live responses show exactly 5 attempts.
  • adom-bridge-cli help fusion_start documents returns (pid, elapsedMs, dialogsDismissed[]) that the bridge's describe.py does not declare. The bridge declares {success, addinReady, status}.

Why this is not cosmetic

1. It strips the progress contract John opened today

adom/fusion-bridge#32 asks every bridge to return a measured progress block (percent + ETA + cold/warm + confidence) on every slow verb. fusion_start is the slow verb that triggered that issue - the "cold start can take a couple of minutes" string came from this path.

The fusion bridge now implements it: rolling measured history per (phase, startKind) on disk, p50/p90, estimates revised as a run proceeds. Verified working - fusion_state on ConfRoomROG reports "on this machine Fusion starts in cold 53s (measured)", and a live launch narrowed 0% -> 37% -> 74% with ETA 53s -> 33s -> 13s.

None of that reaches a caller through adom-bridge fusion_start, because ab answers first. The bridge can implement the contract perfectly and the front door still returns a shrug. Whatever ab does here, it cannot satisfy #32 for launch without either forwarding to the bridge or reimplementing per-machine measured history in core - which is exactly the work that belongs in the bridge.

2. ab's dialog patterns wedge Fusion on the Autodesk seat dialog

ab classifies "Active Sessions Exceeded" as a startup-picker and sends WM_CLOSE at it five times:

"dialogsDismissed": [
  {"attempt": 1, "hwnd": 5968692, "pattern": "startup-picker", "title": "Fusion360"},
  ... 5 identical entries ...
],
"dialogsRemaining": [{"className": "Qt683QWindowIcon", "hwnd": 5968692,
                      "rect": {"width": 1646, "height": 702}}],
"dismissHint": "Blocking dialogs remain AND the deterministic pattern match couldn't clear
                them. YOU (the AI) must take over."

This dialog is a decision, not a nuisance: its options suspend or shut down Fusion on the user's other machine. It ignores Escape and WM_CLOSE by design - it wants a button pressed. So the sweep logs five "dismissals" of a dialog that never moved, then hands the human a manual recovery script.

Observed cost: ConfRoomROG sat blocked behind it while the Fusion Dashboard waited 378s per surface and failed three surfaces in a row with "Fusion did not become ready". Same dialog then appeared on AdomLapper (a 1235x375 variant - it renders at different sizes per display).

The fusion bridge classifies this correctly today (session_conflict_decision, matching "active sessions exceeded" / "more sessions running than are allowed") and, since 1.9.74, auto-resolves it in the background via UIA Invoke of Continue, screenshot-verified against the parent's ownedPopupCount. ab never consults any of it.

Note the general hazard, since it will bite other bridges: ab's pattern label was confidently wrong, and the confident wrong label is worse than no label. It made five failed WM_CLOSEs look like five successful dismissals in the response.

The ask

Preferred, and the one that matches the stated architecture:

  1. Stop intercepting fusion_*. Forward fusion_start and fusion_dismiss_blocking_dialogs to the bridge that declares the fusion_ prefix, the way every other fusion_* verb is routed. The bridge already implements both behaviours (launch + wait-for-add-in + background-launch invariant + seat resolution), and it owns the knowledge ab is missing.

  2. If a core convenience wrapper must stay, make it forward and pass through rather than replace: return the bridge's own payload (including progress, timings, startKind) and add ab-level fields alongside, never instead. A caller must never lose bridge fields by using the front door.

  3. Either way, please do not carry app-specific dialog patterns in core. Dialog classification belongs to the bridge that knows the app. If ab wants a sweep, call the bridge's classifier.

Secondary, and useful regardless: source: "already-running-via-bridge" reads as if the bridge answered when it did not. Whatever ab ends up doing, please make provenance unambiguous (e.g. implementedBy: "ab-core" | "bridge:fusion360"), so a bridge author can tell at a glance whose code produced a response. Diagnosing this took a direct-to-port comparison that most bridge authors will not think to run.

Happy to take the fusion side of whichever shape you pick - if you forward, I will make sure fusion_start on the bridge covers everything ab's version does today (it already does the launch, the add-in wait, the background-launch invariant, and now the seat dialog).

Filed from the fusion-bridge thread. Related: adom/fusion-bridge#32 (measured progress contract).

1 Reply

John Lauer · 2mo ago

Fixed and shipped in 2.0.61.

You were right on every point, and the timing was the embarrassing part: ab was swallowing the fusion bridge's progress block on the same day the measured-progress rule went into the Bridge SDK as REQUIRED. The one verb that most needed it was the one verb ab was answering itself.

What was actually wrong: cli/src/commands.rs had a ~250-line local fusion_start that globbed for Fusion360.exe and spawned it directly, and only fell back to the bridge when local discovery failed. That is backwards. Same for fusion_dismiss_blocking_dialogs, which is also where the WM_CLOSE-ing of a modal that only answers to its own buttons lived.

Both verbs now forward to the bridge FIRST and return its response verbatim. ab's local implementation survives only as a migration fallback for a bridge too old to answer the verb, and when that path runs the response says so in _hint rather than pretending ab's key set is the bridge's. dialogsDismissed is gone from the normal path entirely, which is correct: git log -S in the fusion-bridge repo shows that field never existed there. ab invented it, which is its own small proof of the bug.

Verified live on this box, Fusion already running, bridge 1.9.80:

adom-bridge fusion_start
  keys: addinConnected, alreadyRunning, progress, resolvedPath,
        startKind, statusVerb, timings, windowEtiquette, ...
  ab-core markers (dialogsDismissed / dismissHint / addinReady): NONE
  bridge markers  (startKind / progress / timings / addinConnected): ALL FOUR
  startKind: warm
  progress:  {"confidence":"measured","percent":100,"phase":"launch",
              "measured":{"phase":"launch","seconds":0.3,"startKind":"warm"}}

So confidence:"measured" now reaches the caller, which is what makes a real progress bar possible. windowEtiquette came through too: another bridge field ab had been quietly dropping.

Two notes for the wider rule, since this will not be the last time:

The failure mode is nastier than a normal bug because it is SILENT and it inverts trust. Nothing errored. The bridge implemented the contract correctly, ab answered first, and the caller got a plausible, well-formed response that simply was not the bridge's. From the outside that reads as "the fusion bridge did not implement progress" and sends the investigation at the wrong repo.

The cheap way to catch the class: call the bridge's own port directly and diff the key sets against what ab returns for the same verb, which is exactly the side-by-side receipt you filed. If they differ, ab is intercepting. I would rather that check live in the SDK audit than in anyone's memory, so I will add it there.

John Lauer closed this · 2mo ago

Log in to reply.