← Commit history
BRIDGE_VERSION+1−1
@@ -1 +1 @@-0.9.7+0.9.8
Cargo.lock+1−1
@@ -107,7 +107,7 @@ dependencies = [  [[package]] name = "swx-bridge"-version = "0.9.7"+version = "0.9.8" dependencies = [  "serde",  "serde_json",
Cargo.toml+1−1
@@ -1,6 +1,6 @@ [package] name = "swx-bridge"-version = "0.9.7"+version = "0.9.8" edition = "2021" rust-version = "1.80" description = "Adom SOLIDWORKS bridge — swx_* verbs over the SOLIDWORKS COM API, as a single native binary"
README.md+12−1
@@ -458,7 +458,18 @@ not.  ## Roadmap -- v0.9.7 (this): **no marker history, no lock-out.** 0.9.6 was LIVE-PROVEN on+- v0.9.8 (this): **modality is a persistent measurement.** 0.9.7 was LIVE-PROVEN+  on 2026-10-01 (launch 16/16, READY 16.7 s with the log read as+  rewritten-per-start and complete; drain 5/5; quit gate 11/11; primed quit+  3.4 s). One new measurement: the 3DEXPERIENCE Connected notice, read at an+  instant when SOLIDWORKS was busy inside a sweep's own close, had a DISABLED+  owner frame and stopped the sweep as a modal. When the owner's state is the+  ONLY fact that makes a window blocking, the census now re-reads it (5 reads,+  ~2 s): disabled throughout is still a modal; enabled later is a notification+  reported with `ownerDisabledTransiently:true` and the read history. A window+  with a button, button-like captions or save/recovery/import words is never+  settled. 482 tests.+- v0.9.7: **no marker history, no lock-out.** 0.9.6 was LIVE-PROVEN on   2026-10-01: launch gate 16/16 (READY 32.3 s), gated replies carrying their   readiness, sweep PASS, drain 5/5, quit gate 11/11 printing the bridge's dialog   census, and priming fixed live twice (declined 30.7 s → primed quit 3.4 s).
bridge.json+1−1
@@ -2,7 +2,7 @@   "manifest_version": 1,   "name": "solidworks",   "displayName": "SOLIDWORKS",-  "version": "0.9.7",+  "version": "0.9.8",   "description": "Bridge for SOLIDWORKS desktop CAD \u2014 create and open documents, a modeling DSL, file management (find/open/save/export/Pack and Go), custom properties, BOM generation, design analysis (mass properties, bodies, parameters, interference, rebuild health) and background window/dialog etiquette, over the SOLIDWORKS COM API. Single native binary: no interpreter, no PowerShell, ~10ms cold start.",   "author": "Adom Inc.",   "license": "MIT",
docs/ECOSYSTEM-TABLE.md+1−1
@@ -112,7 +112,7 @@ templates**, alisamsam **22**. | 44 | **Graceful QUIT that is measured, not assumed** (never a kill, never answers a dialog, proves the exit from the process census) | ✗ | **✓ LIVE-PROVEN 2026-09-17 (rung a)** — `swx_stop {userAuthorized, maxRung:"a"}` quit a bridge-launched session on the FIRST rung (UserControl=false; ExitApp; every COM reference released), verified absent from the process census; `swx_shutdown` then removed a stray empty instance (pid 145188) and `tasklist` showed no `sldworks.exe` at all. **`ExitApp` is NOT declined at zero documents on this edition** — the long-held `host_app_stop_declined` belief was a SAVE-PROMPT STALL misread as a refusal, which is why the ladder was built pinned at rung (a) and why the preserve-then-close sweep is what actually unblocked it. A rung ladder (a `ExitApp` → f Restart Manager *without* `RmForceShutdown`), pinned at rung (a); 7 re-measured preconditions where UNKNOWN refuses and `force` waives the UNSAVED question ONLY; every COM reference released before and after each rung, with outstanding refs named in the report; success = *this pid AND this creation time* absent from the census, never `GetActiveObject`; per-session children reaped while the documented platform daemons are left alone; a dialog stops the ladder permanently and is never answered; nothing disowned until the exit is verified. every precondition re-measured before EVERY rung (a mid-ladder refusal is a real outcome), the exit POLLED to the rung's budget rather than sampled once (a real `ExitApp` returns before the process goes), the dialog census covering the `swCefSubProc` web-view children by parentage, the count cross-checked against the enumeration, and the message rungs re-verifying window ownership before they send. `tools/quit-gate.py` proves the session was bridge-launched, and **its FULL CYCLE PASSED LIVE 2026-09-30 through the bridge ab hosts**: rung (a), process gone 29.5 s, per-session servers reaped, nothing on screen, no crash reporter, every rung reported, and SOLIDWORKS relaunched by the bridge 54.7 s later. The gate now reaches the bridge through ab's verb routing (the side-loaded dev copy on 24695 is terminated by ab 2.1.163 within 30 s) and passes `userAuthorized:true` — the standing authorization — rather than the write-gate pair, which the hosted bridge rightly never enables. **MEASURED 2026-10-01: the decline is about session HISTORY** — rung (a) was declined three times on a session that had never held a document and quit in 3.4 s once one had been created and closed; since 0.9.3 the ladder PRIMES such a session (empty, no dialog, user-authorized only) and asks again — **LIVE-PROVEN 2026-10-01 on 0.9.4**: declined at 130 s, primed, quit in 3.4 s (declines measured from 50 s to 8 min old; such sessions quit unprimed at ~25 s), and `swx_status` exposes the session's document history — and the gate's "no longer running" claim — which had passed vacuously after declines — now requires `running:false` and is self-tested | ✗ | ✗ | ✗ | ✗ | ✗ |  | 48 | **A title collision between SAVED documents is impossible here — measured, not assumed** | ✗ | **✓ MEASURED LIVE 2026-09-17** — `tools/drain-gate.py --fixture new` copied a bridge-created document's own file to a second directory under the same basename and called `swx_open` on it. SOLIDWORKS REFUSED: `errors = 65536` (`swFileWithSameTitleAlreadyOpen`). So the drain's title-collision guard — which refuses to close any document whose title another open document shares, and names both paths — can only ever fire for PATHLESS documents, which is exactly the state identity-at-birth removes. The guard stays (a document the bridge did not create can still be pathless, and an older bridge's leftovers are), and the measurement is recorded next to it in `com.rs` so nobody "simplifies" it on the assumption that saved collisions are the common case. No MCP server measures this at all | ✗ | ✗ | ✗ | ✗ | ✗ |-| 51 | **Dialogs judged by MEASURED MODALITY, not by existence** (a notification never blocks; a modal always does) | ✗ | **✓ LIVE-PROVEN 2026-10-01 on 0.9.4** — with the 3DEXPERIENCE "Connected" notice on screen, `swx_stop` quit in 5.1 s and reported it as a notification with its evidence. **0.9.4, from a blocker MEASURED 2026-10-01**: the 3DEXPERIENCE "Connected to …" notice — a title-less `#32770` tool window whose owner frame stayed ENABLED for 95 s, with no Button-class control — blocked every quit, sweep and import for minutes. Now a window blocks when its kind is known-blocking, its owner frame is disabled, or it has a Button-class control; it is a `notification` (reported with owner, ownerEnabled, styles, rect and child controls; never answered) only when all of that was measured; unread evidence fails closed. Template placeholder captions are flagged, never presented as the window's words. **0.9.5 (adversary pass 43): the window's WORDS count — a save/recovery/import kind in its text blocks whatever the evidence, two or more button-like captions in any control class block, and measured notification evidence overrides a licence-type title alone; 0.9.7: swx_dismiss_blocking_dialogs uses the same title-and-words classifier before it closes anything.** No MCP server looks at a dialog at all | ✗ | ✗ | ✗ | ✗ | ✗ |+| 51 | **Dialogs judged by MEASURED MODALITY, not by existence** (a notification never blocks; a modal always does) | ✗ | **✓ LIVE-PROVEN 2026-10-01 on 0.9.4** — with the 3DEXPERIENCE "Connected" notice on screen, `swx_stop` quit in 5.1 s and reported it as a notification with its evidence. **0.9.4, from a blocker MEASURED 2026-10-01**: the 3DEXPERIENCE "Connected to …" notice — a title-less `#32770` tool window whose owner frame stayed ENABLED for 95 s, with no Button-class control — blocked every quit, sweep and import for minutes. Now a window blocks when its kind is known-blocking, its owner frame is disabled, or it has a Button-class control; it is a `notification` (reported with owner, ownerEnabled, styles, rect and child controls; never answered) only when all of that was measured; unread evidence fails closed. Template placeholder captions are flagged, never presented as the window's words. **0.9.5 (adversary pass 43): the window's WORDS count — a save/recovery/import kind in its text blocks whatever the evidence, two or more button-like captions in any control class block, and measured notification evidence overrides a licence-type title alone; 0.9.7: swx_dismiss_blocking_dialogs uses the same title-and-words classifier before it closes anything; 0.9.8, from a live measurement: an owner frame read disabled for an instant (SOLIDWORKS busy) is re-read before a notice is called a modal.** No MCP server looks at a dialog at all | ✗ | ✗ | ✗ | ✗ | ✗ | | 50 | **Readiness gating — attached is not ready** (no document command reaches a SOLIDWORKS that has not finished starting) | ✗ | **✓ 0.9.3, from a crash MEASURED 2026-10-01**: `swx_new_document` 8 s after COM attach, `swx_open` at 9 s and `swx_close_all_safely` at 12 s crashed SOLIDWORKS while its 3DEXPERIENCE add-in was still initialising (performance log: LOAD_ADDIN still running, SWEXH/UNHANDLED, the 3DEXPERIENCE marker only after the document commands, SWERR_REPORT); a control launch with no document commands crashed nothing. Now a READY stage follows `attached`, decided by two independent signals — COM `StartupProcessCompleted` and the performance log's 3DEXPERIENCE marker after the last LOAD_ADDIN — with an honest grace and timeout that never promote to ready; every verb that reaches SOLIDWORKS refuses `host_app_starting` with `retryAfterSeconds` until then — **0.9.5: gated by default with a pinned 16-verb exemption list, the performance log bound to this session's process creation time (an earlier session's log is never "complete"), COM alone never enough on a machine that writes markers, an on-demand add-in told from a start by quiet + which add-in, the grace unlocking reads only, and the window-message quit rungs ungated under userAuthorized; LIVE 2026-10-01 a document verb was refused during startup. 0.9.6: every gated reply carries the readiness its gate evaluated, the log is read from its per-start header, and the on-demand rule keys on the platform DLL SWK3DCompassAddin.dll; 0.9.7 (LIVE-PROVEN 0.9.6): no marker history — a disabled platform add-in never locks the bridge out, and a log still being written holds even a machine's first start**; a sweep during which SOLIDWORKS dies says `host_app_gone` with the log's crash markers. The rescue held under the crash (26,129 bytes verified). No MCP server distinguishes attached from ready at all | ✗ | ✗ | ✗ | ✗ | ✗ | | 49 | **The user's rescued work survives the bridge's own updates — and ab's** (recovery never lives in a directory anything else owns) | ✗ | **✓ 0.9.1: the data root is `%LOCALAPPDATA%\adom-solidworks-bridge\`, a directory only this bridge writes to** — moved out of `Adom Bridge\` altogether after adversary pass 39 found 0.9.0's `Adom Bridge\solidworks-data` sitting inside the directory ab's installer and uninstaller own, whose fate on an ab uninstall or reinstall is documented nowhere; both earlier locations are migrated oldest first, every copy recorded by (root, path, size, mtime, sha256) so a legacy file that changes after its copy is copied again BESIDE it and reported `changedSinceCopy`, never settled by name — and since 0.9.2 a small file whose size and mtime match is hashed too, and two versions of the ledger or the ownership registry are MERGED (union, both versions kept beside) rather than one picked; a temp-directory fallback makes every sweep refuse (`data_root_is_temp`), and an unwritable recovery folder is refused once before the first save (`recovery_dir_unwritable`). **0.9.0, from a SEV-1 MEASURED 2026-09-30**: `bridges-cache\solidworks` was found CREATED 2026-09-18 09:42 by ab's auto-install of 0.8.9 — the updater re-extracts the whole directory, and every recovery folder from the day before, the ledger and the ownership registry went with it (test documents only; nothing of Drew's was lost). The data root is now `%LOCALAPPDATA%\Adom Bridge\solidworks-data\`, a sibling of the install cache; the first start MIGRATES what an older bridge left (copied under a temporary name and renamed into place once verified byte for byte — so a start that dies mid-copy never leaves a truncated rescue under the real name — never over an existing file, originals left in place, each file once, a copy that does not verify removed and retried next start); `swx_recover_list` still reads the old location and marks each rescue there `atRiskFromUpdate`, saying whether a verified copy exists — and in the data root calls a file whose copy has not landed `notCopiedYet` (pointing at its only copy), never "gone"; and a sweep REFUSES (`recovery_inside_install_dir`, nothing written, nothing closed) if its recovery folder would land under the install directory or the install cache — however the path is spelled (links, `..`, case). Every rule mutation-verified. No MCP server keeps a recovery folder at all | ✗ | ✗ | ✗ | ✗ | ✗ | | 47 | **Interconnect-aware import: refuses honestly, with the reason and the setting** | ✗ | **✓ MEASURED + shipped 0.8.2** — on SOLIDWORKS Connected 2026 with `swMultiCAD_Enable3DInterconnect` TRUE there is NO programmatic neutral-file import route at all: LoadFile4 returns nothing (`errors=1`) both with `Nothing` and with a real `GetImportFileData` object, LoadFile2 returns false, OpenDoc6 answers `swFileRequiresRepairError` (2097152) across four option values and `swInvalidFileTypeError` (1024) for docType 0. So `swx_import` makes ONE LoadFile4 attempt (a future edition that accepts it simply works) and then fails with `interconnect_blocks_import`, naming the exact setting — Tools > Options > System Options > Import > Enable 3D Interconnect — the measurement date, and the fact that **this bridge does not change user settings, not even temporarily**. No MCP server reads that toggle at all: they report a translator failure, which sends the reader hunting a missing translator that is not the problem | ✗ | ✗ | ✗ | ✗ | ✗ |
docs/LAUNCH-RESILIENCE.md+9
@@ -1139,6 +1139,15 @@ the quiet window). **Residual:** an edition that writes no start header AND appends would still be read as one block; the mtime binding is then the only tie to the session. +**Measured live on 0.9.7 (2026-10-01).** Launch gate 16/16, READY at 16.7 s, the+performance log reported as `{shape:{headersInTail:1, lengthNow:6449,+model:"rewrittenPerStart"}, lengthAtFirstSight:6449, shrankSinceFirstSight:false,+verdict:"complete"}` — the rewritten-per-start model confirmed by the bridge's+own reading. Drain 5/5; quit gate 11/11. The one new finding is a dialog+measurement (QUIT-INVESTIGATION §5.22): the 3DEXPERIENCE Connected notice read+with its owner frame DISABLED for an instant while SOLIDWORKS was busy inside a+sweep's close — 0.9.8 re-reads the owner before calling such a window a modal.+ ## Live proof 4 — 0.9.3 on Drew's machine (2026-10-01)  | | Measured |
docs/QUIT-INVESTIGATION.md+33
@@ -2078,3 +2078,36 @@ measured, its owner's enablement was not.   reads a window's words before closing it: a save question under a "Warning"   title is refused as `save_changes` "by its text", and a window whose words   cannot be read is not closed.++### 5.22 LIVE 2026-10-01 on 0.9.7 — a notice read while its owner was BUSY; 0.9.8 settles modality++* **Live, all green:** launch gate 16/16 (READY 16.7 s, performance log+  `{model:"rewrittenPerStart", headersInTail:1, lengthNow:6449,+  lengthAtFirstSight:6449, shrankSinceFirstSight:false, verdict:"complete"}`);+  drain 5/5; QUIT GATE 11/11 with relaunch; primed quit of the relaunched empty+  session at ≥ 120 s: [declined 30.7 s, quit primed 3.4 s].+* **The one new measurement.** `swx_close_all_safely {batch:1}` over Part1+  (pathless) + Drew's saved part returned `success:false`, closed 1,+  `documentCountAfter` 1, `stopped_by_dialog`, on: hwnd 4329196, `#32770`,+  style 0x94000044, exstyle 0x90080 (tool window), rect [533,123,834,190]+  (301x67), owner 856844, **ownerEnabled FALSE**, title '', nine Static controls+  (3 visible, 6 hidden), the template placeholder captions — the SAME+  3DEXPERIENCE Connected notice measured that morning (§5.16), read at an+  instant when the main frame was disabled, i.e. while SOLIDWORKS was busy+  inside the sweep's own close/save. Seconds later the quit gate's own sweep+  closed the remaining document and passed: the disabled state was transient.+  Safe direction (stopped, nothing lost, honest count), but a notice could stop+  a sweep.+* **0.9.8: modality is a persistent measurement, not one read.** When a window+  is a notification on every other measured fact (tool window, controls read,+  no Button class, no button-like captions, no save/recovery/import words) and+  ONLY its owner's enabled state says blocking, the census re-reads the owner+  (`LaunchOs::owner_enabled_of`) up to 5 times ~400 ms apart. Disabled (or+  unreadable) on every read → blocking, as before, with the read history.+  Enabled on a later read → a notification, reported with+  `ownerDisabledTransiently:true` and `ownerReads`. The settle is never applied+  to a window with a Button-class child, button-like captions or a+  save/recovery/import kind (judge_window with the owner taken as enabled still+  blocks those). The sweep already censuses before EVERY save and EVERY close,+  between its own COM calls — the live read came right after one close returned+  and before the next was issued; the settle gives that instant time to pass.
package.json+1−1
@@ -1,6 +1,6 @@ {   "slug": "solidworks-bridge",-  "version": "0.9.7",+  "version": "0.9.8",   "type": "app",   "description": "Skills for your container so your AI knows how to drive the SOLIDWORKS bridge (swx_* verbs). The bridge runtime itself is the release zip — a single native swx-bridge.exe; Adom Bridge loads that.",   "tags": [
references/closing-and-recovery.md+2−1
@@ -117,7 +117,8 @@ Read this before closing any document, quitting SOLIDWORKS, draining leaked impo   recovery or import prompt, its owner frame is disabled, or it offers a choice   (a Button control, or two or more captions like Save / Don't Save / Cancel in   any control); a tool window over an enabled frame with nothing to click is a-  `notification` (`kind:"notification"`, `blocking:false`)+  `notification` (an owner frame that reads disabled only for an instant is+  re-read first — `ownerDisabledTransiently:true`) (`kind:"notification"`, `blocking:false`)   — reported under `notifications` with its `evidence`, never answered, and the   quit, sweep or import goes ahead. The 3DEXPERIENCE "Connected to …" notice is   one (measured 2026-10-01). `textIsTemplatePlaceholder:true` means the captions
src/launchos.rs+36
@@ -465,6 +465,14 @@ pub trait LaunchOs: Send + Sync {         Err("this host cannot read dialog contents".into())     } +    /// RE-READ ONE WINDOW'S OWNER: is the frame that owns `hwnd` enabled right+    /// now? `None` = no owner, or the read failed — UNKNOWN, never "enabled".+    /// The owner-state settle (0.9.8) reads this several times, because a frame+    /// that is merely busy for an instant reads disabled too.+    fn owner_enabled_of(&self, _hwnd: i64) -> Option<bool> {+        None+    }+     /// This process's own pid — recorded on a launch attempt so a LATER bridge     /// can tell "somebody is supervising this" from "this was left behind".     fn self_pid(&self) -> u32 {@@ -1151,6 +1159,23 @@ impl LaunchOs for FakeOs {         })     } +    /// The scenario's row for `hwnd` may carry `ownerEnabledReads: [false,+    /// false, true]` — one value per re-read, the last repeating; otherwise its+    /// `ownerEnabled` holds for every read.+    fn owner_enabled_of(&self, hwnd: i64) -> Option<bool> {+        let rows = self.read_json("windows.json");+        let row = rows.as_array()?.iter().find(|r| r.get("hwnd").and_then(|h| h.as_i64()) == Some(hwnd))?.clone();+        match row.get("ownerEnabledReads").and_then(|v| v.as_array()) {+            Some(seq) if !seq.is_empty() => {+                let cursor = self.dir.join(format!("owner-reads-{hwnd}.cursor"));+                let i: usize = std::fs::read_to_string(&cursor).ok().and_then(|t| t.trim().parse().ok()).unwrap_or(0);+                let _ = std::fs::write(&cursor, (i + 1).to_string());+                seq.get(i.min(seq.len() - 1)).and_then(|v| v.as_bool())+            }+            _ => row.get("ownerEnabled").and_then(|v| v.as_bool()),+        }+    }+     fn windows_of_pid(&self, pid: u32) -> Result<Vec<WindowInfo>, String> {         if self.dir.join("window-census-fails").exists() {             return Err("simulated: the window census could not be taken".into());@@ -1639,6 +1664,17 @@ mod win {             Ok(out)         } +        fn owner_enabled_of(&self, hwnd: i64) -> Option<bool> {+            use windows::Win32::UI::WindowsAndMessaging::{GetWindow, GetWindowLongPtrW, GWL_STYLE, GW_OWNER};+            unsafe {+                let w = HWND(hwnd as *mut std::ffi::c_void);+                let owner = GetWindow(w, GW_OWNER).ok().filter(|o| !o.0.is_null())?;+                let style = GetWindowLongPtrW(owner, GWL_STYLE) as u32;+                // A zero style is a failed read, not an enabled window.+                (style != 0).then_some(style & 0x0800_0000 == 0)+            }+        }+         fn windows_of_pid(&self, pid: u32) -> Result<Vec<WindowInfo>, String> {             use windows::Win32::UI::WindowsAndMessaging::{GetClassNameW, GetWindowTextW};             let _serialised = ENUM_LOCK.lock().unwrap_or_else(|e| e.into_inner());
src/quit.rs+79−4
@@ -221,6 +221,11 @@ pub struct BlockingDialog {     /// Does it block? Decided by `judge_window` from the evidence, with the reason.     pub blocking: bool,     pub why: String,+    /// THE OWNER-STATE SETTLE (0.9.8): when ownerEnabled was the ONLY fact that+    /// made this window blocking, the re-reads of its owner's enabled state —+    /// `{reads:[{atMs, enabled}], ownerDisabledTransiently}`. `None` = no settle+    /// was needed or allowed.+    pub owner_settle: Option<Value>, }  /// The captions a custom-drawn window's dialog TEMPLATE carries — measured@@ -408,11 +413,77 @@ pub struct DialogCensus {     pub notifications: Vec<BlockingDialog>, } +/// How many times, and how far apart, a disabled owner is re-read before a+/// window that is a notification on every other fact is called a modal.+pub const OWNER_SETTLE_READS: u32 = 5;+fn owner_settle_ms() -> u64 {+    std::env::var("SWX_OWNER_SETTLE_MS").ok().and_then(|v| v.trim().parse().ok()).unwrap_or(400)+}++/// MODALITY IS A PERSISTENT MEASUREMENT, NOT ONE READ (0.9.8). MEASURED+/// 2026-10-01 on 0.9.7: the 3DEXPERIENCE Connected notice (same class, styles,+/// size and placeholder captions as the morning's) was read with its owner frame+/// DISABLED — at an instant when SOLIDWORKS was busy inside the sweep's own+/// close/save — and stopped the sweep as a modal; seconds later the frame was+/// enabled again. So when a window is a notification on EVERY other measured+/// fact (tool window, controls read, no Button class, no button-like captions,+/// no save/recovery/import words) and ONLY its owner's enabled state says+/// blocking, the owner is re-read up to OWNER_SETTLE_READS times ~400 ms apart:+/// disabled (or unreadable) on every read -> blocking, as before; enabled on a+/// later read -> a notification, reported with `ownerDisabledTransiently:true`+/// and the read history. The settle is never applied to a window with a+/// Button-class child, button-like captions, or a save/recovery/import kind —+/// `judge_window` with the owner taken as enabled still blocks those. The census+/// runs between this bridge's own COM calls: no call of the sweep is in flight+/// while it reads.+fn settle_owner(w: &WindowInfo, contents: Option<&crate::launchos::DialogContents>, os: &dyn crate::launchos::LaunchOs) -> Option<(bool, Value)> {+    if w.owner_enabled != Some(false) {+        return None;+    }+    let mut assumed = w.clone();+    assumed.owner_enabled = Some(true);+    if judge_window(&assumed, contents).0 {+        return None; // something other than the owner's state blocks it+    }+    let mut reads = vec![json!({"atMs": crate::launchos::epoch_ms() as u64, "enabled": false, "read": "census"})];+    for _ in 0..OWNER_SETTLE_READS {+        std::thread::sleep(std::time::Duration::from_millis(owner_settle_ms()));+        let e = os.owner_enabled_of(w.hwnd);+        reads.push(json!({"atMs": crate::launchos::epoch_ms() as u64, "enabled": e}));+        if e == Some(true) {+            return Some((true, json!({"reads": reads, "ownerDisabledTransiently": true})));+        }+    }+    Some((false, json!({"reads": reads, "ownerDisabledTransiently": false})))+}+ pub fn dialog_census(windows: &[WindowInfo], os: &dyn crate::launchos::LaunchOs) -> DialogCensus {     let mut out = DialogCensus { blocking: None, notifications: Vec::new() };     for w in windows.iter().filter(|w| w.is_dialog() && w.visible) {         let contents = os.dialog_contents(w.hwnd).ok();-        let (blocking, kind, why) = judge_window(w, contents.as_ref());+        let (mut blocking, mut kind, mut why) = judge_window(w, contents.as_ref());+        let mut owner_settle = None;+        if blocking {+            if let Some((enabled_later, history)) = settle_owner(w, contents.as_ref(), os) {+                if enabled_later {+                    let mut assumed = w.clone();+                    assumed.owner_enabled = Some(true);+                    let (b, k, y) = judge_window(&assumed, contents.as_ref());+                    blocking = b;+                    kind = k;+                    why = format!(+                        "{y} — its owner frame read DISABLED at first and ENABLED on a re-read: \+                         SOLIDWORKS was busy, not held by this window (ownerDisabledTransiently)"+                    );+                } else {+                    why = format!(+                        "{why} — on every one of {} re-reads over the settle window",+                        OWNER_SETTLE_READS+                    );+                }+                owner_settle = Some(history);+            }+        }         let d = BlockingDialog {             hwnd: w.hwnd,             title: w.title.clone(),@@ -421,6 +492,7 @@ pub fn dialog_census(windows: &[WindowInfo], os: &dyn crate::launchos::LaunchOs)             window: Some(w.clone()),             blocking,             why,+            owner_settle,         };         if blocking {             if out.blocking.is_none() {@@ -463,6 +535,8 @@ impl BlockingDialog {             // may is a question for the user, not an inference from a control             // tree.             "acknowledgeOnly": c.map(|c| json!(c.acknowledge_only())).unwrap_or(Value::Null),+            "ownerDisabledTransiently": self.owner_settle.as_ref().and_then(|o| o.get("ownerDisabledTransiently").cloned()).unwrap_or(json!(false)),+            "ownerReads": self.owner_settle.as_ref().and_then(|o| o.get("reads").cloned()).unwrap_or(Value::Null),             "neverAnswer": true,         })     }@@ -499,6 +573,7 @@ pub fn first_blocking_dialog(windows: &[WindowInfo]) -> Option<BlockingDialog> {             window: Some(w.clone()),             blocking: true,             why: "a visible dialog, not yet judged".into(),+            owner_settle: None,         }     }).next() }@@ -1057,7 +1132,7 @@ Some(0), Some(0), false, Some(3), None, true, Some(false), false, Some(true), So             Some("unsaved_documents")         );         // ...and force waives NONE of the others.-        let dialog = BlockingDialog { hwnd: 9, title: "Save changes?".into(), kind: "save_changes", contents: None, window: None, blocking: true, why: String::new() };+        let dialog = BlockingDialog { hwnd: 9, title: "Save changes?".into(), kind: "save_changes", contents: None, window: None, blocking: true, why: String::new(), owner_settle: None };         let cases: Vec<(&str, Result<(), Refusal>)> = vec![             ("open docs", check_preconditions( Some(2), Some(2), false, Some(0), None, true, Some(false), false, Some(true), Some(&id()), false, true)),@@ -1110,9 +1185,9 @@ Some(0), Some(0), false, Some(0), None, true, Some(false), false, Some(true), No      #[test]     fn a_save_prompt_gets_its_own_code_because_it_should_be_impossible() {-        let save = BlockingDialog { hwnd: 9, title: "Save changes to Part1?".into(), kind: "save_changes", contents: None, window: None, blocking: true, why: String::new() };+        let save = BlockingDialog { hwnd: 9, title: "Save changes to Part1?".into(), kind: "save_changes", contents: None, window: None, blocking: true, why: String::new(), owner_settle: None };         assert_eq!(save.error_code(), "stop_blocked_by_save_prompt");-        let other = BlockingDialog { hwnd: 9, title: "3DEXPERIENCE sign in".into(), kind: "license", contents: None, window: None, blocking: true, why: String::new() };+        let other = BlockingDialog { hwnd: 9, title: "3DEXPERIENCE sign in".into(), kind: "license", contents: None, window: None, blocking: true, why: String::new(), owner_settle: None };         assert_eq!(other.error_code(), "stop_blocked_by_dialog");         // ...and each stops the ladder from the precondition sweep.         for d in [&save, &other] {
tests/readiness.rs+95
@@ -770,3 +770,98 @@ fn a_platform_load_without_its_marker_is_held_and_the_logs_shape_is_recorded() {         s(&r)     ); }++/// MODALITY IS A PERSISTENT MEASUREMENT, NOT ONE READ (0.9.8). MEASURED+/// 2026-10-01 on 0.9.7: the 3DEXPERIENCE Connected notice — hwnd 4329196,+/// `#32770`, style 0x94000044, exstyle 0x90080, 301x67, nine Static controls+/// (3 visible, 6 hidden) reading the template placeholders — was read with its+/// owner frame DISABLED while SOLIDWORKS was busy inside the sweep's own close,+/// and stopped `swx_close_all_safely` as a modal; seconds later the frame was+/// enabled again. The census now re-reads the owner when it is the ONLY fact+/// that blocks: disabled then enabled -> a notification, reported with the read+/// history; disabled on every read -> still a modal. And a window with a+/// Button-class control is never settled at all.+fn sweep_with_notice(tag: &str, owner_reads: Value, extra_controls: Value) -> (Bridge, Value, TempDir, TempDir) {+    let scratch = TempDir::new(tag);+    session(+        &scratch,+        json!([{"docType": "part", "path": "untitled:a", "title": "Part1", "saved": false, "dirty": true}]),+    );+    let machine = TempDir::new(&format!("{tag}-machine"));+    std::fs::write(+        machine.join("windows.json"),+        json!([{"hwnd": 4329196, "className": "#32770", "title": "", "visible": true,+                "owner": 856844, "ownerEnabled": false, "ownerEnabledReads": owner_reads,+                "style": 0x9400_0044u64, "exStyle": 0x90080u64, "rect": [533, 123, 834, 190]}])+        .to_string(),+    )+    .unwrap();+    let mut controls = vec![+        json!(["Static", 0, "Title", true]), json!(["Static", 0, "Description", true]), json!(["Static", 0, "", true]),+        json!(["Static", 0, "Button1", false]), json!(["Static", 0, "Button2", false]),+        json!(["Static", 0, "Hyperlink Text 1", false]), json!(["Static", 0, "Hyperlink Text 2", false]),+        json!(["Static", 0, "", false]), json!(["Static", 0, "", false]),+    ];+    controls.extend(extra_controls.as_array().cloned().unwrap_or_default());+    std::fs::write(+        machine.join("dialog-open.json"),+        json!({"hwnd": 4329196, "controls": controls}).to_string(),+    )+    .unwrap();+    let b = Bridge::start_in(+        TempDir::borrowed(&scratch.0),+        &[("SWX_LAUNCH_FAKE_DIR", &machine.path()), ("SWX_OWNER_SETTLE_MS", "30")],+    );+    b.call("swx_start", json!({}));+    let r = b.call("swx_close_all_safely", json!({"reason": "the user asked me to close SOLIDWORKS"}));+    (b, r, scratch, machine)+}++#[test]+fn an_owner_disabled_for_an_instant_does_not_make_a_notice_a_modal() {+    let (b, r, _s, _m) = sweep_with_notice("settle-transient", json!([false, false, true]), json!([]));+    check!(+        b,+        "swx_close_all_safely: the notice whose owner was busy for an instant does not stop the sweep",+        r["success"] == true && r["done"] == true && r["stoppedByDialog"].is_null(),+        s(&r)+    );+    let n = &r["notifications"][0];+    check!(+        b,+        "...and it is reported as a notification, ownerDisabledTransiently, with the read history",+        n["kind"] == "notification"+            && n["ownerDisabledTransiently"] == true+            && n["ownerReads"].as_array().map(|a| a.len() >= 2 && a[0]["enabled"] == false+                && a.last().map(|l| l["enabled"] == true).unwrap_or(false)).unwrap_or(false),+        s(&r["notifications"])+    );+}++#[test]+fn an_owner_disabled_on_every_read_is_still_a_modal() {+    let (b, r, _s, _m) = sweep_with_notice("settle-persistent", json!([false]), json!([]));+    check!(+        b,+        "swx_close_all_safely: disabled on every re-read => stopped_by_dialog, as before",+        r["errorCode"] == "stopped_by_dialog"+            && r["stoppedByDialog"]["ownerDisabledTransiently"] == false+            && r["stoppedByDialog"]["ownerReads"].as_array().map(|a| a.len() == 6).unwrap_or(false),+        s(&r)+    );+}++#[test]+fn a_window_with_a_button_is_never_settled() {+    // Same evidence, plus a Button-class control: something is on offer. The+    // owner turning enabled later does not make it a notification.+    let (b, r, _s, m) =+        sweep_with_notice("settle-button", json!([false, true]), json!([["Button", 0x50010000u64, "OK", true]]));+    check!(+        b,+        "swx_close_all_safely: a window with a Button is a modal whatever its owner later reads",+        r["errorCode"] == "stopped_by_dialog" && r["stoppedByDialog"]["ownerReads"].is_null(),+        s(&r)+    );+    check!(b, "...and its owner was never re-read", !m.join("owner-reads-4329196.cursor").exists(), String::new());+}