← Commit history

1.9.198: chain hygiene stops chasing Fusion's respawning Untitled (12 phantom closes per run -> the real ~2), and the lose-changes upload modal is answered No + close retried instead of reaching the user (#94)

Colby Knox ·f0b3b97191 ·1mo ago ·parent f575340
3 files changed +96−41
BRIDGE_VERSION+1−1
@@ -1 +1 @@-1.9.197\ No newline at end of file+1.9.198\ No newline at end of file
CHANGELOG.md+17
@@ -3,6 +3,23 @@ All notable changes to the Adom Bridge Fusion 360 bridge, newest first. Canonical: https://wiki.adom.inc/adom/fusion-bridge ยท GitHub backup: `adom-inc/fusion-bridge` +## 1.9.198 - 2026-08-24 (chain hygiene: no phantom closes, no lose-changes modal)++- **The Untitled sweep recognizes Fusion's resting state.** Fusion always keeps one document+  open: closing the very last doc makes it respawn a fresh empty Untitled, so the chain-end+  sweep chased that phantom for all twelve bounded rounds on EVERY run - the log said+  "closed 12 scratch doc(s): Untitled x12" while the user watched tabs flutter, and the count+  was noise (a real chain opens ~2-4 scratch docs). The sweep now stops at one remaining doc.+- **The lose-changes modal is handled, not just guarded against.** The hasActiveJobs drain+  before a close fails its QUERY exactly when the add-in is busiest (which is when uploads+  are likeliest in flight), and the old code treated a failed ask as drained - so "Packages+  are being uploaded to your Fusion Hub..." still reached the user's screen mid-chain+  (caught live on a TMP100 mint). The drain now retries a failed ask, and if the modal rises+  anyway the close answers NO (upload preserved), re-waits the jobs, and retries the close -+  bounded, never left standing, never answered Yes.+- Verified live: a fresh-mint chain (bind 28.5s with a real Hub upload, prove 4/4) ran with+  zero dialogs on screen sampled at 5s cadence, and hygiene closed exactly 2 scratch docs.+ ## 1.9.76 - 2026-08-18 (ralph, dashboard)  - **"Fusion is opening right now" LED (John's ask).** A launch takes 30-90s and the board used to
server.py+78−40
@@ -10080,6 +10080,13 @@ def _close_docs_opened_since(before: set) -> list:         for _ in range(12):             st = _proxy_to_addin("document_info", {}, timeout=8)             docs = (st.get("data") or {}).get("openDocuments") or []+            # Fusion ALWAYS keeps one document open: closing the very last doc just makes+            # it respawn a fresh empty Untitled, so a sweep that keeps going "closes" the+            # same phantom twelve times per chain (every run logged "closed 12 scratch+            # doc(s): Untitled x12" while the user watched tabs flutter; measured live+            # 2026-08-24). One remaining doc IS Fusion's resting state - leave it.+            if len(docs) <= 1:+                break             u = next((dd for dd in docs                       if str(dd.get("name") or "").startswith("Untitled")                       and not (dd.get("isModified") or dd.get("modified"))), None)@@ -10119,49 +10126,80 @@ def _close_bridge_doc_verified(name: str, timeout_s: int = 30) -> dict:     # returns, and a close issued during them raises exactly that modal - blocking the     # main thread AND offering to destroy the upload. app.hasActiveJobs is the     # deterministic signal; wait for it to drain (cap 120s) before touching the close.-    _jobs_deadline = _t.time() + 120-    while _t.time() < _jobs_deadline:-        try:-            jr = _proxy_to_addin("run_modeling_script",-                                 {"script": "import adsk.core\n"-                                            "result={'jobs': adsk.core.Application.get().hasActiveJobs}"},-                                 timeout=10)-            jd = (jr.get("data") or {}).get("result") or {}-            if not jd.get("jobs"):-                break-            print("[Fusion Bridge] close(%r): Fusion Hub upload in flight - waiting, not "-                  "closing (a close now would raise the lose-changes modal)" % n)-        except Exception:-            break  # cannot ask -> proceed; the dialog answer below is the backstop-        _t.sleep(5)-    else:-        return {"closed": False, "deferred": True,-                "why": "Fusion Hub uploads still active after 120s - close deferred so the "-                       "upload is not destroyed; retry after the jobs drain"}-    try:-        _proxy_to_addin("close_document", {"name": n, "save": False}, timeout=20)-    except Exception:-        pass-    deadline = _t.time() + timeout_s-    while _t.time() < deadline:-        try:-            di = _proxy_to_addin("document_info", {}, timeout=6)-            docs = [str(d.get("name") or "").rstrip("*").strip().lower()-                    for d in ((di.get("data") or {}).get("openDocuments") or [])]-            if di.get("success") and n.lower() not in docs:-                return {"closed": True}-        except Exception:-            pass-        # A never-saved doc can raise Save-As despite save:false - answer it NOW, same call.+    for _round in range(3):+        # hasActiveJobs drain. A BUSY add-in fails the QUERY exactly when uploads are+        # likeliest in flight (caught live 2026-08-24: the lose-changes modal reached the+        # user's screen mid-chain), so a failed ask is retried, not treated as drained.+        _ask_fails = 0+        _drained = False+        _jobs_deadline = _t.time() + 120+        while _t.time() < _jobs_deadline:+            try:+                jr = _proxy_to_addin("run_modeling_script",+                                     {"script": "import adsk.core\n"+                                                "result={'jobs': adsk.core.Application.get().hasActiveJobs}"},+                                     timeout=10)+                jd = (jr.get("data") or {}).get("result") or {}+                _ask_fails = 0+                if not jd.get("jobs"):+                    _drained = True+                    break+                print("[Fusion Bridge] close(%r): Fusion Hub upload in flight - waiting, not "+                      "closing (a close now would raise the lose-changes modal)" % n)+            except Exception:+                _ask_fails += 1+                if _ask_fails >= 3:+                    break  # cannot ask repeatedly -> proceed; the dialog answer below is the backstop+            _t.sleep(5)+        if not _drained and _ask_fails < 3 and _t.time() >= _jobs_deadline:+            return {"closed": False, "deferred": True,+                    "why": "Fusion Hub uploads still active after 120s - close deferred so the "+                           "upload is not destroyed; retry after the jobs drain"}         try:-            got = _dismiss_save_prompt_if_bridge_doc()-            if got.get("handled"):-                print("[Fusion Bridge] close(%r): answered the Save prompt inside the same "-                      "call (deterministic close, never left on screen)" % n)+            _proxy_to_addin("close_document", {"name": n, "save": False}, timeout=20)         except Exception:             pass-        _t.sleep(2)-    return {"closed": False, "why": "document still open after %ds" % timeout_s}+        deadline = _t.time() + timeout_s+        _upload_modal = False+        while _t.time() < deadline:+            try:+                di = _proxy_to_addin("document_info", {}, timeout=6)+                docs = [str(d.get("name") or "").rstrip("*").strip().lower()+                        for d in ((di.get("data") or {}).get("openDocuments") or [])]+                if di.get("success") and n.lower() not in docs:+                    return {"closed": True}+            except Exception:+                pass+            # A never-saved doc can raise Save-As despite save:false - answer it NOW, same call.+            try:+                got = _dismiss_save_prompt_if_bridge_doc()+                if got.get("handled"):+                    print("[Fusion Bridge] close(%r): answered the Save prompt inside the same "+                          "call (deterministic close, never left on screen)" % n)+            except Exception:+                pass+            # The lose-changes modal ("Packages are being uploaded to your Fusion Hub...")+            # means the drain lied or raced: answer NO (upload preserved), then re-enter+            # the drain and retry the close. Never left standing, never answered Yes.+            try:+                _ups = [dd for dd in (classify_blocking_dialogs() or [])+                        if dd.get("category") == "upload_in_progress"]+                if _ups:+                    for _d in _ups:+                        _unwrap(_ad_call("desktop_ui_click", {"hwnd": _d.get("hwnd"), "name": "No"}, timeout=12))+                    print("[Fusion Bridge] close(%r): upload-in-progress modal answered NO "+                          "(upload preserved); re-waiting for Hub jobs before retrying the close" % n)+                    _upload_modal = True+                    break+            except Exception:+                pass+            _t.sleep(2)+        if not _upload_modal:+            return {"closed": False, "why": "document still open after %ds" % timeout_s}+        _t.sleep(5)+    return {"closed": False, "deferred": True,+            "why": "the upload-in-progress modal kept rising across 3 close attempts - close "+                   "deferred, upload preserved; retry after the Hub sync settles"}