app
SOLIDWORKS Bridge
Public Made by Adomby adom
Launch, model and quit SOLIDWORKS from any Adom container. Nothing is ever lost.
← Commit history
0.11.2 source
26 files changed
+434−15
BRIDGE_VERSION+1−1Cargo.lock+1−1Cargo.toml+1−1README.md+22−1SKILL.md+4−1bridge.json+1−1docs/LAUNCH-RESILIENCE.md+53−3install.shpackage.json+1−1src/dialogs.rs+61−1src/launch.rs+135−1src/launchos.rs+18−3src/verbs.rs+14tests/adversary.rs+1tests/adversary13.rs+1tests/adversary14.rs+2tests/adversary15.rs+1tests/adversary16.rs+1tests/adversary17.rs+1tests/adversary18.rs+2tests/adversary2.rs+1tests/adversary3.rs+1tests/adversary4.rs+1tests/launch.rs+109tests/signin.rs+1uninstall.shBRIDGE_VERSION+1−1@@ -1 +1 @@-0.11.1+0.11.2
Cargo.lock+1−1@@ -122,7 +122,7 @@ dependencies = [ [[package]] name = "swx-bridge"-version = "0.11.1"+version = "0.11.2" dependencies = [ "miniz_oxide", "serde",
Cargo.toml+1−1@@ -1,6 +1,6 @@ [package] name = "swx-bridge"-version = "0.11.1"+version = "0.11.2" edition = "2021" rust-version = "1.80" description = "Adom SOLIDWORKS bridge — swx_* verbs over the SOLIDWORKS COM API, as a single native binary"
README.md+22−1@@ -506,7 +506,28 @@ not. ## Roadmap -- v0.11.1 (this): **which verb put that dialog up?** Measured 2026-10-15 on+- v0.11.2 (this): **the File ▸ Open was the launch's own.** The laptop's+ ledger had the sequence:+ - the 3DEXPERIENCE helper sign-in took 2 min 13 s;+ - the supervisor called that a stall, killed the chain and invoked the shortcut+ again;+ - SOLIDWORKS came up 6 s later, from the FIRST invoke;+ - the second invoke reached it as an "open" request, and that raised the+ dialog.++ Now:+ - **`helperSignIn` pauses the stall clocks.** Only a dead chain is a stall, and+ past 600 s the launch STOPS rather than retries.+ - **A retry never re-invokes while any `sldworks.exe` from this launch+ exists.** It reads the census before cleaning up and watches for 15 s before+ any retry. It ledgers `launch_retry_declined` and waits to attach instead.+ - **Each route invocation is a verb for the dialog census**+ (`launch.invoke`, `launch.retry`), with a 90 s grace after it ends. An empty+ File ▸ Open a route provably raised is cancelled by the launch itself and by+ `swx_start`, and ledgered.++ See docs/LAUNCH-RESILIENCE.md. 684 tests.+- v0.11.1: **which verb put that dialog up?** Measured 2026-10-15 on adom_drew2: - a File ▸ Open dialog appeared during a bridge battery, with Drew away; - it was `"Open "`, `#32770`, owned by the SOLIDWORKS main frame, with the
SKILL.md+4−1@@ -96,7 +96,10 @@ BEFORE you do it: and `attribution` (the bridge verb that raised it, if one provably did). The bridge cancels only a standard file dialog with an EMPTY name that it raised itself (`swx_dismiss_blocking_dialogs {close, confirm, reason}`); every other- dialog is the user's to answer.+ dialog is the user's to answer. A launch retry never re-invokes over a+ running SOLIDWORKS, and the 3DEXPERIENCE helper sign-in is never treated as a+ stall (a second invoke at a starting SOLIDWORKS raised a File ▸ Open,+ measured 2026-10-15). 9. **Never end a session the user is working in, and never pile windows on them.** Every quit and sweep refuses `user_is_active` when SOLIDWORKS is in front with recent input, or documents appeared that the bridge did not open;
bridge.json+1−1@@ -2,7 +2,7 @@ "manifest_version": 1, "name": "solidworks", "displayName": "SOLIDWORKS",- "version": "0.11.1",+ "version": "0.11.2", "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+53−3@@ -143,9 +143,59 @@ The route that reaches `attached` is written to `launch-state.json` as `preferredRoute` and tried FIRST next time, so a machine pays the fallback cost once. A remembered route that is not a known route is ignored, never run. -**Budgets** (env overrides exist only so the suite can compress them):-`SWX_LAUNCH_RUN_BEGIN_SECS` 20 · `SWX_LAUNCH_SLDWORKS_SECS` 120 ·-`SWX_LAUNCH_ATTACH_SECS` 240 · `SWX_LAUNCH_INFLIGHT_SECS` 240.+**Budgets, per stage** (env overrides exist only so the suite can compress them):++| Stage | What counts as a stall | Budget |+|---|---|---|+| `invoked` / `launcherStarted` | no `Run Begin` | `SWX_LAUNCH_RUN_BEGIN_SECS` 20 |+| `runBegin` (no helper log yet) | no `sldworks.exe` | `SWX_LAUNCH_SLDWORKS_SECS` 120 |+| `helperSignIn` (0.11.2) | **the clocks are PAUSED.** Only a DEAD chain (every process of it gone, no `sldworks.exe`) is a stall. Past `SWX_LAUNCH_HELPER_SECS` (600) the launch STOPS: `helperSignInTooLong`, nothing killed, nothing re-invoked | 600, then stop |+| 3DEXPERIENCE ID sign-in window up | paused (0.6.x) | none |+| `sldworksUp` / `attaching` | COM does not attach → stop, never a stall | `SWX_LAUNCH_ATTACH_SECS` 240 |++`SWX_LAUNCH_INFLIGHT_SECS` 240 · `SWX_LAUNCH_RETRY_SETTLE_MS` 15000 ·+`SWX_LAUNCH_DIALOG_GRACE_MS` 90000.++### A retry never re-invokes over a running SOLIDWORKS (0.11.2)++THE INCIDENT, from the laptop's ledger (2026-10-15, attempt 24, route+`platformShortcut`):++| Time (Z) | Event |+|---|---|+| 14:48:58 | `solidworks_launch_started` |+| 14:49:02 | `launcherStarted` |+| 14:49:04 | `runBegin`, `helperSignIn` |+| **14:51:11** | stage `stalled` + `solidworks_launch_stalled {cleanup:"cleaned", killed:[CATSTART.exe 39612, …]}` |+| 14:51:11 | stage `invoked`: the shortcut invoked AGAIN |+| 14:51:14 / :16 | `launcherStarted`, `runBegin` |+| **14:51:17** | `sldworksUp`, 6 s after the re-invoke: the FIRST invoke's SOLIDWORKS, already coming up |+| 14:51:53 | attached, pid 82476, attributed by ancestry |+| 15:00:15 | first sighting of the dialog: `stop_blocked_by_dialog` "Open " |++The "stall" was false: the 3DEXPERIENCE helper sign-in simply took more than two+minutes. The second invoke reached the already-starting Connected session as an+"open" request, and SOLIDWORKS raised File ▸ Open, which blocked every quit+after it.++So:++- **`helperSignIn` is not a stall.** The clocks pause from the helper's log until+ `sldworks.exe` appears (see the table above).+- **No re-invoke while this launch has a SOLIDWORKS.** Any `sldworks.exe` outside+ the FIRST route's baseline belongs to this launch. When a route stalls, the+ census is read before anything is cleaned up. Before any retry, the supervisor+ watches for `SWX_LAUNCH_RETRY_SETTLE_MS`. If such a SOLIDWORKS is there:+ - nothing is killed and nothing is invoked;+ - `launch_retry_declined {why, pid, createdAt}` is ledgered;+ - the supervisor waits to attach to that SOLIDWORKS.+- **Each route invocation is a "verb" for the dialog census**+ (`launch.invoke`, `launch.retry`), in flight until `SWX_LAUNCH_DIALOG_GRACE_MS`+ after it ends. A File ▸ Open with an EMPTY name that a route provably raised+ (proved absent after it began, no user input since) is cancelled by the+ supervisor itself, and by `swx_start`. It is ledgered as `dialog_cancelled`+ with `by:"the launch supervisor"`. A launch never leaves the session blocked+ by its own dialog. ### When SOLIDWORKS is up but COM will not attach
install.shpackage.json+1−1@@ -1,6 +1,6 @@ { "slug": "solidworks-bridge",- "version": "0.11.1",+ "version": "0.11.2", "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": [
src/dialogs.rs+61−1@@ -142,7 +142,9 @@ pub fn bridge_caused(s: &Value, last_input_now_ms: Option<u64>) -> Option<String let absent = s.get("absentAtMs").and_then(|v| v.as_u64())?; let input_then = s.get("lastInputMs").and_then(|v| v.as_u64())?; let input_now = last_input_now_ms?;- s.get("appearedDuring")?.as_array()?.iter().find_map(|d| {+ // The most recent verb first: a retry in flight beside an earlier route's+ // grace window is the one that raised it.+ s.get("appearedDuring")?.as_array()?.iter().rev().find_map(|d| { let started = d.get("startedMs").and_then(|v| v.as_u64())?; let verb = d.get("verb").and_then(|v| v.as_str())?; (started <= absent && input_then < started && input_now < started).then(|| verb.to_string())@@ -199,6 +201,64 @@ pub fn end(id: u64) -> Vec<Value> { fresh } +/// Cancel every dialog that is up, provably raised by the LAUNCH supervisor+/// (`launch.invoke` / `launch.retry`), and is the standard file dialog with an+/// empty name — what a re-invoke at a starting SOLIDWORKS raises (measured+/// 2026-10-15). Ledgered; a launch never leaves the session blocked by its own+/// Open dialog. Everything else is left alone.+pub fn cancel_launch_file_dialogs(os: &dyn crate::launchos::LaunchOs) -> Vec<Value> {+ let mut out = Vec::new();+ let Some(up) = observe(os) else { return out };+ for s in up {+ let Some(h) = s.get("hwnd").and_then(|h| h.as_i64()) else { continue };+ if let Ok((verb, sighting)) = may_cancel(h, os) {+ if !verb.starts_with("launch.") {+ continue;+ }+ let r = os.cancel_dialog(h);+ crate::ledger::record(+ "dialog_cancelled",+ json!({"hwnd": h, "title": sighting["title"], "raisedDuring": verb, "by": "the launch supervisor",+ "sighting": sighting, "ok": r.is_ok(), "error": r.as_ref().err()}),+ );+ out.push(json!({"hwnd": h, "title": sighting["title"], "raisedDuring": verb, "ok": r.is_ok()}));+ }+ }+ out+}++/// A launch route, bracketed like a verb: in flight from its invoke until+/// `grace` after it ends, watched (and its own empty File ▸ Open cancelled)+/// all that time on a thread of its own.+pub struct LaunchBracket {+ id: u64,+ grace: std::time::Duration,+}++impl LaunchBracket {+ pub fn begin(label: &str, grace: std::time::Duration) -> LaunchBracket {+ LaunchBracket { id: begin(label), grace }+ }+}++impl Drop for LaunchBracket {+ fn drop(&mut self) {+ let (id, grace) = (self.id, self.grace);+ std::thread::spawn(move || {+ let os = crate::launchos::os();+ let until = std::time::Instant::now() + grace;+ loop {+ let _ = cancel_launch_file_dialogs(os.as_ref());+ if std::time::Instant::now() >= until {+ break;+ }+ std::thread::sleep(std::time::Duration::from_millis(500));+ }+ end(id);+ });+ }+}+ /// Does `dispatch` bracket this verb? Every known verb except the ones that /// only read the bridge's own state (they never touch SOLIDWORKS). pub fn brackets(verb: &str) -> bool {
src/launch.rs+135−1@@ -595,6 +595,33 @@ pub fn inflight_budget() -> Duration { pub fn backoff_budget() -> Duration { Duration::from_secs(secs_env("SWX_LAUNCH_BACKOFF_SECS", 60)) }+/// THE 3DEXPERIENCE HELPER SIGN-IN may take this long before the launch gives+/// up on it (0.11.2) — and giving up STOPS, it never re-invokes. MEASURED+/// 2026-10-15: helperSignIn took 2 min 13 s on adom_drew2, the old clock+/// called that a stall, killed the chain and invoked the shortcut again — and+/// the re-invoke reached the already-starting SOLIDWORKS as an "open" request:+/// a File ▸ Open dialog that blocked every quit after it.+pub fn helper_budget() -> Duration {+ Duration::from_secs(secs_env("SWX_LAUNCH_HELPER_SECS", 600))+}+/// Before re-invoking after a stall, watch this long for a SOLIDWORKS the+/// stalled route started anyway (measured: it came up 6 s after the cleanup).+pub fn retry_settle() -> Duration {+ Duration::from_millis(secs_env("SWX_LAUNCH_RETRY_SETTLE_MS", 15_000))+}+/// How long after a route a dialog that appears is still attributed to it+/// (and, when it is a file dialog with an empty name, cancelled by it).+pub fn dialog_grace() -> Duration {+ Duration::from_millis(secs_env("SWX_LAUNCH_DIALOG_GRACE_MS", 90_000))+}++/// A SOLIDWORKS that this launch did not find running before it began: any+/// sldworks.exe outside the FIRST route's baseline. While one exists, no route+/// is invoked again — a second invoke reaches it as an "open" request.+pub fn new_solidworks(census: &[ProcInfo], first_baseline: &[u32]) -> Option<ProcInfo> {+ census.iter().find(|p| p.is("SLDWORKS.exe") && !first_baseline.contains(&p.pid)).cloned()+}+ fn poll_interval() -> Duration { Duration::from_millis(secs_env("SWX_LAUNCH_POLL_MS", 1000).max(50)) }@@ -1599,7 +1626,36 @@ pub fn mark_attached() { fn run_supervisor(channel: Channel, os: Box<dyn LaunchOs>) { let plan = attempt().map(|a| a.plan).unwrap_or_default(); let shortcut = attempt().map(|a| a.shortcut).unwrap_or_default();+ // WHAT RAN BEFORE THIS LAUNCH BEGAN — the FIRST route's baseline. Any+ // sldworks.exe outside it is this launch's, whichever route started it.+ let first_baseline: Vec<u32> = os.processes().map(|c| c.iter().map(|p| p.pid).collect()).unwrap_or_default(); for (index, route) in plan.iter().enumerate() {+ // NEVER RE-INVOKE OVER A RUNNING SOLIDWORKS (0.11.2). Before a retry,+ // watch a moment for a SOLIDWORKS the stalled route started anyway; if+ // one is there, wait for it instead.+ if index > 0 {+ let until = std::time::Instant::now() + retry_settle();+ loop {+ if let Some(sw) = os.processes().ok().and_then(|c| new_solidworks(&c, &first_baseline)) {+ crate::ledger::record(+ "launch_retry_declined",+ json!({"route": route, "why": format!("a SOLIDWORKS from this launch is already running (pid {}, createdAt {:?})", sw.pid, sw.created_at),+ "pid": sw.pid, "createdAt": sw.created_at}),+ );+ wait_for_new_solidworks(&channel, sw.pid);+ return;+ }+ if std::time::Instant::now() >= until {+ break;+ }+ std::thread::sleep(poll_interval());+ }+ }+ // EVERY INVOKE IS A "VERB" FOR THE DIALOG CENSUS (0.11.2): a dialog+ // that appears while this route runs — or within the grace after it —+ // is attributed to it, and a File ▸ Open with an empty name that it+ // raised is cancelled by it.+ let _dialogs = crate::dialogs::LaunchBracket::begin(if index == 0 { "launch.invoke" } else { "launch.retry" }, dialog_grace()); // EVERY PER-ROUTE FACT RESETS. `root_confirmed` and `unattributed_log` // did not, with two consequences: `notConfirmed` became unreachable // after route 1 (so H2 was restored for routes 2-4), and the log gate's@@ -1759,6 +1815,17 @@ fn run_supervisor(channel: Channel, os: Box<dyn LaunchOs>) { return; } RouteOutcome::Stalled { note, saw_log, unattributed_log } => {+ // A SOLIDWORKS THIS LAUNCH STARTED IS ALREADY UP: the "stall" is+ // over. Its chain is not killed and nothing is invoked again.+ if let Some(sw) = os.processes().ok().and_then(|c| new_solidworks(&c, &first_baseline)) {+ crate::ledger::record(+ "launch_retry_declined",+ json!({"route": route, "note": note, "why": format!("a SOLIDWORKS from this launch is already running (pid {}, createdAt {:?})", sw.pid, sw.created_at),+ "pid": sw.pid, "createdAt": sw.created_at}),+ );+ wait_for_new_solidworks(&channel, sw.pid);+ return;+ } // THE FOUR WAYS A ROUTE CAN END, and they are not the same. // * attributed AND confirmed: the invoke told us its root and // we saw it, so the chain is ours by proof and we end it;@@ -1850,6 +1917,30 @@ fn run_supervisor(channel: Channel, os: Box<dyn LaunchOs>) { ); } +/// A SOLIDWORKS from this launch is up: wait for COM to attach to IT, for the+/// attach budget, instead of starting anything else.+fn wait_for_new_solidworks(channel: &Channel, pid: u32) {+ set_stage_at_least(Stage::SldworksUp);+ set_stage_at_least(Stage::Attaching);+ let until = std::time::Instant::now() + attach_budget();+ while std::time::Instant::now() < until {+ if channel.attached() {+ set_stage(Stage::Attached);+ finish("attached", None);+ return;+ }+ std::thread::sleep(attach_poll_interval());+ }+ finish(+ "sldworksUpNotAttached",+ Some(format!(+ "sldworks.exe (pid {pid}) from this launch is running but COM did not attach within {}s — it may be at a \+ 3DEXPERIENCE sign-in only the user can answer. Nothing was invoked again.",+ attach_budget().as_secs()+ )),+ );+}+ fn record_try(route: &str, invoked_at: u64, stalled: bool, c: Cleanup, note: Option<String>) { update(|a| { let stage = a.stage;@@ -2003,6 +2094,12 @@ fn watch_route(channel: &Channel, os: &dyn LaunchOs, route: &str, w: &Watch) -> let mut signed_in_paused = Duration::ZERO; let mut sign_in_announced = false; let mut sign_in_hwnd: Option<i64> = None;+ // ── THE HELPER SIGN-IN PAUSE (0.11.2). From the helper's log appearing+ // until sldworks.exe does, the stall clocks STOP: that stage can take+ // minutes (and may be waiting on the user). Only a DEAD chain ends it.+ let mut helper_since: Option<std::time::Instant> = None;+ let mut helper_paused = Duration::ZERO;+ let mut chain_alive = true; loop { if last_attach_probe.elapsed() >= attach_poll_interval() { last_attach_probe = std::time::Instant::now();@@ -2095,6 +2192,9 @@ fn watch_route(channel: &Channel, os: &dyn LaunchOs, route: &str, w: &Watch) -> .any(|l| baseline_was_empty || l.is_newer_than(w.base_stamp, w.base_created)) { set_stage_at_least(Stage::HelperSignIn);+ if helper_since.is_none() && !seen_sldworks && helper_paused.is_zero() {+ helper_since = Some(std::time::Instant::now());+ } } } }@@ -2232,7 +2332,14 @@ fn watch_route(channel: &Channel, os: &dyn LaunchOs, route: &str, w: &Watch) -> sldworks_pid = Some(sw.pid); set_stage_at_least(Stage::SldworksUp); set_stage_at_least(Stage::Attaching);+ // The helper stage is over: the clocks resume.+ if let Some(t) = helper_since.take() {+ helper_paused += t.elapsed();+ } }+ // IS OUR CHAIN STILL THERE? Only a chain we have SEEN can be dead.+ let ours_now = chain_pids_now();+ chain_alive = !root_confirmed || ours_now.is_empty() || census.iter().any(|p| ours_now.contains(&p.pid)); } if seen_run_begin {@@ -2310,7 +2417,34 @@ fn watch_route(channel: &Channel, os: &dyn LaunchOs, route: &str, w: &Watch) -> // while a human is being asked for a password. let elapsed = started .elapsed()- .saturating_sub(signed_in_paused + sign_in_since.map(|t| t.elapsed()).unwrap_or_default());+ .saturating_sub(signed_in_paused + sign_in_since.map(|t| t.elapsed()).unwrap_or_default())+ .saturating_sub(helper_paused + helper_since.map(|t| t.elapsed()).unwrap_or_default());++ // THE HELPER STAGE: not a stall while the chain lives. A dead chain is+ // one; a helper that outlasts its own (long) budget STOPS the launch —+ // it is never retried, because a retry is a second invoke at whatever+ // the first one is still bringing up.+ if let Some(t) = helper_since {+ if !chain_alive {+ return RouteOutcome::Stalled {+ note: format!("route '{route}' reached the 3DEXPERIENCE helper sign-in and its launcher chain then EXITED with no sldworks.exe"),+ saw_log,+ unattributed_log,+ };+ }+ if t.elapsed() > helper_budget() {+ finish(+ "helperSignInTooLong",+ Some(format!(+ "the 3DEXPERIENCE helper sign-in has run {}s with no sldworks.exe — it may be waiting on the user. \+ Nothing was killed and the shortcut was NOT invoked again (a second invoke reaches a starting \+ SOLIDWORKS as an 'open' request: a File ▸ Open dialog, measured 2026-10-15)",+ t.elapsed().as_secs()+ )),+ );+ return RouteOutcome::Stop;+ }+ } if !seen_run_begin && elapsed > run_begin_budget() { if log_baseline_unknown {
src/launchos.rs+18−3@@ -701,6 +701,10 @@ pub struct RouteSpec { pub helper_after_ms: Option<u64>, pub sldworks_after_ms: Option<u64>, pub attach_after_ms: Option<u64>,+ /// `sldworksSurvivesKill` (0.11.2): the launcher had already spawned+ /// sldworks.exe when its chain was killed, so it comes up anyway — measured+ /// 2026-10-15, 6 s after the "stall" cleanup.+ pub sldworks_survives_kill: bool, } impl Default for RouteSpec {@@ -718,6 +722,7 @@ impl Default for RouteSpec { helper_after_ms: None, sldworks_after_ms: None, attach_after_ms: None,+ sldworks_survives_kill: false, } } }@@ -797,6 +802,7 @@ impl FakeOs { helper_after_ms: ms("helperAfterMs"), sldworks_after_ms: ms("sldworksAfterMs"), attach_after_ms: ms("attachAfterMs"),+ sldworks_survives_kill: entry.and_then(|e| e.get("sldworksSurvivesKill")).and_then(|v| v.as_bool()).unwrap_or(false), } } @@ -867,6 +873,15 @@ impl FakeOs { text.push_str(&line.to_string()); text.push('\n'); let _ = std::fs::write(&path, text);+ // `dialogOnInvoke` (0.11.2): window rows this route's invoke puts up —+ // the File ▸ Open a re-invoke at a starting Connected SOLIDWORKS raised.+ let routes: Value = std::fs::read_to_string(dir.join("routes.json")).ok().and_then(|t| serde_json::from_str(&t).ok()).unwrap_or(Value::Null);+ if let Some(rows) = routes.get(route).and_then(|r| r.get("dialogOnInvoke")).and_then(|d| d.as_array()) {+ let wpath = dir.join("windows.json");+ let mut all: Vec<Value> = std::fs::read_to_string(&wpath).ok().and_then(|t| serde_json::from_str(&t).ok()).unwrap_or_default();+ all.extend(rows.iter().cloned());+ let _ = std::fs::write(&wpath, Value::Array(all).to_string());+ } seq } @@ -886,7 +901,7 @@ impl FakeOs { let dead = me.terminated_pids(); me.invocations() .into_iter()- .filter(|i| me.alive(i, &dead))+ .filter(|i| me.alive(i, &dead) || me.spec(&i.route).sldworks_survives_kill) .find(|i| me.reached(i, me.spec(&i.route).attach_after_ms)) .map(|i| i.sldworks_pid()) }@@ -896,7 +911,7 @@ impl FakeOs { let dead = me.terminated_pids(); me.invocations() .iter()- .filter(|i| me.alive(i, &dead))+ .filter(|i| me.alive(i, &dead) || me.spec(&i.route).sldworks_survives_kill) .any(|i| me.reached(i, me.spec(&i.route).attach_after_ms)) } }@@ -1053,7 +1068,7 @@ impl LaunchOs for FakeOs { } } }- if self.alive(&inv, &dead) && self.reached(&inv, spec.sldworks_after_ms) {+ if (self.alive(&inv, &dead) || spec.sldworks_survives_kill) && self.reached(&inv, spec.sldworks_after_ms) { out.push(ProcInfo { pid: inv.sldworks_pid(), image: "SLDWORKS.exe".into(),
src/verbs.rs+14@@ -806,7 +806,21 @@ fn swx_progress(ctx: &Ctx, _args: &Value) -> Value { // ── verbs: app lifecycle ──────────────────────────────────────────────────── +/// `swx_start`, and then (0.11.2) the launch's own mess cleared: a File ▸ Open+/// with an empty name that a launch route provably raised is cancelled here+/// too — ledgered — so a launch never leaves the session blocked. fn swx_start(ctx: &Ctx, args: &Value) -> Value {+ let mut out = swx_start_inner(ctx, args);+ let cancelled = crate::dialogs::cancel_launch_file_dialogs(crate::launchos::os().as_ref());+ if !cancelled.is_empty() {+ if let Value::Object(m) = &mut out {+ m.insert("dialogsCancelled".into(), json!(cancelled));+ }+ }+ out+}++fn swx_start_inner(ctx: &Ctx, args: &Value) -> Value { // SOLIDWORKS VISUALIZE (0.9.19): its platform shortcut, its own readiness. if arg_str(args, "app") == Some("visualize") { return crate::visualize::start(ctx, args);
tests/adversary.rs+1@@ -88,6 +88,7 @@ impl Sim { ("SWX_LAUNCH_SLDWORKS_SECS", "4"), ("SWX_LAUNCH_ATTACH_SECS", "8"), ("SWX_LAUNCH_POLL_MS", "100"),+ ("SWX_LAUNCH_RETRY_SETTLE_MS", "400"), ("SWX_LAUNCH_ATTACH_POLL_MS", "150"), ("SWX_LAUNCH_INFLIGHT_SECS", "120"), ];
tests/adversary13.rs+1@@ -177,6 +177,7 @@ fn a_session_descended_from_a_foreign_launcher_is_never_adopted_as_ours() { ("SWX_LAUNCH_SLDWORKS_SECS", "2"), ("SWX_LAUNCH_ATTACH_SECS", "3"), ("SWX_LAUNCH_POLL_MS", "100"),+ ("SWX_LAUNCH_RETRY_SETTLE_MS", "400"), ("SWX_LAUNCH_BACKOFF_SECS", "0"), ], );
tests/adversary14.rs+2@@ -59,6 +59,7 @@ fn a_session_with_no_lineage_link_is_never_adopted_however_often_status_is_read( ("SWX_LAUNCH_SLDWORKS_SECS", "2"), ("SWX_LAUNCH_ATTACH_SECS", "3"), ("SWX_LAUNCH_POLL_MS", "100"),+ ("SWX_LAUNCH_RETRY_SETTLE_MS", "400"), ("SWX_LAUNCH_BACKOFF_SECS", "0"), ], );@@ -157,6 +158,7 @@ fn our_own_launcher_is_not_a_foreign_chain() { ("SWX_LAUNCH_SLDWORKS_SECS", "2"), ("SWX_LAUNCH_ATTACH_SECS", "3"), ("SWX_LAUNCH_POLL_MS", "100"),+ ("SWX_LAUNCH_RETRY_SETTLE_MS", "400"), ("SWX_LAUNCH_BACKOFF_SECS", "0"), ], );
tests/adversary15.rs+1@@ -67,6 +67,7 @@ fn an_unattributable_routes_own_chain_is_not_a_foreign_chain() { ("SWX_LAUNCH_SLDWORKS_SECS", "2"), ("SWX_LAUNCH_ATTACH_SECS", "3"), ("SWX_LAUNCH_POLL_MS", "100"),+ ("SWX_LAUNCH_RETRY_SETTLE_MS", "400"), ("SWX_LAUNCH_BACKOFF_SECS", "0"), ], );
tests/adversary16.rs+1@@ -61,6 +61,7 @@ fn a_user_chain_during_an_unattributable_route_is_neither_killed_nor_adopted() { ("SWX_LAUNCH_SLDWORKS_SECS", "2"), ("SWX_LAUNCH_ATTACH_SECS", "3"), ("SWX_LAUNCH_POLL_MS", "100"),+ ("SWX_LAUNCH_RETRY_SETTLE_MS", "400"), ("SWX_LAUNCH_BACKOFF_SECS", "0"), ], );
tests/adversary17.rs+1@@ -75,6 +75,7 @@ fn re_attribution_succeeds_when_every_precondition_is_met() { ("SWX_LAUNCH_SLDWORKS_SECS", "2"), ("SWX_LAUNCH_ATTACH_SECS", "2"), ("SWX_LAUNCH_POLL_MS", "100"),+ ("SWX_LAUNCH_RETRY_SETTLE_MS", "400"), ("SWX_LAUNCH_BACKOFF_SECS", "0"), ], );
tests/adversary18.rs+2@@ -67,6 +67,7 @@ fn re_attribution_claims_only_the_process_com_is_attached_to() { ("SWX_LAUNCH_SLDWORKS_SECS", "2"), ("SWX_LAUNCH_ATTACH_SECS", "2"), ("SWX_LAUNCH_POLL_MS", "100"),+ ("SWX_LAUNCH_RETRY_SETTLE_MS", "400"), ("SWX_LAUNCH_BACKOFF_SECS", "0"), ], );@@ -161,6 +162,7 @@ fn the_reattribution_evidence_names_the_process_it_proved() { ("SWX_LAUNCH_SLDWORKS_SECS", "2"), ("SWX_LAUNCH_ATTACH_SECS", "2"), ("SWX_LAUNCH_POLL_MS", "100"),+ ("SWX_LAUNCH_RETRY_SETTLE_MS", "400"), ("SWX_LAUNCH_BACKOFF_SECS", "0"), ], );
tests/adversary2.rs+1@@ -79,6 +79,7 @@ impl Sim { ("SWX_LAUNCH_SLDWORKS_SECS", "4"), ("SWX_LAUNCH_ATTACH_SECS", "8"), ("SWX_LAUNCH_POLL_MS", "100"),+ ("SWX_LAUNCH_RETRY_SETTLE_MS", "400"), ("SWX_LAUNCH_ATTACH_POLL_MS", "150"), ("SWX_LAUNCH_INFLIGHT_SECS", "120"), ("SWX_LAUNCH_BACKOFF_SECS", "0"),
tests/adversary3.rs+1@@ -80,6 +80,7 @@ impl Sim { ("SWX_LAUNCH_SLDWORKS_SECS", "6"), ("SWX_LAUNCH_ATTACH_SECS", "12"), ("SWX_LAUNCH_POLL_MS", "100"),+ ("SWX_LAUNCH_RETRY_SETTLE_MS", "400"), ("SWX_LAUNCH_ATTACH_POLL_MS", "150"), ("SWX_LAUNCH_INFLIGHT_SECS", "120"), ("SWX_LAUNCH_BACKOFF_SECS", "0"),
tests/adversary4.rs+1@@ -56,6 +56,7 @@ impl Sim { ("SWX_LAUNCH_SLDWORKS_SECS", "6"), ("SWX_LAUNCH_ATTACH_SECS", "12"), ("SWX_LAUNCH_POLL_MS", "100"),+ ("SWX_LAUNCH_RETRY_SETTLE_MS", "400"), ("SWX_LAUNCH_ATTACH_POLL_MS", "150"), ("SWX_LAUNCH_BACKOFF_SECS", "0"), ];
tests/launch.rs+109@@ -114,6 +114,9 @@ impl Machine { ("SWX_LAUNCH_POLL_MS", "100"), ("SWX_LAUNCH_ATTACH_POLL_MS", "150"), ("SWX_LAUNCH_INFLIGHT_SECS", "120"),+ ("SWX_LAUNCH_HELPER_SECS", "30"),+ ("SWX_LAUNCH_RETRY_SETTLE_MS", "400"),+ ("SWX_LAUNCH_DIALOG_GRACE_MS", "3000"), ]; env.extend_from_slice(extra); Bridge::start_in(scratch, &env)@@ -1787,3 +1790,109 @@ fn a_launch_record_that_outlived_its_processes_is_ended_not_attached() { s(l) ); }++// ── 0.11.2: THE FALSE STALL OF 2026-10-15 ───────────────────────────────────+//+// From the laptop's ledger (attempt 24, route platformShortcut): Run Begin at+// 14:49:04 and the helper sign-in with it; at 14:51:11 the supervisor called+// the route STALLED (no sldworks.exe within the budget), killed CATSTART and+// invoked the shortcut AGAIN; at 14:51:17 sldworks.exe came up — the FIRST+// invoke's — and the second invoke reached it as an "open" request: a File ▸+// Open dialog that blocked every quit for the rest of the day.++/// The File ▸ Open as measured, owned by the simulated main frame, its frame disabled.+fn open_dialog() -> Value {+ json!({"hwnd": 205698, "className": "#32770", "title": "Open ", "visible": true, "owner": 6622700,+ "ownerEnabled": false, "style": 0x96CC_02C4u64, "exStyle": 0x10101u64, "rect": [0, 0, 853, 533]})+}++fn ledger_actions(b: &Bridge) -> Vec<Value> {+ b.call("swx_action_ledger", json!({"limit": 500}))["actions"].as_array().cloned().unwrap_or_default()+}++#[test]+fn a_slow_helper_sign_in_is_not_a_stall_and_nothing_is_invoked_again() {+ // The incident scaled to the suite's budgets (sldworks 4 s): the helper+ // sign-in from +0.1 s, sldworks.exe at +5.5 s — past the sldworks budget,+ // as 2 min 13 s was past 120 s — and attach at +6.5 s. A re-invoke would+ // put up the Open dialog.+ let m = Machine::new(+ "helper-slow",+ json!({+ "shellExecute": {"chain": true, "runBeginAfterMs": 0, "helperAfterMs": 100, "sldworksAfterMs": 5500, "attachAfterMs": 6500},+ "powershell": {"chain": true, "runBeginAfterMs": 0, "sldworksAfterMs": 100, "attachAfterMs": 300, "dialogOnInvoke": [open_dialog()]}+ }),+ );+ let b = m.boot(TempDir::new("scratch"), &[]);+ b.call("swx_start", json!({"reason": "modelling for Drew", "waitSeconds": 0}));+ let st = wait_for_launch(&b, 30);+ check!(b, "the launch ATTACHES to the first invoke's SOLIDWORKS", st["launch"]["outcome"] == "attached", s(&st["launch"]));+ check!(b, "ONE invocation: the shortcut is never invoked again", m.invocations() == vec!["shellExecute".to_string()], format!("{:?}", m.invocations()));+ check!(b, "nothing was killed: a chain in its helper sign-in is not stalled", m.terminated().is_empty(), format!("{:?}", m.terminated()));+ check!(+ b,+ "no stall on the record, and no dialog on the screen",+ !ledger_actions(&b).iter().any(|e| e["action"] == "solidworks_launch_stalled")+ && !std::fs::read_to_string(m.dir.join("windows.json")).unwrap_or_default().contains("205698"),+ s(&json!(ledger_actions(&b)))+ );+}++#[test]+fn a_solidworks_that_comes_up_after_a_stall_cleanup_is_waited_for_never_re_invoked_over() {+ // No helper log, so the sldworks budget (4 s) does run out — but the+ // launcher had already spawned sldworks.exe, which comes up at +4.3 s+ // although its chain was killed.+ let m = Machine::new(+ "settle",+ json!({+ "shellExecute": {"chain": true, "runBeginAfterMs": 0, "sldworksAfterMs": 4300, "sldworksSurvivesKill": true, "attachAfterMs": 5000},+ "powershell": {"chain": true, "runBeginAfterMs": 0, "sldworksAfterMs": 100, "attachAfterMs": 300, "dialogOnInvoke": [open_dialog()]}+ }),+ );+ let b = m.boot(TempDir::new("scratch"), &[("SWX_LAUNCH_RETRY_SETTLE_MS", "2500")]);+ b.call("swx_start", json!({"reason": "modelling for Drew", "waitSeconds": 0}));+ let st = wait_for_launch(&b, 30);+ check!(b, "the next route is NEVER invoked over a SOLIDWORKS this launch started", m.invocations() == vec!["shellExecute".to_string()], format!("{:?}", m.invocations()));+ check!(+ b,+ "...the retry is declined on the record, naming the SOLIDWORKS",+ ledger_actions(&b).iter().any(|e| e["action"] == "launch_retry_declined" && e["detail"]["why"].as_str().unwrap_or("").contains("already running")),+ s(&json!(ledger_actions(&b)))+ );+ check!(b, "...and the launch attaches to it", st["launch"]["outcome"] == "attached", s(&st["launch"]));+}++#[test]+fn a_file_dialog_a_retry_raised_is_attributed_to_it_and_cancelled_by_the_launch() {+ // A genuine stall (no Run Begin, no SOLIDWORKS) → a legitimate retry,+ // whose invoke puts up the Open dialog anyway. It is the launch's own: the+ // supervisor attributes it to `launch.retry` and cancels it.+ let m = Machine::new(+ "retry-dialog",+ json!({+ "shellExecute": {"chain": true},+ "powershell": {"chain": true, "runBeginAfterMs": 0, "sldworksAfterMs": 100, "attachAfterMs": 300, "dialogOnInvoke": [open_dialog()]}+ }),+ );+ std::fs::write(m.dir.join("file-dialogs.json"), json!({"205698": {"standard": true, "fileName": ""}}).to_string()).unwrap();+ std::fs::write(m.dir.join("last-input-secs"), "3600").unwrap();+ let b = m.boot(TempDir::new("scratch"), &[]);+ b.call("swx_start", json!({"reason": "modelling for Drew", "waitSeconds": 0}));+ let st = wait_for_launch(&b, 30);+ check!(b, "the retry attaches", st["launch"]["outcome"] == "attached" && m.invocations().len() == 2, s(&st["launch"]));+ let deadline = Instant::now() + Duration::from_secs(8);+ let mut cancelled = None;+ while Instant::now() < deadline && cancelled.is_none() {+ cancelled = ledger_actions(&b).into_iter().find(|e| e["action"] == "dialog_cancelled");+ std::thread::sleep(Duration::from_millis(200));+ }+ let c = cancelled.unwrap_or(Value::Null);+ check!(+ b,+ "the dialog is attributed to launch.retry and CANCELLED by the launch, ledgered",+ c["detail"]["raisedDuring"] == "launch.retry" && c["detail"]["ok"] == true && c["detail"]["hwnd"] == 205698,+ s(&json!(ledger_actions(&b)))+ );+ check!(b, "...the window is gone", !std::fs::read_to_string(m.dir.join("windows.json")).unwrap_or_default().contains("205698"), "");+}
tests/signin.rs+1@@ -51,6 +51,7 @@ impl Machine { ("SWX_LAUNCH_SLDWORKS_SECS", "4"), ("SWX_LAUNCH_ATTACH_SECS", "8"), ("SWX_LAUNCH_POLL_MS", "100"),+ ("SWX_LAUNCH_RETRY_SETTLE_MS", "400"), ("SWX_LAUNCH_ATTACH_POLL_MS", "150"), ("SWX_LAUNCH_INFLIGHT_SECS", "120"), ("SWX_SIGN_IN_VERIFY_SECS", "3"),
uninstall.sh