← Commit history
BRIDGE_VERSION+1−1
@@ -1 +1 @@-0.9.27+0.9.28
Cargo.lock+1−1
@@ -122,7 +122,7 @@ dependencies = [  [[package]] name = "swx-bridge"-version = "0.9.27"+version = "0.9.28" dependencies = [  "miniz_oxide",  "serde",
Cargo.toml+1−1
@@ -1,6 +1,6 @@ [package] name = "swx-bridge"-version = "0.9.27"+version = "0.9.28" edition = "2021" rust-version = "1.80" description = "Adom SOLIDWORKS bridge — swx_* verbs over the SOLIDWORKS COM API, as a single native binary"
README.md+8−1
@@ -492,7 +492,14 @@ not.  ## Roadmap -- v0.9.27 (this): **the local sign-in helper.** ab hands a saved password only to+- v0.9.28 (this): **a launch record that outlived its processes is ended, not+  attached.** Measured 2026-10-12: ten days after a launch, with SOLIDWORKS not+  running, `swx_status.launch` still said "up and COM-attached. Carry on." The+  record is now checked against the live processes (pid plus creation time, so+  a reused pid does not count) and reports `stage:"ended"`, `endedWhy` and+  honest advice. The skills also say where `signInWindow` lives: at the top+  level of `swx_status`, beside `waitingOn`.+- v0.9.27: **the local sign-in helper.** ab hands a saved password only to   a local caller, never to a bridge or over the relay, and a cloud agent is not   allowed to start that local caller either (both measured 2026-10-12). So the   page now ships `tools/local-sign-in.ps1`: run on the PC, it asks the vault
SKILL.md+2−1
@@ -69,7 +69,8 @@ BEFORE you do it:    `host_app_starting` until `solidworks.ready` is true — wait `retryAfterSeconds`    and call again. `startingUnverifiable:true` means only reads may run.    `waitingOn:"userSignIn"` means a 3DEXPERIENCE sign-in is up. `signInWindow`-   names it `{hwnd, title, ownerImage, host}`, and the host can be the platform+   (at the TOP level of the reply, not inside `solidworks`) names it+   `{hwnd, title, ownerImage, host}`, and the host can be the platform    updater (`desktopUpdate`, a window titled just "3DEXPERIENCE" that parks    every app start). Tell the user, or call `swx_sign_in {reason}` (add    `userAuthorized:true, window:<hwnd>` on their say-so for a window the
bridge.json+1−1
@@ -2,7 +2,7 @@   "manifest_version": 1,   "name": "solidworks",   "displayName": "SOLIDWORKS",-  "version": "0.9.27",+  "version": "0.9.28",   "description": "Bridge for SOLIDWORKS desktop CAD — 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/LAUNCH-RESILIENCE.md+14
@@ -1379,3 +1379,17 @@ false, waitingOn: null`. The bridge saw nothing, while a sign-in sat on screen. second step: rejection markers cannot be read from a blind tree, so an unclosed window is `sign_in_unconfirmed`, never retried); whether "Remember me" can be read here; whether any ab verb reads a field's value on this host.++**MEASURED 2026-10-12 19:00Z (hosted 0.9.27), and the fix after it.** With+SOLIDWORKS not running (processCount 0) and the updater's login up,+`swx_status.launch` still said stage "attached" and advice "SOLIDWORKS is up and+COM-attached. Carry on." about the launch of 2026-10-02 (`ageSeconds` 878026,+attribution pid 32224). The record had outlived its process by ten days and+several bridge restarts, and the advice contradicted `solidworks.running:false`+in the same reply. The top-level `waitingOn` / `signInWindow` were right: they+come from a live census, never from the record.++| | Fix |+|---|---|+| A record that outlived its processes | When COM is not attached and no launch is in flight, a finished or "attached" record is checked against the census by IDENTITY (pid + creation time, so a reused pid does not count): the attributed process, the SOLIDWORKS it attached to, and its chain. If none of them is running, `launch` reports `stage:"ended"`, `finished:true`, `endedWhy` (the pids it named), `recordedStage` (what the record said), and advice that the launch is over and `swx_start` begins a new one. It never says "attached" or "carry on" then. The record on disk is not rewritten; the report is |+
install.sh

  
package.json+1−1
@@ -1,6 +1,6 @@ {   "slug": "solidworks-bridge",-  "version": "0.9.27",+  "version": "0.9.28",   "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": [
skills/solidworks-sign-in/SKILL.md+4−1
@@ -50,7 +50,10 @@ Every line below is marked **[measured]** (seen on a real machine, 2026-10-02 to   whatever its title, class or owner. The two titles above are only shortcuts.   `swx_status` reports `waitingOn: "userSignIn"` and   `signInWindow {hwnd, title, ownerImage, host, recognizedBy}` (host is-  `solidworks`, `visualize` or `desktopUpdate`) while any of them is up.+  `solidworks`, `visualize` or `desktopUpdate`) while any of them is up. Both+  sit at the TOP level of the `swx_status` reply, next to each other, not inside+  its `solidworks` block. The `visualize` block repeats `signInWindow` only+  while Visualize is parked behind the sign-in (`parkedBehind`).  ## When the tree shows only the frame 
src/launch.rs+57
@@ -3240,6 +3240,45 @@ pub fn report(os: &dyn LaunchOs) -> Value {     report_with_attach(os, false) } +/// HAS THIS RECORD OUTLIVED EVERY PROCESS IT NAMES? MEASURED 2026-10-12 (hosted+/// 0.9.27): with SOLIDWORKS not running, `launch` still said stage "attached",+/// advice "SOLIDWORKS is up and COM-attached. Carry on.", about a launch of+/// 2026-10-02 — ten days and several bridge restarts after its process was+/// gone. The record names processes by IDENTITY (pid + creation time); when the+/// census holds none of them, the launch is over. `None` = something it names is+/// still alive, or it names nothing (cannot tell).+pub fn record_outlived(a: &Attempt, census: &[ProcInfo]) -> Option<String> {+    let mut named: Vec<(u32, Option<u64>, String)> = Vec::new();+    if let Some(at) = &a.attribution {+        named.push((at.pid, at.created_at, "the launch's attributed process".into()));+    }+    if let Some((pid, created)) = a.attributed_sldworks {+        named.push((pid, Some(created), "the SOLIDWORKS it attached to".into()));+    }+    for p in &a.chain {+        named.push((p.pid, p.created_at, p.image.clone()));+    }+    if named.is_empty() {+        return None;+    }+    let alive = |pid: u32, created: Option<u64>| {+        census.iter().any(|p| {+            p.pid == pid+                && match (created, p.created_at) {+                    (Some(a), Some(b)) => a.abs_diff(b) <= 2,+                    _ => true,+                }+        })+    };+    if named.iter().any(|(pid, c, _)| alive(*pid, *c)) {+        return None;+    }+    Some(format!(+        "none of the processes this record names is running any more ({}); the launch it describes is over",+        named.iter().map(|(pid, _, what)| format!("pid {pid}: {what}")).collect::<Vec<_>>().join(", ")+    ))+}+ /// The same report, told whether COM is attached RIGHT NOW — which is what lets /// a session that came up after a sign-in be re-claimed (see /// [`reattribute_after_attach`]). Only `swx_status` knows that fact, so only@@ -3299,6 +3338,24 @@ pub fn report_with_attach(os: &dyn LaunchOs, attached: bool) -> Value {                 obj.insert("advice".into(), json!(a.stage.advice()));                 obj.insert("consecutiveFailures".into(), json!(store.consecutive_failures));                 obj.insert("backoffSeconds".into(), json!(backoff));+                // A RECORD THAT OUTLIVED ITS PROCESSES IS ENDED (0.9.28): never+                // "attached", never "carry on" while nothing it names runs.+                if !attached && live.is_none() && (a.finished || a.stage == Stage::Attached) {+                    if let Ok(census) = os.processes() {+                        if let Some(why) = record_outlived(&a, &census) {+                            obj.insert("recordedStage".into(), json!(a.stage.name()));+                            obj.insert("stage".into(), json!("ended"));+                            obj.insert("finished".into(), json!(true));+                            obj.insert("endedWhy".into(), json!(why));+                            obj.insert(+                                "advice".into(),+                                json!("This launch record is OVER: the processes it names are gone, so SOLIDWORKS from \+                                       that launch is not running. Read `solidworks.running` for now; swx_start begins \+                                       a new launch."),+                            );+                        }+                    }+                }             }             v         }
tests/launch.rs+42
@@ -1745,3 +1745,45 @@ fn re_attribution_claims_only_the_process_com_is_attached_to() {         format!("{}", st["launch"]["reattributionRefusedBecause"])     ); }++/// MEASURED 2026-10-12 (hosted 0.9.27): with SOLIDWORKS NOT running, `launch`+/// still said stage "attached" and "SOLIDWORKS is up and COM-attached. Carry+/// on." about the launch of 2026-10-02 — its process ten days gone. A record+/// that outlived every process it names is ENDED.+#[test]+fn a_launch_record_that_outlived_its_processes_is_ended_not_attached() {+    let m = Machine::new("launch-stale-record", json!({}));+    // pid 32224 has been REUSED by an unrelated process created just now: the+    // same number, another process (identity is pid + creation time).+    let mut rows: Vec<Value> = serde_json::from_str(&std::fs::read_to_string(m.dir.join("preexisting.json")).unwrap()).unwrap();+    rows.push(json!({"pid": 32224, "image": "SWXDesktopUpdate.exe", "ageSeconds": 5}));+    std::fs::write(m.dir.join("preexisting.json"), Value::Array(rows).to_string()).unwrap();+    let scratch = TempDir::new("launch-stale-record-scratch");+    std::fs::write(+        scratch.join("launch-state.json"),+        json!({"attempt": {+            "id": 7, "startedAt": 1790953690u64, "invokedAt": 1790953690u64, "route": "shellExecute",+            "stage": "attached", "finished": true, "outcome": "attached",+            "attribution": {"pid": 32224, "createdAt": 1790953692u64, "source": "shellExecuteHandle"},+            "attributedSolidworks": {"pid": 41000, "createdAt": 1790953720u64},+            "chainPids": [{"pid": 32224, "image": "CATSTART.exe", "createdAt": 1790953692u64}]+        }}).to_string(),+    )+    .unwrap();+    let b = m.boot(TempDir::borrowed(&scratch.0), &[]);+    let st = b.call("swx_status", json!({}));+    let l = &st["launch"];+    check!(+        b,+        "stage ended (not attached), finished, endedWhy names the gone pids, the old stage kept as recordedStage",+        l["stage"] == "ended" && l["finished"] == true && l["recordedStage"] == "attached"+            && l["endedWhy"].as_str().unwrap_or("").contains("32224"),+        s(l)+    );+    check!(+        b,+        "the advice never says carry on while nothing it names runs",+        !l["advice"].as_str().unwrap_or("").to_lowercase().contains("carry on") && l["advice"].as_str().unwrap_or("").contains("OVER"),+        s(l)+    );+}
tests/signin.rs+9−3
@@ -826,12 +826,18 @@ fn keystroke_ab() -> common::AbStub { fn drive_keystrokes(m: &Machine, ab: &common::AbStub, typed_lands: Option<String>, focus_follows: bool) {     let deadline = Instant::now() + Duration::from_secs(25);     let mut state = 0;+    let mut focused = false;     while Instant::now() < deadline {         let clicks = ab.calls_to("desktop_ui_click");         let clicked = |n: &str| clicks.iter().any(|c| c["args"]["name"] == n);-        if focus_follows && !ab.calls_to("desktop_raise_window").is_empty() {-            std::fs::write(m.dir.join("foreground.json"),-                json!({"hwnd": 777001, "pid": 1, "title": "Login - 3DEXPERIENCE ID", "className": "x"}).to_string()).unwrap();+        // Written ONCE: rewriting it every tick truncates the file under a+        // reader, which then sees "no window has the foreground" (a harness+        // race seen under full-suite load, 2026-10-12).+        if focus_follows && !focused && !ab.calls_to("desktop_raise_window").is_empty() {+            let tmp = m.dir.join("foreground.json.tmp");+            std::fs::write(&tmp, json!({"hwnd": 777001, "pid": 1, "title": "Login - 3DEXPERIENCE ID", "className": "x"}).to_string()).unwrap();+            std::fs::rename(&tmp, m.dir.join("foreground.json")).unwrap();+            focused = true;         }         match state {             0 if !ab.calls_to("desktop_type").is_empty() => {
uninstall.sh